728x90
반응형

개요

  Next.js 서버 기존 Node.js 서버 마이그레이션 Java 서버 DB
State Camel      
Request Snake Snake Snake  
Response Snake Snake Snake  
Model   Snake Camel  
Column       Snake

 

Node.js 서버에서는 Request를 있는 그대로 받아서 사용하였기에 별 상관이 없었지만

Java 서버에서는 Dto 객체를 만들고 Dto를 토대로 Entity에 대한 Query를 해야 하기 때문에 결국 Camel을 한 번 거쳐서 가야 했다.

 

 

문제점

결론부터 얘기하자면 

'insurance_JikJongCd' <-- 이런 애들이 문제다

 

그 당시에는 외부 api를 쓰면서 해당 Request 형식에 맞춰 이렇게 만들었었고 Node.js 에서 사용할 때는 문제가 없었지만

Spring에서 Entity를 사용하면서 문제가 발생했다. 

 

아래와 같이 테이블에 설정해주면 Snake 표기법으로 된 Request Camel 표기법으로 된 엔티티 필드와 매칭시킬 수 있다.

또한 Response 또한 snake case 로 반환할 수 있다.

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User {

 

 

또한 Spring은 똑똑해서 별다른 설정을 해주지 않아도 Camel 표기법으로 된 엔티티 필드 Snake 표기법으로 된 Db Column이랑 매칭을 시켜준다.

 

// Entity
@Column(name="user_name")
private String userName;

 

 

근데 'insurance_JikJongCd' <-- 이런 애들이 문제다

 

Request로 받을 경우 insurance_Jik_jong_cd  이런 식으로 읽기 때문에 인식을 하지 못한다. 

 

 

JsonProperty Annotation을 쓰면 Response를 Snake 표기법으로 보낼 수도 있다.

// Entity
@Column(name="user_name")
@JsonProperty(value="user_name")
private String userName;

// Response
user_name : "teepo"

 

 

참고1

PropertyNamingStrategies 구성 요소들이 Deprecated 되면서 application.properties에서 다음과 같이 Request, Response를

Snake 표기법으로 통신할수 있다.

spring.jackson.property-naming-strategy = SNAKE_CASE

 

이 방법으로 @JsonProperty, @JsonNaming 어노테이션을 사용하지 않아도 된다.

 

 

 

참고

appication.properties 에서 지원해주는 카멜표기법을 사용하지않고 사용자 임의로 설정하는 것이 있다.

#spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

 

결론은 섞어서 쓰지 말고 설계를 잘하고 스프링이 편하라고 제공해준 것을 잘 쓰자.

728x90
반응형
728x90
반응형

1. Redis docker image pull

$ docker pull redis # redis 이미지 받기
$ docker images # redis 이미지 확인
$ docker run -p 6379:6379 --name some-redis -d redis # redis 시작하기
$ docker ps # redis 실행 확인

 

 

2. 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

 

3. application.properties

#Redis
spring.redis.host= localhost
spring.redis.port= 6379

 

 

Redis Cache 활성화를 위한 @Annotation

  • @EnableCaching
    • SpringBoot에게 캐싱 기능이 필요하다고 전달
    • SpringBoot Starter class에 적용
  • @Cacheable
    • DB에서 애플리케이션으로 데이터를 가져오고 Cache에 저장하는 데 사용
    • DB에서 데이터를 가져오는 메서드에 적용
  • @CachePut
    • DB의 데이터 업데이트가 있을 때 Redis Cache에 데이터를 업데이트
    • DB에서 PUT/PATCH와 같은 업데이트에서 사용
  • @CacheEvict
    • DB의 데이터 삭제가 있을 때 Redis Cache에 데이터를 삭제
    • DB에서 DELETE와 같은 삭제에서 사용

 

4. RedisConfig 파일 생성

/backend/redis/RedisConfig

package web.backend.redis;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public CacheManager testCacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofMinutes(3L));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
    }
}

매니저에 도메인을 Serialize 하게끔 설정해두었기에 도메인에 따로 Serializable를 import 안해줘도 된다.

다만 어노테이션에서 매니저를 호출해야 한다.

 

backend/WebConfig

@Import({AopConfig.class, RedisConfig.class})

 

5. LocalDateTime in Domain

