해당 게시물은 백기선님의 스프링 프레임워크 핵심 기술 강좌를 정리한 내용 입니다.
1. 스프링 AOP : 개념 소개
AOP (Aspect-Oriented Programming : 관점 지향 프로그래밍)
- AOP는 OOP를 보완하는 수단으로, 핵심 기능에서 부가 기능을 분리하여 이 분리한 부가 기능을 Aspect라는 모듈 형태로
만드는 프로그래밍 기법을 말합니다.
※ 핵심 기능(Core Concerns) : 비즈니스 로직을 말합니다.
Ex) 게시글 관리, 회원 관리 등
부가 기능(Cross-cutting Concerns, 흩어진 관심사) : 핵심 기능을 도와주는 부가적인 기능을 말합니다.
Ex) 로깅, 보안, 트랜잭션 처리 등
AOP 관련 용어
① Aspect
- 부가 기능을 모듈화한 것을 말함
- Aspect는 Advice와 PointCut을 가지고 있다.
② Target
- 핵심 기능을 담고 있는 모듈로 Target은 부가 기능을 적용할 대상이 된다.
- Aspect가 적용되는 곳
③ Advice
- 실질적인 부가 기능을 담은 구현체 (해야할 일)
④ Join Point
- Advice가 적용될 수 있는 위치
- 메서드 실행 시점, 생성자 호출 시점 등이 JoinPoint에 해당한다. (여러 가지 끼어들 수 있는 지점을 의미)
⑤ PointCut
- Join Point의 부분집합으로 실제 Advice가 적용되는 JoinPoint를 나타낸다.
AOP 적용 방법
1) 컴파일 시점 (AspectJ)
- 자바 파일을 클래스 파일로 만들 때 부가 기능을 추가
2) 클래스 로딩 시점 (AspectJ)
- JVM이 클래스를 로딩하는 시점에 바이트 코드에 부가 기능을 추가
3) 런타임 시점 (Spring AOP)
- JVM이 클래스를 로딩 한 후 Bean을 생성할 때 해당 클래스 타입의 프록시 빈을 생성하여 적용
2. 스프링 AOP : 프록시 기반 AOP
스프링 AOP
- 스프링 AOP는 프록시 기반의 AOP 구현체이다.
- 스프링 빈(Bean)에만 AOP를 적용 할 수 있다.
프록시 패턴
- 프록시 패턴을 사용하는 이유는 접근 제어 또는 부가 기능을 추가하기 위해 사용된다.
- 프록시 패턴에서 클라이언트는 해당 인터페이스 타입으로 프록시 객체를 사용하게 된다.
그리고 프록시 객체는 타겟 객체(Real Subject)를 참조하고 있다.
프록시 객체가 원래 해야 할 일을 가지고 있는 타겟 객체를 감싸서 실제 클라이언트의 요청을 처리하게 된다.

