프록시 패턴은 대상 원본 객체를 대리하여 대신 처리하게 함으로써 로직의 흐름을 제어하는 행동 패턴입니다.
프록시의 사전적 의미는 대리인이라는 뜻으로 누군가에게 어떤 일을 대신 시키는 것을 의미합니다.
이를 객체 지향 프로그래밍에 접목해보면 클라이언트가 대상 객체를 직접 쓰는 것이 아니라 중간에 프록시를 거쳐서 쓰는 코드 패턴 이라고 할 수 있습니다.
그래서 대상 객체의 메서드를 직접 실행하는 것이 아닌, 대상 객체에 접근하기 전에 프록시 객체의 메서드를 접근한 후 추가적인 로직을 처리하고 난 뒤에 접근하게 됩니다.
프록시는 원래 객체에 대한 접근을 제어하므로, 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 합니다.
대상 클래스가 민감한 정보를 가지고 있거나 인스턴스화 하기에 무겁거나 추가기능을 가미하고 싶은데, 원본 객체를 수정할 수 없는 상황일 때를 극복하기 위해서 사용합니다.
📧 얻을 수 있는 효과!!
1️⃣ 보안 : 프록시는 클라이언트가 작업을 수행할 수 있는 권한이 있는지 확인하고 검사 결과가 긍정적인 경우에만 요청을 대상으로 전달합니다.
2️⃣ 캐싱 : 프록시가 내부 캐시를 유지하여 데이터가 캐시에 아직 존재하지 않는 경우에만 대상에서 작업이 실행되도록 합니다.
3️⃣ 데이터 유효성 검사 : 프록시가 입력을 대상으로 전달하기 전에 유효성을 검사합니다.
4️⃣ 지연 초기화 : 대상의 생성 비용이 비싸다면 프록시는 그것을 필요로 할 때까지 연기할 수 있습니다.
5️⃣ 로깅 : 프록시는 메서드 호출과 상대 매개 변수를 인터셉트하고 이를 기록합니다.
6️⃣ 원격 객체 : 프록시는 원격 위치에 있는 객체를 가져와서 로컬처럼 보이게 할 수 있습니다.

프록시는 다른 객체에 대한 접근을 제어하는 객체라고 했습니다. 여기서 다른 객체는 Subject 입니다. 프록시와 대상은 동일한 인터페이스를 가지고 있으며 이를 통해 다른 인터페이스와 완전히 호환되도록 바꿀 수 있습니다.
❗ Subject : Proxy와 RealSubject를 하나로 묶는 인터페이스(다형성)
❗ RealSubject : 원본 대상 객체입니다.
❗ Proxy : 대상 객체(RealSubject)를 중계할 대리자 역할.
❗ Client : Subject 인터페이스를 이용하여 프록시 객체를 생성해 이용합니다.
interface ISubject {
void action();
}
class RealSubject implements ISubject {
public void action() {
System.out.println("원본 객체 액션 !!");
}
}
class Proxy implements ISubject {
private RealSubject subject; // 대상 객체를 composition
Proxy(RealSubject subject) {
this.subject = subject;
}
public void action() {
subject.action(); // 위임
/* do something */
System.out.println("프록시 객체 액션 !!");
}
}
class Client {
public static void main(String[] args) {
ISubject sub = new Proxy(new RealSubject());
sub.action();
}
}
이외에 가상 프록시, 보호 프록시, 로깅 프록시, 원격 프록시, 캐싱 프록시가 있습니다.
☀️ 패턴 사용 시기
접근 제어 기능을 추가하고 싶은데, 기존의 특정 객체를 수정할 수 없는 상황일 때.
초기화 지연, 접근 제어, 로깅, 캐싱 등 기존 객체 동작에 수정 없이 가미하고 싶을 때.
☀️ 패턴 장점
개방 폐쇄 원칙 준수 => 기존 대상 객체의 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
단일 책임 원칙 => 대상 객체는 자신의 기능에만 집중하고, 기 이외 구바 기능을 제공하는 역할을 프록시 객체에 위임하여 다중 책임을 회피할 수 있습니다.
원래 하려던 기능을 수행하며 그외의 부가적인 작업들을 수행하는 데 용이합니다.
클라이언트는 객체를 신경쓰지 않고, 서비스 객체를 제어하거나 생명 주기를 관리할 수 있습니다.
사용자 입장에서는 프록시 객체나 실체 객체나 사용법은 유사하므로 사용성에 문제가 되지 않습니다.
☀️ 패턴 단점

