객체지향 설계에서 자주 등장하는 디자인 패턴 중 하나인 프록시(Proxy) 패턴과 데코레이터(Decorator) 패턴은 구조는 유사하지만 목적은 전혀 다른 패턴이다.
프록시 패턴이란?
프록시(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("Loading image: " + fileName);
}
@Override
public void display() {
System.out.println("Displaying image: " + fileName);
}
}
public class ProxyImage implements Image {
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
public class Main {
public static void main(String[] args) {
Image img = new ProxyImage("sample.jpg");
System.out.println("1. 프록시 객체 생성만 했을 때:");
// 아직 이미지 로딩 안 됨
System.out.println("\n2. display() 호출 시:");
img.display(); // 실제 이미지 로딩됨
System.out.println("\n3. 두 번째 display() 호출 시:");
img.display(); // 로딩 없이 바로 표시
}
}
실행결과
1. 프록시 객체 생성만 했을 때:
2. display() 호출 시:
Loading image: sample.jpg
Displaying image: sample.jpg
3. 두 번째 display() 호출 시:
Displaying image: sample.jpg
- RealImage 객체는 처음 display()가 호출될 때만 생성됨 → 지연 로딩(Lazy Loading)
- 클라이언트는 프록시인지 실제 객체인지 구분하지 않음 → 인터페이스 통일
- 프록시가 객체 생성을 제어함
프록시 패턴이 필요한 문제상황들
그럼 이 프록시 패턴을 사용하면 해결가능한 문제상황들은 무엇이 있을까?
1. 객체 생성 비용이 너무 클 때 (지연 로딩)
-> 이미지나 동영상 같이 대용량의 파일인 경우 필요할 때만 객체를 생성하기 위해 프록시 객체에서 먼저 요청을 받는다.
2. 접근 권한을 제어해야 할 때
-> 관리자만 접근 가능한 데이터의 경우 프록시 객체에서 사용자 인증을 거친 뒤 객체를 생성한다.
3. 객체에 접근한 기록이 필요할 때
-> 관리자 로그, api 호출 내역 등을 기록하고 싶을 때, 프록시 객체를 통해서 들어온 요청들을 기록한다.
데코레이터 패턴이란?
데코레이터(Decorator) 패턴은 기존 객체의 기능을 유지하면서 새로운 기능을 추가할 수 있는 디자인 패턴이다. 프록시처럼 같은 인터페이스를 구현하지만, 접근 제어 목적이 아니라 기능 확장을 위해 사용된다.
- 기존 코드를 변경하지 않고 기능을 추가하고 싶을 때
- 상속보다 합성(composition)을 활용하고 싶을 때
구현
public interface Image {
void display();
}
public class BasicImage implements Image {
private String fileName;
public BasicImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
System.out.println("Displaying image: " + fileName);
}
}
public abstract class Decorator implements Image {
protected Image image;
public Decorator(Image image) {
this.image = image;
}
@Override
public void display() {
image.display(); // 기본 동작을 위임
}
}
public class CaptionDecorator extends Decorator {
private String caption;
public CaptionDecorator(Image image, String caption) {
super(image);
this.caption = caption;
}
@Override
public void display() {
super.display(); // 원래 이미지 표시
System.out.println("→ Caption: " + caption); // 추가 기능
}
}
public class Main {
public static void main(String[] args) {
Image baseImage = new BasicImage("photo.jpg");
Image decorated = new CaptionDecorator(baseImage, "caption~");
System.out.println("[1] 기본 이미지:");
baseImage.display();
System.out.println("\n[2] 캡션 추가된 이미지:");
decorated.display();
}
}
프록시 패턴 vs 데코레이터 패턴
프록시 패턴과 데코레이터 패턴의 차이점은 아래 표와 같다.
항목 | 프록시 패턴 | 데코레이터 패턴 |
목적 | 접근 제어, 지연 로딩, 로깅 등 | 기능 확장 |
객체 생성 시점 | 요청 조건에 따라 생성 | 항상 생성됨 |
대표 예시 | Spring AOP, Hibernate Lazy Loading | Java I/O Stream, UI 컴포넌트 |
구조 | 대리 객체가 실제 객체 대신 동작 | 기능을 덧붙이는 래퍼 객체 |
'JAVA' 카테고리의 다른 글
[JAVA] 코드의 재사용을 상속(Inheritance)으로 모두 해결할 수 있을까? (4) | 2025.06.11 |
---|---|
[JAVA] 이터레이터 패턴(Iterator Pattern)이 없어도 될 것 같은데.. 왜 사용할까? (1) | 2025.06.10 |
[JAVA] 옵저버 패턴(Observer Pattern)이란? (0) | 2025.05.23 |
[JAVA] 전략 패턴(Strategy Pattern)이란? (1) | 2025.05.23 |
[JAVA] 팩토리 패턴(Factory Pattern)이란? (0) | 2025.05.22 |