Development/Spring & Springboot

[기술면접 준비] 비전공자도 쉽게 이해하는 AOP

bbubbush 2023. 3. 26. 15:55

들어가며

스프링의 기술질문 주제로 단골로 등장하는 AOP. 개념은 잘 아는데 자세하게 설명하려고 하니 어렵다. 그래서 자주 나오는 질문과 질문의 답이 되는 코드를 작성해 보며 개념을 확실하게 머릿속에 넣기 위해 준비했다. 실제로 며칠 전 기술면접에서 AOP에 대한 질문을 받았는데 개념적으로 이해한다고 생각해 준비 없이 대답했다가 호되게 당했다.

 

이제부터 여러분과 나는 취준생이 되어 AOP에 대해 자세하게 알아보고 답을 정리해보자. 

 

 

Q. AOP는 무엇이고 왜 사용할까요?

A. 관점지향 프로그래밍이란 뜻으로 OOP의 개념을 보완하기 위해 사용됩니다. 가령 모든 컨트롤러 요청의 파라미터를 로깅하고 싶을 때, 로깅이라는 기능이 중복될 수 있습니다. 이를 컨트롤러에 진입하기 전에 일괄적으로 처리해 줄 수 있다면 각 컨트롤러는 기능에 집중할 수 있고, 파라미터 로깅도 본인의 기능에만 충실할 수 있습니다. AOP가 없다면 여러 객체에 공통적으로 사용되는 코드의 관리가 어렵고, 본연의 기능 외에도 공통적인 기능도 처리해야 하는 상황이 발생합니다.

AOP의 구현 방법에는 xml, 애노테이션, 자바 클래스를 통한 설정이 있습니다.

 

AOP에 대한 개념을 묻는 동시에 자세한 질문을 하기 위한 포석과 같은 질문이다. '저희는 이제부터 당신에게 AOP에 대해 상세하게 물어볼거에요' 라는 뜻이니 알고 있는 지식을 쏟아내기보단 자연스럽게 대화로 질문이 이어질 수 있게 여지를 주는 것이 좋다. 누구나 알 법한 예시를 들면서 설명하는 것도 좋은 방법이다.

 

Q. AOP를 구현할 때 Advice, Pointcut 같은 용어를 사용합니다. 어떤 것들을 알고 계신가요?

A. 먼저 처리될 지점과 무엇을 처리할 것인지 정의한 Advice와 어느 대상에게 Advice를 적용할지 표현하는 Pointcut이 있습니다. Aspect는 공통된 관심사를 묶은 모듈로 하나 이상의 Advice와 Pointcut으로 구성됩니다. Pointcut에 의해 Aspect를 적용할 대상이 된 객체를 Target이라고 합니다.
Jointpoint는 Aspect가 적용되는 지점입니다. 객체의 생성, 대상의 실행 전, 후 등 다양한 지점이 있고 이에 대한 정보를 Aspect의 파라미터로 전달받아 상황에 맞는 이벤트 처리를 할 수 있습니다. 다만 스프링에서는 메서드 Joinpoint만 제공됩니다.
앞서 정의한 Aspect를 애플리케이션 코드와 연결하는 과정을 Weaving이라고 하며 컴파일시점, 클래스 로딩 시점, 런타임 시점에 적용할 수 있지만 스프링은 런타임 시점에 적용합니다.

 

각 용어가 익숙하면 각각 특징이나 세세한 부분을 같이 외우면 좋다. 만약 AOP를 처음 공부해 이런 용어들이 낯설다면 크게 Aspect를 구성하는 Advice와 Pointcut, Target을 묶어서 하나로 공부하고, Aspect와 애플리케이션코드를 연결하는 Weaving과 Aspect에게 적용지점을 알려주는 Joinpoint를 하나로 묶어서 공부하면 개념 정리가 용이하다.

 

그리고 꼭 코드를 치면서 각 용어가 어떤 코드를 의미하는지 사전에 준비해야한다. 다른 부분보다 용어를 외울 땐 코드와 일치시켜서 외우는 게 오래 기억에 남는다.

 

 

Q. Advice 종류에 대해 설명해주세요.

A.타겟 객체가 실행되기 전에 호출하는 @Before와 호출이 끝난 후에 동작하는 @After가 있습니다. @After advice는 조금 더 상세하게 나눠 성공적으로 타겟이 실행되었을 때 실행되는 @AfterReturning과 예외가 발생했을 때 실행되는 @AfterThrowing이 있습니다. 마지막으로 위 기능 모두를 포함할 수 있는 @Around도 있습니다. 

실행되는 시점을 기준으로 Advice를 설명했다. 면접관이 각각의 특징에 대해 자세히 물어봐주면 좋겠지만 그렇지 않을 수도 있으니 살을 조금 더 붙여서 대답해도 좋다.

