스프링 부트 AOP (Aspect-Oriented Programming)
- AOP의 필요성 이해: 소프트웨어 개발에서 반복적으로 발생하는 부가 작업(예: 로깅, 인증/권한 관리, 성능 측정 등)을 핵심 비즈니스 로직과 분리해 유지보수성과 가독성을 높이는 방법 학습.
- 스프링 부트에서의 AOP 활용법 숙지: 스프링 부트의 AOP 모듈을 사용해 간단한 프로젝트를 설계하고 구현.
- 실습 중심 학습: 로깅, 실행 시간 측정 등 실제 개발 환경에서 자주 사용되는 기능을 구현하며 실습.
- 커스텀 어노테이션 활용: AOP와 커스텀 어노테이션을 조합해 동적이고 재사용 가능한 기능 구현.
1.1. 개발 환경 설정
필수 소프트웨어 설치
- IntelliJ (또는 이클립스)
- JDK 17
- Gradle 또는 Maven 설정 (Spring Boot 의존성 관리)
스프링 부트 프로젝트 생성
- Spring Initializr를 사용해 프로젝트 생성: spring-boot-starter-aop는 Spring Boot 기본 제공.
디렉토리 구조
├── src/main/java │ ├── com.example.aopdemo │ ├── AopDemoApplication.java │ ├── aspect (AOP 관련 클래스) │ │ ├── LoggingAspect.java │ │ ├── PerformanceAspect.java │ ├── service (비즈니스 로직 계층) │ │ ├── UserService.java │ ├── annotations (커스텀 어노테이션 저장) │ │ ├── TrackExecutionTime.java │ └── controller (API 엔드포인트) │ ├── UserController.java
실습 환경 준비
- Postman 또는 브라우저로 API 테스트 준비.
- H2 Database 또는 기본 데이터 저장소(Optional).
1.2. AOP가 필요한 이유
코드 중복 제거
- 예: 서비스 클래스마다 System.out.println 로깅 코드를 반복적으로 추가한다면 유지보수하기 어렵고 코드가 지저분해짐.
관심사 분리(Separation of Concerns)
- 핵심 로직과 부가 로직(로깅, 보안 등)을 명확히 분리해 코드의 가독성과 재사용성을 높임.
실제 사례
- 로깅: 애플리케이션의 상태와 실행 흐름을 파악하기 위해 모든 요청을 기록.
- 성능 측정: 메소드의 실행 시간을 측정해 성능 이슈를 발견.
- 보안: 권한 확인 후 메소드 실행 제한.
2. AOP의 기본 개념과 어노테이션
2.1. 주요 개념
Aspect
- 부가 작업(로깅, 보안 등)을 정의하는 모듈.
- 여러 클래스에 공통으로 적용될 로직 포함.
Join Point
- AOP에서 처리 가능한 특정 지점.
- 스프링에서는 보통 메소드 실행 시점을 Join Point로 사용.
Advice
- Join Point에서 실행되는 실제 작업.
- Advice의 종류:
- @Before: 메소드 실행 전 실행.
- @After: 메소드 실행 후 실행.
- @AfterReturning: 메소드가 정상적으로 실행된 후 실행.
- @AfterThrowing: 메소드 실행 중 예외 발생 시 실행.
- @Around: 메소드 실행 전후 모두 처리 가능.
1. @Before: 메소드 실행 전에 로직 실행
package com.example.aopdemo.aspect; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.aopdemo.service.*.*(..))") public void logBeforeMethod() { System.out.println("[LOG] 메소드 실행 전에 로깅 처리"); } }
설명:
- execution(* com.example.aopdemo.service.*.*(..))
- com.example.aopdemo.service 패키지 아래 모든 클래스의 모든 메소드 실행 전에 동작.
2. @After: 메소드 실행 후 로직 실행
package com.example.aopdemo.aspect; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @After("execution(* com.example.aopdemo.service.*.*(..))") public void logAfterMethod() { System.out.println("[LOG] 메소드 실행 후 로깅 처리"); } }
설명:
- 메소드가 성공적으로 실행되든 예외가 발생하든 관계없이 메소드 종료 후 실행.
3. @AfterReturning: 메소드가 정상적으로 실행된 후 실행
package com.example.aopdemo.aspect; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @AfterReturning(pointcut = "execution(* com.example.aopdemo.service.*.*(..))", returning = "result") public void logAfterReturning(Object result) { System.out.println("[LOG] 메소드 정상 실행 완료. 반환값: " + result); } }
설명:
- returning = "result"를 통해 메소드의 반환값을 받을 수 있음.
4. @AfterThrowing: 메소드 실행 중 예외 발생 시 실행
package com.example.aopdemo.aspect; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @AfterThrowing(pointcut = "execution(* com.example.aopdemo.service.*.*(..))", throwing = "exception") public void logAfterThrowing(Exception exception) { System.out.println("[LOG] 메소드 실행 중 예외 발생: " + exception.getMessage()); } }
설명:
- throwing = "exception"을 통해 발생한 예외 객체를 받을 수 있음.
5. @Around: 메소드 실행 전후 모두 처리
package com.example.aopdemo.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class PerformanceAspect { @Around("execution(* com.example.aopdemo.service.*.*(..))") public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); System.out.println("[LOG] 메소드 실행 시작: " + joinPoint.getSignature()); Object result = joinPoint.proceed(); // 실제 메소드 실행 long end = System.currentTimeMillis(); System.out.println("[LOG] 메소드 실행 완료: " + joinPoint.getSignature() + " 실행 시간: " + (end - start) + "ms"); return result; } }
설명:
- joinPoint.proceed()를 호출하면 실제 메소드를 실행.
- 메소드 실행 전/후로 추가 로직 삽입 가능.
6. @Pointcut: 공통 포인트컷 정의
package com.example.aopdemo.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
@Pointcut("execution(* com.example.aopdemo.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("execution(* com.example.aopdemo.controller.*.*(..))")
public void controllerLayer() {}
}
활용 예제:
- 정의된 포인트컷을 다른 Advice에서 재사용 가능
package com.example.aopdemo.aspect; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("com.example.aopdemo.aspect.CommonPointcuts.serviceLayer()") public void logBeforeServiceMethods() { System.out.println("[LOG] 서비스 계층 메소드 실행 전"); } }
설명:
- CommonPointcuts 클래스에 공통 포인트컷 정의.
- 다른 Aspect에서 serviceLayer()를 호출해 재사용 가능.
전체 실행 순서 요약
- @Before: 메소드 실행 전 처리.
- @After: 메소드 실행 후 처리.
- @AfterReturning: 메소드가 정상적으로 실행된 후 처리.
- @AfterThrowing: 메소드 실행 중 예외 발생 시 처리.
- @Around: 메소드 실행 전후 처리.
Pointcut
- 특정 Join Point를 필터링하기 위한 표현식.
- 예: execution(* com.example.service.*.*(..)): com.example.service 패키지의 모든 메소드에 적용.
Weaving
- Aspect를 실제 객체에 연결하는 과정.
- 컴파일, 로드, 런타임 시점에서 수행 가능. 스프링은 런타임 위빙 사용.
2.2. 스프링에서의 AOP 어노테이션
- @Aspect: 클래스를 Aspect로 정의.
- @Component: 스프링에서 관리되는 빈으로 등록.
- @EnableAspectJAutoProxy: AOP 기능 활성화.
- 스프링 부트에서는 자동 활성화되므로 추가 설정 불필요.
3. 간단한 AOP 예제: 로깅 (1시간)
3.1. 요구사항 정의
- 모든 서비스 메소드 호출 시 호출 전에 로그를 출력.
3.2. 코드 구현
Aspect 클래스: LoggingAspect
package com.example.aopdemo.aspect; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.aopdemo.service.*.*(..))") public void logBeforeMethod() { System.out.println("[LOG] 메소드 실행 시작"); } }
2.서비스 클래스: UserService
package com.example.aopdemo.service; import org.springframework.stereotype.Service; @Service public class UserService { public String getUser() { return "사용자 정보 반환"; } }
3.컨트롤러: UserController
package com.example.aopdemo.controller; import com.example.aopdemo.service.UserService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/user") public String getUser() { return userService.getUser(); } }
4.결과 테스트
- /user API 호출 시 [LOG] 메소드 실행 시작 로그 출력.
4. @Around 어노테이션과 성능 측정 실습
4.1. 요구사항 정의
- 특정 메소드의 실행 시간을 측정하고 로그에 출력.
4.2. 커스텀 어노테이션 활용
TrackExecutionTime 어노테이션
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TrackExecutionTime { }
2.Aspect 클래스: PerformanceAspect
@Around("@annotation(com.example.aopdemo.annotations.TrackExecutionTime)") public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); System.out.println(joinPoint.getSignature() + " 실행 시간: " + (end - start) + "ms"); return result; }
3.서비스 메소드 수정
@TrackExecutionTime public String getUser() { Thread.sleep(1000); // 1초 대기 return "사용자 정보 반환"; }
댓글 ( 0)
댓글 남기기