Spring의 프록시(Proxy)가 뭔지 제대로 이해해보자
스프링을 공부하다 보면 "프록시"라는 말이 자꾸 나온다!
@Transactional도 프록시, @Cacheable도 프록시, @Configuration도 프록시...
도대체 프록시가 뭔지, 왜 쓰는 건지 알아보자!
프록시가 뭘까?
프록시를 한 마디로 표현하면 "진짜 객체 앞에 서서 호출을 가로채는 대리인" 이다.
일상적인 비유로 설명하면 이렇다.
팬 → 매니저(프록시) → 연예인(진짜 객체)
팬이 연예인한테 직접 연락하는 게 아니라 매니저를 통해서 연락한다.
매니저는 중간에서 "이 스케줄은 안 돼", "이건 전달할게" 같은 처리를 할 수 있다.
코드로 보면 이렇다.
// 프록시 없이 직접 호출
UserService service = new UserService();
service.createUser(); // 그냥 바로 실행됨
// 프록시가 있으면
public class UserServiceProxy extends UserService {
@Override
public void createUser() {
System.out.println("앞에서 뭔가 처리"); // 전처리
super.createUser(); // 진짜 실행
System.out.println("뒤에서 뭔가 처리"); // 후처리
}
}
호출자 입장에서는 그냥 createUser()를 호출했을 뿐인데,
실제로는 프록시가 앞뒤로 뭔가를 처리하고 진짜 객체한테 넘겨주는 구조다.
@Transactional이 어떻게 동작하냐면
@Service
public class UserService {
@Transactional
public void createUser() {
// DB 저장 로직
}
}
우리가 @Transactional만 붙이면 트랜잭션이 알아서 걸리는데, 어떻게?
Spring이 내부적으로 이런 프록시를 만들어준다.
public class UserServiceProxy extends UserService {
@Override
public void createUser() {
트랜잭션 시작();
try {
super.createUser(); // 진짜 로직
트랜잭션 커밋();
} catch (Exception e) {
트랜잭션 롤백();
}
}
}
개발자는 @Transactional 애노테이션만 붙였을 뿐인데,
Spring이 프록시를 만들어서 트랜잭션 처리를 대신 해주는 것이다.
이게 Spring AOP의 핵심 원리다.
프록시를 만드는 방법 2가지
Spring에서 프록시를 만드는 방식은 두 가지다.
1. CGLIB (클래스 상속 방식)
// 진짜 클래스
public class UserService { ... }
// CGLIB이 만드는 프록시 → 상속으로 감쌈
public class UserService$$CGLIB extends UserService {
@Override
public void createUser() {
// 가로채서 처리
}
}
클래스를 상속해서 메서드를 오버라이드하는 방식이다.
인터페이스가 없어도 된다는 게 가장 큰 장점이다.
2. JDK Dynamic Proxy (인터페이스 방식)
// 인터페이스
public interface UserService {
void createUser();
}
// JDK 프록시 → 같은 인터페이스를 구현
public class UserServiceProxy implements UserService {
private UserService target; // 진짜 객체를 내부에 들고 있음
public void createUser() {
트랜잭션 시작();
target.createUser(); // 진짜 객체한테 위임
트랜잭션 커밋();
}
}
인터페이스를 구현해서 만드는 방식이다.
인터페이스가 반드시 있어야 한다.
비교
CGLIB JDK Dynamic Proxy
| 방식 | 클래스 상속 | 인터페이스 구현 |
| 인터페이스 필요 여부 | 없어도 됨 | 반드시 있어야 함 |
| Spring Boot 기본값 | ✅ | ❌ |
| 제약사항 | final 클래스/메서드 불가 | 인터페이스 없으면 불가 |
Spring Boot 2.x 이후로는 기본값이 CGLIB이다.
인터페이스 없는 클래스도 많고, 개발자가 신경 안 써도 되게 하려는 이유에서다.
final이 왜 문제가 되냐면
CGLIB은 상속으로 프록시를 만드는데, final 클래스는 상속이 안 된다.
public final class UserService { // final = 상속 불가!
public void createUser() { ... }
}
// CGLIB이 이렇게 하려고 하는데... 아예 생성이 안된다!
public class UserService$$CGLIB extends UserService { // 의도한대로 동작하지 않는다!
}
메서드가 final이어도 마찬가지다.
public class UserService {
@Transactional
public final void createUser() { // final = 오버라이드 불가!
...
}
}
프록시가 메서드를 오버라이드해서 가로채야 하는데, final이면 못 하니까
@Transactional이 조용히 무시된다. 에러도 안 나고 그냥 트랜잭션 없이 실행됨.
그래서 Spring Bean으로 쓸 클래스에는 final을 함부로 붙이면 안 될 것 같음 !!
모든 Bean이 프록시는 아니다
흔한 오해 중 하나인데, 모든 Spring Bean이 프록시로 동작하는 건 아니다.
부가기능이 필요한 Bean만 프록시 O
아무것도 없는 일반 Bean은 프록시 X
// 이건 프록시 생김
@Service
@Transactional
public class UserService { ... }
// 이건 프록시 없음, 진짜 객체 그대로 사용
@Service
public class UserService { ... }
확인하고 싶으면 이렇게 해보면 된다.
@Autowired
UserService userService;
System.out.println(userService.getClass());
// 프록시면 → class com.example.UserService$$SpringCGLIB$$0
// 일반 Bean이면 → class com.example.UserService
정리
- 프록시는 진짜 객체를 감싸서 호출을 가로채는 대리 객체다.
- @Transactional, @Cacheable, @Async 같은 애노테이션들이 프록시로 동작한다. (내부 처리가 필요할 때)
- 프록시를 만드는 방식은 CGLIB(상속) 과 JDK Dynamic Proxy(인터페이스) 두 가지다.
- Spring Boot는 기본으로 CGLIB을 사용한다.
- final 클래스나 메서드에는 프록시가 안 만들어지므로 의도한대로 동작하지 않을 가능성이 커서 주의해야 한다.
- 모든 Bean이 프록시는 아니고, 부가기능이 필요한 Bean만 프록시로 감싼다.
'Java & Spring > Spring' 카테고리의 다른 글
| OSIV · 낙관적 락 · 비관적 락 (0) | 2026.05.09 |
|---|---|
| Spring Batch 개념, 사용 (0) | 2026.04.29 |
| 페이지네이션의 원리? 필요성? (0) | 2026.04.10 |
| [MapStruct] 내부 구현 방식 - 설계 문제 해결 (0) | 2026.04.09 |
| MapStruct (0) | 2026.03.22 |