Spring AOP 요약
www.inflearn.com/course/spring-framework_core#
Spring 핵심기술의 AOP 부분 정리.
Spring AOP
스프링 AOP 구현체 제공, 자바에서 제공하는 구현체 (aspect-j) 와 연동. 스프링 txn, 캐시 등 여러 기능에 적용됨
흩어진 Aspect를 모듈화할 수 있는 프로그래밍 기법을 의미함. OOP의 보완관계.
여러 클래스에 걸쳐서 비슷한 코드를 사용해야 하는 경우
- Transaction 처리. setAutoCommit false 설정 -> 쿼리 생성 후 실행 -> commit or 롤백. 이런 코드를 기존 서비스코드에 감싸는 형태로 사용함
- 성능관련 로깅. Class A / Class C의 특정 부분에 로깅 메소드를 적용해야 할 경우, 비슷한 코드가 두 군데에 사용되는 식.
이렇게 각각 Concern마다 코드변경이 일어남. 만약 이렇게 된 코드에서 수정이 필요할 경우, 로직이 적용된 각각의 클래스를 전부 찾아서 변경해줘야 한다. (로그 로직 변경 -> 로깅이 적용된 모든 영역을 찾아가서 일일이 코드변경을 해야 함)
따라서 AOP는 Aspect라는 개념을 적용해 해결. 필요한 로직을 Aspect로 정의하고, 해당 로직이 어느 클래스에 적용되어야 하는지 등록함.
AOP 용어
- Aspect - 일종의 모듈. 해당 모듈에 Advice / Pointcut 정보가 있음
- Advice: 해야 할 일 (로직)
- PointCut: 어디에 적용될 것인지.
- target - 적용될 대상 클래스 (위 사진의 경우 A, B, C)
- join Point: advice가 실행되는 시점이 언제인지. (언제 끼워넣을 것인지)
- method 실행시점 - 가장 많이 사용됨.
- 생성자 호출 직전, 필드에서 값을 가져갔을 때 등등.... 여러 분기점이 있음. join point의 subset이 pointcut이라고 이해해도 됨.
AOP 구현체 - 자바
- AspectJ : 다양한 기능 제공
- Spring AOP : 상대적으로 국한된 기능.
AOP의 적용방법
- Compile Time : 자바파일을 클래스 파일로 변경할 때 적용. advice가 포함된 형태로 바이트코드를 생성하는 방식.
- Loading Time : 클래스는 변경 없이 컴파일 완료, 클래스 파일을 로딩하는 시점 (jvm 메모리에 올라가는 시점)에 advice가 같이 올라가는 것.
- Runtime : Spring AOP가 사용하는 방법. 스프링 내에서 A라는 Bean을 생성할 때.
- A라는 타입의 Proxy Bean를 생성
- 프록시 bean이 해당 메소드를 호출하기 직전에 aspect의 advice 실행.
Spring AOP : 프록시 기반 AOP.
Spring AOP는
- 프록시 기반의 AOP 구현체
- 스프링 Bean에만 AOP를 적용할 수 있음.
- 모든 AOP기능을 제공하는 게 목적이 아니라, 스프링 IoC와 연동해서 엔터프라이즈 애플리케이션에서 가장 흔한 문제를 해결하는 게 목적임
프록시 패턴
- 기존 코드 변경 없이 접근제어 or 부가 기능을 추가할 수 있는 기능
public interface EventService {
void createEvent();
void publishEvent();
void deleteEvent();
}
@Service
public class SimpleEventService implements EventService {
@Override
public void createEvent() {
Thread.sleep(1000);
System.out.println("create Event fin");
}
@Override
public void publishEvent() {
Thread.sleep(2000);
System.out.println("publish Event fin");
}
@Override
public void deleteEvent() {
System.out.println("delete Event fin");
}
}
@Component
public class AppRunner implements AppllicationRunner {
@Autowired
EventService eventService;
@Override
public void run(ApplicationArguments args) throws Exception {
eventService.createEvent();
eventService.publishEvent();
eventService.deleteEvent();
}
}
이렇게 된 상황에서, service의 createEvent / publishEvent에는 코드 실행시간 측정로직을 추가하고 싶은 경우.
proxy를 담당할 객체를 생성한다.
@Primary // 동일한 타입 bean이 있을 경우 우선순위로 설정.
@Service
public class ProxySimpleEventService implements EventService {
// 프록시가 실제로 접근할 클래스 - 예시의 경우 SimpleEventService 객체 - 를 주입받는다.
// EventService 타입을 주입받아도, 사용할 객체 이름이 simpleEventService일 경우 괜찮음
@Autowired
SimpleEventService simpleEventService
// 여기서 필요한 로직들을 추가로 구현하는 식.
@Override
public void createEvent() {
long begin = System.currentTimeMillis();
// delegation. 실제 클래스에서 필요한 로직은 불러온다.
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.deleteeEvent();
}
}
단, 이렇게 만들 경우
- 프록시 내에서도 중복코드 발생
- 매번 이렇게 프록시를 생성해야 하는가? 모든 메소드의 delegation은?
- 여기 말고도 다른 클래스에도 로직을 적용하고 싶다면, 클래스마다 프록시 생성?
그래서 등장한 게 Spring AOP.
-
스프링 IoC 컨테이너가 제공하는 기반시설 + Dynamic 프록시 사용.
-
Dynamic Proxy : 동적으로 프록시 객체를 생성하는 방법.
- 자바는 인터페이스 기반 프록시 생성을 지원함.
- CGlib은 클래스 기반 프록시도 지원.
-
Spring IoC : BeanPostProcessor가 있다. Bean 등록이 끝난 후에 특정 작업을 수행할 수 있는 인터페이스. 여기서 기존 Bean을 대체하는 Dynamic Proxy Bean을 만들어 등록한다.
즉, 위에서 한 것처럼 Proxy 객체를 일일이 개발자가 생성하는 대신 동적으로 프록시 객체를 생성하며, 동적으로 생성된 Bean을 스프링 IoC에서 Bean으로 등록하는 것.
@AOP
어노테이션 기반의 스프링 @AOP.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
로 디펜던시 추가.
// Aspect 정의 후 Bean으로 등록
@Component
@Aspect
public class PerfAspect {
// 두 가지 정보가 필요함 - advice, pointcut
// Advice의 적용타입 정의 어노테이션.
// pointcut 이름을 부여하거나 pointcut 자체를 정의해 사용.
// around : 실행할 메소드를 감싼 형태로 적용한다는 의미. 다용도로 쓰일 수 있다.
@Around("execution(* com.inspirit941..*.EventService.*(..))")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
// advice 영역.
long begin = System.currentTimeMillis();
// > 원래 프로세스 실행
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
단, 위와 같이 execution으로 정의하면 해당 클래스 내의 모든 메소드에 적용된다. deleteEvent 메소드에는 AOP를 적용하고 싶지 않을 경우?
-> annotation을 정의해 처리.
@Retention(RetentionPolicy.CLASS) // 컴파일한 클래스 파일에도 이 annotation 정보가 있어야 한다는 의미. (source로 변경하면 컴파일 시 값이 사라짐.)
@Target(ElementType.METHOD)
@Documented // 문서화하려면 사용
public @interface PerLogging {
}
// 이 어노테이션을, 로그를 붙이고 싶은 컨트롤러 메소드 위에 추가한다.
// Aspect에서는 아래와 같이 코드를 변경함. 이 편이 좀더 유용하다.
@Component
@Aspect
public class PerfAspect {
@Around("@annotation(PerLogging)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
// advice 영역.
long begin = System.currentTimeMillis();
// > 원래 프로세스 실행
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
// 메소드 실행 전에만 뭐 하면 된다 = before
@Before("bean(클래스명)")
public void hello() {
System.out.println("before");
}
}
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts
Null Safety
Spring 5에서 추가된 기능. 컴파일 타임에 최대한 nullpointerException을 막기 위해.
- NonNull
- Nullable
- NonNullApi(패키지 레벨 설정)
- NonNullFields(패키지 레벨 설정)
public class DummyService {
@NonNull // 리턴 시 Null 허용 x
public String createEvent(@NonNull String name) { // 입력값에 Null 허용 x
return "hello " + name;
}
}
IDE에 실제로 이 설정이 효과를 내도록 하려면
- Preference -> Compiler에 보면 Add runtime assertion for notnull-annotated methods and parameters 체크박스에 체크
- Configure annotation 버튼 클릭해서 스프링 관련 어노테이션을 추가해주면 된다. nullable, nonnull 등등.
이러고 IDE 껐다 켜면 적용됨
"패키지 파일" 위에 @NonNullApi를 등록하기도. 이러면 패키지 파일의 모든 파라미터 / 리턴타입에 nonNull 설정한 것과 동일함.
- 필요한 곳에만 nullable을 붙이는 식으로 작업할 수 있음.
@NonNullApi
package com.inspirit941.test;