Java & Spring/Spring

Spring 프록시 원리

승주우에요 2026. 4. 3. 09:46

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