01 AOP
01 AOP
1. Note
- 좋은 듯 하면서 안좋은 느낌
- 정말 공통으로 필수적인 global느낌은 추가하고
- 그외의 것들은 근야 개별로 구현하는게 낫지 않을까?
- 오히려 소스가 복잡해지고 예상하지 못한 상황이 발생할 수도
- 있으나 없으나 차이없는 것들만 셋팅하면 좋을 듯!
- AI를 통해서 한다면, 미리 제한을 걸어야할 듯.
2. AOP
1. AOP
- 관점 지향 프로그래밍
- 애플리케이션의 핵심 비즈니스 로직과 공통적으로 반복되는 기능을 분리하는 프로그래밍 패러다임
- 비즈니스 로직은 순수하게 유지
- 반복되는 기능은 한 곳에 모아서 처리
- Spring에서는 주로 프록시 기반 런타임 AOP 방식으로 구현
- 로그 처리
- 트랜잭션 관리
- 인증/인가
- 성능 측정
2. 횡단 관심사 문제
횡단 관심사란 여러 모듈에 걸쳐서 중복으로 나타나는 기능을 의미
1 2 3 4 5 6 7 8 9 10
# 도메인의 Service - 각 역할이 다름 UserService OrderService PaymentService # 다른 역할에서 필요한 공통 기능이 존재함 - 로그 출력 - 트랜잭션 시작/종료 - 예외 처리 - 권한 체크
개별 구현시 문제점
- 코드 중복 증가
- 유지보수 비용 증가
- 핵심 로직 가독성 저하
- 변경 시 여러 군데 수정 필요
- 실수로 누락될 가능성 증가
3. OOP와의 관계
| 구분 | OOP (Object Oriented Programming) | AOP (Aspect Oriented Programming) |
|---|---|---|
| 목적 | 객체 중심 설계 | 공통 기능 분리 |
| 관심사 | 핵심 비즈니스 로직 | 횡단 관심사(공통 기능) |
| 설계 기준 | 객체/역할 중심 | 기능/관점 중심 |
| 구조 방향 | 수직 분리 | 수평 분리 |
| 주요 책임 | 객체 간 책임 분리 | 반복 로직 공통화 |
| 대표 기능 | 회원, 주문, 결제 같은 도메인 로직 | 로깅, 트랜잭션, 인증, 예외 처리 |
| 코드 중복 해결 | 상속, 조합, 인터페이스 활용 | Aspect로 공통 처리 |
| 적용 방식 | 클래스 내부 구현 | Proxy를 통한 외부 개입 |
| 변경 영향도 | 공통 로직 변경 시 여러 클래스 수정 가능 | Aspect 수정으로 일괄 반영 가능 |
| Spring 활용 예시 | Service / Repository / Domain 설계 | @Transactional, 로깅, 성능 측정 |
3. 핵심 개념
1. Aspect (관점)
- 공통 기능(횡단 관심사)을 모아놓은 객체
여러 클래스에서 반복되는 로직을 하나로 관리하는 역할
1 2 3 4 5 6 7 8 9 10
@Aspect // 애노테이션으로 표기 @Component public class LoggingAspect { // 공통 기능을 모아둔 클래스 // 필요한 기능들을 애노테이션을 표기하여 메소드로 나열함. @Before("execution(* com.example.service.*.*(..))") public void log() { System.out.println("메서드 실행 전 로그"); } }
2. Join Point (조인 포인트)
- AOP가 적용될 수 있는 지점을 의미
- Spring AOP에서는 대부분 메서드 실행 시점만 지원
- 가능한 시점
- 메서드 실행 전
- 메서드 실행 후
- 예외 발생 시
- 정상 반환 시
3. Pointcut (포인트컷)
- 실제로 어디에 AOP를 적용할지 선택하는 조건
예시
1 2 3 4
@Before("execution(* com.example.service.*.*(..))") - com.example.service 패키지의 - 모든 클래스의 - 모든 메서드에 적용표현식 종류
표현식 설명 execution()메서드 실행 기준 within()특정 클래스/패키지 범위 @annotation()특정 어노테이션 대상 bean()특정 Bean 이름
4. Advice (어드바이스)
4-1. Before(메서드 실행 전에 수행)
1
2
3
4
5
6
7
@Before("execution(* com.example.service.*.*(..))")
public void before() {
System.out.println("실행 전");
// 로그
// 권한 검사
// 파라미터 검증
}
4-2. After(메서드 종료 후 실행)
1
2
3
4
5
6
7
@After("execution(* com.example.service.*.*(..))")
public void after() {
System.out.println("무조건 실행");
// 정상 종료든 예외든 무조건 실행
// 리소스 정리
// 공통 후처리
}
4-3. AfterReturning(메서드 정상 종료)
1
2
3
4
5
6
7
8
@AfterReturning(
value = "execution(* com.example.service.*.*(..))",
returning = "result"
)
public void afterReturning(Object result) {
System.out.println(result);
// 반환값 로깅
// 성공 처리
4-4. AfterThrowing(예외 발생 시 실행)
1
2
3
4
5
6
7
8
9
@AfterThrowing(
value = "execution(* com.example.service.*.*(..))",
throwing = "e"
)
public void afterThrowing(Exception e) {
System.out.println(e.getMessage());
// 예외 로그
// 에러 모니터링
}
4-5. Around(메서드 실행 전/후)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Around("execution(* com.example.service.*.*(..))")
public Object around(
ProceedingJoinPoint joinPoint
) throws Throwable {
// 메인 비즈니스 로직 실행전
long start = System.currentTimeMillis();
// 메인 비즈니스로직 수행
Object result = joinPoint.proceed();
// 메인 비즈니스 로직 실행후
long end = System.currentTimeMillis();
System.out.println(end - start);
return result;
// 성능 측정
// 트랜잭션 처리
// 공통 로깅
}
5. Weaving (위빙)
Aspect를 실제 객체에 적용하는 과정
1 2 3 4 5 6 7
Client ↓ Proxy 객체 ↓ AOP 적용 ↓ Target Object
위빙방식(별도 weaving 설정 거의 없음 / 대부분 자동화)
방식 설명 Compile-Time Weaving 컴파일 시 적용 Load-Time Weaving 클래스 로딩 시 적용 Runtime Weaving 실행 중 적용 (Spring AOP)
4. Pointcut 표현식
1. execution 기반 표현식
- 메서드 실행 기준으로 AOP 적용 범위를 지정하는 표현식
문법
1 2 3 4 5 6 7
execution( 접근제어자 반환타입 패키지.클래스.메서드(파라미터) ) execution( public * com.example.service.OrderService.order(..) )체크
요소 의미 publicpublic 메서드 *반환 타입 전체 OrderService특정 클래스 order()특정 메서드 (..)모든 파라미터
2. 패키지 / 클래스 / 메서드 매칭
2-1. 패키지
1
2
3
4
5
# 특정 패키지
execution(* com.example.service.*.*(..))
# 하위 패키지
execution(* com.example.service..*.*(..))
2-2. 클래스
1
2
3
execution(
* com.example.service.OrderService.*(..)
)
2-3. 메소드
1
2
3
execution(
* com.example.service.OrderService.order(..)
)
3. within, this, target, @annotation
3-1. within
1
2
3
# 특정 클래스/패키지 범위 지정
# service 패키지 내부 클래스만 적용
within(com.example.service..*)
3-2. this
1
2
# Proxy 타입이 OrderService인 경우 적용
this(com.example.service.OrderService)
3-3. target
1
2
# 실제 대상 객체 타입 기준 적용
target(com.example.service.OrderService)
3-4. @annotation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 애노테이션 선언
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {
}
// 오더에서 애노테이션 사용
@Logging
public void order() {
}
// 포인트컷 설정
@Before(
"@annotation(com.example.Logging)"
)
public void logging() {
}
5. 실무 활용 사례
1. 로깅 (Logging)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Aspect
@Component
@Slf4j
public class LoggingAspect {
// 반환 타입 전체, service 패키지 및 하위 패키지, 모든 클래스, 모든 메서드+파라미터
@Around("execution(* com.example.service..*(..))")
public Object logging(ProceedingJoinPoint joinPoint) throws Throwable {
// 실행 대상 클래스명 조회
String className = joinPoint.getSignature().getDeclaringTypeName();
// 실행 대상 메서드명 조회
String methodName = joinPoint.getSignature().getName();
// 메서드 실행 전 로그
log.info("[START] {}.{}", className, methodName);
// 실제 비즈니스 메서드 실행
// proceed()를 호출해야 Target Method 실행됨
Object result = joinPoint.proceed();
// 메서드 실행 후 로그
log.info("[END] {}.{}", className, methodName);
// 실제 메서드 반환값 그대로 반환
return result;
}
}
2. 인증/인가
구현
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
@Aspect @Component @Slf4j @RequiredArgsConstructor public class AuthAspect { private final AuthService authService; // @LoginCheck 가 붙은 메서드만 인증 검사 @Before("@annotation(com.example.annotation.LoginCheck)") public void authenticate(JoinPoint joinPoint) { // 현재 사용자 인증 여부 확인 if (!authService.isAuthenticated()) { throw new UnauthorizedException("로그인이 필요합니다."); } // 실행 대상 메서드명 로그 String methodName = joinPoint.getSignature().getName(); log.info("[AUTH SUCCESS] method={}", methodName); } }Service
1 2 3 4 5 6
@LoginCheck public void order() { System.out.println("주문 처리"); }
3. 성능 측정 (Performance Monitoring)
- 구현
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
@Aspect @Component @Slf4j public class PerformanceAspect { // service 패키지 및 하위 패키지의 모든 메서드 성능 측정 @Around("execution(* com.example.service..*(..))") public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { // 메서드 실행 시작 시간 기록 long startTime = System.currentTimeMillis(); // 실행 대상 클래스명 조회 String className = joinPoint.getSignature().getDeclaringTypeName(); // 실행 대상 메서드명 조회 String methodName = joinPoint.getSignature().getName(); try { // 실제 비즈니스 메서드 실행 return joinPoint.proceed(); } finally { // 메서드 실행 종료 시간 기록 long endTime = System.currentTimeMillis(); // 총 실행 시간 계산 long executionTime = endTime - startTime; // 성능 측정 로그 출력 log.info( "[PERFORMANCE] {}.{} executed in {} ms", className, methodName, executionTime ); } } }
4. 예외 처리 공통화
- 구현
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
@Aspect @Component @Slf4j public class ExceptionAspect { // service 패키지 및 하위 패키지의 모든 메서드 예외 처리 @AfterThrowing( pointcut = "execution(* com.example.service..*(..))", throwing = "exception" ) public void handleException( JoinPoint joinPoint, Exception exception ) { // 실행 대상 클래스명 조회 String className = joinPoint.getSignature().getDeclaringTypeName(); // 실행 대상 메서드명 조회 String methodName = joinPoint.getSignature().getName(); // 예외 로그 출력 log.error( "[EXCEPTION] {}.{} message={}", className, methodName, exception.getMessage() ); } }
This post is licensed under CC BY 4.0 by the author.