Server/Spring

[Spring] 스프링 AOP 개념, 프록시 기반 AOP, @AOP 정리

백엔드 규니 2020. 7. 24. 15:20
728x90
반응형

스프링 AOP (Aspect Oriented Programming)란?

AOP Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 그리고 흩어진 Aspect를 모듈화 할 수 있는 프로그래밍 기법이다. 

 

출처 : https://engkimbs.tistory.com/746?category=767795

위의 A, B, C 클래스에서 동일한 색깔의 선들의 의미는 클래스들에 나타나는 비슷한(중복되는) 메소드, 필드, 코드들이 나타난다는 것이다. 이러한 경우 만약 클래스 A에 주황색 부분을 수정해야 한다면 B, C 클래스들에 주황색 부분에 해당하는 곳을 찾아가 전부 코드를 수정해야 한다. (유지보수가 쉽지 않다) 이런식으로 반복되는 코드를 흩어진 관심사 (Crosscutting Concerns)라 부른다.

 

이러한 문제를 AOP는 Aspect를 이용해서 해결한다. 사진 아래쪽을 보면 알 수 있는데, 흩어져 있는 부분들을 Aspect를 이용해서 모듈화 시킨다. (모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.) 그리고 개발자가 모듈화 시킨 Aspect를 사진에서 위에 클래스에 어느 곳에 사용해야 하는지만 정의해주면 된다. 

결론적으로 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

 

 

AOP 주요 개념

  • Aspect : 위의 사진에서 처럼 Aspect 안에 모듈화 시킨 것을 의미한다.
  • Advice : 실질적으로 어떤 일을 해야하는지를 담고 있다.
  • Pointcut : 어디에 적용해야 하는지에 대한 정보를 담고 있다.
  • Target : Aspect에 적용이 되는 대상
  • Join point : Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능(여러가지 합류 지점임)

 

 

AOP 구현체

  • 자바
    • AspectJ
    • Spring AOP

 

 

AOP 적용 방법

  • 컴파일 : 자바 파일을 클래스 파일로 만들 때 바이트코드를 조작하여 적용된 바이트코드를 생성
  • 로드 타임 : 컴파일은 원래 클래스 그대로 하고, 클래스를 로딩하는 시점에 끼워서 넣는다.
  • 런타임 : A라는 클래스를 빈으로 만들 때 A라는 타입의 프록시 빈을 감싸서 만든 후에, 프록시 빈이 클래스 중간에 코드를 추가해서 넣는다.

 

 

스프링 AOP 특징

  • 프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서임
  • 스프링 빈에만 AOP를 적용할 수 있다
  • 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가 ...)에 대한 해결책을 지원하는 것이 목적

 

프록시 패턴

 

Client는 Subject 인터페이스 타입으로 프록시 객체를 사용하게 된다. 프록시는 Real Subject를 감싸서 클라이언트의 요청을 처리하게 된다. 프록시 패턴의 목적은 기존 코드 변경 없이 접근 제어 또는 부가 기능을 추가하기 위해서이다.

public interface EventService {

    void createEvent();
    void publishEvent();
    void deleteEvent();
}

위의 사진에서 Subject 인터페이스 역할이다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired EventService eventService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        eventService.createEvent();
        eventService.publishEvent();
    }
}

위의 코드는 사진에서 Client의 역할이다. 

import org.springframework.stereotype.Service;

@Service
public class SimpleEventService implements EventService {

    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Published an event");
    }
    
    @Override
    public void deleteEvent() {
        
    }
}

위의 코드는 Real Subject라고 생각하면 된다. 이제 Client와 Real Subject 코드를 수정하지 않고 기능을 추가해볼 것이다. 그리고 그 전에 위에서 말했던 중복 되는 코드가 존재하기 때문에 그것을 모듈화 시킨다고 하였다. 그것의 예시를 간단하게 봐보자.

import org.springframework.stereotype.Service;

@Service
public class SimpleEventService implements EventService {

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Published an event");
        System.out.println(System.currentTimeMillis() - begin);
    }
    
    @Override
    public void deleteEvent() {
        
    }
}

위와 같이 시간을 측정하는 코드가 있다고 할 때 메소드 2개에서 중복이 일어난다. 따라서 이러한 경우에서 이렇게 위의 클래스에다 코드를 수정하지 않고 AOP 방법으로 기능을 추가해보는 것을 지금 정리해보려 한다.

 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Primary
