본문 바로가기

Spring&IntelliJ

Spring AOP와 관련 어노테이션

출처: https://pamyferret.tistory.com/51

 

개발을 하다보면 특정 패턴을 가진 메소드에 대해서 공통적으로 적용해줘야하는 기능이 있다.

 

제일 대표적인게 바로 로그 기록이 있는데, 이와 같은 기능을 특정 패턴을 가진 메소드에 넣기 위해서는 로그를 기록하는 공통 Util 메소드를 생성하고 해당 Util 메소드를 특정 패턴을 가진 메소드에 일일이 넣는 것을 생각할 수 있다.

하지만 이렇게 수동으로 공통 기능을 넣어줄 경우 추후 코드를 수정할 곳이 많아진다거나 어디에 Util 메소드를 넣었는지 찾기가 힘들어 유지보수가 힘들어진다.

 

위와 같이 특정 패턴을 가진 메소드에 공통되는 위치(실행 전, 실행 후)에 있는 공통되는 기능을 횡단 관심사(공통 관심사)라고 한다.

그리고 이러한 횡단 관심사를 수동으로 구현할 필요 없이 알아서 지정한 패턴에 넣어주는 방법으로는 Spring AOP와 AspectJ가 있는데,

이 글에서는 AspectJ 기능을 간단하게 구현할 수 있도록 스프링에서 제공하는 Spring AOP를 설명해보겠다.

AOP(관점지향 프로그래밍)란? 공통관심사와 핵심 관심사를 분리하는 것.

Spring AOP는 공통 관심사를 편리하게 구현하도록 어노테이션, 라이브러리등을 제공함.

 

Spring AOP란?

AspectJ는 따로 AspectJ 컴파일러에 사용할 파일을 작성해야하는(java 파일등과 별도로) 번거로움이 있다.

이러한 번거로움을 줄이고자 AspectJ의 모든 기능은 아니지만 횡단 관심사를 구현하는데 필요한 AspectJ의 기능들을 포함해 간편하게 구현하도록 스프링에서 기능을 제공하는데 Spring AOP이다.

Spring AOP ⊃ AspectJ

 

aop에서는 횡단 관심사와 같은 여러 가지 용어들이 있는데 그것까지 정리하면 글이 너무 길어지므로 용어들은 추후 따로 정리해야겠다.

 

 

spring AOP 구현에 필요한 의존성 설정

 

* gradle

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

 

* maven

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

 

 

spring AOP 구현

의존성 설정을 했다면 spring AOP를 사용 가능하다.

횡단 관심사를 구현해놓을 클래스에 @Aspect 어노테이션을 붙여주면 스프링 프레임워크에서 aop를 구현해놓은 것이구나~라고 인지를 하고 aop 구현을 위한 proxy 생성 등을 자동으로 해준다.

@Aspect
@Component
public class TestInerceptor {
	...
}

 

aop를 구현할 것들을 모아놓은 클래스라는 것을 @Aspect 어노테이션으로 표시를 했다면,

이제 그 안에 횡단 관심사를 구현해주면 된다.

이 때 그 횡단 관심사가 어떤 패턴의 위치에 구현되어야하는지에 대한 표시를 아래 어노테이션들이 해준다. 이 때 횡단 관심사들은 메소드를 기준으로 붙는다.

 

 

aop 어노테이션

@Before("${pattern}")

말 그대로 지정한 패턴에 해당하는 메소드가 실행되기 전에, interceptor와 같이 동작하는 것을 의미한다.

이 어노테이션이 붙은 메소드의 반환 값은 void여야 한다.

 

@After("${pattern}")

지정한 패턴에 해당하는 메소드가 실행된 후에 동작한다.

이 어노테이션이 붙은 메소드의 반환 값은 void여야 한다.

 

@Around("${pattern}"}

지정된 패턴에 해당하는 메소드의 실행되기 전, 실행된 후 모두에서 동작한다.

이 어노테이션이 붙은 메소드의 반환 값은 Object여야 한다.(지정된 패턴에 해당하는 메소드의 실행 결과를 반환해야 하므로)

 

위의 어노테이션들 안에는 다 값으로 특정 패턴을 지정한 문자열이 들어간다.

이를 pointcut 표현식이라고 하는데 패턴을 표현하는 방법도 여러 가지가 있다.

 

pointcut 표현식

작성 방법은 여러 가지가 있지만 전부 다 와일드 카드를 이용해서 작성하는 것으로 각 기호가 뜻하는 것은 아래와 같다.

  • *: 모든 것
  • ..: 0개 이상

 

위의 기호를 이용하면 아래와 같은 작성이 가능하다.

*Controller(..): Controller로 끝나는 모든 메서드 중 인자가 0개 이상인 모든 것

 