사용자가 선택한 이미지만 로드해서 렌더링하도록 대상 객체를 제어하면 됩니다.
// 대상 객체와 프록시 객체를 묶는 인터페이스 (다형성)
interface IImage {
void showImage(); // 이미지를 렌더링하기 위해 구현체가 구현해야 하는 추상메소드
}
// 대상 객체 (RealSubject)
class HighResolutionImage implements IImage {
String img;
HighResolutionImage(String path) {
loadImage(path);
}
private void loadImage(String path) {
// 이미지를 디스크에서 불러와 메모리에 적재 (작업 자체가 무겁고 많은 자원을 필요로함)
try {
Thread.sleep(1000);
img = path;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s에 있는 이미지 로딩 완료\n", path);
}
@Override
public void showImage() {
// 이미지를 화면에 렌더링
System.out.printf("%s 이미지 출력\n", img);
}
}
class ImageProxy implements IImage {
private IImage proxyImage;
private String path;
ImageProxy(String path) {
this.path = path;
}
@Override
public void showImage() {
// 고해상도 이미지 로딩하기
proxyImage = new HighResolutionImage(path);
proxyImage.showImage();
}
}
class ImageViewer {
public static void main(String[] args) {
IImage highResolutionImage1 = new ImageProxy("./img/고해상도이미지_1");
IImage highResolutionImage2 = new ImageProxy("./img/고해상도이미지_2");
IImage highResolutionImage3 = new ImageProxy("./img/고해상도이미지_3");
highResolutionImage2.showImage();
}
}
대상 객체 대신에 프록시 객체에 할당했다는 점만 다를 뿐입니다.
프록시 사용하지 않은 방식.
class ImageViewer {
public static void main(String[] args) {
HighResolutionImage highResolutionImage1 = new HighResolutionImage("./img/고해상도이미지_1");
HighResolutionImage highResolutionImage2 = new HighResolutionImage("./img/고해상도이미지_2");
HighResolutionImage highResolutionImage3 = new HighResolutionImage("./img/고해상도이미지_3");
highResolutionImage2.showImage();
}
}
프록시 객체 내에서 경로 데이터를 지니고 있다가 사용자가 showImage를 호출하면 그때서야 대상 객체를 LazyLoad하여, 이미지를 메모리에 적재하고 대상 객체의 showImage() 메서드를 위임한 후 호출함으로써, 실제 메서드를 호출하는 시점에 메모리 적재가 이루어지기 때문에 불필요한 자원 낭비가 발생하지 않게 됩니다.
프록시 패턴을 사용하지 않는다면 직접 객체를 생성하고 사용합니다. 메모리 적재는 객체가 생성되는 시점에 즉시 발생합니다.
즉 지연 로딩의 이점을 사용하지 않습니다.

보안을 위해 데이터 접근을 직책 단위로 세분화 하고자 할 때 사원은 자신 직책과 같은 사원들 정보만 열람 가능하고 그 위의 계급은 열람이 불가능하다고 할 때 프록시가 없다면 어느 누구든지 PrintEmployeeInfo 객체를 통해 `printAllInfo() 메서드를 실행시켜 모든 직원의 리스트를 볼 수 있습니다.
ProtectedEmployee가 없을 경우입니다.우선 대상 객체와 프록시 객체를 모두 묶어주는 인터페이스를 선언해줍니다.
// 구성원 인터페이스
interface IEmployee {
String getName(); // 구성원의 이름
RESPONSIBILITY getGrade(); // 구성원의 직책
String getInfo(IEmployee viewer); // 구성원의 인사정보
}
Employee 클래스 타입으로 받은 모든 변수와 매개변수의 타입을 인터페이스인 IEmployee로 재설정해줍니다.
Employee 로 받았습니다.
PrintEmployee도 마찬가지로 재설정해줍니다.
이제 프록시 클래스를 설정하면 되는데 대상 객체의 getInfo() 메서드의 제어 로직을 추가하고 대상 객체의 메서드를 위임 호출해줌으로써 보호 프록시를 구성할 수 있게 됩니다.
// 보호 프록시 : 인사정보가 보호된 구성원 (인사 정보 열람 권한 없으면 예외 발생)
class ProtectedEmployee implements IEmployee {
private IEmployee employee;
public ProtectedEmployee(IEmployee employee) {
this.employee = employee;
}
@Override
public String getInfo(IEmployee viewer) {
RESPONSIBILITY position = this.employee.getGrade(); // 조회 당하는 자의 직책을 얻기
// 매개변수로 받은 조회자의 직책에 따라 출력을 제어
switch (viewer.getGrade()) {
case DIRECTOR:
// 부사장은 과장, 사원들을 볼 수 있다.
return this.employee.getInfo(viewer);
case MANAGER:
// 과장은 같은 직무와 그 아래 사원들을 볼 수 있다. 사장 정보는 볼 수 없다.
if (position != RESPONSIBILITY.DIRECTOR) {
return this.employee.getInfo(viewer);
}
case STAFF:
// 사원은 같은 직무인 사람들만 볼 수 있다. 과장, 사장 정보는 볼 수 없다.
if (position != RESPONSIBILITY.DIRECTOR && position != RESPONSIBILITY.MANAGER) {
return this.employee.getInfo(viewer);
}
default: return "다른 사람의 인사정보를 조회 할수 없습니다";
}
}
@Override
public String getName() {
return employee.getName();
}
@Override
public RESPONSIBILITY getGrade() {
return employee.getGrade();
}
}
위임 호출한 부분은 프록시 객체가 실제 객체의 메서드를 호출하는 부분입니다. 프록시 객체가 요청을 받아 처리할 때 실제 객체의 메서드를 대신 호출하여 결과를 반환하는 것을 의미합니다.
getInfo가 위임 호출한 부분.위에서 프록시 패턴을 사용하면 여러 장점들이 있었는데 대상 원본 클래스 수만큼 일일이 프록시 클래스를 하나하나 생성해줘야 해서 코드가 복잡하다는 단점이 있었습니다.
이러한 단점들을 보완하여 컴파일 시점이 아닌 런타임 시점에 프록시 클래스를 만들어주는 방식이 JVM에서 공식적으로 지원하는 🖇️ 동적 프록시 입니다.
동적 프록시는 애플리케이션 실행 도중 java.lang.reflect.Proxy 패키지에서 제공해주는 API를 이용하여 동적으로 프록시 인스턴스를 만들어 등록하는 방법으로서, 🖇️ 자바의 Reflection API 기법을 응용한 연장선의 개념입니다.
스프링 프레임워크에서는 내부적으로 프록시 기술을 많이 사용합니다. (AOP, JPA 등)
스프링에서는 Bean을 등록할 때 싱글톤을 유지하기 위해 동적 프록시 기법을 이용해 프록시 객체를 Bean으로 등록합니다.
또한 Bean으로 등록하려는 기본적으로 객체가 Interface를 하나라도 구현하고 있으면 JDK를 이용하고 Interface를 구현하고 있지 않으면 내장된 CGLIB 라이브러리를 이용합니다.
🖇️
🖇️ https://velog.io/@winsome_joo/%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0-%ED%8C%A8%ED%84%B4
🖇️ https://velog.io/@wjd15sheep/%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4-Strategy
🖇️ 프록시 패턴 완벽 정리