이전에 Proxy가 무엇인지 자세히 한번 알아보았었다.
오늘은 AOP의 기본 원리인 프록시 패턴과 데코레이터 패턴의 차이를 명확하게 이해하고 Spring이 프록시를 만드는 2가지 방법인 JDK Dynamic Proxy, CGLIB, 그리고 AOP의 한계를 이해할 수 있는 @Transactional이 동작하지 않는 케이스들을 통해 알아볼 것이다
Proxy는 "객체를 감싸는"패턴이라고도 이야기 한다.
그러나 "객체를 감싸는" 패턴은 하나가 아니다.
바로 데코레이터 패턴이 있으며 이 데코레이터 패턴과 프록시 패턴의 차이점을 확인해보자.
이전 프록시 패턴의 목적은 핵심 로직외의 부가적인 작업들에 대한 중복 작성을 피하기 위함이라고 했다.
그리고 Proxy는 이러한 부가적인 작업을 이용한 몇가지의 목적을 더 가지고 있다.
원본 객체로의 직접 접근을 막고 제어
아래의 코드는 이미지 로딩 시나리오에 대입하여 접근제어, 지연로딩에 대한 예시를 보여주고 있다.
public interface Image {
void display();
}
// 진짜 객체 (무거움)
public class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk(); // 디스크에서 로딩 (느림)
}
private void loadFromDisk() {
System.out.println("디스크에서 " + filename + " 로딩 중...");
}
@Override
public void display() {
System.out.println(filename + " 표시");
}
}
// 프록시 (접근 제어)
public class ImageProxy implements Image {
private String filename;
private RealImage realImage; // 실제 객체는 필요할 때만 생성
public ImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // 지연 로딩
}
realImage.display();
}
}
// 사용
Image image = new ImageProxy("photo.jpg");
// 이 시점엔 아직 디스크에서 안 읽음
image.display(); // 이 시점에 디스크에서 읽음
여기서 핵심은 원본 객체에 대한 대리 역할이다. 이 예시에서는 접근 제어(지연 로딩)를 위해 사용되었다.
데코레이터의 패턴의 목적은 기능 추가 및 확장인데 이름에서 짐작 가능한데 기존 객체를 꾸며주는 것이다.
커피로 예를 들어보자
public interface Coffee {
int cost();
String description();
}
// 기본 커피
public class SimpleCoffee implements Coffee {
@Override
public int cost() {
return 1000;
}
@Override
public String description() {
return "커피";
}
}
// 데코레이터 1: 우유 추가
public class MilkDecorator implements Coffee {
private Coffee coffee;
public MilkDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public int cost() {
return coffee.cost() + 500; // 기능 추가
}
@Override
public String description() {
return coffee.description() + " + 우유"; // 기능 추가
}
}
// 데코레이터 2: 시럽 추가
public class SyrupDecorator implements Coffee {
private Coffee coffee;
public SyrupDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public int cost() {
return coffee.cost() + 300; // 기능 추가
}
@Override
public String description() {
return coffee.description() + " + 시럽"; // 기능 추가
}
}
// 사용
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SyrupDecorator(coffee);
System.out.println(coffee.description()); // "커피 + 우유 + 시럽"
System.out.println(coffee.cost()); // 1800
핵심: "원본 기능을 유지하면서 무언가를 추가한다"
정답은 둘 다 사용한다.
Spring AOP = 프록시 패턴 + 데코레이터 패턴
이라고 볼 수 있다.
일단 프록시를 만드는 방법 JDK Dynamic Proxy와 CGLIB 두가지를 살펴보자
프록시 원본 객체
// 인터페이스
public interface UserService {
void createUser(User user);
}
// 구현체
@Service
public class UserServiceImpl implements UserService {
@Override
public void createUser(User user) {
System.out.println("사용자 생성: " + user.getName());
}
}
JDK Dynamic Proxy 생성
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class}, // 인터페이스
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
System.out.println("메서드 실행 전");
Object result = method.invoke(realService, args);
System.out.println("메서드 실행 후");
return result;
}
}
);
특징:
인터페이스가 아닌 일반 클래스
@Service
public class UserService {
public void createUser(User user) {
System.out.println("사용자 생성: " + user.getName());
}
}
CGLIB 프록시 생성:
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class); // 클래스 상속
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
System.out.println("메서드 실행 전");
Object result = proxy.invokeSuper(obj, args);
System.out.println("메서드 실행 후");
return result;
}
});
UserService proxy = (UserService) enhancer.create();
특징:
Spring Boot 2.0 이후 (기본값)
CGLIB를 기본으로 사용
@Service
public class UserService { // 인터페이스 없음
@Transactional
public void createUser(User user) {
// ...
}
}
// Spring이 CGLIB으로 프록시 생성
UserService$$EnhancerBySpringCGLIB$$12345
혹시 인터페이스가 있으면 JDK DynamicProxy 사용하나?
//Spring Boot 2.0 이상
public interface UserService {
void createUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
@Transactional
@Override
public void createUser(User user) {
// ...
}
}
응 아니야
2.0 이후에는 CGLIB를 사용한다
강제로 JDK DynamicProxy를 사용하는 방법이 있긴한데
굳이 지금 알아둘 필요는 없을 것 같다.
@Service
public class UserService {
@Transactional // ❌ 안 먹힘!
private void createUser(User user) {
userRepository.save(user);
}
}
CGLIB은 상속 기반이므로 private 메소드는 오버라이드 불가능하다. Transactional을 사용하려면 public을 사용하자
@Service
public class UserService {
@Transactional // ❌ 안 먹힘!
public final void createUser(User user) {
userRepository.save(user);
}
}
마찬가지로 final 메소드는 오버라이드가 불가능하다 final을 제거하도록 하자.
@Service
public class UserService {
public void outerMethod() {
innerMethod(); // ❌ 프록시를 안 거침!
}
@Transactional
public void innerMethod() {
userRepository.save(user);
}
}
내부 메소드는 원본을 호출하므로 트랜잭션 범위에 포함되지 않는다.
/ 프록시
public class UserService$$Proxy extends UserService {
@Override
public void outerMethod() {
super.outerMethod(); // 원본 호출
}
@Override
public void innerMethod() {
// 트랜잭션 시작
super.innerMethod();
// 트랜잭션 종료
}
}
// 원본
public class UserService {
public void outerMethod() {
this.innerMethod(); // ← this = 원본 객체, 프록시 아님!
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserInternalService internalService;
public void outerMethod() {
internalService.innerMethod(); // ✅ 프록시 거침
}
}
@Service
public class UserInternalService {
@Transactional
public void innerMethod() {
userRepository.save(user);
}
}
@Service
public class UserService {
@Autowired
private UserService self; // 자기 자신 (프록시)
public void outerMethod() {
self.innerMethod(); // ✅ 프록시 거침
}
@Transactional
public void innerMethod() {
userRepository.save(user);
}
}
당연히 Bean이 아니면 프록시 생성이 안된다.
public class UserService { // ← @Service 없음!
@Transactional // ❌ 안 먹힘!
public void createUser(User user) {
userRepository.save(user);
}
}
@Service
public class UserService {
@Transactional // ❌ 롤백 안 됨!
public void createUser(User user) throws Exception {
userRepository.save(user);
throw new Exception("에러 발생"); // Checked Exception
}
}
@Transactional은 기본적으로 RuntimeException만 롤백한다.