핵심적인 부분은 @Around는 리턴타입이 Object이고, joinpoint.proceed()를 꼭 실행해 줘야 한다. Around는 타겟의 전과 후, 심지어 예외나 성공적인 처리에 대한 기능을 모두 정의할 수 있기 때문에 자유도가 높지만 꼭 해줘야 하는 부분이 있다고 생각해야 한다.

다른 4가지 Advice는 리턴타입이 void면서 첫 번째 파라미터로 Joinpoint를 받는다.

  • @Before: 실행 전
  • @After: 실행 후(성공과 예외 모두 동작)
  • @AfterReturning: 정상적으로 끝나면
  • @AfterThrowing: 예외가 발생하면
  • @Around: 실행 전, 실행 후 모두 포함. ProceedingJoinPoint를 파라미터로 받는 유일한 Advice. joinpoint.procedd()를 필수로 실행

 

번외로 아래와 같이 Advice가 설정된 코드가 있다면 일반적인 요청과 예외 요청에 따라 실행 순서는 어떻게 될까?

@Aspect
@Component
@Slf4j
public class CarAspect {
  @Before("execution(* com.example.aopdemo.api.CarApiController.*(..))")
  public void beforeCar(JoinPoint joinPoint) {
    log.debug(" beforeCar");
  }

  @Around("execution(* com.example.aopdemo.api.CarApiController.*(..))")
  public Object aroundCar(ProceedingJoinPoint joinPoint) throws Throwable {
    log.debug("around before proceed");
    final Object response = joinPoint.proceed();
    log.debug("around after proceed");
    return response;
  }

  @After("execution(* com.example.aopdemo.api.CarApiController.*(..))")
  public void afterCar(JoinPoint joinPoint) {
    log.debug(" afterCar");
  }

  @AfterReturning("execution(* com.example.aopdemo.api.CarApiController.*(..))")
  public void afterReturningCar(JoinPoint joinPoint) {
    log.debug(" afterReturningCar");
  }
  @AfterThrowing("execution(* com.example.aopdemo.api.CarApiController.*(..))")
  public void afterThrowingCar(JoinPoint joinPoint) {
    log.debug(" afterThrowingCar");
  }
}

 

먼저 예외의 발생 없이 요청된 AOP의 요청순서다. 이해를 돕기 위해 들여 쓰기를 추가했다.

@Around가 먼저 실행되고 proceed()가 실행됨에 따라 @Before와 @After가 실행된다. @After 보다 @AfterReturning이 먼저 실행되는 것도 알 수 있다.

 

 

그럼 예외가 발생하면 어떻게 될까? 아래는 예외가 발생한 AOP의 요청순서다.

@AfterReturning 대신 @AfterThrowing이 호출된 것 말고는 큰 차이가 없어 보이지만, @Around의 proceed() 후의 로직이 실행되지 않음을 알 수 있다.  작다면 작은 부분이지만 혼동하기 쉬운 부분이다.

 

 

 

Q. AOP는 프록시 객체를 사용하는데 크게 JDK dynamic 방식과 CGLib방식이 있습니다. 두 방식에 대해 설명해 주세요.

A. JDK dynamic 방식은 자바의 리플릭터를 기반합니다. 인터페이스를 구현한 클래스에만 적용할 수 있다는 특징이 있어 인터페이스 없이는 사용할 수 없다는 단점이 있습니다. 대신 자바 표준 라이브러리에서 제공하기 때문에 스프링에 종속적이지 않습니다.
반면, CGLib방식은 바이트 코드를 기반으로 동작합니다. 타겟 객체의 상속을 통한 프록시 객체를 생성하여 메서드를 오버라이딩하므로 인터페이스 없이도 구현이 가능합니다. 
스프링에서는 두 가지 방식을 모두 지원하며, 타겟 객체가 인터페이스를 구현했다면 JDK dynamic 방식으로 동작하고, 그렇지 않다면 CGLib 방식으로 동작합니다.

AOP 면접내용에 포함되었지만 다이나믹 프록시 패턴의 대한 내용에 가깝다. 조금 더 AOP스럽게 학습하려면 Advice가 실행될 때의 이미지를 그려보면 좋다. 아래는 프록시 객체가 타겟 객체를 어떻게 감싸는지 스스로 그려본 이미지다.

 

위에 예시로 든 코드에서는 인터페이스 없이 AOP를 구현했으므로 CGLib 방식으로 구현되었다. 실제로 아래 이미지를 보면 어떤 방식으로 프록시 객체가 만들어졌는지 알 수 있다.

 

CglibAopProxy 객체를 확인할 수 있다.

 

 

마치며

지난번 면접의 아픔을 달래기 위해 AOP를 정리해 보았다. AOP의 개념으로 시작해 구성요소들이 어떤 의미인지, 구성요소를 어떻게 사용할 수 있는지 이해하고 설명하는 부분에 중점을 두고 질문과 대답을 준비해보았다. 부족한 질문은 향후에 추가로 넣을 예정이다. 

 

 

참고자료

망나니 개발자님이 자세한 정리한 포스팅