필자는 AOP 에 관심이 많다.
공부해오고 프로젝트를 진행해오면서 조금씩 AOP 관련 지식을 쌓아가며 AOP 기술을 점진적으로 발전시키며 활용해왔고, 프로젝트 중 하나에는 어노테이션을 이용해서 AOP 처리를 통해 인가를 적용한 후 코드 및 DB 호출의 중복을 방지하기 위해 ThreadLocal에 관련 정보를 저장해서 비즈니스 로직으로 전달하는 코드까지 구현해보았다.
어느정도 AOP 에 대한 지식이 있다고 생각하고 있던 찰나, 최근 스프링 관련 강의를 들으며 공부를 하다보니 문득 궁금증이 생겼다.
'내가 구현한 AOP를 처리하는 프록시 객체는 뭘까?'
'어떤 건 프록시고 어떤 건 실제 객체네? 뭐지?'
최근 스프링의 원리를 확실하게 이해하기 위해 강의를 들으며, 궁금한 부분은 바로바로 조사하고 공부하는 습관이 들어있다.
이번에 공부한 내용을 기록으로 남겨두면 여러모로 도움이 많이 될 것 같아서, 관련 내용을 따로 정리해보았다.
SOLID 중 OCP 원칙을 준수AOP 는 기존 기능을 전혀 수정하지 않고, 로깅, 트랜잭션 등 부가기능을 추가AOP 가 제시한 패러다임을 수행할, 대리자 역할의 객체를 생성하여 사용@Component 로 등록된 스프링 빈은 원칙적으로 실제 객체@Component 를 포함하고 있는 @Controller , @Service 등도 마찬가지AOP 를 구현한 로직이 하나라도 존재할 경우, 실제 객체가 아닌 프록시 객체가 빈으로 등록@Transactional 또한 내부에서 AOP 를 구현@Transactional 이 하나라도 존재할 경우, 스프링 빈 등록 시 이를 탐지하여 타겟(실제 인스턴스)를 감싸고 있는 프록시 객체로 등록@Component 로 등록되어 있으므로, 싱글톤 객체로 빈에 등록@Configuration 으로 등록된 클래스는, 프록시 객체가 스프링 빈으로 등록CGLIB 라이브러리를 사용하여 바이트코드가 조작 된 프록시 객체@ComponentScan 진행 시, @Configuration 내의 모든 @Bean 을 찾아서 메서드명을 이름으로, 스프링 컨테이너에 스프링 빈으로 등록AOP 를 한 번이라도 구현한 경우 프록시 객체가 스프링 빈으로 등록@Transactional , @Aspect 등 AOP 가 적용된 경우AOP 에 등록된 코드를 수행 후 proceed() 로 타겟 메서드 실행@Transactional , @Aspect 등 AOP 가 적용되지 않은 경우AOP 를 적용하지 않고 바로 proceed() 로 타겟 메서드 실행@Configuration 으로 등록된 CGLIB 프록시 객체의 역할을 여기서 확인 가능@Configuration 의 @Bean 메서드를 모두 호출@Configuration 의 @Bean 메서드를 스프링 빈으로 등록@Configuration 의 가장 큰 역할은, 해당 클래스 내 모든 스프링 빈 객체의 싱글톤화💡
@Configuration또는@Bean이 없다면?
@Bean 만 단독으로 사용@Bean 메서드 자체가 호출 불가능@Configuration 만 단독으로 사용CGLIB 프록시 객체가 스프링 빈으로 등록@Bean 이 없으므로, 스프링 빈 등록 과정에서 메서드를 호출 불가능@Component + @Bean 조합으로 사용@Component 로 인해 실제 객체가 스프링 빈으로 등록@Bean 메서드 또한 호출 가능@Bean 메서드 내부에서 다른 빈의 주입이 필요할 경우, 해당 메서드를 매번 호출TypeA 라는 빈을 여러 @Bean 메서드에서 DI 할 경우, 매번 다른 TypeA 인스턴스가 생성AOP 적용 시 항상 JDK Dynamic Proxy 를 사용@EnableAspectJAutoProxy(proxyTargetClass = true) 설정을 해줌으로, 항상 CGLIB 프록시를 사용하도록 변경할 수 있음proxyTargetClass = true 가 기본값으로 설정되어, 사실상 JDK 동적 프록시는 사용되지 않음public interface UserService {
void findById(Long userId);
void findByUsername(String username);
}
@Service
public class UserServiceImpl implements UserService {
@Override
void findById(Long userId) {
// 구현
}
@Override
void findByUsername(String username) {
// 구현
}
void findByEmail(String email) {
// 구현
}
}
UserService 가 존재UserServiceImpl 이 존재@Service 로 이 클래스를 스프링 빈으로 등록userServiceImplUserServiceImplnew UserServiceImpl()AOP 미적용 시, 프록시 객체가 아닌 실제 인스턴스를 스프링 빈으로 등록@Service 가 붙어있는 UserserviceImpl 을 대상으로 스프링 빈 등록userServiceImplUserServiceUserService 구현 프록시 객체UserServiceImpl 의 경우, 인터페이스의 구현체AOP 를 적용하면, 반드시 JDK 동적 프록시에 의해 프록시 객체 생성🥷
UserServiceImplDI 🆚UserServiceDI
1) UserServiceImpl DI :
UserServiceImpl 에 대해 AOP 를 적용했더라도, UserServiceImpl 를 DI 받으면 실제 UserServiceImpl 인스턴스가 주입2) UserService DI:
UserServiceImpl 에 대해 AOP 를 적용하면, UserService 를 DI 받아서 UserService 내의 모든 기능은 AOP 가 적용UserService 인터페이스가 특정 인터페이스를 상속받고, 그 인터페이스는 다른 특정 인터페이스를 상속받는 구조라도, AOP 가 UserServiceImpl 을 대상으로 적용되면 UserService 내의 모든 기능에서는 AOP 가 적용📋 즉, JDK 동적 프록시를 사용하면 확장된 기능에 대한
AOP처리가 불가능하다.
AOP 적용 시, 해당 클래스 타입의 프록시 객체를 스프링 빈으로 등록UserService 관련 동일한 시나리오를 가정:userServiceImplUserServiceImplnew UserServiceImpl()@Component 클래스이므로, 동일하게 실제 객체 생성userServiceImplUserServiceImplUserServiceImpl 상속 프록시 객체CGLIB 는 빈 등록 시 모든 것을 클래스 기반으로 등록UserServiceImpl 를 DI 받을 수 있으므로, 해당 클래스의 모든 기능에 AOP 를 적용 가능JDK Dynamic Proxy 🆚 CGLIBAOP 를 구현체 대상으로 설정했을 때, 인터페이스 DI는 AOP 가 작동할 수 있지만 구현체 DI는 프록시 객체가 아닌 실제 구현체 인스턴스가 등록되므로 AOP 작동 불가능SOLID1) ISP 준수
AOP 적용을 위해서는 인터페이스 DI를 강제2) OCP 위반
AOP 를 적용하기 위해서는 앞서 말했듯, 인터페이스 DI가 강제AOP 적용 불가능AOP 를 적용하기 위해서는, 기존 인터페이스 수정이 필수불가결AOP 작동에 관해서 자유로움SOLID1) ISP , DIP 위반 가능
CGLIB 는 인터페이스 DI와 인터페이스 구현체 DI가 모두 가능ISP 와 DIP 를 준수AOP 적용을 위해 구현체 DI를 할 경우, ISP 와 DIP 를 위반AOP 적용 불가능 이라는 큰 단점을 해소 가능2) OCP 준수
CGLIB 는 프록시 객체가 구현체를 상속하기 때문에, 기능 확장 후 AOP 적용이 얼마든지 가능ISP 와 DIP 를 위반
CGLIB가 훨씬 유연한 방식을 제공하므로, 현재 스프링 2.0 이상 버전부터는 프록시 객체 사용 시CGLIB사용이 기본값으로 변경CGLIB를 사용함으로 얻을 수 있는 이점이 더 많다!
검수) Google Gemini ( https://gemini.google.com/app )