Serialize를 위해 LocalDateTime 타입 필드는 설정을 따로 해줘야 한다.

backend/module/user/User

...

    @CreatedDate
    @Column(name="user_createdat", updatable = false)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime user_creadtedat;

    @LastModifiedDate
    @Column(name="user_updatedat")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime user_updatedat;
    
...

 

6. Service 적용

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {

    private final UserQueryRepository userQueryRepository;
    private final UserSpringJpaRepository userSpringJPARepository;

    @Cacheable(value = "User", cacheManager = "testCacheManager")
    public List<User> findAll() {
        return userSpringJPARepository.findAll();
    }

    @Cacheable(value = "User", key = "#id", cacheManager = "testCacheManager")
    public User findByIndexId(Long id) {
        return userSpringJPARepository.findById(id).get();
    }

    public String save(User user) {
        userSpringJPARepository.save(user);
        return "ok";
    }

    @CachePut(value = "Order", key = "#id", cacheManager = "testCacheManager")
    public String update(Long id, User user) {
        User userOne = userSpringJPARepository.findById(id).get();
        userOne.changeUser(user.getUserId());
        return "ok";
    }

    @CacheEvict(value = "Order", key = "#id", cacheManager = "testCacheManager")
    public String delete(Long id) {
        userSpringJPARepository.deleteById(id);
        return "ok";
    }


}

 


테스트

1. 데이터 추가

테스트를 위해 WebConfig에 어플리케이션 빌드 시 데이터를 추가하는 로직을 작성해준다.

package web.backend;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.aop.AopConfig;
import web.backend.interceptor.LogInterceptor;
import web.backend.module.user.User;
import web.backend.module.user.repository.UserSpringJpaRepository;
import web.backend.redis.RedisConfig;

@Configuration
@Import({AopConfig.class, RedisConfig.class})
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico"
                        , "/error", "/error-page/**" //오류 페이지 경로
                );
    }

    @Autowired
    UserSpringJpaRepository userSpringJpaRepository;

    @Bean
    public void addUsers() {
        for(int i = 0; i < 100; i++) {
            User user = new User();
            user.changeUser("member" + i);
            userSpringJpaRepository.save(user);
        }
    }

}

 

 

2. Redis Docker Container

Spring Server를 키고 Redis Container에 접속한 후 요청을 보내서 로그를 확인해보자.

$ docker ps    
$ docker exec -it some-redis /bin/bash
$ redis-cli monitor

 

postman으로 요청을 보내보자.

그러면 아래와 같이 캐시가 저장된 것을 확인할 수 있다.

 

postman에도 데이터가 잘 도착했다.

이번엔  key를 지정하고 다시 보내보자. ( id를 파라미터로 보내는 로직 )

 

역시 정상적으로 동작이 잘 되었다.

 

 

3. 성능 확인

 

- 처음 캐시가 사용되기 전 요청

 

- 두번 째 요청

 

 


다만,

아래와 같은 점을 고려해야 한다.

  1. 리스트를 처음에 캐싱한다.
  2. 리스트의 어떤 항목이 생성되거나 수정되거나 삭제된다.
  3. 캐시된 리스트의 데이터가 바뀌어야 하지만 그렇지 못한다.

캐시에 변경사항이 생길 경우 다른 캐시에 영향이 끼친다면 로직을 따로 만들어 주어야 한다. 당장에라도 적용하고 싶으면

findAll()의 @Cacheable 어노테이션은 제거한 상태로 쓰고 나중에 각 테이블의 연관관계를 고려해서 로직을 구성해주자.

728x90
반응형
728x90
반응형

외부 api에 접속할 때 간혹 가다 한 번이 아니라 두세 번 정도 시도를 해야 성공하는 경우가 있다. ( 외부 사이트가 불안정할 경우 )

 

그럴 때 만들 수 있는 Aspect가 있다. default 값은 꼭 정해주도록 하자.

 

1. Retry Annotation

backend/annotation/Retry

package web.backend.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    int value() default 3;
}

 

2. RetryAspect

backend/aop/RetryAspect

package web.backend.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import web.backend.annotation.Retry;

@Slf4j
@Aspect
public class RetryAspect {

