Blog
강의 리뷰
백기선 GoF 디자인 패턴

디자인 패턴의 종류

디자인 패턴은 크게 생성 패턴, 구조 패턴, 행위 패턴으로 나눌 수 있다.

  • 생성 패턴 : 생성 디자인 패턴은 기존 코드의 유연성과 재사용을 증가시키는 객체를 생성하는 다양한 방법을 제공합니다.
  • 구조 패턴 : 구조를 유연하고 효율적으로 유지하면서 객체들과 클래스들을 더 큰 구조로 조립하는 방법을 설명합니다.
  • 행위 패턴 : 알고리즘과 객체 간의 책임 할당과 관련이 있습니다.
생성 패턴구조 패턴행위 패턴
싱글톤(Singleton)프록시(Proxy)옵저버(Observer)
팩토리(Factory)데코레이터(Decorator)스테이트(State)
추상팩토리(Abstract Factory)어댑터(Adapter)템플릿 메소드(Template Method)
빌더(Builder)컴포지트 or 복합체(Composite)스트래티지(Strategy)
프로토 타입(Prototype)브릿지(Bridge)커맨드(Command)
퍼사드(Facade)이터레이터(Iterator)
플라이웨이트(Flyweight)메멘토(Memento)
비지터(Visitor)
체인 오브 레스폰시빌리티(Chain of Responsibility)
중재자(Mediator)

암기요령

  • 구조패턴 : A, B, D, 2F, P
  • 행위패턴 : CMOS TV

구조 패턴

프록시 패턴

특정한 객체나 오퍼레이션에 접근하기 전에 프록시 객체를 거쳐서 접근하게 하는 패턴이다. 예를 들자면 사장을 만나기 위해서 직접 만나는게 아니라 비서를 거쳐서 만나는 것과 같다.

구현

아래와 같이 Proxy를 구현해서 최초의 Client 코드와 DefaultGameService의 구현체는 어떠한 변경도 없이 새로운 기능을 추가할 수 있다. Client 코드에서 GameService 객체에 DefaultGameService가 아닌 GameServiceProxy를 생성하게 되면 GameServiceProxy의 startGame() 메소드가 호출되어 DefaultGameService의 startGame() 메소드를 호출하게 된다.

public class GameServiceProxy implements GameService{
    private GameService gameService;
 
    public void startGame() {
        long before= System.currentTimeMillis();
 
        // Lazy Initialization 해서 실제로 사용될 때까지 객체를 생성하지 않음
        if(gameService == null) {
            this.gameService = new DefaultGameService();
        }
        gameService.startGame();
        System.out.println("소요시간: " + (System.currentTimeMillis() - before) + "ms");
    }
}

Reflection을 활용한 동적 프록시

Reflection의 Proxy 클래스를 활용하면 프록시를 객체를 간편하게 생성할 수 있는데 Spring AOP에서도 이러한 방식을 사용한다.

public class GameServiceProxyInJava {
    public static void main(String[] args) {
        GameServiceProxyInJava proxyInJava = new GameServiceProxyInJava();
        proxyInJava.dynamicProxy();
    }
 
    private void dynamicProxy() {
        GameService gameService = getGameServiceProxy(new DefaultGameService());
    }
 
    // 이 코드가 실행될 때 Proxy Instance를 생성하게 된다.
    private GameService getGameServiceProxy(GameService target) {
        return (GameService) Proxy.newProxyInstance(this.getClass()
                        .getClassLoader(), new Class[]{GameService.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // target을 args를 넣어서 실행하겠다.
                        method.invoke(target, args);
                        return null;
                    }
                }
        );
    }
}

Spring Proxy

Spring에서는 다음과 같이 Aspect@Around를 사용해서 프록시를 구현할 수 있다.

@Service
public class GameService {
    public void startGame() {
        System.out.println("Game started");
    }
}

적용하는 상황

  1. 초기화 지연 : 객체가 매우 크거나 복잡한 경우, 객체를 미리 생성하는게 아니라 필요할 때 생성하는 방식으로 사용한다.
  2. 보안 : 객체에 대한 접근을 제어하고 싶을 때 사용한다.
  3. 캐싱 : 어떤 오퍼레이션을 거쳐서 데이터를 가져오는데 프록시 안에 캐싱해둔 데이터가 있다면 오퍼레이션까지 가지도 않고 리턴을 하는것이다.

