이 글은 아래 두 글과 함께 보면 더 좋습니다.
한 번에 글을 보고 싶으신 분은 아래를 참고해주세요.
프록시 패턴
프록시 패턴은 특정 객체를 대리하여 작업을 처리하는 프록시 객체를 제공하여 로직의 흐름을 제어하기 위한 패턴이다.
클라이언트와 실제 객체를 중계해주는 대리자 역할을 한다고 생각하면 된다.
구조
이번에도 아래 그림에 따라 각 요소에 대한 설명과 예제를 통해 설명해보겠다.
이번 예제에서는 자세한 설명을 위해 주문 도메인을 작성해보았다.
public class Order {
private int orderId;
private String ordername;
private int price;
public Order(int orderId, String ordername, int price) {
this.orderId = orderId;
this.ordername = ordername;
this.price = price;
}
public int getOrderId() {
return orderId;
}
public String getOrdername() {
return ordername;
}
public int getPrice() {
return price;
}
}
Subject
Subject
는 프록시 객체와 실제 객체의 기능을 구현할 인터페이스이다.
public interface OrderService{
void saveOrder(Order order);
Order findByOrderId(int orderId);
}
RealSubject
RealSubject
는 Subject를 상속받아 구현하는 실제 객체 클래스다.
실제 객체는 실제로 실행되어야 하는 로직을 구현하면 된다.
public class OrderServiceImpl implements OrderService {
private List<Order> orders = new Order<>();
public void saveOrder(Order order) {
order.add(reservation);
}
public Order findByOrderId(int orderId) {
for (Order order : orders) {
if (reservation.getReservationId() == reservationId) {
return reservation;
}
}
return null;
}
}
Proxy
Proxy
도 Subject를 상속받아 구현하는 클래스이다.
프록시 객체는 실제 객체를 포함하고 있어 실제 객체의 동작을 호출 할 수 있다.
public class OrderServiceProxy implements OrderService {
private OrderService orderService;
public OrderServiceProxy() {
}
@Override
public void saveOrder(Order order) {
// 프록시 객체 추가 동작
System.out.println("프록시 객체 - 저장 시작!");
// 가상 프록시 (지연 로딩)
if (orderService == null) {
orderService = new OrderServiceImpl();
System.out.println("프록시 객체 - 지연 로딩!");
}
// 실제 객체 호출
orderService.saveOrder(order);
// 프록시 객체 추가 동작
System.out.println("프록시 객체 - 저장 완료!");
}
@Override
public Order findByOrderId(int orderId) {
// 프록시 객체 추가 동작
System.out.println("프록시 객체 - 주문 검색!");
// 실제 객체 호출
return orderService.findByOrderId(orderId);
}
}
이를 실제로 실행하면 아래와 같은 결과를 얻을 수 있다.
public class Main {
public static void main(String[] args) {
OrderService orderserviceProxy = new OrderServiceProxy();
Order order = new Order(0, "베이컨 치즈 햄버거", 10500);
orderserviceProxy.saveOrder(order);
Order findOrder = orderserviceProxy.findByOrderId(order.getOrderId());
if (findOrder != null) {
System.out.println("주문 번호: " + findOrder.getOrderId() +
", 주문명: " + findOrder.getOrdername() +
", 가격: " + findOrder.getPrice());
} else {
System.out.println("주문 정보를 찾을 수 없습니다.");
}
}
}
프록시 객체 - 저장 시작!
프록시 객체 - 지연 로딩!
프록시 객체 - 저장 완료!
프록시 객체 - 주문 검색!
주문 번호: 0, 주문명: 베이컨 치즈 햄버거, 가격: 10500
위와 같이 프록시 패턴을 적용하면 작업 흐름을 조정하여 중간에 계층을 도입하여 여러 기능을 수행하거나 제어하며 실제 기능을 대신 호출해줄 수 있다.
위의 예제에서는 단순하게 프록시 객체에서 가상 프록시 작업(지연 로딩)과 로깅 작업만 진행하였지만, 실제로는 캐싱, 보안 등과 같은 추가 작업도 진행할 수 있다.
첨언하자면, Spring AOP에서는 트랜잭션 관리, 로깅, 보안과 같은 작업을 프록시에서 처리하게 한다.
이번에도 프록시 패턴이 적용되지 않았을 때에는 단순하게 아래와 같이 클라이언트가 구현 객체로 바로 접근하는 구조를 생각하면 된다.
public class Main {
public static void main(String[] args) {
OrderService oderservice = new OrderServiceImpl();
Order order = new Order(0, "베이컨 치즈 햄버거", 10500);
oderservice.saveOrder(order);
Order findOrder = oderservice.findByOrderId(order.getOrderId());
if (findOrder != null) {
System.out.println("주문 번호: " + findOrder.getOrderId() +
", 주문명: " + findOrder.getOrdername() +
", 가격: " + findOrder.getPrice());
} else {
System.out.println("주문 정보를 찾을 수 없습니다.");
}
}
}
특징
장점
- 실제 객체는 핵심 로직에만 집중하고 그 외의 부가기능은 프록시 객체에 위임할 수 있으므로 단일 책임 원칙(SRP)을 준수 할 수 있다.
- 실제 객체의 코드 수정 없이 부가기능을 추가할 수 있으므로 개방 폐쇄 원칙(OCP)를 준수 할 수 있다.
- 기존 기능을 수정하지 않고도 프록시 객체를 통해 부가기능을 수행할 수 있어 유용하다.
단점
- 만일 여러 클래스에 부가기능을 추가하고 싶은 상황이라면 비슷한 코드임에도 불구하고 적용하고 싶은 클래스에 대한 프록시 클래스를 만들어 대응해야 하므로 코드량이 많아지고 중복이 발생할 수 있다.
위와 같은 문제는 동적 프록시 방법을 사용하여 해결할 수 있다.
실제로 Spring 에서는 interface가 있을 시에는 JDK Dynamic Proxy
를 사용하며, 그렇지 않을 때에는 필요에 따라 CGLIB Proxy
를 사용한다고한다.
또한, Spring boot에서는 기본적으로 CGLIB Proxy
를 사용하고 있다.
이런 부분에 대한 자세한 내용은 아래 글에서 한 번 다뤄보았다.
참고
'오늘의 공부 > 디자인 패턴' 카테고리의 다른 글
데코레이터 패턴과 프록시 패턴의 차이 (2) | 2024.04.12 |
---|---|
데코레이터 패턴 (0) | 2024.04.12 |