DeJa
Techvu
DeJa
전체 방문자
51,021
오늘
19
어제
54
  • Techvu (60)
    • DesignPatterns (3)
      • 생성 (0)
      • 구조 (1)
      • 행동 (2)
    • Refactoring (0)
    • DataStructures (0)
    • Algorithms (24)
      • 기본 지식 (12)
      • 문제 풀이 (12)
    • OOP (0)
    • TDD (2)
    • DDD (0)
    • Programming Languages (9)
      • Java (9)
      • Kotlin (0)
    • Spring (1)
    • JPA (7)
    • Web (1)
      • 기본 지식 (1)
      • 실무 경험 (0)
    • CS (12)
      • Network (1)
      • OS (8)
      • DataBase (3)
      • Server (0)
    • Git (1)
    • Conferences (0)

블로그 메뉴

  • 홈
  • 태그
  • 미디어로그
  • 위치로그
  • 방명록

공지사항

  • Study
  • GitHub
  • Medium Blog

인기 글

  • 스키마(Schema)
    2022.01.08
    스키마(Schema)
  • 자바 버전별 역사 및 특징
    2022.01.12
    자바 버전별 역사 및 특징
  • 깃허브 사용 방법
    2021.12.15
    깃허브 사용 방법
  • 동시성 이슈(Concurrency Issue)
    2022.03.20
    동시성 이슈(Concurrency Issue)
  • JPA 는 과연 1차 캐시를 통해서 Repeatable R⋯
    2021.12.27
    JPA 는 과연 1차 캐시를 통해서 Repeatable R⋯

태그

  • OS
  • network
  • web
  • CS
  • 알고리즘
  • 디자인패턴
  • Spring
  • JPA
  • TDD
  • java
  • DATABASE

최근 댓글

  • 글 잘읽고 가요.
    아이폰
  • 컴파일러자체에서 꼬리재귀를 지원하지 않으니 static으로⋯
    aaa
  • 압도적 감사
    ㅇㅇㅇ

최근 글

  • Write a test code right now
    2022.03.24
    Write a test code right now
  • 동시성 이슈(Concurrency Issue)
    2022.03.20
    동시성 이슈(Concurrency Issue)
  • POJO, JavaBean, Entity, VO, DTO
    2022.02.08
    POJO, JavaBean, Entity, VO, DTO
  • TDD with Agile
    2022.02.05
    TDD with Agile
  • Java Stream 기초
    2022.01.23
    Java Stream 기초

티스토리

hELLO · Designed By 정상우.
DeJa

Techvu

프록시와 데코레이터 패턴
DesignPatterns/구조

프록시와 데코레이터 패턴

2021. 12. 19. 23:02
728x90
728x90

프록시와 데코레이터 패턴

이번에 배울 디자인 패턴은 프록시 패턴이다. GOF 의 디자인 패턴에서 프록시 패턴은 크게 프록시 패턴과 데코레이터 패턴으로 나뉜다. 자세히 배워보자.

프록시 패턴

프록시(Proxy)는 대리자라는 의미를 갖고 있다. 의미만 봤을때 어떤 일을 대신 해준다라는 느낌을 받을 수 있다.

요청하는 쪽이 Client, 응답하는 쪽이 Server 라고 하자. (보통 이렇게 부른다.) Proxy 가 없는 경우에는 Client 와 Server 의 관계가 다음과 같다.

위 경우를 직접 호출이라고 부른다. Proxy 가 도입되면 다음과 같다.

위 경우를 간접 호출이라고 부른다.

예시