프록시 패턴 예시
1) 앞서 살펴본 "그림"의 Subject에 해당하는 EventService 인터페이스를 작성한다.
public interface EventService {
void createEvent();
void publishEvent();
}
2) 앞서 살펴본 "그림"의 Real Subject에 해당하는 SimpleEventSerivce 클래스를 작성한다.
그리고 해당 클래스를 빈으로 등록한다. (@Service)
@Service
Public class SimpleEventSerivce implements EventService {
@Override
public void createEvent() {
try {
Thread.sleep(1000); // 1초 쉬고
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Created an event"); // 메시지 출력
}
@Override
public void publishEvent() {
try {
Thread.sleep(2000); // 2초 쉬고
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Published an event"); // 메시지 출력
}
}
3) 앞서 살펴본 "그림"의 Client에 해당하는 AppRunner 클래스를 작성한다.
해당 코드는 createEvent()를 호출하고 1초 후에 "Created an event” 메시지를 출력하고
publishEvent()를 호출하고 나서 2초 후에 "Published an event“ 메시지를 출력하게 된다.
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
EventService eventService; // 인터페이스가 있다면 인터페이스 타입으로 주입 받는 것이 좋다.
@Override
public void run(ApplicationArguments args) throws Exception {
eventService.createEvent();
eventService.publishEvent();
}
}
4) EventService 인터페이스에 deleteEvent()를 새롭게 추가하고 그림의 "Real Subject"에 해당하는 SimpleEventService 에
createEvent()와 publishEvent()의 실행 시간을 측정하는 기능을 추가한다.
그리고 AppRunner에는 deleteEvent()를 호출하는 코드를 추가한다.
public interface EventService {
void createEvent();
void publishEvent();
void deleteEvent();
}
public class SimpleEventSerivce 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");
}
}
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
EventService eventService;
@Override
public void run(ApplicationArguments args) throws Exception {
eventService.createEvent();
eventService.publishEvent();
eventService.deleteEvent();
}
}
5) 매번 측정 할 때 마다 기존의 코드에 성능을 측정하는 코드를 추가해주어야 한다.
기존의 코드를 건들지 않고 성능을 측정할 수는 없을까?
이러한 문제를 해결 하기 위해 Proxy 패턴을 적용한다.
다음과 같이 SimpleEventSerivce 클래스에서 성능을 측정하는 코드를 제거한다.
@Service
public class SimpleEventSerivce 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); // 2초 취고
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Published an event"); // 메시지 출력
}
public void deleteEvent(){
System.out.println("Delete an event");
}
}
6) 그 다음, Proxy 클래스를 작성하는데 SimpleEventService와 동일한 인터페이스를 구현 해야한다.
그리고 빈으로 등록한 다음, @Primary로 우선 순위를 가지는 빈으로 지정하여 해당 빈이 주입 되도록 한다.
프록시는 "그림"의 Real Subject에게 위임을 하여 일을 대신 처리 하도록 한 다음, 부가 기능(성능을 측정하는 코드)를 추가한다.
@Primary
@Service
public class ProxySimpleEventService implements EventService {
@Autowired
SimpleEventSerivce simpleEventSerivce; // Proxy는 Real Subject의 빈을 주입 받아 사용해야 한다.
@Override
public void createEvent() {
long begin = System.currentTimeMillis(); // 흩어진 관심사
simpleEventSerivce.createEvent(); // Proxy는 SimpleEventSerivce로 위임을 한다.
System.out.println(System.currentTimeMillis() - begin); // 흩어진 관심사
}
@Override
public void publishEvent() {
long begin = System.currentTimeMillis(); // 흩어진 관심사
simpleEventSerivce.publishEvent();
System.out.println(System.currentTimeMillis() - begin); // 흩어진 관심사
}
@Override
public void deleteEvent() {
simpleEventSerivce.deleteEvent();
}
}
그러면 "그림"의 Client에 해당하는 AppRunner는 @Autowired로 EventService를 주입 받는데 @Primary로 지정한
ProxySimpleEventService 빈을 주입 받게 된다.
7) 앞서 Proxy 클래스를 만들어서 모든 문제가 해결된 것처럼 보이지만 아직도 문제점이 존재한다.
매번 프록시 클래스를 작성해야 되며 이를 여러 클래스, 여러 메서드에 적용해야 된다면 상당히 번거롭다고 느끼게 될 것이다.
이러한 이유로 등장한 것이 스프링 AOP 이다.
스프링 IoC 컨테이너가 제공하는 기반 시설과 동적 프록시를 사용하여 여러 복잡한 문제 해결 하였다.
3. 스프링 AOP : @AOP
스프링 AOP 애노테이션
1) 애스팩트 정의
① @AsPect
② 빈으로 등록해야 하므로 컴포넌트 스캔을 사용한다면 @Component 도 추가
2) 포인트 컷 정의
@Pointcut
3) 어드바이스 정의
@Before
@AfterReturning
@AfterThrowing
@Around
애노테이션 기반 스프링 AOP 구현
1) 스프링 AOP를 구현하기 위해서는 의존성을 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2) PerfAspect 클래스를 작성하고 @Aspect 애노테이션을 붙여서 해당 클래스가 Aspect 클래스라는 것을 명시하고
@Component를 붙여 스프링 빈으로 등록한다.
@Component
@Aspect
public class PerfAspect {
}
3) ProceedingJoinPoint의 proceed() 메서드로 타겟 메서드를 호출 하고 그 결과 값을 전달 받습니다.
@Component
@Aspect
public class PerfAspect {
// me.whiteship 패키지와 하위 패키지에 있는 EventService의 모든 메서드에 advice를 적용 합니다.
@Around("execution(* me.whiteship..*.EventService.*(..))")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
long begin = System.currentTimeMillis();
// ProceedingJoinPoint의 proceed() 메서드로 타겟 메서드를 호출 하고 그 결과 값을 전달 받습니다.
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
ProceedingJoinPoint : 해당 advice가 적용되는 대상으로 보면 된다.
advice가 적용되는 SimpleEventService의 createEvent(), publishEvent() 메서드 자체라고 보면 된다.
@Around의 경우 반드시 proceed() 메서드가 호출되어야 한다.
proceed() 메서드는 타겟 메서드를 지칭하며 proceed 메서드를 실행시켜야만 타겟 메서드가 수행이 된다.
특정 메서드에만 AOP를 적용
1) 앞서 살펴본 예제에서 deleteEvent()는 적용 대상에서 제외를 해야 된다면 어떻게 해야 될까?
애노테이션을 직접 정의하는 방법으로 해결 가능하다.
/*
* 이 애노테이션을 사용하면 성능을 로깅해 줍니다.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS) // 기본 값인 CLASS 이상을 유지해야 됨
public @interface PerLogging {
}
RetentionPolicy : 애노테이션 정보를 얼마나 유지할 것인지를 지정한다.
@Retention(RetentionPolicy.RUNTIME) : 컴파일 이후에도 JVM에 의해서 참조가 가능합니다.
@Retention(RetentionPolicy.CLASS) : 컴파일러가 클래스를 참조할 때까지 유효합니다. (기본 값)
@Retention(RetentionPolicy.SOURCE) : 어노테이션 정보는 컴파일 이후 없어집니다.
2) 정의한 애노테이션을 AOP를 적용할 메서드에 붙여 줍니다.
@Service
public class SimpleEventSerivce implements EventService {
@PerLogging
@Override
public void createEvent() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Created an event");
}
@PerLogging
@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");
}
}
3) 애노테이션 표현식으로 @PerLogging 애노테이션이 붙여진 곳에 advice를 적용한다.
@Component
@Aspect
public class PerfAspect {
@Around("@annotation(PerLogging)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
* 빈 표현식으로 특정 빈에 적용하는 것도 가능 합니다.
'Spring > Spring Core' 카테고리의 다른 글
[Section 4] SpEL (스프링 Expression Language) (0) | 2020.10.10 |
---|---|
Section 3. 데이터 바인딩 추상화: Converter와 Formatter (0) | 2020.10.05 |
[Section 3] 데이터 바인딩 추상화 : PropertyEditor (0) | 2020.10.01 |
[Section 2] Validation 추상화 (0) | 2020.09.13 |
[Section 2] Resource 추상화 (0) | 2020.09.13 |
댓글