목차
진짜에 곧바로 닿으면 곤란하다
고해상도 이미지를 다루는 코드가 있다. 이미지 객체를 만드는 순간 디스크에서 큰 파일을 읽어 온다.
Image image = new RealImage("huge.png"); // 만드는 즉시 무거운 로딩그런데 이 이미지를 화면에 안 띄울 수도 있다. 목록만 훑고 지나가면 그 무거운 로딩은 통째로 낭비다. 또 어떤 자원은 아무나 접근하면 안 되고, 어떤 건 매번 같은 걸 다시 계산하기 아깝다.
공통점은 하나다. 진짜 객체에 곧바로 닿기 전에, 뭔가를 걸러야 한다.
프록시 = 진짜 앞에 대리인을 세운다
프록시 패턴이 이걸 한다.
진짜 객체를 대신하는 대리인을 세워, 진짜로의 접근을 제어한다.
클라이언트는 진짜 대신 대리인에게 말을 건다. 대리인이 “지금 진짜를 만들까”, “이 사람이 접근해도 되나”, “캐시로 답할까”를 판단한 뒤에야 진짜에게 넘긴다. 진짜 앞에 문지기를 하나 세우는 것이다.
대리인도 같은 인터페이스다
프록시의 조건은 진짜와 같은 인터페이스를 갖는 것이다.
public interface Image {
void display();
}RealImage도 Image이고, 그 앞에 세우는 프록시도 Image다. 그래서 클라이언트는 자기가 쥔 게 진짜인지 대리인인지 모른다. 그냥 Image.display()를 부를 뿐이다. 대리인을 진짜인 척 끼워 넣을 수 있는 건 이 덕분이다.
지연 로딩: 진짜를 쓸 때 만든다
가장 흔한 프록시가 **지연 로딩(virtual proxy)**이다. 진짜를 미리 안 만들고, 실제로 필요할 때 만든다.
public class ImageProxy implements Image {
private final String file;
private RealImage real; // 아직 없다
public ImageProxy(String file) { this.file = file; }
@Override
public void display() {
if (real == null) {
real = new RealImage(file); // 처음 display할 때 진짜를 만든다
}
real.display();
}
}ImageProxy를 만들 땐 아무것도 안 읽는다. display()를 부르는 순간에야 RealImage가 생긴다. 끝내 안 부르면 무거운 로딩은 아예 안 일어난다.
접근 제어: 통과시킬지 대리인이 정한다
**접근 제어(protection proxy)**는 진짜에 닿기 전에 권한을 검사한다.
public class ProtectedImage implements Image {
private final Image real;
private final User user;
public ProtectedImage(Image real, User user) {
this.real = real; this.user = user;
}
@Override
public void display() {
if (!user.canView()) {
throw new AccessDeniedException(); // 막는다
}
real.display(); // 통과한 요청만 넘긴다
}
}대리인이 문지기가 되어, 자격이 되는 요청만 진짜에게 넘긴다. 진짜(RealImage)에는 권한 검사 코드가 한 줄도 없다. 그 책임을 프록시가 떠안는다.
그 밖의 대리인들
프록시는 “무엇을 걸러주느냐”에 따라 여러 종류가 있다.
| 종류 | 대리인이 하는 일 |
|---|---|
| 지연 로딩 | 진짜를 실제 쓸 때 만든다 |
| 접근 제어 | 권한을 검사하고 통과시킨다 |
| 캐싱 | 같은 요청은 저장해둔 결과로 답한다 |
| 원격 | 다른 서버의 객체를 로컬 객체처럼 대신한다 |
| 로깅·측정 | 호출을 기록하거나 시간을 잰다 |
전부 “진짜 앞에서 접근을 가로채 무언가 한다”는 뼈대는 같다.
데코레이터와 무엇이 다른가
구조가 데코레이터와 똑 닮아서 자주 헷갈린다. 둘 다 같은 인터페이스로 감싸고 위임한다. 다른 건 의도다.
- 데코레이터는 진짜의 동작에 기능을 더한다. 감싼 진짜는 늘 존재하고 늘 불린다.
- 프록시는 진짜로의 접근을 제어한다. 진짜를 안 만들 수도(지연), 안 부를 수도(권한 거부, 캐시 히트) 있다.
한 줄로 줄이면, 데코레이터는 “더 해준다”, 프록시는 “대신 지킨다”. 구조가 같아도 향하는 목적이 다르다.
실무: Spring이 프록시로 돌아간다
프록시는 실무 프레임워크의 뼈대다. Spring은 @Transactional·@Cacheable·@Async가 붙은 빈을 프록시로 감싼다. 메서드를 부르면 프록시가 먼저 받아 트랜잭션을 열거나 캐시를 확인한 뒤 진짜 메서드로 넘긴다(AOP).
그래서 나오는 유명한 함정이 있다. 같은 클래스 안에서 자기 메서드를 직접 부르면 프록시를 안 거쳐서 @Transactional이 안 먹는다. 프록시를 통과해야 부가 동작이 걸리는데, 내부 호출은 진짜를 곧장 부르기 때문이다.
JPA의 지연 로딩도 프록시다. 연관 엔티티를 진짜로 안 불러오고 프록시로 채워뒀다가, 실제 접근하는 순간 쿼리를 날린다. 앞의 이미지 프록시와 똑같은 발상이다.
정리
| 프록시란 | 진짜 앞에 대리인을 세워 접근을 제어한다 |
| 조건 | 진짜와 같은 인터페이스 - 클라이언트는 대리인인지 모른다 |
| 지연 로딩 | 진짜를 실제 쓸 때 만든다 (안 쓰면 안 만듦) |
| 접근 제어 | 권한을 검사해 통과한 요청만 넘긴다 |
| 데코레이터와 | 구조는 같고 의도가 다르다 - 기능 추가 vs 접근 제어 |
| 실무 | Spring AOP(@Transactional 등)·JPA 지연 로딩이 전부 프록시 |
진짜에 곧바로 닿게 두지 않고, 앞에 대리인을 세워 언제·누구에게·어떻게 닿을지 제어하는 것 - 그게 프록시다.