"관점지향프로그래밍 AOP에 대해 아는대로 설명해주세요" 몇 일전 기술 면접에서 받은 질문이었다.
아는대로 설명했지만 여태 적용 해본 적이 없기에 이번 기회를 통해 제대로 학습하고자 한다.
AOP는 "Aspect-Oriented Programming"의 줄임말로 "관심사의 분리"를 위해 사용된다.
반복되고 공통적으로 사용되는 부분을 분리함으로써 모듈성을 증가시키는 프로그래밍 방법이다.
어렵다...
조금 더 쉽게 설명하자면
AOP를 사용하면 코드에서 로깅, 보안, 트랜잭션 관리 등을 따로 떼어 관리할 수 있다.
예를 들어, 어떤 프로그램에는 사용자가 로그인할 때마다 로그를 남기는 기능이 필요할 수 있다.
이 로그 기능은 여러 곳에서 사용되고 같은 코드가 여러 곳에 흩어져 있을 수 있다.
AOP를 사용하면 이런 로그 기능을 한 곳에서 관리할 수 있는 것이다.
그러면 로그인 관련 코드를 변경해야할 때 해당부분만 수정하면 된다.
다른 부분은 건드리지 않아도 되고, 결과적으로 코드가 더 깔끔해지고 유지보수하기 편해진다.

먼저, gradle에 의존성을 추가해주자.
implementation 'org.springframework.boot:spring-boot-starter-aop'
공통 로깅 모델을 구현하기 위해 LoggingAspect 클래스를 생성해주었다.
@Slf4j
@Aspect
@Component
public class LoggingAspect {
}
스프링 AOP를 사용한다는 의미인 @Aspect 어노테이션을 추가하고 @Component 어노테이션을 함께 추가하였다.
이때 여러 블로그를 보면 @EnableAspectJAutoProxy 어노테이션에 대해 언급하지만, 스프링부트에서는 자동으로 AOP 프록시 빈을 등록하기 때문에 굳이 붙여주지 않아도 된다!
다음으로 @Pointcut 어노테이션으로 sopio.app.server.controller 하위의 모든 메소드에 적용되도록 설정하였다.
@Slf4j
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* sopio.app.server.controller.*.*(..))")
private void cut() {}
}
Advice 관련 어노테이션은 다음과 같은 것이 있다.
아래 코드가 내가 AOP로 구현한 로그 모듈이다!!
@Slf4j
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* sopio.app.server.controller.*.*(..))")
private void cut() {}
@Around("cut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Method method = getMethod(joinPoint);
log.info("Method Log: {} || Args: {}", method.getName(), Arrays.toString(joinPoint.getArgs()));
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
log.info("Method {} is finished || Execution Time: {} ms", method.getName(), executionTime);
return result;
}
@AfterThrowing(pointcut = "cut()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception) {
Method method = getMethod(joinPoint);
log.error("AfterThrowing Method: {} || Exception: {}", method.getName(), exception.getMessage());
log.error("Exception type: {}", exception.getClass().toGenericString());
log.error("Exception point: {}", exception.getStackTrace()[0]);
}
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getMethod();
}
}
@Around 어노테이션을 사용하여, 메소드의 이름과 파라미터를 출력한다.
proceed() 함수를 통해 메소드가 실행되고 결과를 받으면, 성공 메시지와 함께 실행 시간을 출력한다.
만약 예외가 발생하면 @AfterThrowing 어노테이션을 통해 예외에 대한 정보(메시지, 예외타입, 발생지점)를 출력한다.
swagger로 테스트 해보니 모두 잘 작동한다!!
진작 알았으면, 면접 때 술술 말했을텐데... 약간의 아쉬움이 밀려오지만 그래도 뿌듯하다.
