
한국어로는 "대리인"이라는 뜻
대상 객체로의 접근을 중간에서 대신 중개하여 로직에 흐름을 제어하거나
특정 작업을 하도록 설계하는 개발 방법이다.
여기에서 어떤 오브젝트라 함은, 매우 넓은 의미인데
말 그대로 Object, Server, Network 등 다양하다.
이 포스팅에서는 Spring에서의 Proxy 적용 방법과 쓰임을 정리해보고자 한다.
직접 Proxy 객체를 구현하기보다는,
Library나 Framework에서 제공하는 Proxy 기능을 사용해 본 경험이 더 많을 것이다.
예를 들어 @Transactional 어노테이션이 있습니다.
만약 이를 사용하지 않는다면, 개발자는 모든 비즈니스 로직마다 직접 트랜잭션 코드를 작성해야 한다.
하지만 @Transactional을 적용하면, Spring 컨테이너가 Proxy 객체를 생성하여 트랜잭션을 관리한다.
덕분에 기존 코드를 수정하지 않고도 트랜잭션 기능을 손쉽게 추가할 수 있다.
이는 SOLID원칙 중 개방-폐쇄 원칙 과 관련이 있다.
만약 Proxy를 사용하지 않는다면,
기존 비즈니스 로직에 기능을 추가할 때 새로운 기능이 주는 이점과 기존 코드를 수정함으로써 생기는 리스크
를 모두 고려햐야하는 상황이 발생한다.
그러나 Proxy를 사용하여 기존 비즈니스 로직을 감싸는 Proxy 객체에 기능을 추가한다면
기존 객체를 수정해야 하는 리스크를 피하면서도, 기능을 확장할 수 있다.
따라서 확장에는 열려(Open) 있고, 수정에는 닫혀(Close) 있는 구조를 구현할 수 있게된다.
public class PaymentService implements Payment {
@Override
public void confirm() {
System.out.println("[PaymentService.java - confirm()] : 결제완료!");
}
@Override
public void cancel() {
System.out.println("[PaymentService.java - cancel()] : 결제취소!");
}
}
package org.practice.proxy;
public interface Payment {
void confirm();
void cancel();
}
public class PaymentBaseProxy implements Payment {
private final PaymentService paymentService;
PaymentBaseProxy(PaymentService paymentService) {
System.out.println("[PaymentBaseProxy.java - 생성자] : PaymentService 초기화 실행!");
this.paymentService = paymentService;
}
@Override
public void confirm() {
System.out.println("[PaymentBaseProxy.java - confirm()] : 프록시 실행!");
paymentService.confirm();
}
@Override
public void cancel() {
System.out.println("[PaymentBaseProxy.java - cancel()] : 프록시 실행!");
paymentService.cancel();
}
}
Base Proxy 패턴입니다. 호출자와 본 객체 사이에서 요청의 흐름을 조율합니다.
이를 이용해 다양한 기능을 추가 할 수 있습니다.
위 코드에서는 PaymentService를 생성자 주입하고 이를 실행하는 메서드를 설정합니다.
이때 Proxy 객체가 PaymentService가 Implement했던 인터페이스를 동일하게 Implement하여
동일한 메서드를 재정의하여 사용하게 됩니다.
public class PaymentProtectionProxy implements Payment {
private PaymentService paymentService;
private User user;
PaymentProtectionProxy(User user) {
System.out.println("[PaymentProtectionProxy.java - 생성자] : PaymentService 초기화 실행!");
paymentService = new PaymentService();
this.user = user;
}
@Override
public void confirm() {
System.out.println("[PaymentProtectionProxy.java - confirm()] : 프록시 실행!");
if (user.hasUserRole("Admin")) { // 프록시에서 권한을 체크한다.
System.out.println("[PaymentProtectionProxy.java - confirm()] : 어드민 인증 성공!");
paymentService.confirm();
} else {
System.out.println("[PaymentProtectionProxy.java - confirm()] : 어드민 인증 실패!");
}
}
@Override
public void cancel() {
System.out.println("[PaymentProtectionProxy.java - cancel()] : 프록시 실행!");
if (user.hasUserRole("Manager")) { // 프록시에서 권한을 체크한다.
System.out.println("[PaymentProtectionProxy.java - cancel()] : 매니저 인증 성공!");
paymentService.cancel();
} else {
System.out.println("[PaymentProtectionProxy.java - cancel()] : 매니저 인증 실패!");
}
}
}
검증 기능을 수행하는 Protection Proxy 패턴입니다.
본 객체에 메서드를 호출하기전 Proxy 객체에서 권한(검증)을 진행하여 접근을 검증하거나 실행 흐름을 관리합니다.
Proxy 객체에 검증을 위임했기 때문에 본 객체는 변경없이 기능이 추가된 효과를 볼 수 있습니다.
public class PaymentVirtualProxy implements Payment {
// PaymentService가 무겁기 때문에 사용될 시점에 초기화 하고 싶을때 - Lazy-Loading
private PaymentService paymentService;
PaymentVirtualProxy() {
}
@Override
public void confirm() {
lazyInitialized();
System.out.println("[PaymentVirtualProxy.java - confirm()] : 프록시 실행!");
paymentService.confirm(); // 본 객체에 위임
}
@Override
public void cancel() {
lazyInitialized();
System.out.println("[PaymentVirtualProxy.java - confirm()] : 프록시 실행!");
paymentService.cancel(); // 본 객체에 위임
}
private void lazyInitialized() {
if (paymentService == null) {
System.out.println(
"[PaymentVirtualProxy.java - ensureInitialized()] : PaymentService 지연 초기화 실행!");
paymentService = new PaymentService();
}
}
}
대상 서비스나 객체의 생성 비용이 큰 경우, 프록시를 사용하여 지연 초기화(Lazy Initialization) 를 구현할 수 있습니다. 이를 통해 불필요한 리소스 낭비를 줄이고, 실제 사용 시점에만 객체를 생성하여 시스템 효율성을 높일 수 있습니다.