프록시의 예시를 보자.

  • 카페에 들어가서 손님이 캐셔에게 커피 주문을 한다. 캐셔는 바리스타에게 커피를 만들어 달라고 한다.
    • 요청 : 손님, 응답 : 캐셔
    • 손님은 커피를 누가 만드는지 알 수없고 신경도 안써도 된다.
    • 만약에 매장 블랙리스트의 손님이 온다면 캐셔(Proxy)가 미리 막아서 바리스타가 커피를 만들지 못하게 할 수 있다.
    • 이것을 권한에 따른 접근 차단라고 한다.
  • 카페에 들어가서 커피를 주문하는데, 나는 항상 같은 시간에 먹던 것만 먹어서 이미 나를 위한 커피가 만들어져 있다면 기대한 것 보다 빨리 커피를 받을 수 있다.
    • 이것을 캐싱이라고 한다.
  • 카페에 들어가서 커피를 주문하는데, 캐셔가 바리스타 A 에게 커피를 만들라고 요청 했는데, 바리스타 A 가 화장실이 급하다고 바리스타 B 에게 커피를 만들라고 요청할 수 도 있다. 손님 입장에서는 누가 만들든 상관 없이 캐셔를 통해서 커피를 잘 받기만 하면 된다.
    • 이것을 프록시 체인이라고 한다.
  • 친구에게 음료를 사오라고 요청 했는데, 친구가 토핑을 추가해서 온다.
    • 이것을 부가 기능 추가라고 한다.

종류

프록시 패턴은 크게 프록시와, 데코레이터 패턴으로 나뉜다고 설명했다. 따라서, 데코레이터 패턴도 프록시를 사용하는 패턴이다. 다만 프록시 객체를 만드는 의도 가 무엇인지에 따라서 두 가지로 나뉜다.

접근 제어

  • 권한에 따른 접근 차단
  • 캐싱
  • 지연 로딩

접근 제어가 목적인 경우 프록시 패턴을 사용한다.

부가 기능 추가

  • 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행

부가 기능 추가가 목적인 경우 데코레이터 패턴을 사용한다.

프록시 체인

프록시 체인은 다음과 같다.

대체 가능

프록시는 실제 객체랑, 프록시 객체랑 서로 막 바꾸어도 클라이언트 코드를 변경하지 않고 동작 할 수 있어야 하는 대체 가능한 경우에만 객체를 프록시로 만들 수 있다.

프록시 패턴은 다음과 같은 구조로 생겼다.

Subject Interface 를 Proxy 와 RealSubject 모두 구현하고, Proxy 는 위임을 사용하여 RealSubject 에 접근한다.

프록시 패턴을 코드로 살펴보자.

 

구현: 캐싱

public interface Subject {
    String call();
}
@Slf4j
public class RealSubject implements Subject {

