on
스프링 AOP 총정리 : 개념, 프록시 기반 AOP, @AOP
스프링 프레임워크 핵심기술을 공부하고 정리하는 포스트입니다.
스프링 AOP
AOP란 Aspect-oriented Programming의 약어로 스프링 AOP는 AOP의 구현체를 제공하며, 자바에 만들어져 있는 프레임워크 AspectJ1라는 또 다른 구현체와 연동해서 사용할 수 있는 기능을 제공한다. 이러한 기능을 기반으로 스프링 트랜잭션과 같은 다른 기능이 적용되고 있다.
그렇다면 AOP는 정확히 무엇일까? AOP, 관점 지향 프로그래밍은 흩어진 Aspect를 모듈화 할 수 있는 프로그래밍 기법을 말한다. 관점 지향은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어 본다는 말이고 따라서 관점을 기준으로 각각 모듈화하는 프로그래밍 기법인 것이다. OOP(Object-oriented Programming, 객체 지향 프로그래밍)과 나란히 하는, 서로 보완관계에 있는 기술이다.
AOP에 대한 내용을 그림을 보면서 알아보자.
각각의 색들은 Concern이라고 보면된다. 여기서 Concern이란 여러 클래스, 메서드에 걸쳐서 나타나는 비슷한 코드들을 의미한다.
각 클래스에 있는 Crosscutting Concerns, 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.
AOP 주요 개념
▶ Aspect
흩어진 관심사(Crosscutting Concerns)를 묶어서 모듈화 한 것. 하나의 모듈. Advice와 Point Cut이 들어간다.
▶ Target
Aspect가 가지고 있는 Advice가 적용되는 대상(클래스, 메서드 등등)을 말한다.
▶ Advice
어떤 일을 해야할 지에 대한 것. 해야할 일들에 대한 정보를 가지고 있다.
▶ Join Point
가장 흔한 Join Point는 메서드 실행 시점이다. Advice가 적용될 위치, 끼어들 수 있는 지점. 생성자 호출 직전, 생성자 호출 시, 필드에 접근하기 전, 필드에서 값을 가져갔을 때 등등.
▶ Point Cut
Join Point의 상세한 스펙을 정의한 것. 어디에 적용해야 하는지에 대한 정보를 가지고 있다. “A 클래스에 B 메서드를 적용할 때 호출을 해라.”와 같은 구체적인 정보를 준다.
AOP 구현체
▶ 자바
- AspectJ
- 스프링 AOP
▶ 그 외
- 위키피디아 참고
AOP 적용 방법
▶ 컴파일 타임에 적용
컴파일 시점(.java 파일을 .class 파일로 만들 때)에 바이트 코드를 조작하여 조작된(AOP가 적용된) 바이트코드를 생성.
▶ 로드 타임에 적용
순수하게 컴파일한 뒤, 클래스를 로딩하는 시점에 클래스 정보를 변경(Load Time Weaving, 로드타임 위빙).
▶ 런타임에 적용
스프링 AOP가 사용하는 방법. A 클래스 타입의 Bean을 만들 때 A 타입의 Proxy Bean2을 만들어 Proxy Bean이 Aspect 코드를 추가하여 동작.
프록시 기반 AOP
스프링 AOP 특징
스프링 AOP의 특징에 대해서 알아보자.
- 프록시 기반의 AOP 구현체이다. 프록시 객체를 사용하는 이유는 접근 제어 및 부가 기능을 추가하기 위해서이다.
- 스프링 빈에만 AOP를 적용할 수 있다.
- 모든 AOP 기능을 제공하는 것이 목적이 아니라, 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체 간 관계 복잡도 증가 등등…)를 해결하기 위한 솔루션을 제공하는 것이 목적.
프록시 패턴
프록시 패턴에는 interface가 존재하고 Client는 이 interface 타입으로 Proxy 객체를 사용하게 된다.
Proxy 객체는 기존의 타겟 객체(Real Subject)를 참조하고 있다. Proxy 객체와 Real Subject의 타입은 같고, Proxy는 원래 해야 할 일을 가지고 있는 Real Subject를 감싸서 Client의 요청을 처리한다.
왜? 이렇게 해서 패턴을 사용하는 걸까?
그 이유는 기존 코드의 변경 없이 접근 제어 또는 부가 기능 추가를 위해서다.
▶ AppRunner.java
package me.gracenam.demospring51;
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();
}
}
▶ EventService.java
package me.gracenam.demospring51;
public interface EventService {
void createEvent();
void publishEvent();
}
▶ SimpleEventService.java
package me.gracenam.demospring51;
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");
}
}
AppRunner는 Client, EventService는 Subject, SimpleEventService는 Real Subject라고 할 수 있다.
EventService 인터페이스를 상속받은 SimpleEventService에 실질적인 동작 코드가 작성되어 있고 AppRunner를 통해서 실행이 된다.
이제 Proxy로 SimpleEventService(Real Subject)와 AppRunner(Client)를 건들이지 않고 성능을 테스트할 수 있는 기능을 추가해보자.
package me.gracenam.demospring51;
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);
}
public void deleteEvent() {
System.out.println("Delete an event");
}
}
package me.gracenam.demospring51;
public interface EventService {
void createEvent();
void publishEvent();
void deleteEvent();
}
package me.gracenam.demospring51;
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();
eventService.deleteEvent();
}
}
먼저, 프로그램의 성능을 테스트 하는 코드를 추가했다. 시작 시간과 종료 시간을 구해서 동작하는데 걸리는 시간을 출력하도록 했는데, 두 개만 측정하고 하나는 측정하지 않도록 했다. 위 코드처럼 작성할 경우에는 기존의 코드를 건드리게 된다.
기존의 코드를 건드리지 않고 하는 방법이 바로 프록시 패턴을 사용하는 것이다.
package me.gracenam.demospring51;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
@Primary
@Service
public class ProxySEService 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() {
simpleEventService.deleteEvent();
}
}
@Primary
는 같은 타입의 빈이 여러가지일 때 그 중 하나를 선택하여 사용하는 애노테이션이다.
@Autowired
는 이론적으로 인터페이스 타입의 빈을 받는 것이 추천된다. 하지만 여기서 Proxy의 경우 Real Subject의 빈을 주입받아서 사용해야하기 때문에 해당 타입인 SimpleEventService를 명시해주면 주입받아 사용할 수 있다. 또는 EventService 타입을 받지만 빈의 이름(simpleEventService)에 기반해서 주입받아도 상관은 없다.
성능을 테스트하고 싶은 두 개에만 코드를 추가했다. 이렇게 코드를 완성하면 Proxy가 Real Subject를 가지고 있고, Real Subject에 일을 위임해서 대신 처리하고, 부가적인 기능들은 가지고 있다.
Client는 @Autowired
로 EventService를 주입받지만 @Primary
로 등록된 빈을 가져다 쓰게 될 것이다.
실행해보면 마찬가지로 실행된 시간이 찍히는 것을 확인할 수 있다.
이렇게 Proxy를 사용해서 만들면 원래의 Client와 Real Subject의 코드를 건드리지 않고 부가기능을 추가할 수 있었다. 하지만 Proxy에 중복코드가 생기고, Proxy 클래스를 만드는데 생기는 비용과 수고가 발생한다는 문제가 있다.
만일 Proxy를 여러 클래스, 여러 메소드에 적용시켜야 한다고 생각해보자. 매번 프록시 클래스를 작성해야하고, 또 작성한 프록시 클래스 내에서 중복이 발생할 것이다.
위 예제에서는 Proxy를 클래스로 만들어서 사용했지만, 동적으로 Proxy 객체를 만드는 방법이 있다. 여기서 동적이란, 런타임, 즉 애플리케이션이 동작하는 중에 동적으로 어떤 객체의 Proxy 객체를 만드는 것을 말한다.
스프링 IoC 컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용해서 여러 복잡한 문제(중복 코드, 매번 Proxy를 생성 등등)를 해결할 수 있다. 이것이 바로 Spring AOP이다.
@AOP
스프링 애노테이션 기반의 AOP를 살펴보자.
먼저, @AOP
를 사용하기 위해서 의존성을 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
이제 Aspect를 나타내는 클래스를 생성한다.
package me.gracenam.demospring51;
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(* me.gracenam..*.EventService.*(..))")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
@Aspect
애노테이션으로 이 클래스가 Aspect 클래스임을 알려준다. @Component
애노테이션을 사용해서 빈으로 등록하는데 이는 애노테이션 기반의 스프링 IoC를 사용하기 때문에 Component Scan을 통해서 빈 등록을 하기 때문이다.
두 가지 정보가 필요한데 해야할 일과 어디에 적용할 것인가이다. 해야할 일은 Advice, 어디에 적용할 것인가는 Point Cut에 해당하고 이 두 가지를 정의해야 한다.
ProceedingJoinPoint
, PJP는 Advice가 적용되는 대상이다. 즉, Advice가 적용되는 createEvent
, publishEvent
와 같은 메서드 자체라고 보면 된다. method invocation과 비슷한 개념이다.
시간을 측정하고 걸린 시간을 출력해주는 기능을 추가하면 Advice가 완성이 된다. 완성된 Advice는 Around Advice라고 해서 @Around
애노테이션을 붙여준다. 그리고 value에 Point Cut 이름을 주거나 직접 정의할 수도 있다.
execution
은 Point Cut 표현식인데 이 표현식을 사용해서 어디에 적용할 지를 정의할 수 있다. * me.gracenam..*.EventService.*(..)
는 me.gracenam 패키지 밑에 있는 모든 클래스 중 EventService에 있는 모든 메소드에 Advice를 적용하라고 정의한 것이다. 만일 EventService
를 *
로 변경하면 여러 클래스에 적용할 수 있다.
하지만 이렇게 할 경우에는 적용하고 싶지 않은 메서드에도 적용이 될 수 있다. 그럴 땐 execution을 사용하는 대신 애노테이션으로 적용하면 원하는 곳에만 적용할 수 있다.
package me.gracenam.demospring51;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerfLogging {
}
package me.gracenam.demospring51;
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(PerfLogging)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
package me.gracenam.demospring51;
import org.springframework.stereotype.Service;
@Service
public class SimpleEventService implements EventService {
@PerfLogging
@Override
public void createEvent() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Created an event");
}
@PerfLogging
@Override
public void publishEvent() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Published an event");
}
public void deleteEvent() {
System.out.println("Delete an event");
}
}
애노테이션을 만들 때 한 가지 주의해야 할 점은 RetentionPolicy
를 Class 이상으로 주어야 한다. 이 RetentionPolicy
라는 것은 기본 값이 Class인데 이 애노테이션 정보를 얼마나 유지할 것인가를 말하는 것이다. 즉, 기본 값이 Class라는 것은 ‘.class 파일까지 유지하겠다’라는 말이다.
Aspect에서는 execution
대신 @annotation
이라는 표현식으로 PerfLogging이라는 애노테이션이 달린 곳에 적용되도록 지정했다.
그 외
예제에서는 @Around를 통해서 타겟 메서드의 Aspect 실행 시점을 지정했지만 다른 어노테이션들도 있다.
- @Before (이전) : 어드바이스 타겟 메소드가 호출되기 전에 어드바이스 기능을 수행
- @After (이후) : 타겟 메소드의 결과에 관계없이(즉 성공, 예외 관계없이) 타겟 메소드가 완료 되면 어드바이스 기능을 수행
- @AfterReturning (정상적 반환 이후)타겟 메소드가 성공적으로 결과값을 반환 후에 어드바이스 기능을 수행
- @AfterThrowing (예외 발생 이후) : 타겟 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
- @Around (메소드 실행 전후) : 어드바이스가 타겟 메소드를 감싸서 타겟 메소드 호출전과 후에 어드바이스 기능을 수행
Reference
Comments
SPRING 의 다른 글
-
스프링 타입 컨터버 24 Jun 2022
-
API 예외 처리 17 Jun 2022
-
예외 처리와 오류 페이지 12 Jun 2022
-
로그인 처리 - 인터셉터 08 Jun 2022
-
로그인 처리 - 필터 06 Jun 2022
-
로그인 처리 - 쿠키, 세션 31 May 2022
-
Bean Validation 22 May 2022
-
검증 22 May 2022
-
메시지, 국제화 21 May 2022
-
타임리프 - 스프링 통합과 폼 19 May 2022
-
타임리프 - 기본 기능 10 May 2022
-
스프링 MVC 기본 기능 - 웹 페이지 만들기 02 May 2022
-
스프링 MVC 기본 기능 - HTTP 응답 30 Apr 2022
-
스프링 MVC 기본 기능 - HTTP 요청 24 Apr 2022
-
스프링 MVC 기본 기능 - 요청 매핑 19 Apr 2022
-
스프링 MVC 기본 기능 19 Apr 2022
-
스프링 MVC 구조 이해 14 Apr 2022
-
MVC 프레임워크 만들기 - V4, V5 12 Apr 2022
-
MVC 프레임워크 만들기 - V1, V2, V3 09 Apr 2022
-
서블릿, JSP, MVC 패턴 05 Apr 2022
-
서블릿 29 Mar 2022
-
웹 애플리케이션 이해 24 Mar 2022
-
스프링 웹 계층이란? 05 Nov 2021
-
스프링 시큐리티 공식문서 번역 27 Sep 2021
-
스프링 AOP 총정리 : 개념, 프록시 기반 AOP, @AOP 27 Apr 2021
-
SpEL (스프링 Expression Language) 25 Apr 2021
-
데이터 바인딩 추상화 : Converter와 Formatter 21 Apr 2021
-
데이터 바인딩 추상화 : PropertyEditor 12 Apr 2021
-
Validation 추상화 10 Apr 2021
-
Resource 추상화 08 Apr 2021
-
IoC 컨테이너 9부 07 Apr 2021
-
IoC 컨테이너 8부 06 Apr 2021
-
IoC 컨테이너 7부 02 Apr 2021
-
IoC 컨테이너 6부 29 Mar 2021
-
IoC 컨테이너 5부 27 Mar 2021
-
IoC 컨테이너 4부 23 Mar 2021
-
IoC 컨테이너 3부 20 Mar 2021
-
IoC 컨테이너 2부 18 Mar 2021
-
IoC 컨테이너 1부 12 Mar 2021
-
스프링 PSA 07 Jan 2021
-
스프링 @AOP 실습 07 Jan 2021
-
프록시 패턴 06 Jan 2021
-
스프링 AOP 04 Jan 2021
-
의존성 주입(Dependency Injection) 04 Jan 2021
-
스프링 빈(Bean) 02 Jan 2021
-
스프링 IoC 컨테이너 01 Jan 2021
-
스프링 IoC 01 Jan 2021