본문 바로가기
IT/Spring boot

[Spring boot] AOP (Aspect Oriented Programming)

by kyu-nahc 2024. 8. 28.
반응형

 

 

AOP ( Aspect Oriented Programming )

AOP는 관점(Aspect) 지향 프로그래밍으로,

관점을 기준으로 다양한 기능을 분리하여 보는 프로그래밍이다.

관점(Aspect)이란, 부가 기능과 그 적용처를 정의하고 합쳐서 모듈로 만든 것이다.

여기서 관점은 공통 관심 사항과 핵심 관심 사항으로 나눌 수 있다.

  • 공통 관심 사항 : 보안, 로깅, 성능, 파일 입출력
  • 핵심 관심 사항 : 개발자가 구현하는 비즈니스 로직

단 여기서 Spring AOP는 Spring Bean에 적용이 가능하다.

Spring Bean 객체의 메소드 호출을 인터셉트한다면,

Spring Bean 객체의 해당 메소드가 호출되기 전에 인터셉트하고 특정 로직을 실행되게 할 수 있다.

인터셉트를 통해 공통 관심 사항에 대한 로직을 수행하는 것이 AOP의 목적이다.

 

 

AOP 핵심 용어

AOP의 핵심 용어들을 정리하면 다음과 같다.

  • Advice
    공통 관심 분야의 실행할 코드
  • Pointcut
    인터셉트하려는 메소드에 대한 호출
  • Aspect
    Advice와 Pointcut의 조합
  • JoinPoint
    포인트컷 조건이 참이면 어드바이스가 실행된다.
    만약 포인트컷 조건을 만족시키는 메소드가 1000개라면,
    어드바이스를 실행해야 하는 인스턴스는 모두 1000개가 된다.
    이때 어드바이스를 실행하는 인스턴스를 가리켜 JoinPoint라고 한다.
  • Target
    Aspect가 적용되는 곳 ( Class / Method..)
  • Proxy

    클라이언트와 타겟 사이에 투명하게 존재하며 부가기능을 제공하는 오브젝트.

    DI를 통해 타겟 대신 클라이언트에게 주입되며,
    클라이언트의 메소드 호출을 대신 받아서 타겟에 위임하며 이 과정에서 부가기능을 부여

 

 

AOP Annotation

@Pointcut("")

어떤 메소드 호출을 인터셉트할 것인가에 대한 명시

@Before("")

메소드가 호출되기 전에 해당 포인트컷의 메소드를 실행

@After

메소드가 실행되고 난 뒤 수행할 작업을 지시.

메소드가 성공적으로 실행되었든 예외를 던지든 실행된다.

@AfterReturning

메소드가 예외 없이 성공적으로 실행됐다면 실행한다.

예외 발생 시 해당 AOP 로직을 실행하지 않는다.

대표적인 인자로는 포인트컷, returning이 있다.

성공적으로 return 된 Java Object를 메소드의 매개변수로 받을 수 있으며,

이때 매개변수의 변수명과 returning의 이름이 반드시 동일해야 한다.

@AfterReturning(
    pointcut = "execution(* com.example.demo.respository..*(..))",
    returning = "resultValue"
)public void exceptionLogging(JoinPoint joinPoint,Object resultValue){
		... AOP Code
}

 

@AfterThrowing

메소드가 예외를 발생시키면 실행된다.

대표적인 인자로는  포인트컷, throwing이 있다.

throwing은 발생한 예외를 바인딩시킬 Advice 시그니처의 인자 이름이다.

예외 객체는 메소드의 매개변수를 통해 받을 수 있으며,

@AfterReturning에서처럼 반드시 이름이 동일해야만 한다.

@AfterThrowing(
    pointcut = "execution(* com.example.demo.respository..*(..))",
    throwing = "exception"
)public void exceptionLogging(JoinPoint joinPoint,Exception exception){
		... AOP Code
}

 

@Around

@Around는 메소드 실행 전과 후에 실행할 로직을 구현한다.

포인트컷 지정의 예시는 다음과 같다.

@Around("execution(* com.example.demo..*(..))")

이것은 com.example.demo 아래에 존재하는 모든 메소드에 적용한다는 것으로

상황에 따라 com.example.demo.repository..*(..)) 이렇게 설정할 경우