    @Around("@annotation(retry)")
    public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
        log.info("[retry] {} retry={} ", joinPoint.getSignature(), retry);

        int maxRetry = retry.value();
        Exception exceptionHolder = null;

        for (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
            try {
                log.info("[retry] try count={}/{}", retryCount, maxRetry);
                return joinPoint.proceed();
            } catch (Exception e) {
                exceptionHolder = e;
            }
        }
        throw exceptionHolder;
    }
}

 

3. AopConfig

backend/aop/AopConfig

...

    @Bean
    public RetryAspect retryAspect() { return new RetryAspect(); }

...

 


테스트

Service 부분을 잠시 바꿔보고 해보자.

backend/module/user/UserService

...

    private static int seq = 0;

    @Retry(value = 2)
    public String save(User user) {
        if (seq == 0) {
            seq++;
            throw new IllegalStateException("예외 발생!!");
        }
        return "ok";
    }
    
...

 

실행 결과

728x90
반응형
728x90
반응형

전 포스트에서 만든 공통 응답객체를 활용해서 모든 컨트롤러에서 발생하는 에러를 핸들링해보자.

 

1. CustomRuntimeException

backend/exception/CustomRuntimeException

package web.backend.exception;

public class CustomRuntimeException extends RuntimeException {
    public CustomRuntimeException() {
    }

    public CustomRuntimeException(String message) {
        super(message);
    }

    public CustomRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }

    public CustomRuntimeException(Throwable cause) {
        super(cause);
    }

    public CustomRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

2. ExControllerAdvice

backend/advice/ExControllerAdvice

package mobile.backend.advice;

import lombok.extern.slf4j.Slf4j;
import mobile.backend.exception.CustomRuntimeException;
import mobile.backend.response.CommonResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.NoSuchElementException;

@Slf4j
@RestControllerAdvice(annotations = RestController.class)
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    @ExceptionHandler
    public CommonResponse<String> exHandle(Exception e) {
        log.error("[exceptionHandle] ex", e);
        return new CommonResponse<String>(false, "시스템 오류");
    }

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler
    public CommonResponse<String> customExHandler(CustomRuntimeException e) {
        log.error("[exceptionHandle] ex", e);
        return new CommonResponse<>(false, e.getMessage());
    }

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler
    public CommonResponse<String> customExHandler(NoSuchElementException e) {
        log.error("[exceptionHandle] ex", e);
        return new CommonResponse<>(true, null);
    }
}

 


테스트

Service에서 임의로 에러를 던져보자.

backend/module/user/UserController

@GetMapping
    public CommonResponse<List<User>> findAll() throws Exception {
        return new CommonResponse<List<User>>(true, userService.findAll());
    }

 

backend/module/user/UserService

    public List<User> findAll() throws Exception {
        throw new Exception();
//        return userSpringJPARepository.findAll();
    }
    
        public String save(User user) {
        throw new CustomRuntimeException("요청이 잘못됨");
//        userSpringJPARepository.save(user);
//        return "ok";
    }

 

1. GET Method ( findAll )

 

2. POST Method ( save )

 

원하는 에러 메세지가 정상적으로 출력되었다.

728x90
반응형
728x90
반응형
  • 성공 시 { success : true, result : data }
  • 실패 시 { success : false, result : "에러 사유" }

 

1. CommonResponse

backend/response/CommonResponse

package web.backend.response;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class CommonResponse<T> {
    private boolean success;
    private T result;

}

 

이제 컨트롤러에서 성공 시 반환할 데이터를 수정해보자.

 

2. UserController

backend/module/user/UserController

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import web.backend.annotation.LogTrace;
import web.backend.module.user.repository.UserUpdateDto;
import web.backend.response.CommonResponse;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @GetMapping
    public CommonResponse<List<User>> findAll() {
        return new CommonResponse<List<User>>(true, userService.findAll());
    }

    @GetMapping("/{userId}")
    public CommonResponse<User> findByIndexId(@PathVariable(value = "userId") Long userId ) {
        return new CommonResponse<User>(true, userService.findByIndexId(userId));
    }

    @PostMapping
    public CommonResponse<String> save(@RequestBody User user) {
        return new CommonResponse<String>(true, userService.save(user));
    }

    @PatchMapping
    public CommonResponse<String> update(@RequestBody UserUpdateDto user) {
        return new CommonResponse<String>(true, userService.update(user.getId(), user.getUser()));
    }

    @DeleteMapping
    public CommonResponse<String> delete(@RequestParam Long id) {
        return new CommonResponse<String>(true, userService.delete(id));
    }

}

 


