Spring AOP

진조던·2022년 4월 20일
0

Spring

목록 보기
2/3
post-thumbnail

1. Spring AOP란?



Aspect Oriented Programming의 약자로 관점지향 프로그래밍이다. IoC가 낮은 결합도와 관련된 것이라면, AOP는 높은 응집도와 관련되어 있다.
서비스들의 비즈니스 메소드들은 복잡한 코드로 구성되어있다. 하지만 이들 중 대부분은 로깅(logging), 트랜잭션 처리, 보안 등과 관련된 코드로 구성되어 있을 수 있다. 이럴 때 핵심 로직을 제외하고 꼭 필요하면서 모듈화 할 수 있는 부분을 분리하여 관리하는 것이다.

좀 더 쉽게 설명을하자면, 어플리케이션에서 관심사(기능)의 분리이자 핵심적인 기능에서 부가적인 기능을 분리하는 것이다. 분리한 부가 기능은 Aspect라는 독특한 모듈 형태로 만들어서 설계하고 개발하는 방법이라고 할 수 있다.

위의 A, B, C 클래스의 중복되는 메소드, 필드, 코드들이 나타나는 경우 Class A의 주황색 부분을 수정해야 할 경우, B와 C클래스의 주황색 부분을 찾아가 전부 코드를 수정해야 한다. 즉, SOLID 원칙도 위배하고 유지보수도 어려워지는데, 이러한 반복되는 코드를 흩어진 관심사(Crosscutting Concerns)라고 한다. 이렇게 흩어진 관심사를 AOP는 Aspect를 통해 해결한다. 위의 그림에서도 공통되는 부분을 Aspect X, Y, Z로 각각 모듈화한 것을 볼 수 있다. 결국 개발자는 모듈화시킨 Aspect를 어느 클래스(위 그림의 경우 Class A or B or C)에 사용하면 되는지 정의해주면 된다.

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

1-1. 스프링 AOP의 특징과 프록시 패턴


스프링 AOP를 공부하다보면 항상 나오는 실행 시간 출력 예제를 살펴보자.

