프록시 패턴이란?
- 프록시는 사전적 의미로 대리자·대리인을 뜻한다. Http 프로토콜을 사용하는 Client-Server 아키텍처에서 Client가 요청을 보내면 Server는 응답을 전달하게 되는데, 이 사이에 일종의 미들웨어처럼 프록시가 적용될 수 있다. 프록시 서버는 클라이언트의 요청을 대신 받아 서버에 전달한다.
- 프록시 디자인 패턴 역시 동일한 원리가 적용된다고 볼 수 있다. 스프링 부트에서 Aop는 CGlib 방식의 Proxy 패턴을 적용하여 부가 기능 (Aspect-Advice)을 주 관심사로부터 분리한 후 Runtime Weaving을 수행한다. 즉 프록시 패턴을 활용하면 타겟이 되는 객체의 로직을 수행하기 전에 프록시 객체가 이를 intercept 하여 부가 기능을 주입하게 되는 것이다.
Runtime Weaving
Spring Aop는 CGLib 기반의 Proxy 방식으로 동작한다. Aspect를 구성한 후 Advice를 적용하게 되면, 핵심 관심사의 코드를 수정하지 않아도 스프링이 런타임 시점에 동적으로 Target Pointcut에 Advice를 주입하여 부가 기능을 수행할 수 있도록 한다.
https://velog.io/@dnjwm8612/AOP-Weaving-Proxy
이 글에서는 Spring Aop가 아닌, 순수한 자바 코드로 프록시 패턴을 구현하는 방법에 대해 정리하려고 한다. 개발 방향과 구현 예제를 순서대로 살펴보도록 하자.
개발 방향
kylen은 밥을 안먹은지 하루가 지났다. 참다참다 배가 고파서 빵을 사먹으려고 한다. 그런데 무슨 빵을 먹을지 결정을 하지 못하였다. 불행중 다행으로 매장에서는 결정을 잘 못하는 사람을 위해 랜덤으로 빵을 골라주는 메뉴를 출시하였다. 이제 kylen은 매장에 비용만 지불하면 된다. 빵은 매장에서 랜덤으로 선택하여 전달해줄 것이다.
위 이야기를 코드로 구현해보자. 먼저 매장 클래스가 있어야 할 것이다. 해당 클래스에는 빵을 구매하는 기능이 필요하며, 빵을 랜덤으로 골라주는 기능은 구매하기 기능에 프록시를 적용하여 프록시 객체에서 담당하도록 한다.
// 기능을 수행할 구현체와 구현체를 감싸는 Proxy 객체의 기능을 정의한 Interface
public interface AbstractStore {
void process(Bread bread, int cash);
}
매장에서 빵을 판매할 수 있도록 인터페이스에 메서드를 선언한다. 프록시 객체와 매장 객체는 메서드를 오버라이딩 할 것이다.
// 매장에서 판매하는 빵을 정의한 Enum
@Getter
public enum Bread {
소보로빵("소보로빵", 3000, "땅콩 소보로빵 입니다."),
팥빵("팥빵", 3500, "팥이 들어간 팥빵 입니다."),
메론빵("메론빵", 2500, "메론이 들어간 메론빵 입니다."),
맘모스빵("맘모스빵", 8000, "맘모스빵 입니다."),
소금빵("소금빵", 3300, "소금이 얹어진 소금빵 입니다."),
;
private final String code;
private final Integer price;
private final String desc;
Bread(String code, Integer price, String desc) {
this.code = code;
this.price = price;
this.desc = desc;
}
}
// 매장 객체
public class Store implements AbstractStore {
@Override
public void process(Bread bread, int cash) {
System.out.println("[매장] 빵 구매 시작... cash : '" + cash + "원' 빵: '" + bread + "'");
System.out.println("[매장] 구매할 빵은 '" + bread.getCode() + "' 입니다.");
System.out.println("[매장] 빵의 가격은 '" + bread.getPrice() + "'원 입니다.");
if(bread.getPrice() > cash) {
System.out.println("돈이 모자랍니다. 빵의 가격: '" + bread.getPrice() + "' 지불한 금액: '" + cash + "'");
return;
}
System.out.println("맛있게 드세요.");
}
}
매장 객체에서는 메서드를 구현하여 빵을 구매할 수 있도록 한다. 이 때, 지불된 비용을 검증하여 빵의 가격을 모두 지불하였는지를 확인한다.
// 매장 객체 앞에서 요청을 가로채어 부가 기능을 주입할 프록시 객체
public class StoreProxy implements AbstractStore {
private final AbstractStore store;
public StoreProxy() {
this.store = new Store();
}
@Override
public void process(Bread bread, int cash) {
System.out.println("[매장 Proxy] 빵을 고르지 않은 경우 랜덤으로 빵을 고릅니다.");
if(ObjectUtils.isEmpty(bread)) {
int index = (int) (Math.random() * 5) + 1;
bread = makeBreadMap().get(index);
}
store.process(bread, cash);
}
private Map<Integer, Bread> makeBreadMap() {
AtomicInteger index = new AtomicInteger(1);
return Arrays.stream(Bread.values())
.collect(
Collectors.toUnmodifiableMap(
data -> index.getAndIncrement(),
Function.identity()
)
);
}
}
프록시 객체 역시 인터페이스를 구현하여 메서드를 재정의한다. 구매자가 빵을 선택한 경우에는 해당 빵을 구매하도록 하고, 선택하지 않은 경우에는 매장이 보유한 빵 목록에서 랜덤으로 한 가지를 선택하여 판매한다.
public class ProxyPractice {
public static void main(String[] args) {
int cash = 3000;
AbstractStore store = new StoreProxy();
store.process(null, cash);
}
}
// 1차 수행
[매장 Proxy] 빵을 고르지 않은 경우 랜덤으로 빵을 고릅니다.
[매장] 빵 구매 시작... cash : '3000원' 빵: '메론빵'
[매장] 구매할 빵은 '메론빵' 입니다.
[매장] 빵의 가격은 '2500'원 입니다.
맛있게 드세요.
// 2차 수행
[매장 Proxy] 빵을 고르지 않은 경우 랜덤으로 빵을 고릅니다.
[매장] 빵 구매 시작... cash : '3000원' 빵: '팥빵'
[매장] 구매할 빵은 '팥빵' 입니다.
[매장] 빵의 가격은 '3500'원 입니다.
돈이 모자랍니다. 빵의 가격: '3500' 지불한 금액: '3000'
// 3차 수행
[매장 Proxy] 빵을 고르지 않은 경우 랜덤으로 빵을 고릅니다.
[매장] 빵 구매 시작... cash : '3000원' 빵: '맘모스빵'
[매장] 구매할 빵은 '맘모스빵' 입니다.
[매장] 빵의 가격은 '8000'원 입니다.
돈이 모자랍니다. 빵의 가격: '8000' 지불한 금액: '3000'
빵을 정하지 않고 구매를 하여도 프록시 객체에서 랜덤으로 빵을 골라주었고, 요청을 다시 매장 객체에 넘겨 정상적으로 매장의 구매 기능이 수행된 것을 확인할 수 있다.
'development > design pattern' 카테고리의 다른 글
[Java] Design-Pattern Observer (4) | 2024.09.24 |
---|---|
[Java] Design-Pattern Singleton (0) | 2024.09.19 |