@Service
public class ProxySimpleEventService implements EventService {

    @Autowired
    SimpleEventService simpleEventService;

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();
        simpleEventService.createEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();
        simpleEventService.publishEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }
    
    @Override
    public void deleteEvent() {
        
    }
}

이처럼 Proxy를 코드로 구현하였다. Proxy가 Real Subject에 해당하는 SimpleEventService를 가지고 있고 시간을 측정하는 기능도 대신 가지고 있다. 이렇게 하면 Real Subject와 Client의 코드를 수정하지 않아도 기능을 추가 할 수 있다. 하지만 문제는 Proxy 클래스에서도 중복코드가 발생한다는 점과 Proxy 클래스를 만들어야 하는 비용이 발생한다는 점이다.

 

그래서 동적으로 Proxy를 만드는 방법이 있다. 스프링 IoC 컨테이너가 제공하는 기반 시설과 Dynamic Proxy를 혼합해서 사용하여 중복코드가 발생하는 점, Proxy를 매번 만들어야 하는 단점을 해결해준다. 그것이 Spring AOP 이다. 이런게 있구나 정도만 받아들이면 될 것 같다. 

 

 

어노테이션 기반의 스프링 @AOP

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

스프링 AOP 의존성을 pom.xml에 추가해주자. 그리고 이제 Aspect를 만들어 볼 것인데 필요한 것이 2가지가 있다. Pointcutadvice가 필요하다. 

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class PerfAspect {

    @Around("execution(* com.example..*.EventService.*(..))")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object reVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return reVal;
    }
}

위의 코드는 Aspect를 구현한 클래스이다. logPerf 메소드는 advice의 역할로 어떤 일을 해야 할지를 담고 있다. 그리고 @Around()은 Aspect의 실행 시점을 지정할 수 있는 어노테이션을 이용해서 적용할 범위를 지정해준다. 코드를 보면 "com.example 패키지 밑에 있는 모든 클래스에 적용을 하고, EventService 밑에 있는 모든 메소드에 적용해라" 라는 의미이다. 하지만 우리는 deleteEvent() 메소드의 성능 측정은 하고 싶지 않다면 어떻게 해야할까?

 

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerLogging {

}

위와 같이 Annotaion으로 하나 만들어 보자.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class PerfAspect {

    @Around("@annotation(PerLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object reVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return reVal;
    }
}

그리고 @Arount() 어노테이션에 위와 같이 수정을 하게 되면 위에서 만든 어노테이션 파일과 연결이 된다.

import org.springframework.stereotype.Service;

@Service
public class SimpleEventService implements EventService {

    @PerLogging
    @Override
    public void createEvent() {
        System.out.println("Created an event");
    }

    @PerLogging
    @Override
    public void publishEvent() {
        System.out.println("Published an event");
    }

    @Override
    public void deleteEvent() {
        System.out.println("deleteEvent");
    }
}

그리고 성능을 측정하고자 하는 메소드에 @perLogging 어노테이션을 추가해주면 해당 메소드가 실행 될 때 성능 측정도 출력이 된다.

 

 

이 밖에도 @Around 외에 타겟 메서드의 Aspect 실행 시점을 지정할 수 있는 어노테이션이 있다. 

  • @Before (이전) : 어드바이스 타켓 메소드가 호출되기 전에 어드바이스 기능을 수행
  • @After (이후) : 타켓 메소드의 결과에 관게없이 (즉 성공, 예외 관계없이) 타겟 메소드가 완료 되면 어드바이스 기능을 수행
  • @AfterReturning (정상적 반환 이후) : 타켓 메소드가 성공적으로 결과값을 반환 후에 어드바이스 기능을 수행
  • @AfterThrowing (예외 발생 이후) : 타켓 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
  • @Around (메소드 실행 전후) : 어드바이스가 타겟 메소드를 감싸서 타켓 메소드 호출전과 후에 어드바이스 기능을 수행



 

 

Reference

https://www.inflearn.com/course/spring-framework_core/dashboard

https://engkimbs.tistory.com/746?category=767795

 

[Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP

| 스프링 AOP ( Aspect Oriented Programming ) AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으..

engkimbs.tistory.com

반응형