AOP 에 대해 알아보자!

위승현·2024년 12월 27일
0

Spring

목록 보기
8/12

AOP(Aspect-Oriented Programming) 란 애플리케이션의 공통된 관심사
(예: 로깅, 트랜잭션 관리, 권한 검사 등)를 모듈화하여
코드 중복을 줄이고 유지보수성을 높이는 데 사용되는 기술이다.


1. AOP의 주요 구성 요소

Spring AOP를 구현하려면 다음과 같은 구성 요소가 필요하다.

  • Aspect: AOP의 주요 모듈로, 공통 관심사를 캡슐화
  • Advice: 특정 시점에 실행되는 코드 조각. 종류로는 @Before, @After, @Around
  • JoinPoint: Advice가 실행되는 특정 시점, 즉 메서드 호출, 예외 발생 등
  • Pointcut: Advice를 실행할 조건을 정의
  • Weaving: Advice를 애플리케이션 코드에 적용하는 과정

2. Spring AOP 구현 단계

2.1 의존성 추가

Spring AOP를 사용하려면 의존성을 추가해야 한다.

Gradle

implementation 'org.springframework.boot:spring-boot-starter-web'

starter-web 을 추가하면 자동적으로 spring-aop 가 딸려온다.


2.2 Aspect 작성

  1. Aspect 클래스 선언

    • @Aspect@Component 애너테이션을 사용하여 Aspect 클래스를 정의
  2. Advice 추가

    • @Before, @After, @Around, @AfterThrowing 등을 사용하여 특정 시점에 실행하고 싶은 로직을 작성

예제: 로깅 Aspect

import org.aspectj.lang.annotation.*;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // Pointcut 정의: 특정 패키지의 모든 메서드
    @Pointcut("execution(* com.example.service..*(..))")
    public void serviceLayer() {}

    // Before Advice
    @Before("serviceLayer()")
    public void logBefore() {
        System.out.println("메서드 실행 전 로깅");
    }

    // After Advice
    @After("serviceLayer()")
    public void logAfter() {
        System.out.println("메서드 실행 후 로깅");
    }

    // Around Advice
    @Around("serviceLayer()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("메서드 실행 전");
        Object result = joinPoint.proceed(); // 원래 메서드 실행
        System.out.println("메서드 실행 후");
        return result;
    }
}

2.3 애플리케이션에서 Aspect 적용

Spring Boot에서는 @ComponentScan 기능을 통해
위와 같이 @Component로 등록된 Aspect가 자동적으로 적용된다.


3. Advice 종류와 사용법

3.1 @Before

메서드 실행 이전에 실행

@Before("execution(* com.example.service..*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("메서드 이름: " + joinPoint.getSignature().getName());
    System.out.println("메서드 실행 전 로깅");
}

3.2 @After

메서드 실행 이후에 실행

@After("execution(* com.example.service..*(..))")
public void logAfter() {
    System.out.println("메서드 실행 후 로깅");
}

3.3 @AfterReturning

메서드가 정상적으로 종료된 후 실행

@AfterReturning(pointcut = "execution(* com.example.service..*(..))", returning = "result")
public void logAfterReturning(Object result) {
    System.out.println("메서드 반환 값: " + result);
}

3.4 @AfterThrowing

메서드 실행 중 예외가 발생하면 실행

@AfterThrowing(pointcut = "execution(* com.example.service..*(..))", throwing = "exception")
public void logAfterThrowing(Exception exception) {
    System.out.println("예외 발생: " + exception.getMessage());
}

3.5 @Around

메서드 실행 전후의 동작을 모두 처리

@Around("execution(* com.example.service..*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("메서드 실행 전: " + joinPoint.getSignature());
    Object result = joinPoint.proceed(); // 원래 메서드 실행
    System.out.println("메서드 실행 후: " + joinPoint.getSignature());
    return result;
}

4. Pointcut 표현식

Spring AOP의 Pointcut 표현식은 특정 메서드를 타겟팅할 때 사용된다.
주요 표현식은 다음과 같이 사용할 수 있다.

4.1 메서드 실행 타겟팅

@Pointcut("execution(* com.example.service..*(..))")
  • execution: 메서드 실행 시점을 타겟팅.
  • com.example.service..*: com.example.service 패키지와 하위 패키지의 모든 클래스.
  • *(..): 모든 메서드와 매개변수를 타겟팅.

4.2 클래스 내 메서드 타겟팅

@Pointcut("within(com.example.service.MyService)")
  • within: 특정 클래스의 모든 메서드를 타겟팅.

4.3 특정 애너테이션 기반

@Pointcut("@annotation(com.example.annotation.Loggable)")
  • @annotation: 특정 애너테이션이 붙은 메서드를 타겟팅.

Advice랑 Pointcut 의 차이가 뭔데?

