[Java] 프록시 패턴(Proxy Pattern)이란? - 개념 및 예제
프록시 패턴(Proxy Pattern)
프록시(Proxy)는 대리자, 대변인이라는 뜻을 가진 단어다. 대리자/대변인은 다른 누군가를 대신해 그 역할을 수행하는 존재를 말한다. 따라서 프록시 패턴은 특정 객체의 대리자나 대변인 역할을 하는 프록시 객체를 제공하는 디자인 패턴이다.
프록시 패턴을 사용함으로써 클라이언트는 특정 객체를 직접 참조하여 접근하는 것이 아닌 프록시 객체를 통해 상호작용한다.
프록시 객체의 장단점
장점
- 접근 제어 : 클라이언트가 실제 객체에 직접 접근하지 않도록 제어하여 객체의 접근을 관리하고 권한 검사 등을 수행할 수 있다.
- 지연 초기화 : 실제 객체의 생성 및 초기화를 지연시키는 데 사용하여 필요한 순간만에 생성 및 초기화하여 성능을 최적화할 수 있다.
- 캐싱 : 결과를 캐싱하여 중복 계산을 피하고 성능을 향상시킬 수 있다.
- 유효성 검사 : 실제 객체에 접근하기 전에 데이터의 유효성 검사를 통해 검증할 수 있다.
- 원격 액세스 : 원격 프록시를 사용하여 다른 시스템에서 실행 중인 객체에 접근할 수 있으며 분산 시스템에서 객체 간 통신을 용이하게 한다.
단점
- 복잡성 증가 : 추가적인 객체를 도입하기 때문에 코드의 복잡성이 증가할 수 있다.
- 성능 저하 : 프록시 객체에 접근하는 데 추가적인 오버헤드가 발생할 수 있으며, 일부 성능 저하가 발생할 수 있다.
- 디자인 복잡성 : 프록시 패턴을 오용하면 코드를 과도하게 복잡하게 만들 수 있다.
프록시 패턴은 특정 객체에 대한 접근 제어, 지연 초기화, 유효성 검사, 로깅, 원격 액세스 등 특정 상황에서 유용하며 여러 가지 상황을 고려하여 도입하는 것이 중요하다. 잘못된 사용은 코드를 더 복잡하게 하며 성능 저하가 일어날 수 있다.
프록시 패턴의 구조
프록시 패턴은 클라이언트가 접근할 Subject와 이에 대한 구현체인 RealSubject, Proxy가 존재한다.
- Subject : Proxy와 RealSubject가 모두 구현하는 인터페이스로 클라이언트가 프록시와 실제 대상을 동일하게 다룰 수 있도록 정의한다.
- RealSubject : 클라이언트가 직접 상호작용하는 실제 객체다.
- Proxy : RealSubject를 감싸며 실제 작업을 수행하는 주체로 클라이언트와 RealSubject 사이에 위치한 중간 객체다. RealSubject의 같은 이름의 메서드를 호출하며, 클라이언트의 요청을 처리하기 전이나 후에 추가적인 작업을 수행할 수 있다.
프록시 패턴 예제
여행 예약 시스템이 있다고 가정하자.
Reservation.java
public class Reservation {
private int reservationId;
private String travelerName;
private String destination;
public Reservation(int reservationId, String travelerName, String destination) {
this.reservationId = reservationId;
this.travelerName = travelerName;
this.destination = destination;
}
public int getReservationId() {
return reservationId;
}
public String getTravelerName() {
return travelerName;
}
public String getDestination() {
return destination;
}
}
- 여행의 예약 정보를 포함하는 Reservation 클래스다. 예약 id와 여행자 이름, 목적지 등을 포함하고 있다.
- 여행을 예약하고 조회하기 위한 서비스를 프록시를 적용하지 않은 경우와 적용한 경우로 구현하여 비교해 보자.
프록시를 사용하지 않은 경우
ReservationService.java
import java.util.ArrayList;
import java.util.List;
public class ReservationService {
private List<Reservation> reservations = new ArrayList<>();
public void addReservation(Reservation reservation) {
reservations.add(reservation);
}
public Reservation findReservation(int reservationId) {
for (Reservation reservation : reservations) {
if (reservation.getReservationId() == reservationId) {
return reservation;
}
}
return null;
}
}
- addReservation 메서드를 통해 Reservation 객체를 예약 리스트에 추가하여 여행에 대한 예약을 할 수 있고, findReservation 메서드를 통해 예약된 예약 정보를 조회할 수 있는 간단한 서비스다.
Main.java
public class Main {
public static void main(String[] args) {
ReservationService reservationService = new ReservationService();
// 여행 예약 추가
Reservation reservation = new Reservation(1, "홍길동", "프랑스 파리");
reservationService.addReservation(reservation);
// 여행 예약 검색
Reservation findReservation = reservationService.findReservation(1);
if (findReservation != null) {
System.out.println("예약 번호: " + findReservation.getReservationId() +
", 여행자: " + findReservation.getTravelerName() +
", 목적지: " + findReservation.getDestination());
} else {
System.out.println("예약 정보를 찾을 수 없습니다.");
}
}
}
- 위 경우 클라이언트가 여행을 예약하거나 예약된 여행을 조회하기 위해 ReservationService 클래스를 접근하고 있다.
- 이는 클라이언트 코드가 직접 해당 객체에 접근하고 상호작용하는 것이다.
프록시를 사용하는 경우
ReservationServiceSubject.java
public interface ReservationServiceSubject {
void addReservation(Reservation reservation);
Reservation findReservation(int reservationId);
}
- 클라이언트가 접근할 Subject인 ReservationServiceSubject 인터페이스다.
ReservationServiceRealSubject.java
import java.util.ArrayList;
import java.util.List;
public class ReservationServiceRealSubject implements ReservationServiceSubject {
private List<Reservation> reservations = new ArrayList<>();
public void addReservation(Reservation reservation) {
reservations.add(reservation);
}
public Reservation findReservation(int reservationId) {
for (Reservation reservation : reservations) {
if (reservation.getReservationId() == reservationId) {
return reservation;
}
}
return null;
}
}
- 예약과 조회 기능이 구현되어 있는 ReservationServiceRealSubject 클래스로 ReservationServiceSubject 인터페이스를 구현한다.
- 앞에서 프록시를 사용하지 않는 경우인 ReservationService 클래스와 같은 기능을 한다.
ReservationServiceProxy.java
class ReservationServiceProxy implements ReservationServiceSubject {
private ReservationServiceRealSubject reservationService = new ReservationServiceRealSubject();
public void addReservation(Reservation reservation) {
// 여행 예약을 추가하기 전에 어떤 작업을 수행할 수 있음
reservationService.addReservation(reservation);
}
public Reservation findReservation(int reservationId) {
// 여행 예약을 조회하기 전에 어떤 작업을 수행할 수 있음
return reservationService.findReservation(reservationId);
}
}
- ReservationServiceProxy 클래스는 실제 서비스인 ReservationServiceRealSubject 클래스를 감싼 프록시 객체다.
- ReservationServiceRealSubject 객체를 가지고 있으며, 여행 예약이나 조회를 하기 전에 특정 작업을 수행하고 realService의 기능을 호출하여 실제 예약 및 조회를 한다.
- 주석 부분에는 여행을 예약 및 조회하기 위한 권한 제어, 입력 데이터의 유효성 검사 등을 수행할 수 있다.
ProxyMain.java
public class ProxyMain {
public static void main(String[] args) {
ReservationServiceProxy reservationService = new ReservationServiceProxy();
// 여행 예약 추가
Reservation reservation = new Reservation(1, "홍길동", "프랑스 파리");
reservationService.addReservation(reservation);
// 여행 예약 검색
Reservation findReservation = reservationService.findReservation(1);
if (findReservation != null) {
System.out.println("예약 번호: " + findReservation.getReservationId() +
", 여행자: " + findReservation.getTravelerName() +
", 목적지: " + findReservation.getDestination());
} else {
System.out.println("예약 정보를 찾을 수 없습니다.");
}
}
}
- 위 경우 클라이언트가 여행을 예약하거나 예약된 여행을 조회하기 위해 ReservationServiceProxy 클래스를 접근하고 있다.
- ReservationServiceProxy의 기능을 통해 특정 작업을 수행하고 예약하거나 조회할 수 있다.
- 이는 클라이언트 입장에서 요청이 ReservationServiceRealSubject에 접근하는지, 아니면 ReservationServiceProxy에 접근하는지 알 수 없다.
정리
프록시 패턴의 중요 포인트는 다음과 같다.
- 프록시는 실제 서비스와 같은 이름의 메서드를 구현하고, 이때 인터페이스를 사용한다.
- 프록시는 실제 서비스에 대한 참조 변수를 갖는다.(합성)
- 프록시는 실제 서비스의 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 돌려준다.
- 프록시는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있다.
프록시 패턴은 제어 흐름을 조정하기 위한 목적으로 중간에 계층을 도입하여 여러 기능을 추가하거나 제어하면서 실제 서비스를 대신 수행하는 패턴이다.
추가로 앞서 설명한 예제의 구조를 살펴보면, 개방 폐쇄 원칙(OCP)과 의존성 역전 원칙(DIP)를 준수하고 있다. 프록시 패턴은 OCP와 DIP를 준수하도록 설계된 패턴이기 때문이다.