테스트

POST Method

 

GET Method

728x90
반응형
728x90
반응형

전체 순서

  1. 인터셉터 ( 요청 객체 로깅 ) 
  2. Basic Aspect ( try catch 에러 핸들링 ) - Order(2) , 모든 controller, service, repository
  3. LogTraceAspect ( log 추적기 ) - Order(1), @LogTrace 어노테이션 지정한 곳
  4. 로직 실행
  5. LogTraceAspect ( log 추적기 ) - Order(1)
  6. Basic Aspect ( try catch 에러 핸들링 ) - Order(2)
  7. 인터셉터 ( 응답 객체 로깅 )

 

LogTrace는 필요한 경우에만 사용하도록 하자.

 

1. BasicAspect 생성

/backend/aop/BasicAspect

package web.backend.aop;


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;

@Slf4j
@Aspect
@Order(2)
public class BasicAspect {

    @Around("execution(* web.backend.module..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

        try {

            Object result = joinPoint.proceed();

            return result;
        } catch (Exception e) {
            throw e;
        }
    }
}

 

2. TraceStatus

backend/aop/trace/TraceStatus

package web.backend.aop.trace;

public class TraceStatus {

    private TraceId traceId;
    private Long startTimeMs;
    private String message;

    public TraceStatus(TraceId traceId, Long startTimeMs, String message) {
        this.traceId = traceId;
        this.startTimeMs = startTimeMs;
        this.message = message;
    }

    public Long getStartTimeMs() {
        return startTimeMs;
    }

    public String getMessage() {
        return message;
    }

    public TraceId getTraceId() {
        return traceId;
    }
}

 

3. TraceId

backend/aop/trace/TraceId

package web.backend.aop.trace;

import java.util.UUID;

public class TraceId {

    private String id;
    private int level;

    public TraceId() {
        this.id = createId();
        this.level = 0;
    }

    private TraceId(String id, int level) {
        this.id = id;
        this.level = level;
    }

    private String createId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    public TraceId createNextId() {
        return new TraceId(id, level + 1);
    }

    public TraceId createPreviousId() {
        return new TraceId(id, level - 1);
    }

    public boolean isFirstLevel() {
        return level == 0;
    }

    public String getId() {
        return id;
    }

    public int getLevel() {
        return level;
    }
}

 

4. LogTrace Interface

backend/aop/trace/logtrace/LogTrace

package web.backend.aop.logtrace;

import hello.proxy.trace.TraceStatus;

public interface LogTrace {

    TraceStatus begin(String message);
    void end(TraceStatus status);
    void exception(TraceStatus status, Exception e);
}

 

5. ThreadLocalLogTrace

backend/aop/trace/logtrace/ThreadLocalLogTrace

package web.backend.aop.trace.logtrace;

import lombok.extern.slf4j.Slf4j;
import web.backend.aop.trace.TraceId;
import web.backend.aop.trace.TraceStatus;

@Slf4j
public class ThreadLocalLogTrace implements LogTrace {

    private static final String START_PREFIX = "-->";
    private static final String COMPLETE_PREFIX = "<--";
    private static final String EX_PREFIX = "<X-";

    private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();

    @Override
    public TraceStatus begin(String message) {
        syncTraceId();
        TraceId traceId = traceIdHolder.get();
        Long startTimeMs = System.currentTimeMillis();
        log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);