장점과 단점

  • 장점
    • 기존의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있다. (OCP 원칙)
    • 기존 코드가 해야 하는 일만 유지할 수 있다. (SRP)
    • 기능 추가 및 초기화 지연, 캐싱 등으로 다양하게 활용할 수 있다.
  • 단점
    • 코드의 복잡도가 증가한다.

행위 패턴

Template Method

알고리즘의 구조를 템플릿으로 제공하고 구체적으로 입력, 출력 등의 구현은 서브클래스에서 구현하도록 하는 패턴이다.
추상 클래스는 템플릿을 제공하고 하위 클래스는 구체적인 알고리듬을 제공한다.

적용 예시

아래와 같은 코드가 있다고 가정할 때 result += Integer.parseInt(line) 부분의 요구사항이 변경되어서 곱셈으로 변경되야 한다고 가정해보자. 그러면 아래의 클래스를 복사한 후 result += Integer.parseInt(line) 부분만 변경한 중복이 굉장히 많이 발생하는 코드가 발생할 수 있다.

public class FileProcessor {
    private String path;
 
    public FileProcessor(String path) {
        this.path = path;
    }
 
    public int process() {
        // try문이 끝날 때 BufferedReader가 자동으로 close된다.
        // Python의 with open() as f: 와 같은 기능을 한다.
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            int result = 0;
            String line = null;
            while ((line = reader.readLine()) != null) {
                /**
                 * parseInt는 int type을 반환한다.
                 * valueOf는 Integer type을 반환한다.
                 */
                result += Integer.parseInt(line);
                // result += Integer.valueOf(line);
            }
            return result;
        } catch (IOException e) {
            throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
        }
    }
}

위와 같은 문제를 해결하기 위해서 FileProcessor 부분을 Abstract class로 변경하고 입력과 출력 부분만 구현한채 result += Integer.parseInt(line) 부분에 해당하는 세부 구현만 자식 클래스에서 구현하도록 다음과 같이 변경할 수 있다.

public abstract class FileProcessor {
    private String path;
 
    public FileProcessor(String path) {
        this.path = path;
    }
 
    public int process() {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            int result = 0;
            String line = null;
            while ((line = reader.readLine()) != null) {
                result = getResult(result, Integer.parseInt(line));
            }
            return result;
        } catch (IOException e) {
            throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
        }
    }
 
    // protected로 구현클래스에서 접근 가능하게 설정
    protected abstract int getResult(int result, int line);
}

Template Callback

  1. 구체적인 알고리즘(구현) 부분에 넣을 Interface 정의
  2. 템플릿에 위에서 정의한 Interface를 주입받아서 Callback을 실행하는 메소드를 정의
  3. Client에서 Interface의 구현체 주입
public interface Operator {
    abstract int getResult(int result, int number);
}

장점과 단점

  • 장점
    • 템플릿 코드를 재사용하고 중복 코드를 줄일 수 있다.
    • 템플릿 코드를 변경하지 않고 상속을 받아서 구체적인 알고리즘만 변경할 수 있다.
  • 단점
    • 리스코프 치환 원칙을 위반할 수 있다.
      • 템플릿에 해당하는 메서드, 위에서 예를 들면 process 부분이 재정의되서 이상한 동작을 할 수 있을 가능성이 있는데, 이는 process 부분을 final로 정의해서 재정의를 막을 수도 있다.
    • 알고리즘 구조가 복잡할 수록 템플릿을 유지하기 어려워진다.

Spring에서 사용하는 예시

  • HttpServlet
    • HttpServlet을 상속받아서 쓸 때 doGet, doPost, service 등의 메서드를 필요한 부분만 구현해서 쓰도록 한다.
  • ConfigurerAdapter
  • JdbcTemplate
    • excute라는 메서드는 데이터를 삽입할 때, query라는 메서드는 수정할 때 사용한다.
    • 두 메서드 다 callback이 필요한데 여기서 템플릿 콜백 패턴이 사용되었다.
  • RestTemplate

Reference