애플리케이션 로직은 크게 핵심기능과 부가 기능으로 나눌 수 있다.
핵심기능은 해당 객체가 제공하는 고유의 기능이다.
부가기능은 핵심기능을 보자하기위해 제공되는기능이다.(로구추적기 등등..)
위 기능들이 하나의 객체에 모두 들어있게되면 여러가지 문제점이 발생 한다.
1. 공통로직이 많이 생긴다.(부가기능은 어느 객체에서나 똑같이 작동하기 때문)
2. 로직의 복잡성이 증가한다.
3. 부가기능 적용 클래스가 100개면 100개에 다 추가해야한다.
4. 부가기능 코드가 복잡해진다면 더욱더 헬오픈이다.
5. 부가기능의 수정요구가 들어오면 수정하기위해서 헬이 오픈된다.
소프트웨어 개발에서 변경지점은 하나가 될수 있도록 잘 모듈화 해야한다.이러한 문제를 해결하기위해서 기존에 OOP의 방식으로는 해결이 어렵다.
핵심기능과 부가기능을 분리
누군가는이러한 부가 기능 도입의 문제점들을 해결하기 위해 오랜기간 고민해왔다.
그 결과 부가 기능을 핵심 기능에서 분리하고 한곳에서 관리하도록 개발됐다.
그리고 해당 부가기능을 어디에 적용할지 선택하는 기능도 만들어졌다.
이렇게 부가기능과 부가기능을 어디에 적용할지 다 합해서 하나의 모듈로 만든것이 Aspect이다.
Aspect는 관점이라는 뜻인데 애플리케이션을 바라보는 관점을 하나하나 기능에서 횡단 관심사의 관점으로 달리 보는것이다.
이러한 Aspect를 활용한 프로그래밍 방식을 관점지향 프로그래밍 AOP라고 한다.
참고로 AOP는 OOP를 대체하기 위한 것이 아니라 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발됐다.
AOP의 대표적인 구현으로 AspectJ 프래임워크 가 있다. 스프링도 AOP를 지원하지만 대부분 AspectJ의 문법을 차용하고 AspectJ가 제공하는 기능의 일부만 제공한다.
AOP를 사용하면 핵심 기능과 부가기능이 코드상 완전히 분리되어서 관리된다.
그렇다면 AOP를 사용할때 부가 기능 로직은 어떤 방식으로 실제 로직에 추가되나?
.java: 소스코드를 컴파일러를 사용해서 .class를 만드는 시점에 부가기능 로직을 추가할 수 있다. 이때 컴파일되는 타이밍에
컴파일러가 부가기능의 로직이 쏙 들어가서 부가기능이 추가된 .class파일이 되는것이다.
이렇게 원본 로직에 부가기능 로직이 추가되는것을 위빙이라고 한다.
단점:
컴파일 시점에 부가기능을 적용하려면 특별한 컴파일러도 필요하고 복잡다.
자바언어는 .class파일을 JVM내부의 클래스 로더에 보관한다. 이 시점에 중간에 .class파일을 조작한다음 JVM에 올릴수 있다.
단점
런타임 시점은 컴파일도 다 끝나고 클래스 로드에 클래스도 이미 다 올라가서 이미 자바가 다 실행된 다음을 말한다. main이 실행된상태
그렇기때문에 자바언어의 문법 법위안에서 실행하는것이다. 최종적으로 프록시를 통해 스프링 빈에 부가기능을 적용하는것이다.
프록시를 사용하기 때문에 AOP기능의 일부 제약이 있다.하지만 특별한 컴파일러나 자바를 실행할때 복잡한 옵션과 클래스 로더 조작기를 설정 하지 않아도 된다.
스프링만 있으면 얼마든지 AOP를 적용할 수 있다.
AOP는 적용가능 지점(조인포인트 ) : 생성자, 필드값 접근, static 메서드 접근, 메서드 실행
@Pintcut을 사용한다.
클래스로 따로 빼서 @Order로 순서 지정
@Around를 제외한 나머지 어드바이스들은 어라운드가 할 수 있는 일 의 일부만 제공할 뿐이다.
모든 어드바이스는 joinPoint를 첫번째 파라밍터에 사용할 수 있다.
단 @Around는 ProceedingJoinPoint를 사용해야한다.
왜냐하면 다음어드바이스나 타겟을 호출하는 proceed가 있기 때문이다.
@Around는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출되지 않으면
다음대상이 호출 되지 않는다.
애스팩트 J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.
PCD라고도 한다.
포인트컷 지시자의 종류 종류
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-
pattern(param-pattern)
throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
해당타입이 매칭되면 그안에서 메서드들이 자동으로 매칭된다. execution에서 타입부분만 사용한다고 보면 된다.
주의해야할 점이 표현식에 부모타입을 지정하면 안된다.정확하게 타입이 맞아야 한다.
this : 스프링 빈 객체 스프링 aop프록시를 대상으로 하는 조인포인트
target: 스프링 aop프록시가 가르키는 실제 대상을 대상으로 하는 조인포인트
this vs target
단순히 하나를 정하면 되는데 this와 target은 어떤 차이가 있을까?
스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다.
this는 빈에 등록된 프록시 객체를 대상으로 포인트컷을 매칭
target은 실제 target객체를 대상으로 포인트컷을 매칭한다.
왜 이렇게 두개가 나눠졌냐?
프록시 생성 방식의 차이에서 온다.
JDK동적프록시와 CGLIB의 프록시 생성방식에 차이가 있기 때문이다.
this는 jdk동적 프록시 할당에서 프록시의 할당받는 객체를 알지 못한다.그럼 그 객체는 aop의 적용대상이 안된다.
target은 빈에 등록된 객체를 찾아오기때문에 aop의 대상이 될 수 있다.
스프링은 프록시 방식의 AOP를 사용한다.
따라서 AOP를 적용하려면 항상 프록시를 통해서 대상 객체를 호출해야 한다.
이렇게 해야 프록시에 먼저 어드바이스를 호ㅜㄹ하고 이후에 대상 객페를 호출한다.
만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 어드바이스 호출되지 않는다.
AOP를 적용하면 스프링은 대상객체 대신에 프록시를 스프링 빈으로 등록한다. 다라서 스프링 의존관계 주입시에 항상프록시 객체를 주입한다.
프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발새하지 않는다. 하지만 대상객체 내부에서 메서드 호출이 발생하면 프록시를
거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다. 실무에서 반드시 한번은 만나서 고생하는 문제라 꼭이해해야한다.
자바 언어에서 메서드 앞에 별도의 참조가 없으면 this로 자기 자신의 인스턴스를 가리킨다.
결과적으로 자기 자신 내부 메서드를 호출하는 메서드는 실제 대상 객체의 인스턴스를 뜻한다. 결과적으로 이러한 내부 호출은 프록시를 거치지 않는다.
즉 어드바이스가 적용이 안된다.
스프링은 프록시 방식의 AOP를 사용한다. 프록시 방식의 AOP는 메서드 내부 호출에 프록시 적용할 수 없다.
자기 자신을 DI를 해서 내부에서 호출할때 프록시.내부호출() 이런 방식으로 외부메서드를 호출하는 것처럼 해서 해결하는 방법이 있다.
이때 주의 사항은 DI를 할때 생성자가 순환참조 되기때문에 setter로 의존관계를 주입한다.
이렇게하면 생성자를 di하는 타이밍과 setter를 di하는 타이밍이 다르기때문에 해결할 수 있다.
이렇게하면 프록시 인스턴스를 통해서 호출하기때문에 AOP적용이 가능하다.
ObjectProvider 받아서 내부 호출을할때 주입받은 ObjectProvider 있는 빈을 주입받아서 나중에 호출하는 방법도 있다.
ObjectProvider는 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다.
callServiceProvider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다.
여기서는 자기자신을 주입받는 것이 아니기 때문에 순환 사이클이 발생하지 않는다.
내부 호출 메서드를 별도의 클래스로 분리하고 DI를 통해서 사용하는 방법이 있다.
클라이언트에서 둘다 호출하도록 변경하는 방법도 있다.
대부분 위 방법처럼 억지로 해결하는 방법보다 대안3이그나마 자연스러운 해결방안이다.
AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용한다. 인터페이스에 메서드가 나올정도의 규모에 AOP를 적용하는 것이 적당하다.
더 풀어서 이야기하면 AOP는 public메서드만 적용한다 private메서드처럼 작은 단위에는 AOP를 적용하지 않는다.
하지만 AOP적용을 위해 private메서드를 외부 클래스로 변경하고 public으로 변경하는 일은 거의 없다. 그러나 위 예제와 같이 public메서드에서 public메서드를
내부호출하는 경우에는 문제가 발생한다. 실무에서 꼭 한버는 만나는 문제이다.
aop가 잘 적용되지 않는다면 내부호출을 의심해보자.
JDK동적 프록시와 CGLIB를 사용해서 AOP프록시를 만드는 방법에는 각각 장단점이 있다.
JDK동적 프록시는 인터페이스가 필수이기 때문에 인터페이스가 있는경우에 JDK동적 프록시와 CGLIB를 선택하는 경우에문제가 발생한다.
구체 클래스로 타입 캐스팅이 불가능한 한계가 있다.
즉 빈에 등록된 인터페이스로 프록시를 만들었기때문에 해당 인터페이스의 구현 클래스로는 캐스팅을 할 수 없다는 뜻이다.
이때 CGLIB로 구체클래스를 기반으로 프록시를 생성되기때문에 인터페이스의 구체 클래스를 캐스팅하는 것이 가능하다.
어떠한 인터페이스와 그 인터페이스의 구체클래스가 동시에 DI를 했을때 JDK동적프록시를 하면 인터페이스를 기반으로 프록시를 주입하기때문에
구체클래스 타입에 캐스팅이 불가능해서 오류가 발생한다.
이럴땐 CGLIB로 구체클래스를 상속받아서 만들기 때문에 그 부모인 구체클래스의 인터페이스까지 캐스팅이 가능해 지는것이다.
JDK동적 프록시의 한계점들을 보면 무조건 CGLIB를 사용하는 것이 좋아보인다. CGLIB의 단점은 무엇일까
CGLIB는 구체 클래스를 상속받는다. 자바 언어에서 상속을 받으면 자식클래스의 생성자를 호출할때 자식 클래스의 생성자에서
부모 클래스의생성자도 호출해야한다.(이부분이 생략되어 있다면 자식 클래스의 생성자 첫줄에 부모클래스의 기본 생성자를 호출하는 super() 가 자동으로 들어간다.)
CGLIB는 구체클래스를 상속받는다 자바 언어에서 상속을 받으면 자식 클래스의 생성자를 호출할때 부모 클래스의 생성자도 호출해야한다.
1. 실제 target의 객체를 생성할때
2. 프록시 객체를 생성할때 부모 클래스의 생성자 호출
final 키워드가 클래스에 있으면 상소기 불가능하고 메서드에 있으면 오버라이딩이 불가능하다.
CGLIB는 상속을 기반으로 하기때문에 두경우 프록시가 생성되지 않거나 정상 동작하지 않는다.
하지만 일반적인 웹 에플리케이션을 개발할때는 final키워드를 잘 사용하지 않는다.
스프링은 AOP프록시 생성을 편리하게 제공하기 위해 오랜시간 고민하고 문제들을 해결했다.
스프링 3.2는 스프링 내부에 CGLIB에 함께 패키징했다.
스프링 4.0부터 objenesis 라는 특별한 라이브러리를 사용해서 기본생성자 없이 객체 생성이 가능하다.
이라이브러리는 생성자 호출없이 객체를 생성할 수 있게 해준다.
그리고 덕분에 생성자가 1번만 호출한다.
위 문제가 다 해결했기때문이다.