        return new TraceStatus(traceId, startTimeMs, message);
    }

    @Override
    public void end(TraceStatus status) {
        complete(status, null);
    }

    @Override
    public void exception(TraceStatus status, Exception e) {
        complete(status, e);
    }

    private void complete(TraceStatus status, Exception e) {
        Long stopTimeMs = System.currentTimeMillis();
        long resultTimeMs = stopTimeMs - status.getStartTimeMs();
        TraceId traceId = status.getTraceId();
        if (e == null) {
            log.info("[{}] {}{} time={}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs);
        } else {
            log.info("[{}] {}{} time={}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs, e.toString());
        }

        releaseTraceId();
    }

    private void syncTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId == null) {
            traceIdHolder.set(new TraceId());
        } else {
            traceIdHolder.set(traceId.createNextId());
        }
    }

    private void releaseTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId.isFirstLevel()) {
            traceIdHolder.remove();//destroy
        } else {
            traceIdHolder.set(traceId.createPreviousId());
        }
    }

    private static String addSpace(String prefix, int level) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < level; i++) {
            sb.append( (i == level - 1) ? "|" + prefix : "|   ");
        }
        return sb.toString();
    }
}

 

6. LogTrace Annotation

backend/annotation/LogTrace

package web.backend.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogTrace {
}

 

7. LogTraceAspect

backend/aop/LogTraceAspect

package web.backend.aop;


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import web.backend.aop.trace.TraceStatus;
import web.backend.aop.trace.logtrace.LogTrace;

@Slf4j
@Aspect
@Order(1)
public class LogTraceAspect {

    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Around("@annotation(web.backend.annotation.LogTrace)")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        TraceStatus status = null;

        try {
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);

            Object result = joinPoint.proceed();

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }

}

 

8. AopConfig 생성

backend/aop/AopConfig

package web.backend.aop;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import web.backend.aop.trace.logtrace.LogTrace;
import web.backend.aop.trace.logtrace.ThreadLocalLogTrace;

@Configuration
public class AopConfig {

    @Bean
    public BasicAspect basicAspect() {
        return new BasicAspect();
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }

    @Bean
    public LogTraceAspect logTraceAspect() {
        return new LogTraceAspect(logTrace());
    }
}

9. WebConfig 등록

backend/WebConfig

package web.backend;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.aop.AopConfig;
import web.backend.interceptor.LogInterceptor;

@Configuration
@Import(AopConfig.class)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico"
                        , "/error", "/error-page/**" //오류 페이지 경로
                );
    }

}

 

 


테스트

 

save 로직에 @LogTrace 어노테이션을 주고 findAll() 메소드와 비교해보겠다.

 

// controller    
    @PostMapping
    @LogTrace
    public String save(@RequestBody User user) {
        return userService.save(user);
    }
// service
    @LogTrace
    public String save(User user) {
        userSpringJPARepository.save(user);
        return "ok";
    }
// repository
    @Override
    @LogTrace
    <S extends User> S save(S entity);

 

 

Find 

 

Save

728x90
반응형
728x90
반응형

로깅 내용

  • UUID
  • METHOD
  • URL

 

1. LogInterceptor

backend/interceptor/LogInterceptor

package web.backend.interceptor;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;

@Slf4j
@RequiredArgsConstructor
@Component
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uuid = UUID.randomUUID().toString();
        String requestURI = request.getRequestURI();
        String method = request.getMethod();

        request.setAttribute(LOG_ID, uuid);
        log.info("REQUEST  [{}][{}][{}][{}]", uuid, method, requestURI, handler);
        return true;
    }


//    @Override
//    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        log.info("postHandle [{}]", modelAndView);
//    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String logId = (String) request.getAttribute(LOG_ID);
        String requestURI = request.getRequestURI();
        String method = request.getMethod();

        log.info("RESPONSE [{}][{}][{}]", logId, method, requestURI);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}

 

2. WebConfig 

backend/WebConfig

package web.backend;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.interceptor.LogInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico"
                        , "/error", "/error-page/**" //오류 페이지 경로
                );
    }
    
}

 

3. 테스트

 

728x90
반응형
728x90
반응형

VPC

항목 기준 제공사양
VPC 개수 리전 3개
Subnet 개수 VPC 200개
IP 개수(IPv4) VPC /16-28(65,536개~16개)
NAT Gateway 개수 1개
Network ACL 개수 VPC 200개
Network ACL 규칙 개수 Network ACL 40개
ACG 개수 VPC 500개
ACG 규칙 개수 ACG 50개
ACG 적용 개수 Network Interface 3개
Route Table 개수 VPC 200개
Route Table 규칙 개수 Route Table 50개
VPC Peering 개수 VPC 20개

 

Load Balancer