com.example.demo.repository 안에 존재하는

모든 Spring Bean에 대해서는 AOP 로직을 실행한다는 의미이다.

 

 

 

AOP Example

예를 들어 회원가입시간, 회원 조회 시간을 측정하고 싶다면 어떻게 해야 할까?

이에 대한 문제점을 살펴보면 다음과 같다.

  • 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다.
  • 시간을 측정하는 로직은 공통 관심 사항이다.
  • 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
  • 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다.

이에 대한 문제점의 해결방안이 바로 AOP이다.

공통 관심 사항인 시간 측정 로직을 구현하고,

적용시키고 싶은 패키지 혹은 메소드를 포인트컷으로 등록하면 된다.

즉 AOP를 사용하지 않는다면, 모든 메소드에 비즈니스 로직과 함께 시간 측정 로직을 적용해야 하지만,

AOP를 사용하여 공통 관심 사항과 핵심 비즈니스 로직을 분리한 것이다. 

시간 측정 예제를 보자.

먼저 @Aspect를 통해 Advice와 Pointcut을 사용하는 AOP Class임을 명시하고

@Component를 통해 Spring Bean으로 등록해 준다.

예제 코드는 다음과 같다.

@Aspect
@Component
public class TimeTraceAop {

    @Around("execution(* com.example.demo..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("Start : " + joinPoint.toString());
        try{
            return joinPoint.proceed();
        }finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("End : " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

public class MemoryMemberRepository implements MemberRepository{

    private static final Map<Long,Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(),member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }


    public void clearStore(){
        store.clear();
    }
}

@Service
public class MemberService {

    private final MemoryMemberRepository repository = new MemoryMemberRepository();
    
    public List<Member> findMembers(){
        return repository.findAll();
    }

}

 

TimeTraceAOP Class에서 execute() 메소드를 보면

매개변수로 ProceedingJoinPoint 객체를 받는 것을 볼 수 있다.

JoinPoint 인터페이스호출되는 객체, 메서드, 전달 파라미터 목록에 접근할 수 있는 메소드 제공한다.

 

ProceedingJoinPoint는 JoinPoint를 상속받은 객체이다.
ProceedingJoinPoint는 Around advice(@Around)에서만 지원되는 JoinPoint이다.

Around Advice는 ProceedingJointPoint 객체를 첫 번째 파라미터로 전달받으며,
Target Object의 메소드 정보를 포함한다. 또한 proceed()를 통해 전달받은 메소드를 실행 가능하다.

시간 측정에서 finish 타임을 찍기 위해 ProceedingJoinPoint을 통해 메소드의 실행을 해야만 한다.

 

즉 정리하자면 메소드 호출을 인터셉트해서 타이머를 시작하고,

proceed()를 통해 메소드를 실행하고 실행 결과를 받은 후 타이머를 멈춘다.

그 후 reutrn 하는 값은 대상 메소드의 반환값을 반환해 주는 것이다.

따라서 모든 메소드의 반환 값을 반환하는 것이므로 최상위 Java 객체인 Object로 해준다.

출력 결과를 보면 다음과 같다.

위의 코드에서 MemberService에서 MemoryMemberRepository 객체를 DI를 통해

Bean으로 등록한 것이 아닌 멤버변수로 생성하여 findAll() 메소드를 호출하고 있다.

이때 AOP의 시간 추적 로직에서

MemoryMemberRepository의 findAll() 메소드에 대해 AOP가 적용되지 않은 것을 볼 수 있다.

MemoryMemberRepository가 Spring Bean으로 등록이 안되어있기 때문이다.

따라서 MemoryMemberRepository를 @Repository를 통해

Spring Bean으로 등록해 주고 결과를 보면 아래와 같이 3개의 시간 추적 결과가 나오는 것을 볼 수 있다.

이처럼 Spring AOP는 Spring Bean에만 적용되는 것을 볼 수 있다.

 

따라서 AOP를 통해 회원가입, 회원 조회 등 핵심 관심사항과 시간 측정 공통 관심 사항을 분리하고,

핵심 관심 사항만을 유지 및 유지보수 용이할 뿐만 아니라 원하는 적용대상만을 선택 가능하다.

 

 

 

AOP Proxy

AOP 핵심 용어 Proxy에 대해 설명하면 다음과 같다.

Spring은 AOP 포인트컷에 등록된 모든 Bean 객체에 대해 Proxy Bean 객체를 생성한다.

Spring boot 시작 후 의존성 주입 단계에서 Proxy 객체가 생성된다.

AOP의 대상이 되는 Spring Bean 객체에 대해 프록시 객체가 생성되고,

의존관계도 프록시 객체를 통하여 이루어지는 것이다.


각각 객체의 기능들을 수행하려 메서드를 호출하면 

요청을 프록시 객체가 전달받아서 전처리/후처리 등 추가적인 작업을 수행 후
실제 객체(Real Subject)의 로직을 수행하게 된다.

즉 MainContoller에서는 Proxy Bean 객체인 Proxy MainController 객체를 먼저 호출하게 되고,

Proxy Bean 객체의 전처리/후처리 작업이 완료된 후에 실제 객체 로직을 수행하게 된다.

이에 대한 다이어그램을 보면 다음과 같다.

 

여기까지 설명만 보면 AOP에서는 유지보수에 대한 문제점을 떠올릴 수 있다.

하지만 여기서 @Aspect를 붙인 AOP Class에서 @After,@Around 등 모든 어노테이션마다

포인트컷을 지정한다면 유지보수도 힘들고, 중복되는 부분이 많이 늘어난다.

따라서 공용포인트컷 및 여러 가지 다른 방법을 통해 포인트컷을 지정할 수 있다.

 

 

 

AOP 공용 포인트컷

@Pointcut을 통해 공용 포인트컷 지정

public class CommonPointcutConfig {

    @Pointcut("execution(* com.example.demo..*(..))")
    public void businessPackageConfig(){

    }
}
@Aspect
@Component
public class TimeTraceAop {

    @Around("com.example.demo.aop.CommonPointcutConfig.businessPackageConfig()")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("Start : " + joinPoint.toString());
        try{
            return joinPoint.proceed();
        }finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("End : " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

 

Bean 지시자 사용

public class CommonPointcutConfig {

    @Pointcut("execution(* com.example.demo..*(..))")
    public void businessPackageConfig(){}

    @Pointcut("bean(*Service*)")
    public void serviceLayeredConfig(){}
}
@Around("com.example.demo.aop.CommonPointcutConfig.serviceLayeredConfig()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    System.out.println("Start : " + joinPoint.toString());
    try{
        return joinPoint.proceed();
    }finally {
        long finish = System.currentTimeMillis();
        long timeMs = finish - start;
        System.out.println("End : " + joinPoint.toString() + " " + timeMs + "ms");
    }
}

괄호 안에는 Bean으로 인터셉트할 이름을 명시해야 한다.

위의 코드에서는 Service라는 단어가 포함된 Bean이라면 인터셉트 대상이 된다.

따라서 MemberService만이 시간 측정 결과가 나온 것을 볼 수 있다.

이 값을 기준으로 메소드 호출을 인터셉트할 수 있다.

 

Custom Annotation을 생성하여 AOP 적용

@Pointcut("@annotation(com.example.demo.annotation.TrackTime)")
public void annotationPackageConfig(){}


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackTime { }


@Around("com.example.demo.aop.CommonPointcutConfig.annotationPackageConfig()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    System.out.println("Start : " + joinPoint.toString());
    try{
        return joinPoint.proceed();
    }finally {
        long finish = System.currentTimeMillis();
        long timeMs = finish - start;
        System.out.println("End : " + joinPoint.toString() + " " + timeMs + "ms");
    }
}

@GetMapping("/test")
@ResponseBody
@TrackTime
public List<Member> testMethod(){
    return memberService.findMembers();
}

커스텀 어노테이션을 생성하고,

적용하고 싶은 메소드에 생성한 어노테이션을 붙여준다면

다음과 같이 해당 메소드에서만 AOP가 적용되는 것을 볼 수 있다.

 

 

참고자료

https://catsbi.oopy.io/fb62f86a-44d2-48e7-bb9d-8b937577c86c

https://velog.io/@kai6666/Spring-Spring-AOP-%EA%B0%9C%EB%85%90

 

 

 

 

반응형

loading