본문 바로가기
Books

스프링부트로 개발하는 MSA 컴포넌트 [7]장

by YoonJong 2023. 8. 26.
728x90

 스프링 AOP 와 테스트, 자동 설정 원리

  1. 스프링 프레임워크의 3가지 핵심 기술

 

스프링 AOP

  1. 관심사의 분리

 

  1. 대상객체 (target object) : 공통 모듈을 적용할 대상 ex) logging 공통모듈 / HotelDisplayService 대상 객체
  2. 관점 (aspect) : 어드바이스 + 포인트컷
  3. 어드바이스 (advice) : 공통 로직이 작성된 모듈
  4. 포인트컷 (point cut) : 적용할 위치 설정
  5. 조인포인트 (join point) : 어드바이스가 적용된 위치
  6. 프록시 객체 (proxy object) : 스프링 AOP는 기능을 조합하기 위해 동적으로 프록시객체를 만든다.

 

스프링 AOP 와 프록시 객체

  1. 스프링 AOP 프레임워크는 대상 객체를 감싸는 프록시 객체를 동적을 생성.
  2. 프록시 객체가 클라이언트의 요청을 가로챈다.
  3. 어드바이스 타입에 따라 적절히 실행.
  • 런타임 시 프록시 객체가 생성
  • 어드바이스 로직과 대상 객체의 로직이 함께 실행되는 것을 위빙이라고 한다.
  • 관점 클래스와 대상 클래스는 Bean 으로 등록되어야 정상 작동 가능.
  • 인터페이스를 구현하고 있으면 JDK Proxy  사용
  • 인터페이스를 구현하고 있지 않으면 CGLIB 사용

 

포인트 컷과 표현식

  • 포인트 컷은 어드바이스를 적용할 위치를 선정하는 것.
  • 포인트 컷 지정자 ( 리턴타입 / 패키지경로 / 클래스 이름 / 메서드 이름 / 인자 )
  execution (* com.springboot.example.testService.getMember (..))
  • 작성한 포인트 컷을 적용하는 방법
  •  

 

JoinPoint 와 ProceedingJoinPoint

  • JoinPoint : 선정된 위치 ( 대상 객체의 메서드 )
  • ProceedingJoinPoint 의 메서드인 proceed() 사용 가능
  • Around 어드바이스는 반드시 ProceedingJoinPoint 를 주입받아서 proceed() 메서드를 명시적으로 호출해야 한다.
  • → Around 는 대상 객체의 메서드를 감싸고 있는 형태이므로, 프레임워크가 관여할 수 없다.

 

사용법

  • @Aspect 을 추가해주어야 하며, 스프링 빈으로 등록하기 위해 @Component 설정 필요.

 

  • 아래 예제는 HotelRequest 의 인자를 가로채서 사용자가 어떤 요청을 했는지 로그로 남기는 예제.
@Slf4j
@Aspect
@Component
@Order(1)
public class ArgumentLoggingAspect {

    // 다음의 pointcut expression 들은 모두 HoteDisplayService 의 getHotelsByName() 메서드를 잡을 수 있다.
    //    @Before("execution(* getHotelsByName(..))")
    //    @Before("execution(* com.springtour.example.chapter07.service.*.getHotelsByName(..))")
    //    @Before("execution(* com.springtour.example.chapter07.service.*.get*(..))")
    @Before("execution(* *(com.springtour.example.chapter07.controller.HotelRequest, ..))")
    public void printHotelRequestArgument(JoinPoint joinPoint) {

        String argumentValue = Arrays.stream(joinPoint.getArgs())
                .filter(obj -> HotelRequest.class.equals(obj.getClass()))
                .findFirst()
                .map(HotelRequest.class::cast)
                .map(hotelRequest -> hotelRequest.toString())
                .orElseThrow();

        log.info("argument info : {}", argumentValue);
    }
}

 

 

  • 아래 예제는 메서드가 종료되는 시점 , 메서드가 예외를 발생한 시점에 로그를 남기는 예제.
@Slf4j
@Aspect
@Component
@Order(1)
public class ReturnValueLoggingAspect {

// 객체를 어드바이스 메서드 인자로 받는 변수 이름을 설정가능 -> retVal
    @AfterReturning(pointcut = "execution(* getHotelsByName(..))", returning = "retVals")
    public void printReturnObject(JoinPoint joinPoint, List<HotelResponse> retVals) throws Throwable {
        retVals.stream()
                .forEach(response -> log.info("return value : {}", response));
    }

// 객체를 어드바이스 메서드 인자로 받는 변수 이름을 설정가능 -> th
    @AfterThrowing(pointcut = "execution(* getHotelsByName(..))", throwing = "th")
    public void printThrowable(JoinPoint joinPoint, Throwable th) throws Throwable {
        log.error("error processing", th);
    }

}

 

애너테이션을 사용한 AOP - 사용자 정의 애너테이션

  • 예제 참고  - 400p
  • 주의 할점 !
    1. 조인 포인트에서 발생하는 예외를 어드바이스 내부에서 직접 처리하는 것을 지양.
    2. @Transactional 을 사용하게 되면 예외 발생 시 롤백하고 사용하던 커넥션 객체를 커넥션 풀에 반납하는데, 어드바이스가 예외를 던지지않고 직접 처리하면 트랜잭션 매니저의 예외 처리 부분이 정상적으로 동작하지 않는다.
// 포인트 컷을 지정하는 마킹 애너테이션이므로, 별도의 속성 값을 지정하지 않는다.
@Target({ElementType.METHOD})       // 메서드에 적용
@Retention(RetentionPolicy.RUNTIME) // 런타임시 적용
public @interface ElapseLoggable {
}


-- ElapseLoggable 설정 부분
@Slf4j
@Component
@Aspect
@Order(2)
public class ElapseLoggingAspect {

    @Around("@annotation(ElapseLoggable)") // ElapseLoggable 애너테이션이 적용된 모든 메서드가 포인트 컷 대상
    public Object printElapseTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        log.info("start time clock");

        Object result;
        try {
            // 대상 클래스의 메서드가 실행
            result = proceedingJoinPoint.proceed();
        } finally {
            stopWatch.stop();
            String methodName = proceedingJoinPoint.getSignature().getName();
            long elapsedTime = stopWatch.getLastTaskTimeMillis();
            log.info("{}, elapsed time: {} ms", methodName, elapsedTime);
        }

        return result; // 반드시 리턴해야 한다.
    }
}

-- 에너테이션을 적용할 메서드
@Slf4j
@Service
public class HotelDisplayService implements DisplayService {

    @Override
    @ElapseLoggable
    public List<HotelResponse> getHotelsByName(HotelRequest hotelRequest) {

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            log.error("error", e);
        }

        return List.of(
                new HotelResponse(1000L,
                        "Ragged Point Inn",
                        "18091 CA-1, San Simeon, CA 93452",
                        "+18885846374"
                )
        );
    }
}
728x90

댓글