프록시 패턴 — 객체 생성을 지연시키는 구조
프록시 패턴은 어떤 객체에 대한 접근을 다른 객체가 대신 제어하는 구조다.
클라이언트는 실제 객체를 직접 다루지 않고, 대리 객체(Proxy) 를 통해 간접적으로 접근한다.
이 패턴은 다음과 같은 상황에서 자주 사용된다.
- 객체 생성 비용이 매우 클 때
- 실제 객체 사용이 항상 필요한 것이 아닐 때
- 접근 시점에 부가 로직을 끼워 넣고 싶을 때
Hibernate의 지연 로딩(Lazy Loading) 이 대표적인 실사용 사례다.
예제로 볼 프록시 구조
이번 예제는 이미지 로딩을 예로 든 전형적인 프록시 패턴이다.
역할 분리
역할클래스
| 공통 인터페이스 | Image |
| 실제 객체 (Target) | RealImage |
| 프록시 객체 | ProxyImage |
| 클라이언트 | ProxyMain |
공통 인터페이스
package com.example.demo.proxy;
// HibernateProxy
public interface Image {
void display();
}
- 클라이언트는 인터페이스만 알고
- 실제 구현체가 프록시인지, 진짜 객체인지는 모른다
실제 객체 (Target)
package com.example.demo.proxy;
// Target Entity
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);
}
}
중요한 포인트
- 생성자에서 디스크 로딩 작업 수행
- 즉, new RealImage() 자체가 비싼 연산
이 객체는 필요할 때만 생성되어야 하는 객체다.
프록시 객체
package com.example.demo.proxy;
// Entity$HibernateProxy
public class ProxyImage implements Image {
private String fileName;
private RealImage realImage;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
if (realImage == null) { // LazyInitializer.initialize()
realImage = new RealImage(fileName);
}
System.out.println("프록시가 요청 가로 챔 -> ");
realImage.display();
}
}
여기서 프록시가 하는 일
- RealImage를 바로 생성하지 않는다
- display() 호출 시점에만
- 실제 객체가 없으면 생성
- 이후부터는 동일 객체 재사용
이 구조가 바로 지연 초기화(Lazy Initialization) 다.
Hibernate의 내부 프록시 클래스도 개념적으로 동일하다.
클라이언트 코드
package com.example.demo.proxy;
public class ProxyMain {
public static void main(String[] args) {
Image image = new ProxyImage("photo.png");
System.out.println("-- 첫번째 호출 --");
image.display();
System.out.println("-- 두번째 호출 --");
image.display();
}
}
중요한 점
- 클라이언트는 Image 타입만 사용
- 프록시인지, 실제 객체인지 전혀 신경 쓰지 않는다
실행 결과 분석
-- 첫번째 호출 --
디스크에서 파일 로딩 중photo.png
프록시가 요청 가로 챔 ->
이미지 출력photo.png
-- 두번째 호출 --
프록시가 요청 가로 챔 ->
이미지 출력photo.png
흐름 해석
첫 번째 display()
- realImage == null
- 프록시가 실제 객체 생성
- 디스크 로딩 발생
- 실제 로직 위임
두 번째 display()
- 이미 생성된 객체 존재
- 디스크 접근 없음
- 바로 실제 객체로 위임
“생성은 한 번, 사용은 여러 번”
Hibernate 지연 로딩과의 정확한 대응
이 예제는 Hibernate의 다음 구조와 1:1로 대응된다.
예제Hibernate
| Image | 엔티티 타입 |
| ProxyImage | Entity$HibernateProxy |
| realImage == null | LazyInitializer |
| display() 호출 | 엔티티 필드 접근 |
Hibernate는 엔티티를 조회할 때 실제 엔티티 대신
프록시 객체를 먼저 반환하고,
- 필드 접근
- 메서드 호출
- 연관 객체 접근
같은 실제 데이터가 필요한 순간에만 SQL을 날린다.
프록시 패턴의 본질 요약
프록시 패턴의 핵심은 기능이 아니라 “시점 제어”다.
- 객체를 미리 만들지 않는다
- 필요해질 때 만든다
- 접근 전/후에 로직을 끼워 넣을 수 있다
- 클라이언트는 이를 인지하지 않는다
그래서 프록시는 단순한 디자인 패턴을 넘어서
ORM, AOP, 보안, 캐시, 원격 호출 전반에서 쓰인다.
프록시 패턴은
객체 자체를 감추는 패턴이 아니라, 객체가 언제 개입할지를 통제하는 패턴이다.