애플리케이션은 핵심기능과 부가기능으로 나눌수 있다.
핵심기능
: 해당 객체가 제공하는 고유의 기능.
부가기능
: 핵심기능을 보조하기 위해 제공되는 기능. 단독사용x. 부가기능과 함꼐 사용
같이 사용하기위해서는 부가기능과 핵심기능이 하나의 객체 안에 섞여 들어가게 됨.
보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다. 이러한 부가 기능을 횡단 관심사
(cross-cutting concerns)가 된다. 하나의 부가기능이 여러 곳에 동일하게 사용된다는 뜻.
Aspect란 쉽게 이야기해서 부가 기능과, 해당 부가 기능을 어디에 적용할지 정의한 것
참고 : AOP는 OOP를 대체하기위한 것이 아니라 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한
부분을 보조하는 목적으로 개발되었다.
AspectJ 프레임워크
AOP의 대표적인 구현으로 AspectJ 프레임워크가 있다. 스프링도 AOP를 구현하지만 대부분 AspectJ의
문법을 차용하고, AspectJ가 제공하는 기능의 일부만 제공한다.(AspectJ란 Aspect Java)
AspectJ 프레임워크의 설명
3가지 방법
1. 컴파일 시점
2. 클래스 로딩 시점
3. 런타임 시점(프록시)
1. 컴파일 시점
.java
소스 코드를 컴파일러를 사용해 .class
를 만드는 시점에 부가기능 로직을 추가한다.
쉽게 이야기하자면 부가 기능 코드가 컴파일 되면서 실제 클래스에 들어간다고 생각하면 된다.
이렇게 원본 로직에 부가기능 로직이 추가되는 것을 위빙(Weaving)이라 한다
단점 : 컴파일 시점에 부가기능을 적용하려면 특별한 컴파일러도 필요하고 복잡하다. 잘사용안함
2. 클래스 로딩 시점
자바를 실행하면 자바 언어는.class
파일을 JVM 내부의 클래스 로더에 보관한다. 이때 중간에서
.class
파일을 조작한 다음 JVM에 올릴 수 있다. 자바 언어는 .class
를 JVM에 저장하기 전에
조작할 수 있는 기능을 제공한다. java.instrumentation
이라 검색해보면 나옴. (참고 :
모니터 툴들이 이 방식을 사용한다) 이런 로딩시점에 에스펙트를 적용하는 것을 로드 타임 위빙이라 한다.
단점 : 자바를 실행할 때 특별한 옵션(java -javaagent
)을 통해 클래스 로더 조작기를 지정해야 하는데,
이 부분이 번거롭고 운영하기 어렵다.
3. 런타임 시점
런타임 시점은 컴파일도 끝나고, 클래스 로더에 클래스도 다 올라가서 이미 자바가 실행되고 난 다음을
말한다. 자바의 메인 메서드가 이미 실행된 다음이다. 따라서 자바 언어가 제공하는 범위안에서 부가기능
을 적용해야 한다. 컨테이너의 도움을 받고, 프록시와 DI, 빈 포스트 프로세서 같은 개념들을 총 동원.
프록시를 사용하기 때문에 AOP 기능에 일부 제약이 있다.(final이 붙으면 상속이나 오버라이드가 안됨..
, 다형성이 적용되는 메서드에만 적용가능)
하지만 자바를 실행할 때 복잡한 옵션과 클래스 로더 조작기를 설정하지 않아도 된다. 스프링만 있으면
얼마든지 AOP를 적용할 수 있다.
3가지의 차이점
AOP 적용 위치
AOP는 지금까지 학습한 메서드 실행 위치 뿐만 아니라 아래의 위치에 적용 가능
스프링 AOP의 조인 포인트는 메서드 실행으로 제한
된다.스프링은 AspectJ의 문법을 차용, 프록시 방식의 AOP방식을 적용. AspectJ를 직접 사용하는것은 아니다.
AspectJ를 사용하려면 너무 방대한 양을 공부해야됨. 자바 관련 설정도 복잡함. 반면 스프링 AOP는 별도의
추가 자바 설정 없이 스프링만 있으면 편리하게 AOP를 사용할 수 있다. 실무에서는 스프링이 제공하는
AOP 기능만 사용해도 대부분의 문제를 해결할 수 있다. (스프링 AOP 기능 학습만해도 힘듬...
근데 이것보다 더 훨씬 복잡하고 어려운 AspectJ는????)
@Aspect
를 생각 하면됨hello.aop.order
패키지와 그 하위패키지를 지정하겠다는@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){}
@Around("allOrder() && allService()")
pubic Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
//로직 코드...
}
포인트 컷을 따로 빼내서 모듈화 시켜둘수도 있다. 이렇게 따로 포인트컷을 관리하고,
joinpoint에서는 다양하게 조합해서 사용 가능. 이때 &&(and), ||(or), !(not) 연산자를 사용 할 수 있음.
여러개의 로직에 범위를 지정할때 관리가 더 쉬워짐. 같은 클래스내에서 따로 빼서 만들어도 되고 아예
외부에서 만들어서 관리해도 된다.
@Pointcut("execution(* *..*Service.*(..))")
public void allService(){}
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){}
@Pointcut("allService() && allOrder()")
public void orderServcieAndOrder() {}
---------------------------
다른 클래스
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
pubic Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
//로직
}
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
pubic Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
//로직
}
외부에 뺀 포인트컷을 사용하려면 이렇게 패키지명과 클래스를 다 붙여서 적어주어야 한다.
조금 귀찮긴 하지만 알아서 따로 포인컷들을 관리하려면 이런식으로 해줘야됨.
그리고 포인트컷끼리도 서로 연산해서 가능.
@Aspect
적용 단위로 @Aspect
@Order(2)
public static class LogAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
pubic Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
//로직
}
}
@Aspect
@Order(1)
public static class TxAspect{
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
pubic Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
//로직
}
}
@Around : 메서드 전후에 수행되고 가장 강력한 어드바이스. 조인 포인트 실행 여부 선택, 반환 값
변환 , 예외 변환 등이 가능. 나머지 들은 사실 어라운드의 기능의 조각내서 조금씩 범위를 가지고 있는 것
- 조인 포인트의 실행 여부를 결정할수있음.
- 전달 값, 반환 값, 예외 변환
- 트랜잭션 처럼 try~catch~fianlly
모두 들어가는 구문 처리 가능
- 어드바이스의 첫번째 파라미터는 ProceedingJoinPoin
를 사용해야 한다.
- proceed()를 통해 대상을 실행. proceed()를 여러번 실행할 수도 있음(재시도).
@Before : 조인 포인트 실행 이전에 실행
@After Returning : 조인 포인트가 정상 완료후 실행. 반환값을 쓸수는 있지만 변환 불가능.
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.allOrder()", returning = "result")
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
모든 어드바이스는 JoinPoint를 첫번째 파라미터에 사용할 수 있다.
단@Around
는ProceedingJoinPoin
를 사용해야 한다.(다음 어드바이스나 타겟을 호출 해야됨.)
ProceedingJoinPoin는 JoinPoin 인터페이스의 하위 타입 인터페이스이다.
JoinPoin 인터페이스 주요기능
getArgs() : 메서드 인수를 반환
getThis() : 프록시 객체를 반환
getTarget() : 대상 객체를 반환
getSignature() : 조언되는 메서드에 대한 설명을 반환
toString() : 조언되는 방법에 대한 유용한 설명을 인쇄
ProceedingJoinPoint 인터페이스의 주요 기능
proceed() : 다음 어드바이스나 타겟을 호출한다.
순서
스프링 5.27 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
실행 순서 : -> @Around
(1) -> @Before
(2) , @After
(5) <- @AfterReturning
(4) <- @AfterThrowing
(3) <-
어드바이스가 적용되는 순서는 이렇지만, 호출순서와 리턴순서는 반대라는 점을 알아두자.
@Aspect 안의 동일한 종류의 어드바이스는 순서 보장이 되지 않는다. 이때는 클래스를 분리해 @Order()를
통해 순서 적용 가능
@Around 외에 다른 어드바이스가 존재하는 이유는? (다른 어드바이스는 @Around의 하위호환들)
항상 다음 타겟을 호출해줘야됨. 타겟 실행전에 하나만 하고 끝내고 싶은데(@Before범위) 다음 타겟
호출을 적어줘야되고, 실수로 까먹기라도 하면 큰일난다.
따라서 기능은 적지만 실수할 가능성이 적은 @Before, @After 같은 어드바이스. 코드도 단순하다.
의도도 명확하게 들어남. @Before를 처음 보는 순간 타겟 실행 전에 한정해서 어떤 일을 하는 코드라는 것을 명확하게 알 수 있다.
결국 위에서 말하는것은 "제약" 이다.
제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고
코드의 의도도 파악하기 쉽다.
AspectJ 는 포인트컷을 편리하게 표현하기 위해 특별한 표현식을 제공한다.
포인트컷 지시자의 종류
execution
: 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용.기능도 복잡
whithin
: 특정 타입 내의 조인 포인트를 매칭한다.
args
: 인자가 주어진 타입의 인스턴스인 조인 포인트
this
: 스프링 빈 객체(스프링AOP프록시)를 대상으로 하는 조인 포인트
target
: Target 객체(스프링AOP프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
@target
: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
@within
: 주어진 애노테이션이 있는 타입 내 조인 포인트
@annotation
: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
@args
: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
execution은 가장 많이 사용하고, 나머지는 자주 사용하지 않는다. execution을 중점적으로 이해하자.
execution(modifiers-pattern?
ret-type-pattern
declaring-type-pattern?
.name-pattern(param-pattern)``throws-pattern?
)
*
같은 패턴을 지정할 수 있다.실제 코드를 보며 execution을 이해하자
//public java.lang.String
hello.aop.member.MemberServiceImpl.hello(java.lang.String)
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
pointcut.setExpression("execution(* *(..))");
//(접근제어자생략), *(반환타입) , (선언유형생략), hel* (hel로 시작하는 메서드, (..) 메서드 파라미터와 관계없이 메서드에 적용
pointcut.setExpression("execution(* hel*(..))");
//* (모든 반환 유형 ) hello메서드에 (..) 아무 파라미터나
pointcut.setExpression("execution(* hello(..))");
//nono 메서드에 (..) 아무 파라미터
pointcut.setExpression("execution(* nono(..))");
//(접근제어생략), *(반환타입), 선언유형,메서드, (..) 메서드파라미터 아무거나
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
//(접근제어생략), *(반환타입), 패키지 hello.aop.member에 있는 *(하위클래스),*(메서드) (..) 파라미터
pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
//(접근제어생략), *(반환타입), hello.aop패키지에 *하위클래스,*매서드(..)파라미터
// 이때 hello.aop. 바로 아래에 있는 클래스의 메서드를 대상
pointcut.setExpression("execution(* hello.aop.*.*(..))");
// hello.aop.member.. 은 아래에 있는 하위 패키지 또는 중첩된 패키지까지 대상으로 한다.
pointcut.setExpression("execution(* hello.aop.member..*.*(..))");
// hello.aop.. -> aop뿐 아니라 그 하위 패키지에 있는 클래스와 메서드들을 대상
pointcut.setExpression("execution(* hello.aop..*.*(..))");
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
within 지시자는 특정 타입 내의 조인 포인트에 대한 매칭을 제한한다. 쉽게 말해 해당 타입이
매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭된다.
execution에서 타입 부분만 사용한다고 보면 된다.
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
pointcut.setExpression("within(hello.aop.member.*Service*)");
pointcut.setExpression("within(hello.aop..*)");
args
: 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭
기본문법은 execution
의 args
부분과 같다.
execution과 args의 차이점
execution
은 파라미터 타입이 정확하게 매칭되어야 한다. execution
은 클래스에 선언된 정보를 기반으로 판단한다.
args
는 부모 타입을 허용한다. args
는 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.
execution이랑 parameter 지정하는 부분과 똑같다.
"execution - 메서드의 시그니처로 판단(정적), args - 런타임에 전달된 인수로 판단(동적)"
pointcut("args(Object)")
pointcut("args()")
pointcut("args(..)")
pointcut("args(*)")
pointcut("args(String,..)")
pointcut("args(java.io.Serializable)") // String의 상위 클래스인 Serializable을 보고 동적으로 판단
pointcut("execution(* *(java.io.Serializable))") // isFalse() , 허용하지 않는다.
pointcut("execution(* *(Object))") // isFalse(), 허용하지 않는다.
args
지시자는 단독으로 사용되기 보다는 나중에 설명할 파라미터 바인딩에서 주로 사용된다.
@target
: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
@within
: 주어진 애노테이션이 있는 타입 내 조인 포인트
@target
, @within
은 다음과 같이 타입에 있는 애노테이션으로 AOP 적용여부를 판단.
-@target(hello.aop.member.annotation.ClassAop)
-@within(hello.aop.member.annotation.ClassAop)
무슨 차이일까? @target vs @within
@target
은 인스턴스의 모든 메서드를 조인 포인트로 적용
@within
은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.
즉 @target
은 부모 클래스의 메서드까지 어드바이스를 다 적용, @within
은 자기 자신의 클래스에
정의된 메서드에만 어드바이스를 적용한다.
테스트에 앞서, @target 과 @within은 pointcut으로 바로 판단이 안됨. 실제 객체 인스턴스가 등록되고 실행되는
과정에서 어드바이스 적용 여부 파악 가능.
한번더 강조.. 중요!!
@target
은 부모 클래스의 메서드까지 어드바이스 적용!
@within
은 자기 자신의 클래스에 정의된 메서드에만 어드바이스 적용!
뒤에 설명할 parameter 바인딩에 적용 가능하다.
주의점
args
,@args
,@target
다음 포인트컷 지시자는 단독사용 하면 안됨
args
,@args
,@target
은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있다. 프록시가
없다면 판단 자체가 불가능하다.
그런데 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩시점에
적용할 수 있다. 따라서args
,@args
,@target
같은 포인트컷 지시자가 있으면 스프링은
모든 스프링 빈에 AOP를 적용하려고 한다. 이때 final 이 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.
따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해라
@annotation
: 메서드가 주어진 애노테이션을 가지고 있는 조인포인트 매칭
@annotation(hello.aop.member.annotation.MethodAop)
@args
: 전달된 실제 인수의 런타입 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
@args(test.Check)
: 전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우에 매칭한다.
bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.
설명
포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.
@Before("allMember() && args(param,..)")
public void logArgs3(String param) {
log.info("[logArgs3] param={}", param);
}
포인트컷의 이름과 매개변수의 이름을 맞춰야 한다. 여기서는 param
추가로 타입이 메서드에 지정한 타입으로 제한됨(여기선 String)
args(arg,..) -> args(String,..)
// 배열의 인덱스로 받는 방법.딱 봐도 안좋아 보임.
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
return joinPoint.proceed();
}
// args를 이용해서 받는 방법. 이 방법을 많이 사용
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs1]{} arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
//완전 최적화. 타입까지 맞춰주었고, Before를 사용
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
// this는 proxy 객체 (실제 스프링 빈에 등록된 객체)를 가지고옴
@Before("allMember() && this(obj)")
public void thisArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
// target은 프록시가 아니라 프록시가 호출하는 실제 객체를 가지고 옴.
@Before("allMember() && target(obj)")
public void targetArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
@Before("allMember() && @target(annotation)")
public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @within(annotation)")
public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
}
// @annotation 같은경우 실제 annotation에 값이 붙어 넘어오면 그 값을 받을 수 있음.
@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation.value());
}
this
: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target
: Target 객체(스프링 AOP프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
*
과 같은 패턴을 사용할 수 없다. this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
this vs target
스프링은 프록시를 생성할때 JDK 동적 프록시와 CGLIB을 선택할 수 있다. 둘의 프록시를 생성하는 방식이 다르기
때문에 차이가 발생한다.
JDK 동적 프록시 적용 상황에서 ...
MemberService 인터페이스 지정(hello.aop.member.MemberService)
this
는 부모 타입을 허용하기 때문에 AOP가 적용된다.target
은 부모타입을 허용하기때문에 AOP가 적용된다MemberServiceImpl 구체 클래스 지정(hello.aop.member.MemberServiceImpl)
정리
프록시를 대상으로하는 this
의 경우 구체클래스를 지정하면 프록시 생성 전략에 따라 다른 결과가 나올 수 있다
는 점을 알아두자.
this
와target
지시자는 단독으로 사용되기 보다는, 파라미터 바인딩에 주로 사용된다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int value() default 3;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trace {
}
애노테이션기반으로 AOP 적용을 하기 위한 것. 이름으로 알아보듯 Trace 기능과, Retry 기능이다.
이때 Retry는 주의해야되는데, 무한히 요청하면 안됨.(서버가 터지거나 문제가 생긴다). 그래서 int value()
값을 넣어두었다.(defalut 3)
이런 식으로 애노테이션을 만든뒤 Aspect 클래스를 만들어 adviser를 만들어 주면 됨
@Before("@annotation(hello.aop.exam.annotation.Trace)")
public void doTrace(JoinPoint joinPoint) {
// 로직
}
@Around("@annotation(retry)")
public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
// 로직
}
이렇게 애노테이션 기반의 어드바이저를 적용하기위한 기반이 다 마련되었다. 실제 적용은 적용하고 싶은
메서드에 @Trace
, @Retry
를 붙여 주기만 하면 적용 된다.
이와 비슷하게 동작하는 스프링의 대표적인 애노테이션 기반 AOP는 바로 @Transactional
이 있다.
스프링은 프록시 방식의 AOP를 사용한다.
AOP를 적용하려면 항상 프록시를 통해서 대상 객체(Target)를 호출해야 한다.
프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고, 어드바이스도 호출x
AOP를 적용하려면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다.
대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않는다.
하지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는
문제가 발생한다.
public class CallServiceV0 {
public void external() {
log.info("call external");
internal(); //내부 메서드 호출 this.internal();
}
public void internal() {
log.info("call internal");
}
}
this
가 붙어있다. 즉 this.internal()프록시 AOP 방식의 한계
스프링 AOP는 프록시 방식이기에 내부 호출에 프록시를 적용할 수 없다. 참고로 실제 코드에 AOP를
직접 적용하는 AspectJ를 사용하면 이러한 문제가 발생하지 않는다. 다만 AspectJ는 사용하기 너무
어렵다. 그리고 프록시 방식의 AOP 내부 호출에 대응할 다른 대안들도 존재한다.
이제부터 이러한 방식을 알아보자
가장 간단한 방법은 자기 자신을 의존관계 주입하는 것이다.
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
//생성자는 할 수 없다. 자기자신이 존재하지도 않는데 생성자를 받게되면 순환참조 문제가 발생함
//이때 사용할 수 있는 방법이 setter 주입 방식.
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
this.callServiceV1 = callServiceV1;
}
public void external() {
log.info("call external");
callServiceV1.internal();
}
public void internal() {
log.info("call internal");
}
}
지연 조회를 하면 된다. 필요할때, 자기 자신을 주입 받게 만들면 setter를 사용할 필요 없이 생성자로 만들 수 있다.
ApplicationContext
를 바로 사용해도 되지만 이 ApplicationContext
는 너무 기능이 많고 거대하다. 우리는 지연 로딩만
시킬 수 있으면 된다.
이때 사용할 수 있는 기능이 있다. 기억이 나나?
ObjectProvider
Provider
앞선 방법들은 조금 어색한 모습을 만들었다. 가장 나은 대안은 내부 호출이 발생하지 않도록 구조를 변경해버리는 것이다.
스프링에서는 이 방법을 권장하며, 이 방법으로 사용하는것이 좋다고 한다.
단순하게 내부 메서드를 따로 클래스로 만들어서, 분리하는 방법뿐 아니라 다양한 방법들이 있을 수 있다.
아예 클라이언트측에서 둘 다 호출하는 방법도 있겠다. 이 경우에는 내부 호출을 하지 않도록 코드 변경.
참고
AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용한다. 쉽게 말해서 인터페이스에
메서드가 나올 정도의 규모에 AOP를 적용하는 것이 적당하다. 또한 AOP는public
메서드에만 적용.
private
메서드 처럼 작은 단위에는 적용하지 않는다.(적용도 안됨).
public
메서드에서public
메서드를 내부 호출하는 경우에는 문제가 발생한다.
스프링은 JDK 동적 프록시와 CGLIB을 사용해서 AOP 프록시를 만드는데 이 방법에는 각각 장단점이 있다.
스프링이 프록시를 만들때 제공하는 ProxyFactory
에 proxyTargetClass
옵션에 따라 둘중 하나를 선택해서
프록시를 만들 수 있다.
JDK 동적 프록시 한계
인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능한 한계가 있다.
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문이다. 인터페이스로 캐스팅은 가능하지만
그 구현체로의 캐스팅은 불가능하다.(이는 자바 문법)
CGLIB는 인터페이스이든 구현체든 둘다 캐스팅이 가능하다. 왜냐하면 구현체를 통해 프록시를 생성하기 때문이다.
정리
JDK 동적 프록시는 대상 객체인 MemberServiceImpl
로 캐스팅 할 수 없다.
CGLIB 프록시는 대상 객체인 MemberServiceImpl
로 캐스팅 할 수 있다.
그런데 이게 문제가 될까? 프록시에서 타입 캐스팅을 할 일이 얼마나 있다고? -> 진짜 문제는 의존관계
주입에서 일어난다.
@Autowired MemberService memberService;
- 문제 없음. 인터페이스를 기반으로 만들어지는 JDK 동적 프록시
@Autowired MemberServiceImpl memberServiceImpl;
- 문제가 발생. 인터페이스를 기반으로 만들어지는 JDK 동적 프록시.
MemberServiceImpl의 타입이 뭔지 전혀 모른다. 그래서 해당 타입에 주입할 수 없다.
Interface 타입으로는 캐스팅 가능. MemberService = JDK Proxy가 성립한다.
반대로 저 둘다 CGLIB을 사용하면 가능하다.
JDK 동적 프록시가 가지는 한계점을 알아 보았다. 실제 개발때는 인터페이스를 기반으로 의존관계를 주입
받는 것이 맞다. 다만 여러가지 사정에 의해 AOP 프록시가 적용된 구체 클래스를 직접 의존관계 주입
받아야 하는 경우가 있을 수 있다. 이때는 CGLIB을 통해 구체 클래스 기반으로 AOP 프록시를
적용하면 된다.
여기까지 보면 CGLIB가 더 좋아 보인다. CGLIB를 사용하면 이런 고민을 하지 않아도 된다. 지금부터는
CGLIB의 단점을 알아보자.
CGLIB의 문제점
구체 클래스를 상속받기 때문에 다음과 같은 문제가 있다.
1. 대상 클래스에 기본 생성자 필수
2. 생성자 2번 호출 문제
3. final 키워드 클래스, 메서드 사용 불가
대상 클래스에 기본 생성자 필수
상속을 받으면 부모 생성자를 호출해야한다. 생략하면 자동 super()들어감. CGLIB는 우리가 호출하는 것이
아니다. CGLIB 프록시는 대상 클래스를 상속받고, 생성자에서 대상 클래스의 기본 생성자를 호출. 따라서
대상 클래스에 기본 생성자가 필수로 있어야 한다.
생성자 2번 호출 문제
CGLIB는 구체 클래스를 상속. 상속 받으면 자식 생성자를 호출할때 부모 클래스의 생성자도 호출해야한다.
그래서 CGLIB는 아래와 같이 2번 생성자 호출을 한다.
1번째 - 실제 target의 객체를 생성할 때
2번째 - 프록시 객체를 생성할 때 부모 클래스의 생성자 호출
final 키워드 클래스, 메서드 사용 불가
fianl 키워드가 클래스에 있으면 상속이 불가능, 메서드에 있으면 오버라이딩이 불가능. 때문이 CGLIB가
상속을 기반으로 하기 때문에 두 경우에는 프록시가 생성되지 않거나 정상 작동하지 않는다.
사실 프레임 워크 같은 개발이 아니라 일반적인 웹 애플리케이션 개발할 때는 final
키워드를 잘 사용하지
않아 이 부분은 문제가 크게 되지 않는다.
JDK 동적 프록시와 CGLIB의 단점을 알아보았다. 그렇다면 스프링은 이런 단점과 한계를 어떻게 해결하고 있는가?
CGLIB 기본 생성자 필수 문제 해결
objenesis
라는 특별한 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능(생성자 호출없이 객체 생성)생성자 2번 호출 문제
objenesis
라는 특별한 라이브러리 덕분에 가능해졌다. 이제 생성자가 1번 호출된다.스프링부트 2.0 - CGLIB 기본 사용
final 키워드
사실 개발자 입장에서는 어떤 프록시 기술을 사용하든 상관이 없다. JDK 동적 프록시이든 CGLIB든 또는 어떤 새로운
프록시 기술을 사용해도 상관이 없다. 단지 문제가 없고 개발만 편리하면 되는 것이다.
지금은 CGLIB의 문제점이 거의 다 해결 되었고, 기본으로 CGLIB가 선택되어 있다. 즉 그말은 그냥 AOP를 사용하면 된다는 뜻이다.
내가 특별하게 설정을 하지 않는이상 JDK 동적 프록시가 사용 될 일이 없다.
참고 : 본 글은 김영한님의 스프링 강의를 정리한 것이다.