    @Override
    public String call() {
        log.info("RealSubject Call");
        // Database 에서 data 를 조회하는데 1초 걸린다고 가정
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Client {

    private Subject subject;

    public Client(Subject subject) {
        this.subject = subject;
    }

    public void execute() {
        subject.call();
    }
}
@Slf4j
public class CacheProxy implements Subject {

    private Subject target;
    private String cache;

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String call() {
        log.info("CacheProxy Calls");
        if (cache == null) {
            cache = target.call();
        }
        return cache;
    }
}
@DisplayName("프록시 적용 X")
@Test
void noProxyTest() throws Exception {
    RealSubject realSubject = new RealSubject();
    Client client = new Client(realSubject);
    client.execute();
    client.execute();
    client.execute();
}

@DisplayName("캐시 프록시 적용 : 캐싱 기능 테스트")
@Test
void cacheProxyTest() {
    // 마치 JPA 에서 1차 캐시에 데이터가 있으면 DB 조회를 하지 않고, 캐시에서 꺼내는 것처럼 사용된다.
    RealSubject realSubject = new RealSubject();
    CacheProxy cacheProxy = new CacheProxy(realSubject);
    Client client = new Client(cacheProxy);
    client.execute();
    client.execute();
    client.execute();
}

캐시 프록시를 적용하지 않은 경우에는 총 3초의 시간이 걸린 반면, 캐시 프록시를 적용한 경우에는 3초 미만의 시간으로 데이터를 조회해 온다.

구현: 지연 로딩

Team 과 Member 엔티티가 존재하고, Team 엔티티안에 Member 가 선언되어있다고 가정

@Getter @Setter
public class Team {

    private Long id;
    private Member member;
    private String teamName;

}
@Slf4j
public class EarlyLoadingRealSubject implements Subject {

    @Override
    public String call() {
        log.info("EarlyLoadingRealSubject Call");
        log.info("EarlyLoadingRealSubject : Team 조회 & Member 조회");
        // Database 에서 data 를 조회하는데 1초 걸린다고 가정
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

EarlyLoadingRealSubject 에서는 Team 엔티티를 조회할 때, Member 엔티티까지 같이 조회한다.

@Slf4j
public class LazyLoadingProxy implements Subject {

    private Subject target;
    private boolean cached = false;

    public LazyLoadingProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String call() {
        log.info("LazyLoadingProxy Call");
        log.info("LazyLoadingProxy : Team 조회");
        if(notCached() && callGetMember()) {
            log.info("LazyLoadingProxy : Member 조회");
            cached = true;
        }
        return "data";
    }

    private boolean callGetMember() {
        log.info("Member Entity 의 Get 메서드가 호출되면");
        return true;
    }

    private boolean notCached() {
        log.info("Cache 된 내역이 존재하지 않으면");
        return !cached;
    }
}

LazyLoadingProxy 에서는 Team 을 먼저 조회한 후, Member 엔티티에 대한 Get 메서드가 호출되면서, Cache 된 내역이 존재하지 않을 경우에만 데이터베이스에서 Member 데이터를 조회한다.

@DisplayName("지연로딩 테스트")
@Test
void lazyLoadingTest() throws Exception {
    EarlyLoadingRealSubject realSubject = new EarlyLoadingRealSubject();
    LazyLoadingProxy lazyLoadingProxy = new LazyLoadingProxy(realSubject);
    Client client = new Client(lazyLoadingProxy);
    client.execute();
    client.execute();
    client.execute();
}

실제 Member 객체가 사용되는 시점까지 조회 기능을 미루는 것을 지연 로딩(Lazy Loading)이라고 한다.

구현 : 권한에 따른 접근 제어

public interface SubjectHandler {
    String call(String method);
}
@Slf4j
public class SecureRealSubject implements SubjectHandler {
    @Override
    public String call(String method) {
        log.info("SecureRealSubject Call");
        return "data";
    }
}
@Slf4j
public class SecureProxy implements SubjectHandler {

    private SubjectHandler target;
    private String[] patterns;

    public SecureProxy(SubjectHandler target, String[] patterns) {
        this.target = target;
        this.patterns = patterns;
    }

    @Override
    public String call(String method) {
        log.info("SecureProxy Call");
        if (!PatternMatchUtils.simpleMatch(patterns, method)) {
            log.info("filtered");
            return "filtered";
        }
        return target.call(method);
    }
}
public class SecureClient {

    private SubjectHandler subjectHandler;

    public SecureClient(SubjectHandler subjectHandler) {
        this.subjectHandler = subjectHandler;
    }

    public void execute(String method) {
        subjectHandler.call(method);
    }
}
@DisplayName("보호 프록시 테스트")
@Test
void secureProxyTest() throws Exception {
    final String[] PATTERNS = {"call*"};
    SecureRealSubject realSubject = new SecureRealSubject();
    SecureProxy secureProxy = new SecureProxy(realSubject, PATTERNS);
    SecureClient client = new SecureClient(secureProxy);
    client.execute("execute");
    client.execute("call");
}

실행 되는 메서드 이름이 call 이 아니면 filtered 가 출력되고, call 일 경우에 RealSubject 의 call 메서드가 호출된다.

데코레이터 패턴

데코레이터 패턴은 부가 기능 추가가 목적이다. 데코레이터 패턴도 프록시를 사용하기 때문에, 기본적인 UML 구조는 위와 동일하다.단, 구현 방법이 인터페이스를 활용한 구현 방법만 있는 것은 아니기 때문에, 어떤 식으로 구현했는지에 따라서 UML 구조가 달라질 수 있다.

public interface Component {
    String call();
}
@Slf4j
public class RealComponent implements Component {
    @Override
    public String call() {
        log.info("음료 주문");
        return "data";
    }
}
@Slf4j
public class JellyToppingDecorator implements Component {

    private Component component;

    public JellyToppingDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String call() {
        log.info("JellyToppingDecorator 실행");
        String result = component.call();
        String decoResult = "*젤리추가*" + result + "*젤리추가*";
        log.info("JellyToppingDecorator 꾸미기 적용 전={}, 적용 후={}", result, decoResult);
        return decoResult;
    }
}
@Slf4j
public class ChocolateToppingDecorator implements Component {

    private Component component;

    public ChocolateToppingDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String call() {
        log.info("ChocolateToppingDecorator 실행");
        String result = component.call();
        String decoResult = "*초콜릿추가*" + result + "*초콜릿추가*";
        log.info("ChocolateToppingDecorator 꾸미기 적용 전={}, 적용 후={}", result, decoResult);
        return decoResult;
    }
}
@Slf4j
public class DecoratorClient {

    private Component component;

    public DecoratorClient(Component component) {
        this.component = component;
    }

    public void execute() {
        String result = component.call();
        log.info("result={}", result);
    }
}
@DisplayName("데코레이터 테스트")
@Test
void decoratorTest() throws Exception {
    Component realComponent = new RealComponent();

    // Proxy Chain
    Component jellyToppingDecorator = new JellyToppingDecorator(realComponent);
    Component chocolateToppingDecorator = new ChocolateToppingDecorator(jellyToppingDecorator);
    DecoratorClient client = new DecoratorClient(chocolateToppingDecorator);
    client.execute();
}

위 테스트 코드 처럼 프록시 체인(Proxy Chain) 을 활용하여 여러 부가 기능들을 추가할 수 있다.

GOF 데코레이터 패턴

Decorator 구현체에 중복되는 기능들이 있을 수 있다. 이러한 기능들을 Decorator라는 추상 클래스로 만들어 중복을 제거할 수 있는데, Decorator 추상 클래스 내부에서 Component 를 속성으로 가지고 있어야 한다. 이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트 인지, 데코레이터인지 명확하게 구분할 수 있다. 이것이 바로 GOF 에서 설명하는 데코레이터 패턴이다.

클래스 기반 프록시

지금까지 보여준 코드는 인터페이스 기반으로 구현한 프록시 패턴들이다. 하지만 굳이 인터페이스를 사용하지 않고 클래스를 사용하여 프록시 패턴을 구현할 수 도 있다.

@Slf4j
public class ConcreteLogic {

    public String call() {
        log.info("ConcreteLogic 실행");
        return "data";
    }
}
@Slf4j
public class TimeProxy extends ConcreteLogic {

    private ConcreteLogic concreteLogic;

    public TimeProxy(ConcreteLogic concreteLogic) {
        this.concreteLogic = concreteLogic;
    }

    @Override
    public String call() {
        log.info("TimeDecorator 실행");
        long startTime = System.currentTimeMillis();

        String result = concreteLogic.call();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeDecorator 종료 resultTime={}ms", resultTime);
        return result;
    }
}
public class ConcreteClient {

    private ConcreteLogic concreteLogic;

    public ConcreteClient(ConcreteLogic concreteLogic) {
        this.concreteLogic = concreteLogic;
    }

    public void execute() {
        concreteLogic.call();
    }
}
@DisplayName("구체 클래스 기반 프록시 테스트")
@Test
void concreteProxyTest() throws Exception {
    ConcreteLogic concreteLogic = new ConcreteLogic();
    TimeProxy timeProxy = new TimeProxy(concreteLogic);
    ConcreteClient client = new ConcreteClient(timeProxy);
    client.execute();
}

인터페이스 기반 vs 클래스 기반

인터페이스 기반과 클래스 기반 중 어떤것이 더 좋을까 ? 클래스 기반 프록시는 상속을 사용하기 때문에 상속에서 오는 단점들을 다 안고 간다. 하지만, 인터페이스를 사용해야할 이유가 없는 경우(Ex. 구현이 변경될 가능성 등)에는 클래스 기반으로 프록시를 구현하는 것이 좋다고 생각한다.

References

  • 인프런. 스프링 핵심 원리 고급
728x90
저작자표시 비영리 변경금지
  • 카카오스토리
  • 트위터
  • 페이스북
    디자인패턴
    DeJa
    DeJa
    Tech Blog
    댓글쓰기

    티스토리툴바