execution

제일 기본적인 방법으로 특정 메서드를 지정하는 패턴을 작성할 수 있는 방법이다.

아래와 같은 형태로 작성되며 특정 메소드까지의 패턴을 딱 지정하는 표현식이다.

참고로 아래에 나올 다른 표현식들은 일부를 제외하고 execution으로 대체가 가능하다.

 

execution([접근제어자] 반환타입 패키지.패키지.패키지.패키지.클래스.메소드(인자))

@Around("execution(public * com.pamyferret.test.controller.*.*(..)")

 

within

특정 클래스 안에 있는 메서드들 모두를 지정하는 패턴을 작성할 수 있는 방법이다.

패키지명까지가 아니라 클래스명까지 지정할 수 있는 것을 유의해야한다.

 

within(패키지.패키지.패키지.패키지.클래스)

@Around("within(com.pamyferret.controller.*)")

 

@within

특정 어노테이션 타입을 갖는 객체에 대해 aop를 지정하기 위해서 사용한다.

여기서는 경로가 아니라 aop를 적용할 메서드가 있는 클래스 타입을 지정한다.

단, 그 어노테이션의 retention은 CLASS여야 한다.

 

@within(어노테이션 타입)

@within(org.springframework.stereotype.Controller)

 

this, target

둘 다 특정 클래스를 상속 받는 하위객체를 지정하는 표현식이다.

하지만 둘은 하위객체가 어떻게 생성되는지에 따라 사용되는 것이 다르다. this는 CGLB 기반 프록시로 생성되는 객체를 지정할 때 사용하는 것이고 target는 JDK 기반 프록시를 사용하는 것에 붙인다.

 

예를 들어 아래와 같이 TestImpl이라는 인터페이스가 있고 TestImpl을 상속받은 하위 객체들에 대해 aop를 적용하고 싶으면 아래와 같이 사용하면 된다.

 

this(상위 객체 타입) / target(상위 객체 타입)

this(com.pamyferret.impl.TestImpl)
target(com.pamyferret.impl.TestImpl)

 

@target

특정 타입의 어노테이션이 붙어있는 객체를 지정할 수 있는 점에서 @within이랑 똑같이 동작한다.

실제 이 둘의 차이점은 잘 느껴지지 않는다고 하는데, @target은 런타임 때 객체가 일치하는지 확인하므로 해당 어노테이션의 retention이 Runtime이어야 한다. 

 

@target(어노테이션 타입)

@target(org.springframework.stereotype.Repository)

 

 

@annotation

특정 어노테이션이 붙은 객체에 대해 aop를 적용시킨다.

뭔가 @target과 @within보다 더 넓은 범위로 사용할 수 있는 것이라고 생각된다. 커스텀 어노테이션에 대해 aop를 적용할 떄 이 어노테이션을 자주 쓴다.

이@target, @within, @annotation 세 가지가 정확히 어떻게 차이가 나는지는 좀 찾아봐야겠다.

 

@annotation(어노테이션 타입)

@annotation(com.pemyferret.test.CumstomAnnotation)

 

 

횡단 관심사를 구현한 메소드에는 @Aspect와 위의 pointcut 표현식 어노테이션을 붙여서 어떤 메소드에 언제 구현한 횡단 관심사가 동작하게 될지 지정을 해야한다.

 

예를 들면 아래와 같이 횡단 관심사를 정의할 수 있다.

아래는 단순하게 Controller 어노테이션을 붙인 클래스 안의 메소드들이 실행되기 전에 현재 시간을 찍는 것을 구현한 것이다.

Around응 위에도 얘기했듯이 메소드가 실행되기 전과 실행된 후를 지정할 수 있는데,

인자로 받는 PreccedingJoinPoint(pointcut)의 proceed()를 실행시키는 것이 aop로 잡는 메소드를 실행시키는 것을 의미한다.

이 함수 return 값을 통해 메소드의 반환 값을 받아 aop에서 활용할 수 있다.

@Aspect
@Component
public class TestInerceptor {
	@Around("@within(org.springframework.stereotype.Controller)")
        public Object printCurrentTime(ProceedingJoinPoint pointcut) {
            System.out.println(new LocalDateTime(System.currentMillsecond()));
            
            return pointcut.preceed();
        }
}

 

 

이번에 직접 aop를 구현할 일이 있어서 어노테이션을 붙여 공통 관심사를 구현했지만 실제로 동작을 하지 않아서 애를 먹었었다.

하지만 찾아보니 삽질한게 허무할 정도로... 그저 pointcut 표현식이 잘못되서 적용되어야할 메소드를 제대로 찾지 못해서 생기는 일이었다. 이래서 제대로 알고 사용을 해야하는 것이다...😐😐