살짝 봐도 둘은 하나의 쌍으로 붙어서 사용된다는 것을 알 수 있다.

어드바이스(Advice)포인트컷(Pointcut)은 보통 서로 결합되어
하나의 동작 단위처럼 움직이는 경우가 많다.

어드바이스는 특정 포인트컷에서 실행되기 때문에, 둘이 항상 짝을 이루어 동작한다.

1. Advice는 "무엇을"

  • Advice는 "특정 시점에서 실행할 동작(로직)"을 정의
  • 예를 들어, 메서드 실행 전후 로깅, 권한 검사, 예외 처리 등을 포함

2. Pointcut은 "어디에서"

  • Pointcut은 Advice가 실행될 대상(메서드, 클래스, 패키지 등)을 정의
  • 예를 들어, com.example.service 패키지의 모든 메서드, 특정 애노테이션이 붙은 메서드 등을 지정할 수 있다.

Advice와 Pointcut은 하나로 움직이는 느낌

일반적인 패턴

Spring AOP에서 Advice를 작성할 때 Pointcut 표현식이 항상 뒤따르는 모습을 보았다.

@Around("execution(* com.example.service..*(..))")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("메서드 실행 전 로깅");
    Object result = joinPoint.proceed();
    System.out.println("메서드 실행 후 로깅");
    return result;
}

동작 설명

  1. @Around언제 Advice가 실행될지를 정의 (여기서는 메서드 실행 전후).
  2. "execution(* com.example.service..*(..))"어디에서 Advice를 실행할지를 정의

따라서, Advice와 Pointcut은 하나의 동작 단위처럼 결합된다.


Advice와 Pointcut이 분리되어 있는 이유

둘은 하나처럼 사용되지만 그렇다고 같은 것은 아니다.
이는 재사용성과 역할 분리를 위한 설계이다.

  1. 재사용성
    Pointcut은 "어디에서"라는 정의를 반복적으로 사용할 수 있어 유지보수가 용이하다.
    동일한 포인트컷을 여러 어드바이스에서 사용할 때 이를 분리해서 재사용할 수 있는 것이다.
  • 예: serviceLayer()라는 Pointcut을 여러 Advice에서 재사용.
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}

@Before("serviceLayer()")
public void logBefore() {
    System.out.println("메서드 실행 전");
}

@After("serviceLayer()")
public void logAfter() {
    System.out.println("메서드 실행 후");
}
  1. 역할 분리
  • Pointcut은 적용 대상을 정의하는 데 집중
  • Advice는 실행할 로직을 정의하는 데 집중

5. AOP 구현 사례

5.1 권한 검사

@Aspect
@Component
public class AuthorizationAspect {

    @Before("@annotation(com.example.annotation.RoleCheck)")
    public void checkRole(JoinPoint joinPoint) {
        System.out.println("권한 검사 로직 실행");
        // 권한 체크 로직 구현
    }
}

5.2 로깅

@Aspect
@Component
public class LoggingAspect {

    @Around("execution(* com.example.service..*(..))")
    public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("메서드 실행 시작: " + joinPoint.getSignature());
        Object result = joinPoint.proceed();
        System.out.println("메서드 실행 종료: " + joinPoint.getSignature());
        return result;
    }
}

6. JointPoint? ProceedingJoinPoint?

JoinPointProceedingJoinPoint는 둘 다 Spring AOP에서 메서드 호출을 가로챌 때 사용하는 객체지만, 기능과 사용 범위에서 차이가 있다.

  • JoinPoint: 메서드 호출에 대한 정보만 제공하며, 메서드를 실행할 수는 없다.
  • ProceedingJoinPoint: JoinPoint를 확장한 객체로, 메서드 실행(proceed()) 기능을 제공한다.

1. JoinPoint

JoinPointAOP의 특정 지점에 대한 정보를 제공한다.
메서드 호출, 예외 발생, 필드 접근 등의 정보를 얻는 데 사용된다.

  • 주로 @Before, @After, @AfterThrowing 등의 Advice에서 사용된다.
  • 메서드를 실행하지 않고, 호출 시점의 정보만 제공한다.

주요 메서드

  1. getArgs():

    • 호출된 메서드의 매개변수 목록을 배열로 반환
    • 예: [arg1, arg2, ...].
  2. getSignature():

    • 호출된 메서드의 서명을 반환 (메서드 이름, 반환 타입 등)
  3. getTarget():

    • 실제 호출 대상 객체(타겟)를 반환
  4. getThis():

    • 프록시 객체를 반환

예제: JoinPoint 사용

	//이런 메서드가 존재할 때
 	@RoleCheck(allowRoles = {Role.MEMBER, Role.OWNER})
    public WorkspaceUpdateResponseDto updateWorkspace(
        Long workspaceId,
        Long loginUserId,
        WorkspaceUpdateRequestDto requestDto
    )
    