1. 기능

  • TCP 고성능 분산 처리(Network Load Balancer): 초당 연결 수 기준 최소 100,000개에서 최대 400,000개까지 성능을 보장하며, 서비스 규모에 최적화된 분산 처리 성능을 제공
  • TCP 세션 관리(Network Proxy Load Balancer): TCP 기반 애플리케이션에 사용할 수 있는 Proxy 방식의 통신을 제공
  • SSL 인증 및 암호화 설정(Application Load Balancer, Network Proxy Load Balancer): TLSv1/TLSv1.1/TLS1.2 등 SSL 프로토콜을 제공하며, Certificate Manager 서비스와 연동하여 인증서 관리 가능
  • 다양한 서버 부하 분산 방식(Application Load Balancer, Network Proxy Load Balancer): 3가지 서버 부하 분산 방식 제공
    • Round Robin(순환 순서)
    • Least Connection(최소 연결)
    • Source IP Hash(IP 해시)
  • L7(Application Layer) 기능 제공(Application Load Balancer): Load Balancer에 지원되는 규칙으로 클라이언트의 요청을 세분화하여 서버에 전달 가능
  • Load Balancer 모니터링: 일정 주기별로 수집된 모니터링 정보 제공
  • Load Balancer 포트 설정: 여러 개 Load Balancer 규칙의 동시 적용 가능

 

Round Robin
  • 로드밸런싱으로 지정된 서버들에 대해 공평하게 순차적으로 클라이언트 요청을 전달하는 방식
  • 서버 커넥션 수나 응답 시간에 상관없이 그룹 내의 모든 서버를 동일하게 처리하여 일반적인 구성에 있어 다른 알고리즘에 비해 가장 빠름
Least Connection
  • 클라이언트 연결이 가장 적은 서버에 클라이언트 요청을 전달하는 방식
  • 서버들의 성능이 비슷하게 구성되어 있을 때 가장 효과적으로 트래픽 분산 가능
멀티존 지원
  • 클라이언트의 Source IP 주소 정보를 바탕으로 hash한 결과에 따라, 클라이언트 IP에 매핑되는 서버에 클라이언트 요청을 전달하는 방식
  • SSL 프로토콜을 사용하는 경우, Source IP Hash 알고리즘 권장

 

2. 자주 묻는 질문

  • 하나의 Load Balancer는 최대 50대의 서버를 바인드할 수 있습니다.
  • 리스너(Listener)를 설정한 후 추가해 주십시오 HTTPS또는 TLS프로토콜을 추가한 경우 Target Group을 선택하기 전에 사전 작업 시 등록한 외부 인증서를 선택한 후 Certificate 설정을 해주십시오.
  • Network Load Balancer는 다음과 같이 5-튜플 해시 알고리즘에 따라 부하를 분산합니다. 결과적으로 새로운 요청이 Load Balancer로 인입될 경우, Source Port가 변경되므로 다른 서버로 전달될 수 있습니다.
    • Source IP
    • Source Port
    • Destination IP
    • Destination Port
    • Protocol
728x90
반응형

'Server > Naver Cloud' 카테고리의 다른 글

Naver Cloud | Compute | AutoScaling  (1) 2022.09.22
Naver Cloud | Compute | VPC  (0) 2022.09.21
728x90
반응형

1. UserUpdateDto

backend/module/user/repository/userUpdateDto

package web.backend.module.user.repository;

import lombok.Data;
import web.backend.module.user.User;

@Data
public class UserUpdateDto {
    private Long id;
    private User user;
}

 

2. Service

backend/module/user/UserService

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {

    private final UserQueryRepository userQueryRepository;
    private final UserSpringJpaRepository userSpringJPARepository;

    public List<User> findAll() {
        return userSpringJPARepository.findAll();
    }

    public User findByIndexId(Long id) {
        return userSpringJPARepository.findById(id).get();
    }

    public String save(User user) {
        userSpringJPARepository.save(user);
        return "ok";
    }

    public String update(Long id, User user) {
        User userOne = userSpringJPARepository.findById(id).get();
        userOne.changeUser(user.getUserId());
        return "ok";
    }

    public String delete(Long id) {
        userSpringJPARepository.deleteById(id);
        return "ok";
    }


}

 

3. Controller

