6주차 위클리페이퍼의 첫 번째 주제이다.
- Springo| AOP(Aspect Oriented Programming)가 필요한 이유와 이를 활용한 실제 애플리케이션 개발 사례에 대해 설명하세요.
먼저 AOP에 대해서 기본 개념을 설명하자면 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라는 뜻으로, 공통 관심사 (Cross Cutting Concern)를 비즈니스 로직과 분리하는 기술이다.
AOP가 필요한 이유는 핵심 비즈니스 로직 이외에 공통으로 필요한 기능(공통 관심사)를 한 곳으로 모아서 적용하므로 다음과 같은 장점이 있기 때문이다.
- 코드 중복 방지: 공통적으로 반복되는 기능을 한 곳으로 모아서 중복 제거
- 일관성 유지 및 유지보수성 향상: 공통 기능이 다르게 작동하지 않고 일관되게 작동할 수 있도록 함으로써 유지보수성 향상
- 관심사의 분리: 개발자가 핵심 비즈니스 로직에만 집중할 수 있도록 함
대표적인 공통 관심사는
1. 로깅
2. 트랜잭션
3. 권한 체크
4. 성능 측정
5. 예외 처리
6. 캐싱
7. 비동기 처리
등이 있다.
아래는 AOP와 관련된 어노테이션이다.
| @Aspect | 이 클래스가 AOP 횡단 관심사(공통 기능)를 정의한 클래스임을 스프링에 알립니다. |
| @Pointcut | AOP 기능이 적용될 지점(메서드, 패키지 등)을 정의합니다. |
| @Before | 대상 메서드가 실행되기 직전에 동작합니다. |
| @After | 대상 메서드 실행 후 동작합니다. (정상 종료, 예외 발생 상관없이 무조건 실행되는 finally 블록 같은 역할) |
| @AfterReturning | 대상 메서드가 정상적으로 종료되었을 때만 동작하며, 반환값을 확인할 수 있습니다. |
| @AfterThrowing | 대상 메서드에서 예외가 발생했을 때만 동작합니다. |
| @Around | 가장 강력한 어드바이스로, 메서드 실행 전후의 제어권을 모두 가집니다.(@Before + @After) |
이 외에도 @Transaction, @Async 어노테이션은 AOP로 작동된다.
유의 해야할 점으로 AOP는 현재 스프링에서 CGlib 방식이며 상속을 기반으로 작동하기에 public(또는 protected)에만 사용할 수 있으며 self invocation이 발생할 수 있다.(동일 클래스 내에서 AOP 작동 안됨)
이제 실제로 애플리케이션 개발에서는 어떻게 AOP를 적용하는지 알아보자.
아래는 RestController에 적용하는 로깅 AOP의 형태이다.
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
public void restController() {}
@Around("restController()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
log.info("▶ {}.{} start", className, methodName);
try {
Object result = joinPoint.proceed();
long time = System.currentTimeMillis() - start;
log.info("✔ {}.{} end ({}ms)", className, methodName, time);
return result;
} catch (Exception e) {
log.error("✖ {}.{} exception: {}", className, methodName, e.getMessage());
throw e;
}
}
}
이때 컨트롤러 코드가 다음과 같다고 가정한다.
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public String findUser(@PathVariable Long id) {
return "User " + id;
}
}
클라이언트가 GET /users/1 요청을 할 때 호출 순서는
Client
→ DispatcherServlet
→ CGLIB Proxy(UserController)
→ LoggingAspect (@Around)
→ UserController.findUser()
출력 로그는 이렇게 된다.
▶ com.example.UserController.findUser start
▶ com.example.UserController.findUser end