public interface EventService {
	void createEvent();
    void publishEvent();
    void deleteEvent();
}
@Service
public class SimpleEventService implements EventService {
	@Override
    public void createEvent() {
    	long begin = System.currentTimeMills();
        try {
        	Thread.sleep(1000);
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
        System.out.println("Created an Event!");
        System.out.println("System.currentTimeMills() - begin);
    }
    
    @Override
    public void publishEvent() {
    	long begin = System.currentTimeMills();
        try {
        	Thread.sleep(2000);
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
        System.out.println("Published an Event!");
        System.out.println("System.currentTimeMills() - begin);
    }
    
    @Override
    public void deleteEvent(){}

위와 같이 EventService 인터페이스를 구현한 SimpleEventService 클래스가 있다. 우리는 createEvent()와 publishEvent() 메소드의 실행 시간을 측정하고자한다. 이 때 두 메소드에서는 중복되는 코드가 발생한다. 예시와 다르게 실제 프로젝트에서는 메소드의 수가 많기 때문에 더 많은 중복이 발생할 수 있다. 이 때 우리는 AOP를 사용한다. AOP는 프록시 패턴을 사용하여 기존 코드의 수정 없이 메소드들의 성능을 측정할 수 있다.

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

@Primary
@Service
public class ProxySimpleEventService implements EventService {
	@Autowired
    private SimpleEventService simpleEventservice;
    
    @Override
    public void createEvent() {
    	long begin = System.currentTimeMills();
        simpleEventService.createEvent();
        System.out.println(System.currentTimeMills() - begin);
    }
    
    @Override
    public void publishEvent() {
    	long begin = System.currentTimeMills();
        simpleEventService.publishEvent();
        System.out.println(System.currentTimeMills() - begin);
    }
    
    @Override
    public void deleteEvent() {}
}

우선 @Primary 어노테이션은 같은 타입의 빈이 여러개 존재할 때 우선순위를 높게 주는 어노테이션이다. 그리고 위의 Proxy 클래스의 코드를 보면 필드로 실제 핵심 코드를 담당하는 SimpleEventService를 가지고 있다. 이러한 구조를 프록시 패턴이라고 하는데, 기존의 SimpleEventService 클래스의 코드에는 성능을 측정하는 코드를 작성하지 않아도 된다는 장점이 생긴 것이다. 하지만 이런 방식도 메소드가 많다면 코드의 중복이 많이 일어날 것이고 매번 Proxy 클래스를 직접 만들어야하는 번거로움이 존재하기 때문에 Spring AOP를 사용한다.

스프링도 위에서 본 프록시를 이용하여 AOP를 구현하고 있다. AOP의 핵심 기능은 코드를 수정하지 않으면서 공통의 기능 구현을 추가하는 것이라고 강조한다. 핵심 기능에 공통 기능을 추가하는 방법에는 아래와 같이 3가지가 있다.

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

스프링에서 많이 사용하는 방식은 프록시를 이용하는 세번째 방법으로 스프링 AOP는 프록시 객체를 자동으로 만들어준다. 따라서 위에서 본 ProxySimpleEventService 클래스처럼 상위 타입의 인터페이스를 상속 받은 클래스를 직접 구현할 필요가 없다. 단지 공통 기능을 구현한 클래스만 잘 구현하면 된다.

maven 기준 아래와 같은 dependency를 추가하여 사용 가능하다. AspectJ도 있다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Component
@Aspect
public class PerfAspect {
      @Around("execution(* com.example..*.EventService.*(..))")
      public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
      long begin = System.currentTimeMills();
      Object reVal = pjp.proceed();
      System.out.println(System.currentTimeMills() - begin);
      return reVal;
    }
}

위에서 실행 시간을 측정하는 공통 코드를 Aspect 모듈로 분리시켜서 작성해보았다. @Around 어노테이션에서 execution을 사용하여 Advice를 적용할 범위를 지정할 때 사용할 수 있다. 즉, 위의 코드를 해석해보면 com.example 패키지 밑에 있는 모든 클래스에 적용을하고, EventService 밑에 있는 모든 메소드에 적용하라는 뜻이 된다.

2. AOP 용어


  • 조인포인트(JoinPoint): JoinPoint객체는 쉽게 말하면 각 컨트롤러에 정의된 메소드들의 매개변수(args)를 담고 있는 객체다.
    Advice가 적용될 위치, 끼어들 수 있는 시점, 메소드 진입 시점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용 가능.
    클라이언트가 호출하는 모든 비즈니스 메소드, 조인포인트 중에서 포인트컷되기 때문에 포인트컷의 후보로 생각할 수 있다.
  • 포인트컷(Pointcut): JoinPoint의 상세한 스펙을 정의한 것으로 'methodA()의 진입 시점에 호출!'과 같이 구체적으로 Advice가 실행될 지점을 정할 수 있다.
    특정 조건에 의해 필터링된 조인포인트이다. 수 많은 조인포인트 중에서 특정 메소드에서만 횡단 공통 기능을 수행시키기 위해서 사용한다.
    즉, 어디에 적용해야 하는지에 대한 정보를 담고 있다.
    - 표현식: 리턴타입 패키지경로 클래스명 메소드명(매개변수)
  • 어드바이스(Advice): 실질적으로 어떤 일을 해야할 지에 대한 것으로 실질적인 부가기능을 담은 구현체
    단 관심에 해당하는 공통 기능의 코드, 독립된 클래스의 메소드로 작성한다.

<어드바이스 동작시점>

실행시점설명
Before메소드 실행 전에 동작
After메소드 실행 후에 동작
After-returning메소드가 정상적으로 실행된 후에 동작
After-throwing예외가 발생한 후에 동작
Around메소드 호출 이전, 이후, 예외발생 등 모든 시점에서 동작
  • 위빙(Weaving): 포인트컷으로 지정한 핵심 관심 메소드가 호출될 때, 어드바이스에 해당하는 횡단 관심 메소드가 삽입되는 과정을 의미한다. 이를 통해 비즈니스 메소드를 수정하지 않고도 횡단 관심에 해당하는 기능을 추가하거나 변경이 가능해진다. 이를 통해 비즈니스 메소드를 수정하지 않고도 횡단 관심에 해당하는 기능을 추가하거나 변경이 가능해진다.
  • 애스팩트(Aspect): 포인트컷과 어드바이스의 결합이다. 어떤 포인트컷 메소드에 대해 어떤 어드바이스 메소드를 실행할지 결정한다.

3. 포인트컷 표현식


3-1. 지시자(PCD, AspectJ pointcut designators)의 종류

  1. execution: 가장 정교한 포인트 컷을 만들 수 있다. 리턴타입 패키지경로 클래스명 메소드명(매개변수)
  2. within: 타입패턴 내에 해당하는 모든 것들을 포인트컷.
  3. bean: bean이름으로 포인트컷

3-2. 리턴 타입 지정

표현식설명
*모든 리턴타입 허용
void리턴타입이 void인 메소드 선택
!void리턴타입이 void가 아닌 메소드 선택

3-3. 패키지 지정

표현식설명
com.jjd.domain정확하게 com.jjd.domain 패키지만 선택
com.jjd.domain..com.jjd.domain 패키지로 시작하는 모든 패키지 선택

3-4. 클래스 지정

표현식설명
UserService정확하게 UserService 클래스만 선택
*Service이름이 Service로 끝나는 클래스만 선택
SampleObject+클래스 이름 뒤에 '+'가 붙으면 해당 클래스로부터 파생된 모든 자식 클래스 선택
인터페이스 이름 뒤에 '+'가 붙으면 해당 인터페이스를 구현한 모든 클래스 선택

3-5. 메소드 지정

표현식설명
*(..)모든 메소드 선택
insert*(..)메소드명이 update로 시작하는 모든 메소드 선택

3-6. 매개변수 지정

표현식설명
(..)모든 매개변수
(*)반드시 1개의 매개변수를 가지는 메소드만 선택
(com.jjd.domain.model.User)매개변수로 User를 가지는 메소드만 선택
꼭 풀 패키지 명이 있어야함
(!com.jjd.domain.model.User)매개변수로 User를 가지지 않는 메소드만 선택
(Integer, ..)한 개 이상의 매개변수를 가지고
첫번째 매개변수 타입이 Integer인 메소드만 선택
(Integer, *)반드시 2개 이상의 매개변수를 가지고
첫번째 매개변수 타입이 Integer인 메소드만 선택

4. JoinPoint 인터페이스


어드바이스 메소드를 의미있게 구현하려면 클라이언트가 호출한 비즈니스 메소드의 정보가 필요하다. 예를 들면, 예외가 발생했는데 예외를 던진 메소드의 이름이 뭔지 등을 기록할 필요가 있을 수도 있다. 이럴 때 JoinPoint 인터페이스가 제공하는 유용한 API들이 있다.

메소드설명
Signature getSignature()클라이언트가 호출한 메소드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체 리턴
Object getTarget()클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체 리턴
Object[] getArgs()클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴
String getName()클라이언트가 호출한 메소드 이름 리턴
String toLongString()클라이언트가 호출한 메소드의 리턴타입, 이름, 매개변수(시그니처)를 패키지 경로까지 포함하여 리턴
String to ShortString()클라이언트가 호출한 메소드 시그니처를 축약한 문자열로 리턴

사용법

JoinPoint를 어드바이스 메소드 매개변수로 선언해야한다. 이 때 인자는 스프링 컨테이너가 넘겨준다. 예) method(JoinPoint jp)
이 때 Around 어드바이스와 약간 다른데, ProceedingJoinPoint 객체를 인자로 선언해야한다. proceed() 등이 추가로 구현되어있다. ProceedingJoinPoint는 JoinPoint를 상속받는다.

Reference


빨간색코딩
코딩하는 흑구
Gyun's 개발일지

0개의 댓글