Post

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.