backend/module/user/UserController

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import web.backend.module.user.repository.UserUpdateDto;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @GetMapping
    public List<User> findAll() {
        return userService.findAll();
    }

    @GetMapping("/{userId}")
    public User findByIndexId(@PathVariable(value = "userId") Long userId ) {
        return userService.findByIndexId(userId);
    }

    @PostMapping
    public String save(@RequestBody User user) {
        return userService.save(user);
    }

    @PatchMapping
    public String update(@RequestBody UserUpdateDto user) {
        return userService.update(user.getId(), user.getUser());
    }

    @DeleteMapping
    public String delete(@RequestParam Long id) {
        return userService.delete(id);
    }

}

 

 


테스트 코드 작성

 

/src/test/java/web/backend/module/user/UserServiceTest

package web.backend.module.user;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

import javax.persistence.EntityManager;

import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Slf4j
class UserServiceTest {

    @Autowired
    EntityManager em;

    @Autowired
    UserSpringJpaRepository userSpringJpaRepository;

    @Autowired
    UserQueryRepository userQueryRepository;

    @BeforeEach
    public void before() {
        User user = new User("userId");
        User savedUser = userSpringJpaRepository.save(user);
    }

    @AfterEach
    public void after() {
        em.flush();
        em.clear();
    }


    @Test
    @Transactional
    void save() {
        Optional<User> findUser = userSpringJpaRepository.findByUserId("userId");
        String userId = findUser.get().getUserId();

        assertEquals("userId",userId);
    }

    @Test
    @Transactional
    void find() {

        List<User> findAll = userSpringJpaRepository.findAll();

        for( User userOne : findAll) {
            assertEquals("userId",userOne.getUserId());
        }

        User findOne = userSpringJpaRepository.findByUserId("userId").get();
        assertEquals("userId",findOne.getUserId());
    }

    @Test
    @Transactional
    void update() {
        Optional<User> findUser = userSpringJpaRepository.findByUserId("userId");
        User userOne = findUser.get();
        userOne.changeUser("newUserId");

        Optional<User> findNewUser = userSpringJpaRepository.findByUserId("newUserId");

        assertEquals("newUserId",findNewUser.get().getUserId());
    }

    @Test
    @Transactional
    void delete() {
        userSpringJpaRepository.deleteById(1L);

        assertEquals(0,userSpringJpaRepository.findAll().size());
    }


}

 

 

 

728x90
반응형
728x90
반응형

본 프로젝트는 Querydsl, SpringDataJpa 둘 다 사용 가능하게끔 설계했습니다.

 

1. 파일 만들기

2. User DAO

backend/module/user/User

package web.backend.module.user;

import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@Table(name="user_info")
public class User {

    @Id @GeneratedValue
    @Column(name="user_index")
    private Long id;

    @Column(name="user_id")
    private String userId;

    @CreatedDate
    @Column(name="user_createdat", updatable = false)
    private LocalDateTime user_creadtedat;

    @LastModifiedDate
    @Column(name="user_updatedat")
    private LocalDateTime user_updatedat;

    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        user_creadtedat = now;
        user_updatedat = now;
    }

    @PreUpdate
    public void preUpdate() {
        user_updatedat = LocalDateTime.now();
    }

    public User(String userId) {
        this.userId = userId;
    }

    /**
     * user 수정
     */
    public void changeUser(String userId) {
        this.userId = userId;
    }

}

 

3. UserController

backend/module/user/UserController

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

}

 

4. UserService

backend/module/user/UserController

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {

    private final UserQueryRepository userQueryRepository;
    private final UserSpringJpaRepository userSpringJPARepository;

}

 

5. UserQueryRepository

backend/module/user/repository/UserQueryRepository

package web.backend.module.user.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;

@Repository
@Slf4j
public class UserQueryRepository {

    private final JPAQueryFactory query;

    public UserQueryRepository(EntityManager em) {
        this.query = new JPAQueryFactory(em);
    }
}

 

6. UserSpringJpaRepository

backend/module/user/repository/UserSpringJpaRepository

package web.backend.module.user.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import web.backend.module.user.User;

import java.util.Optional;

public interface UserSpringJpaRepository extends JpaRepository<User, Long> {

    Optional<User> findByUserId(String userId);
}

 

728x90
반응형

+ Recent posts