@Before("@annotation(roleCheck)")
public void checkRole(JoinPoint joinPoint) {
    // 메서드 정보
    System.out.println("메서드 이름: " + joinPoint.getSignature().getName());

    // 매개변수
    Object[] args = joinPoint.getArgs();
    System.out.println("매개변수: " + Arrays.toString(args));

    // 타겟 객체
    System.out.println("타겟 객체: " + joinPoint.getTarget());
}

실행 결과

메서드 이름: updateWorkspace
매개변수: [1, 2, "workspaceDto"]
타겟 객체: com.example.service.WorkspaceService@12345

2. ProceedingJoinPoint

ProceedingJoinPointJoinPoint를 확장한 인터페이스로,
원래 메서드를 실행하거나 실행을 제어할 수 있는 기능을 제공한다.

  • 주로 @Around Advice에서 사용
  • 메서드 실행 전후의 작업을 처리할 수 있다.

주요 메서드

  1. proceed():

    • 원래 메서드를 실행
    • 메서드 실행을 가로채고, 실행 순서를 제어하거나 반환 값을 변경할 수 있다.
  2. proceed(Object[] args):

    • 메서드의 매개변수를 변경한 뒤 호출

예제: ProceedingJoinPoint 사용

@Around("execution(* com.example.service..*(..))")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
    // 메서드 실행 전
    System.out.println("메서드 실행 전: " + joinPoint.getSignature());

    // 원래 메서드 실행
    Object result = joinPoint.proceed();

    // 메서드 실행 후
    System.out.println("메서드 실행 후: " + joinPoint.getSignature());

    return result; // 원래 메서드의 반환 값
}

실행 결과

메서드 실행 전: updateWorkspace
핵심 비즈니스 로직 실행 중...
메서드 실행 후: updateWorkspace

3. JoinPointProceedingJoinPoint의 차이점

특징JoinPointProceedingJoinPoint
Advice 타입@Before, @After, @AfterThrowing@Around
메서드 실행 제어불가능가능 (proceed() 사용)
주요 역할메서드 호출 정보 조회메서드 호출 정보 조회 + 메서드 실행 전후 로직 추가 및 실행 제어
주요 메서드getArgs(), getSignature(), getTarget()proceed(), proceed(Object[] args)

4. 실제 사용 예제 비교

4.1 JoinPoint 사용 (@Before Advice)

@Before("execution(* com.example.service..*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("메서드 호출: " + joinPoint.getSignature().getName());
    System.out.println("매개변수: " + Arrays.toString(joinPoint.getArgs()));
}
  • 사용 목적:
    • 메서드 호출 전, 호출 정보를 로깅하거나 검증할 때 사용.
  • 제한 사항:
    • 메서드를 실행하거나 반환 값을 변경할 수 없음.

4.2 ProceedingJoinPoint 사용 (@Around Advice)

@Around("execution(* com.example.service..*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("메서드 실행 전: " + joinPoint.getSignature().getName());

    // 메서드 실행
    Object result = joinPoint.proceed();

    System.out.println("메서드 실행 후: " + joinPoint.getSignature().getName());
    return result;
}
  • 사용 목적:
    • 메서드 실행 전후의 로직 처리, 실행 순서 변경, 반환 값 수정 등이 필요할 때 사용.
  • 장점:
    • proceed()를 통해 메서드 실행을 완전히 제어 가능.

5. 정리

  • JoinPoint:

    • 메서드 호출 시점에 대한 정보만 필요할 때 사용.
    • 메서드 실행을 직접 제어할 필요가 없으면 적합.
  • ProceedingJoinPoint:

    • 메서드 실행 전후의 로직을 처리하거나, 실행 순서와 반환 값을 제어하고 싶을 때 사용.
    • @Around Advice에서만 사용 가능.

7. 주의사항

  1. Spring AOP의 한계:

    • Spring AOP는 프록시 기반으로 작동하므로, 같은 클래스의 내부 메서드 호출에는 AOP가 적용되지 않는다.
    • 해결 방법: @EnableAspectJAutoProxy(exposeProxy = true)를 설정하고, AopContext.currentProxy()를 사용하여 자신을 호출.
  2. AOP 성능 고려:

    • 지나치게 많은 Aspect를 정의하면 성능 저하를 초래할 수 있으므로 적절히 설계해야 함.
  3. Pointcut 정확성:

    • Pointcut 표현식이 정확하지 않으면 의도한 대로 작동하지 않을 수 있으므로 꼼꼼히 작성해야 함.

이제 실제로 적용해보러 가자!

AOP 역할 췤~!

profile
개발일기

0개의 댓글

관련 채용 정보