개발자로 후회없는 삶 살기

디자인 패턴 PART.프록시 패턴 본문

[백엔드]/[Java | 학습기록]

디자인 패턴 PART.프록시 패턴

몽이장쥰 2023. 8. 21. 15:33

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

본론

- 패턴 소개

특정한 객체를 접근하기 전에 프록시를 먼저 지나도록 하는 패턴으로 대리인이라는 사전 뜻을 가집니다. 클라이언트가 원래 사용하려는 객체를 직접 쓰는 게 아니라 대리인을 거쳐서 쓰는 패턴입니다. 

 

맨 처음 요청은 무조건 프록시가 받습니다. 이렇게 만듬으로 해서 목적 객체의 접근 제어를 할 수도 있고 지연 로딩도 할 수 있고 로깅을 할 수 도 있고 캐싱도 할 수 있습니다. 프록시 안에 미리 캐시한 게 있다면 타겟을 가지고 않고 얻을 수 도 있습니다. 이렇게 프록시를 통해서 굉장히 다양한 장점이 생깁니다.

 

- 코드

public class Client {
    
    public static void main(String[] args) throws InterruptedException {
        GameService gameService = new GameService();
        gameService.startGame();
    }
}

public class GameService {
    
    public void startGame() {
        System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
    }
    
}

클라이언트가 게임 서비스로 게임하는 코드입니다. 이 서비스를 사용할 때 이 oper가 시간이 얼마나 걸리는 지 시간을 재고 싶습니다. 기존 코드를 변경할 수 없고 원래 하던 일은 남겨두고 시간 체크를 하고 싶습니다. 프록시를 사용하면 이런 것을 코드를 그대로 유지하면서 할 수 있습니다.

 

=> 구조

원래 사용할 코드가 RealSubject입니다. 클라이언트가 real subject을 사용하도록 코드를 바꾸고 Subject 인터페이스를 프록시와 real을 둘 다 구현합니다. 게임 서비스가 real이고 인터페이스를 만들 것입니다. 그때 클라이언트는 real을 쓰는게 아니라 프록시를 쓰게 될 것입니다.

 

근데 프록시의 특징은 데코레이터 패턴처럼 인터페이스(본인의 타입)로 필드를 하나 가지고 있습니다. 이건 프록시에서 real을 참조하기 위함입니다. 따라서 프록시로 들어온 oper 중에서는 원본 코드를 사용하고 그전에는 추가 작업을 할 수 있게끔 합니다.

 

- 패턴 적용

프록시를 적용해서 StartGame 메서드가 얼마나 걸리나 보겠습니다. 

 

-> 인터페이스로 만들기 X

public class GameServiceProxy extends GameService {
    @Override
    public void startGame() {
        long bef = System.currentTimeMillis();
        super.startGame();
        System.out.println(System.currentTimeMillis() - bef);
    }
}

정말 코드를 하나도 안 건드리고 시간을 재보겠습니다. 이런 경우에는 상속을 써서 GameServiceProxy 를 만들어야 합니다. 재정의를 하는데 super 메서드를 하기 전에 시간을 재면 됩니다.

 

-> 인터페이스 만들기

// 인터
public interface GameService {
    void startGame();
}

// real
public class DefaultGameService implements GameService{
    @Override
    public void startGame() {
        System.out.println("이 자리에 오신,,,");
    }
}

프록시 패턴 그림 그대로를 만들어 봅니다. GameService를 인터페이스로 만듭니다. 그리고 구현 클래스로 Default를 만듭니다. 이게 real입니다. 이렇게 subject와 real subject가 만들어졌고 이제 프록시를 만듭니다.

 

-> 프록시

public class GameServiceProxy implements GameService {
    private GameService gameService;
    
    public GameServiceProxy(GameService gameService) {
        this.gameService = gameService;
    }
    
    @Override
    public void startGame() {
        long bef = System.currentTimeMillis();
        gameService.startGame();
        System.out.println(System.currentTimeMillis() - bef);
    }
}

 

얘도 인터페이스를 구현한다고 했고 이게 마치 데코처럼 필드로 subject 타입을 그대로 가지고 있고 이 타입으로 타겟에 해당하는 것을 주입받아서 기존 메서드에 시간을 추가할 것입니다.

 

public class Client {
    public static void main(String[] args) {
        GameService gameService = new GameServiceProxy(new DefaultGameService());
        gameService.startGame();
    }

이제 클라에서 프록시에 real을 주입해주면 됩니다. 클라이언트가 목적을 쓰기 전에 반드시 프록시를 거쳐서 게임 서비스를 쓰는 것입니다. 이렇게 하면 DefaultGameService는 원래 본인이 해야하는 일들을 하고 부가적인 일들은 프록시에서 하고 있습니다.

 

public class GameServiceProxy implements GameService {
    private GameService gameService;
    
    @Override
    public void startGame() {
        long bef = System.currentTimeMillis();
        if (this.gameService == null) {
            this.gameService = new DefaultGameService();
        }
        
        gameService.startGame();
        System.out.println(System.currentTimeMillis() - bef);
    }
}

이렇게 하면 지연 로딩도 할 수 있고 , 보안 적용도 할 수 있고 리턴 타입을 변경할 수도 있습니다. 따라서 이 패턴은 여러 곳에서 활용할 수 있습니다.

 

- 장, 단점

기존 코드를 변경하지 않고 새로운 기능을 추가하는 OCP를 만족합니다. 또한 원본에 추가 기능을 넣지 않기에 SIP도 만족하며 유연하게 코드를 만들 수 있습니다. 만약 default Service가 생성하는데 소모가 많은 클래스였다면 이 기능이 크게 다가왔을 것입니다.

 

 

Comments