프로그래밍-학습기록/객체 지향 프로그래밍

주요 디자인 패턴

leesche 2021. 1. 15. 01:47

주요 디자인 패턴

디자인 패턴이란?

반복적으로 사용되는 소프트웨어 설계 패턴

전략(Strategy) 패턴

  • 예시 상황 → 과일 매장에서 상황에 따라 다른 가격 할인 정책을 적용할 때
    • 서로 다른 계산 정책들이 한 코드에 섞여 있어, 정책이 추가될수록 코드 분석이 어려움
    • 가격 정책이 추가될 때마다 calculate 메서드를 수정하는 것이 점점 어려워진다. 예를 들어 마지막 손님 50% 할인과 같은 새로운 가격 정책이 추가될 경우, calculate 메서드에 마지막 손님을 구분하기 위한 lastGuest 파라미터가 추가되고 if 블록이 하나 더 추가되어야 한다.
  • 일반 상황
    • if - else로 구성된 코드 블록이 비슷한 기능(비슷한 알고리즘)을 수행하는 경우
    • 완전히 동일한 기능을 제공하지만 성능의 장단점에 따라 알고리즘을 선택해야 하는 경우
  • 해결
    • 가격 할인 정책을 별도 객체로 분리
    • 상품의 할인 금액 계산을 추상화하는 인터페이스를 만들고 상황에 맞는 할인 계산 알고리즘을 제공하는 각 콘크리트 클래스를 만든다.
    • 이 때 가격 할인 알고리즘을 추상화하고 있는 인터페이스를 '전략'이라 부르고 가격 계산 기능 자체의 책임을 갖고 있는 클래스콘텍스트라고 부른다. 특정 콘테스트에서 알고리즘(전략)을 별도로 분리하는 설계 방법이 전략 패턴이다.
  • 고려할 점
    • 콘텍스트를 사용하는 클라이언트가 전략의 상세 구현에 대한 의존이 발생한다. 이는 문제처럼 보일 수 있으나 전략의 콘크리트 클래스와 클라이언트의 코드가 쌍을 이루기 떄문에 유지 보수 문제가 발생할 가능성이 줄어든다. 특정 할인 정책 적용 기능이 추가될 때 전략 객체도 동시에 생성되고 제거될 때에도 함께 제거된다. 따라서 클라이언트의 정책 선택 코드에서 전략 개체를 직접 생성하는 것은 오히려 코드 이해를 높이고 코드 응집을 높여주는 효과가 있다.
    • 전략 패턴을 적용할 때 얻는 이점은 콘텍스트 코드의 변경 없이 새로운 전략을 추가할 수 있다는 것이다. 다른 할인 정책을 추가하는 경우, 계산의 틀을 제공하는 클래스의 코드는 변경되지 않는다. 단지 새로운 할인 정책을 구현한 클래스를 추가하고, 할인 정책 선택을 처리하는 코드에서 해당 클래스(새로운 할인 정책을 구현한)를 생성해 주기만 하면 된다.
  • 의문
    • 여러 할인 정책이 겹칠 경우(할인을 중첩해서 적용해야 할 때)엔 어떻게 설계할까?

템플릿 메서드(Template Method) 패턴

실행 과정 단계는 동일한데 각 단계 중 일부의 구현이 다른 경우에 사용할 수 있는 패턴이 템플릿 메서드 패턴이다. 템플릿 메서드 패턴은 다음 두 가지로 구성된다.

  • 실행 과정을 구현한 상위 클래스
  • 실행 과정의 일부 단계를 구현한 하위 클래스

상위 클래스는 실행 과정을 구현한 메서드를 제공한다. 이 메서드는 기능을 구현하는데 필요한 각 단계를 정의하며 이 중 일부 단계는 추상 메서드를 호출하는 방식으로 구현된다. 이때 추상 메서드는 구현이 다른 단계에 해당된다.

상위클래스에 모든 하위 타입에 동일하게 적용되는 실행 과정을 제공하는 메서드를 작성한다. 이 메서드가 템플릿 메서드이다. 하위 타입마다 다르게 적용되는 단계는 추상 메서드로 분리한다. 이 상위 클래스를 상속받은 하위 클래스는 추상 메서드만 알맞게 재정의해주면 된다.

템플릿 메서드 패턴을 사용하게 되면, 동일한 실행 과정의 구현을 제공하면서 동시에 하위 타입에서 일부 단계를 구현하도록 할 수 있다. 이는 각 타입에서 코드가 중복되는 것을 방지한다. 그 만큼 코드의 재사용 및 유지 보수가 쉬워진다.

상위 클래스가 흐름 제어 주체

템플릿 메서드 패턴의 특징은 하위 클래스가 아닌 상위 클래스에서 흐름 제어를 한다는 것이다. 일반적인 경우 하위 타입이 상위 타입의 기능을 재사용할지 여부를 결정하기 때문에, 흐름 제어를 하위 타입이 하게 된다. 반면 템플릿 메서드 패턴에서는 상위 타입의 템플릿 메서드가 모든 실행 흐름을 제어하고, 하위 타입의 메서드는 템플릿 메서드에서 호출되는 구조를 갖게 된다.

템플릿 메서드와 템플릿 메서드에서 호출하는 메서드의 접근 범위는 각각 public과 protected로 설정되어야 한다. 템플릿 메서드는 외부에 제공하는 기능이므로 public, 템플릿 메서드에서 호출하는 메서드는 하위 타입에서 재정의할 수 있어야 하기 때문에 private이 아닌 protected 접근 범위를 가져야 한다.

  • 훅(hook) 메서드

    상위 클래스에서 실행 시점이 제어되고, 기본 구현을 제공하면서, 하위 클래스에서 알맞게 확장할 수 있는 메서드를 훅 메서드라 부른다.

템플릿 메서드와 전략 패턴의 조합 (템플릿 메서드의 변형)

템플릿 메서드와 전략 패턴을 함께 사용하면 상속이 아닌 조립의 방식으로 템플릿 메서드 패턴을 활용할 수 있다. 대표적인 예가 스프링 프레임워크의 Template으로 끝나는 클래스들이다. 이 클래스들은 템플릿 메서드를 실행할 때, 변경되는 부분을 실행할 객체를 파라미터를 통해서 전달받는 방식으로 구현되어 있다.

템플릿 메서드 패턴과 전략 패턴을 조합하게 되면, 상속에 기반을 둔 템플릿 메서드 구현과 비교해서 유연함을 갖는다. 상속을 통한 재사용의 경우 클래스가 불필요하게 증가할 수 있고 런타임에 교체할 수 없는 단점이 있는 반면에 조립/위임을 사용하는 경우에는 런타임에 템플릿 메서드에서 사용할 객체를 교체할 수 있는 장점을 가진다.

하지만, 상속 방식의 경우 훅 메서드를 재정의하는 방법으로 하위 클래스에서 쉽게 확장 기능을 제공할 수 있는 장점이 있는 반면, 조립/위임 방식에서는 확장 기능을 제공하려면 구현이 다소 복잡해지는 단점이 있다.

상태(State) 패턴

기능이 상태에 따라 다르게 동작해야 할 때 사용할 수 있는 패턴이 상태 패턴이다.

상태 패턴에서 중요한 점은 상태 객체가 기능을 제공한다는 점이다.

콘텍스트(기능 수행 책임을 갖는 객체)는 필드로 상태 객체를 갖고 있다. 콘텍스트는 클라이언트로부터 기능 실행 요청을 받으면, 상태 객체에 처리를 위임하는 방식으로 구현된다.

상태 별 처리 코드를 상태로 분리함으로써 콘텍스트의 코드가 간결해지고 변경의 유연함을 얻게 된다.

상태 패턴의 장점은 새로운 상태가 추가되더라도 콘텍스트 코드가 받는 영향을 최소화된다는 점이다.

상태 패턴의 두 번째 장점은 상태에 따른 동작을 구현한 코드가 각 상태 별로 구분되기 때문에 상태 별 동작을 수정하기가 쉽다는 점이다.

상태 변경은 누가?

상태 패턴을 적용할 때 고려할 문제는 콘텍스트의 상태 변경을 누가 하느냐에 대한 것이다. 상태 변경을 하는 주체는 콘텍스트나 상태 객체 둘 중 하나가 된다.

상태 객체에서 콘텍스트의 상태를 변경하려면 콘텍스트의 다른 값에 접근해야 할 때도 있다. 이는 상태 객체에서 콘텍스트의 상태를 변경할 수 있는 조건을 확인할 수 있도록 콘텍스트 인터페이스에 메서드를 추가해야 한다는 것을 의미한다.

콘텍스트의 상태 변경을 누가 할지는 주어진 상황에 알맞게 정해 줘야 한다. 먼저 콘텍스트에서 상태를 변경하는 방식은 비교적 상태 개수가 적고 상태 변경 규 칙이 거의 바뀌지 않는 경우에 유리하다. 왜냐면 상태 종류가 지속적으로 변경되거나 상태 변경 규칙이 자주 바뀔 경우 콘텍스트의 상태 변경 처리 코드가 복잡해질 가능성이 높기 때문이다. 상태 변경 처리 코드가 복잡해질수록 상태 변경의 유연함이 떨어지게 된다.

반면, 상태 객체에서 콘텍스트의 상태를 변경할 경우, 콘텍스트에 영향을 주지 않으면서 상태를 추가하거나 상태 변경 규칙을 바꿀 수 있게 된다. 하지만, 상태 변경 규칙이 여러 클래스에 분산되어 있기 때문에, 상태 구현 클래스가 많아질수록 상태 변경 규칙을 파악하기 어려워지는 단점이 있다. 또한, 한 상태 클래스에서 다른 상태 클래스에 대한 의존도 발생한다.

두 방식은 명확하게 서로 상반되는 장단점을 갖고 있기 때문에, 상태 패턴을 적용할 때에는 주어진 상황에 알맞은 방식을 선택해야 한다.

데코레이터(Decorator) 패턴

데코레이터 패턴은 상속이 아닌 위임을 하는 방식으로 기능을 확장해 나간다.

데코레이터 패턴의 장점은 데코레이터를 조합하는 방식으로 기능을 확장할 수 있다는 데에 있다.

데코레이터 패턴을 사용하면 각 확장 기능들의 구현이 별도의 클래스로 분리되기 때문에, 각 확장 기능 및 원래 기능을 서로 영향 없이 변경할 수 있도록 만들어 준다. 즉, 데코레이터 패턴은 단일 책임 원칙을 지킬 수 있도록 만들어 준다.

스프링 프레임워크의 경우 트랜잭션 처리를 위해 데코레이터 패턴을 사용한다.

데코레이터 패턴을 적용할 때 고려할 점

데코레이터 대상이 되는 타입의 기능 개수를 고려해야 한다. 정의되어 있는 메서드가 증가하게 되면 그만큼 데코레이터의 구현도 복잡해진다.

데코레이터 객체가 비정상적으로 동작할 때 어떻게 처리할 것인지 고려해야 한다.

데코레이터는 사용자 입장에서 데코레이터 객체와 실제 구현 객체의 구분이 되지 않기 때문에 코드만으로는 기능이 어떻게 동작하는지 이해하기 어렵다.

프록시(proxy) 패턴

  • 문제 상황
    • 스크롤을 이용해서 제품 목록을 보여주는 화면에서, 목록 하단에 위치한 이미지는 실제로 스크롤을 하기 전까지는 화면에 보이지 않음에도 불구하고 목록을 구성할 때 메모리에 이미지 정보를 로딩하게 된다. 특히 이미지를 로컬 파일 시스템이 아닌 웹에서 읽어 온다면 이미지 로딩으로 인해 제품 목록을 보여주기까지 대기 시간이 길어지게 된다.
  • 해결 방법
    • 이미지가 실제로 화면에 보여질 때 이미지 데이터를 로딩하는 것이 가장 쉬운 방법이다. 필요한 시점에 Image 클래스를 이용해서 이미지를 로딩하는 DynamicLoadingImage 클래스를 추가하고, 목록을 보여주는 클래스에서 Image 클래스 대신 DynamicLoadingImage를 사용하게 만든다.
  • 또 문제 상황
    • 하지만, 이미지 로딩 방식을 변경해야 할 때 ListUI 코드(가장 하위 클래스)를 변경해야 하는 문제가 발생한다.

이런 상황에서 ListUI 변경 없이 이미지 로딩 방식을 교체할 수 있도록 해주는 패턴이 프록시 패턴이다. 프록시 패턴은 실제 객체를 대신하는 프록시 객체를 사용해서 실제 객체의 생성이나 접근 등을 제어할 수 있도록 해주는 패턴이다.

필요한 순간에 실제 객체를 생성해주는 프록시를 가상 프록시라고 부르는데, 프록시에는 가상 프록시 외에 보호 프록시나 원격 프록시 등이 존재한다. 보호 프록시는 실제 객체에 대한 접근을 제어하는 프록시로서, 접근 권한이 있는 경우에만 실제 객체의 메서드를 실행하는 방식으로 구현한다. 원격 프록시는 자바의 RMI(Remote Method Invocation)처럼 다른 프로세스에 존재하는 객체에 접근할 때 사용되는 프록시이다. 원격 프록시는 내부적으로 IPC(Inter process communication)이나 TCP 통신을 이용해서 다른 프로세스의 객체를 실행하게 된다.

프록시 패턴을 적용할 때 고려할 점

실제 객체를 누가 생성할 것인지 고려해야 한다. 가상 프록시는 필요한 순간에 실제 객체를 생성하는 경우가 많기 때문에 가상 프록시에서 실제 생성할 객체의 타입을 사용하게 된다. 반면에 접근 제어를 위한 목적으로 사용되는 보호 프록시는 보호 프록시 객체를 생성할 때 실제 객체를 전달하면 되므로, 실제 객체의 타입을 알 필요 없이 추상 타입을 사용하면 된다(무슨 말인지 모르겠다😢 보호 프록시 객체 생성은 무조건이므로 타입을 정확히 알 필요 없이 생성하면 된다는 말일까?).

위임 방식이 아닌 상속을 사용해서 프록시를 구현할 수도 있다. 예를 들어, 특정 기능은 관리자만 실행할 수 있어야 한다고 할 경우 보호 프록시를 사용할 수 있다. 이때 보호 프록시는 다음과 같이 상위 클래스의 메서드를 재정의하는 방법으로 구현할 수 있다.

상속 방식을 사용하면 위임 방식에 비해 구조가 단순해서 구현이 비교적 쉽다. 하지만 상속 방식의 프록시는 객체를 생성하는 순간 실제 객체가 생성되기 때문에 가상 프록시를 구현하기에는 적합하지 않다.

어댑터(Adapter) 패턴

클라이언트가 요구하는 인터페이스와 재사용하려는 모듈의 인터페이스가 일치하지 않을 때 사용할 수 있는 패턴이 어댑터 패턴이다.

어댑터에 해당하는 클래스는 재사용하려는 모듈의 인터페이스를 클라이언트가 요구하는 인터페이스에 맞춰 주는 책임을 갖는다. 어댑터 클래스의 메서드는 재사용하려는 모듈의 인터페이스 객체를 실행하고 그 결과를 클라이언트가 요구하는 인터페이스에 맞는 리턴 타입으로 변환해준다.

어댑터 패턴이 적용된 예를 SLF4J라는 로깅 API이다. SLF4J는 단일 로깅 API를 사용하면서 자바 로깅, log4j, LogBack 등의 로깅 프레임워크를 선택적으로 사용할 수 있도록 해 주는데, 이 때 SLF4J가 제공하는 인터페이스와 각 로깅 프레임워크를 맞춰 주기 위해 어댑터를 사용하고 있다.

어댑터 패턴은 개방 폐쇄 원칙을 따를 수 있도록 도와준다.

옵저버(Observer) 패턴

한 객체의 상태 변화를 정해지지 않은 여러 다른 객체에 통지하고 싶을 때 사용되는 패턴이 옵저버 패턴이다.

옵저버 패턴에는 크게 주제(subject) 객체와 옵저버 객체가 등장하는데, 주제 객체는 다음의 두 가지 책임을 갖는다.

  • 옵저버 목록을 관리하고, 옵저버를 등록하고 제거할 수 있는 메서드를 제공한다.
  • 상태의 변경이 바생하면 등록된 옵저버에 변경 내역을 알린다.

옵저버 객체를 구현한 클래스는 주제 객체가 호출하는 메서드에서 필요한 기능을 구현하면 된다.

주제 객체의 상태에 변화가 생길 때 그 내용을 통지받도록 하려면, 옵저버 객체를 주제 객체에 등록해줘야 한다.

옵저버 패턴을 적용할 때의 장점은 주제 클래 변경 없이 상태 변경을 통지 받을 옵저버를 추가할 수 있다는 점이다.

옵저버 객체에게 상태 전달 방법

옵저버 객체가 기능을 수행하기 위해 주제 객체의 상태가 필요한 필요할 수 있다. 경우에 따라서 옵저버 객체의 메서드를 호출할 때 전달한 객체만으로는 옵저버의 기능을 구현할 수 없을 수도 있다.

이런 경우 옵저버 객체에서 콘크리트 주제 객체에 직접 접근하는 방법을 사용하기도 한다. 그러면 콘크맅 트 옵저버 클래스는 필요에 따라 특정한 콘크리트 주제 클래스에 의존하게 된다.

옵저버에서 주제 객체 구분

옵저버 패턴이 가장 많은 사용되는 영역 중 하나는 GUI 프로그래밍 영역이다. 버튼이 눌릴 때 로그인 기능을 호출한다고 할 때, 버튼이 주제 객체가 되고 로그인 모듈을 호출하는 개체가 옵저버가 된다.

한 개의 옵저버 객체를 여러 주제 객체에 등록할 수도 있을 것이다. GUI 프로그래밍을 하면 이런 상황이 흔하게 발생한다.

한 옵저버 객체를 여러 주제 객체에 등록하면, 옵저버 객체에서 각 주제 객체를 구분할 수 있는 방법이 필요하다.

한 주제에 대한 다양한 구현 클래스가 존재한다면, 옵저버 객체 관리 및 통지 기능을 제공하는 추상 클래스를 제공함으로써 불필요하게 동일한 코드가 여러 주제 클래스에서 중복되는 것을 방지할 수 있을 것이다. 하지만, 해당 주제 클래스가 한 개뿐이라면 옵저버 관리를 위한 추상 클래스를 따로 만들 필요가 없다.

옵저버 패턴 구현의 고려 사항

  • 주제 객체의 통지 기능 실행 주제
    • 버튼처럼 주제 객체의 상태가 바뀔 때마다 옵저버에게 통지를 해줘야 한다면, 주제 객체에서 직접 통지 기능을 실행하는 것이 구현에 유리하다. 왜? 주제 객체를 사용하는 코드에서 통지 기능을 실행한다면 상태를 변경하는 모든 코드에서 통지 기능을 함께 호출해줘야 하는데, 이런 방식은 통지 기능을 호출하지 않는 등 개발자의 실수를 유발할 수 있기 때문이다.
    • 반대로, 한 개 이상의 주제 객체의 연속적인 상태 변경 이후에 옵저버에게 통지를 해야 한다면, 주제 객체가 아닌 주제 객체의 상태를 변경하는 코드에서 통지 기능을 실행해 주도록 구현하는 것이 통지 시점을 관리하기가 수월하다.
  • 옵저버 인터페이스의 분리
    • 한 주제 객체가 통지할 수 있는 상태 변경 내역의 종류가 다양한 경우에는 각 종류 별로 옵저버 인터페이스를 분리해서 구현하는 것이 좋다. 모든 종류의 상태 변경을 하나의 옵저버 인터페이스로 처리할 경우, 옵저버 인터페이스는 거대해진다. 그에 따라 콘크리트 옵저버 클래스는 모든 메서드를 구현해줘야 한다. 즉, 불필요한 코드를 만들어야 한다.
    • 주제 객체 입장에서도 각 상태마다 변경의 이유가 다르기 때문에, 이들을 한 개의 옵저버 인터페이스로 관리하는 것은 향후에 변경을 어렵게 만드는 요인이 될 수 있다.
  • 통지 시점에서의 주제 객체 상태
    • 통지 시점에서 주제 객체 상태에 결함이 없어야 한다.
    • 옵저버 객체가 올바르지 않은 상태 값을 사용하게 되는 문제가 발생하지 않도록 하려면 상태 변경과 통지 기능에 템플릿 메서드 패턴을 적용하는 것이다.
  • 옵저버 객체의 실행 제약 조건
    • 옵저버 인터페이스를 정의할 때 옵저버 메서드의 실행 제한에 대한 명확한 기준이 필요하다.

미디에이터(Mediator) 패턴

객체 간 메시지 흐름을 각 클래스에 직접적인 의존으로 구현하게 되면, 개별 클래스의 재사용이 어려워지고 메시지 흐름을 변경하려면 관련된 클래스들을 모두 변경해주어야 하는 문제가 발생한다.

미디에이터 패턴을 사용하면 이런 문제를 해소할 수 있다. 미디에이터 패턴은 각 객체들이 직접 메시지를 주고받는 대신, 중간에 중계 역할을 수행하는 미디에이터 객체를 두고 미디에이터를 통해서 각 객체들이 간접적으로 메시지를 주고받도록 한다.

비슷하게 다른 협업 객체들도 모든 요청을 미디에이터에 보내며, 미디에이터는 그 요청을 처리할 알맞은 객체를 실행한다. 이렇게 각 협업 객체가 서로 알 필요 없이 미디에이터가 각 객체 간의 메시지 흐름을 제어하기 때문에, 새로운 협업 객체가 추가되더라도 기존 클래스를 수정할 필요 없이 미디에이터 클래스만 수정해주면 된다. 물론, 메시지 흐름이 변경되더라도 메시지 흐름을 실제로 제어하는 건 미디에이터이므로 미디에이터만 수정될 뿐 각 협업 클래스를 수정할 필요는 없으며 수정하더라도 변경 범위가 최소화된다.

미디에이터 패턴은 각 협업 클래스에 흩어져 있는 흐름 제어를 미디에이터로 모으기 때문에, 각 협업 클래스의 코드는 단순해진다. 각 협업 클래스는 미디에이터에만 의존하거나 또는 (옵저버 패턴 등을 사용해서) 미디에이터나 다른 협업 클래스에 의존하지 않기 때문에, 개별 협업 클래스를 수정하거나 확장하거나 재사용하기가 쉬워진다. 또한 미디에이터에 각 협업 객채의 흐름 제어 코드가 모여 있기 때문에 전체 협업 객체 간 메시지 흐름을 이해하고 수정하고 확장하는 것을 상대적으로 쉽게 만들어 준다.

반면, 미디에이터 패턴을 사용할 때의 단점은 협업 클래스의 개수가 증가할수록 미디에이터의 코드는 복잡해지기 때문에, 미디에이터 자체를 유지 보수하는 것은 협업 클래스에 비해 어려워진다는 것이다.

추상 미디에이터 클래스의 재사용

미디에이터 패턴을 적용할 때 협업 객체 간의 동일한 메시지 흐름이 서로 다른 기능에서 반복해서 사용될 경우, 미디에이터 추상 클래스를 사용함으로써 미디에이터 자체의 재사용을 높일 수 있다.

파사드(Facade) 패턴

코드 중복과 직접적인 의존을 해결하는데 도움을 주는 패턴이 파사드 패턴이다. 파사드 패턴은 서브 시스템을 감춰주는 상위 수준의 인터페이스를 제공함으로써 이 문제를 해결한다.

각 클라이언트는 파사드를 이용해서 원하는 기능을 수행하게 된다. 파사드 패턴 적용 전에는 각 클라이언트가 직접 서브 시스템에 접근했다면, 파사드 패턴 적용 후에는 파사드를 통해서 간접적으로 서브 시스템에 접근한다.

파사드 패턴을 적용함으로써 코드가 간결해지고 클라이언트와 서브 시스템 간 직접적인 의존이 제거된다.

파사드 패턴의 장점과 특징

클라이언트와 서브 시스템 간 결합을 제거함으로써 얻을 수 있는 또 다른 이점은 파사드를 인터페이스로 정의함으로써 클라이언트의 변경 없이 서브 시스템 자체를 변경할 수 있다는 것이다.

파사드 패턴을 적용한다고 해서 서브 시스템에 대한 직접적인 접근을 막는 것은 아니다. 파사드 패턴은 단지 여러 클라이언트에 중복된 서브 시스템 사용을 파사드로 추상화할 뿐이다. 따라서 다수의 클라이언트에 공통된 기능은 파사드를 통해서 쉽게 서브 시스템을 사용할 수 있도록 하고, 보다 세밀한 제어가 필요한 경우에는 서브 시스템에 직접 접근하는 방식을 선택할 수 있다.

추상 팩토리(Abstract Factory)패턴

비행기 슈팅 게임에서, 적과 장애물 객체의 생성을 Stage 클래스에서 직접 수행하면서 여러 문제가 발생한다. 새로운 적 클래스가 추가되거나 각 단계의 보스 종류가 바뀔 때 Stage 클래스를 함께 수정해줘야 한다. 단계별로 적 생성 규칙이 달라질 경우에도 Stage 클래스를 수정해줘야 한다. 또한, 중첩되거나 연속된 조건문으로 인해 코드가 복잡해지기 쉽고 이는 코드 수정을 어렵게 만드는 원인이 된다.

Stage 클래스로부터 객체 생성 책임을 분리함으로써 이 문제를 해소할 수 있다. 이 때 사용되는 패턴이 바로 추상 팩토리 패턴이다.

추상 팩토리 패턴에서는 관련된 객체 군을 생성하는 책임을 갖는 타입을 별도로 분리한다.

추상 팩토리 패턴을 사용할 때의 장점은 클라이언트에 영향을 주지 않으면서 사용할 제품(객체) 군을 교체할 수 있다는 점이다.

만약 팩토리가 생성하는 객체가 늘 동일한 상태를 갖는다면, 프로토타입 방식으로 팩토리를 구현할 수 있다. 프로토타입 방식은 생성할 객체의 원형 객체를 등록하고, 객체 생성 요청이 있으면 원형 객체를 복제해서 생성한다.

프로토타입 방식의 팩토리를 사용하면, 객체 군 마다 팩토리 클래스를 작성할 필요 없이 객체 군 마다 팩토리 객체를 생성해 주면 된다.

프로토타입 방식을 사용하면 추상 팩토리 타입과 콘크리트 팩토리 클래스를 따로 만들 필요가 없어 구현이 쉽지만, 반면에 제품 객체의 생성 규칙이 복잡할 경우 적용할 수 없는 한계가 있다.

추상 팩토리 패턴을 적용한 대표적인 예가 자바의 JDBC API이다.

컴포지트(Composite) 패턴

거의 동일한 코드가 중복된다는 점은 결국 복잡도를 높여서 코드의 수정이나 확장을 어렵게 만드는데, 이런 단점을 해소하기 위해 사용되는 패턴이 컴포지트 패턴이다. 컴포지트 패턴은 이 문제를 전체 - 부분을 구성하는 클래스가 동일 인터페이스를 구현하도록 만듦으로써 해결한다.

컴포지트 패턴에서 컴포지트는 다음과 책임을 갖는다.

  • 컴포넌트 그룹을 관리한다.
  • 포지트에 기능 실행을 요청하면, 컴포지트는 포함하고 있는 컴포넌트들에게 기능 실행 요청을 위임한다.

그러면 전체냐 부분이냐에 상관 없이 클라이언트는 단일 인터페이스로 기능을 실행할 수 있는 장점이 생긴다.

또 다른 장점은 컴포지트 자체도 컴포넌트이기 때문에, 컴포지트에 다른 컴포지트를 등록할 수 있다는ㄱ ㅓㅅ이다.

컴포지트 패턴 구현 시 고려 사항

컴포넌트를 관리하는 인터페이스를 어디서 구현할지 고려해야 한다.

널(Null) 객체 패턴

여러 코드에서 한 객체에 대한 null 검사를 하게 되면 null 검사ㅣ 코드를 누락하기 쉬우며, 이는 프로그램 실행 도중에 NullPointerException을 발생시킬 가능성을 높여준다.

널 객체 패턴은 null 검사 코드 누락에 따른 문제를 없애준다. 널 객체 패턴은 null을 리턴하지 않고 null을 대신할 객체를 리턴함으로써 null 검사 코드를 없앨 수 있도록 한다. 널 객체 패턴은 다음과 같이 구현한다.

  • null 대신 사용될 클래스를 구현한다. 이 클래스는 상위 타입을 상속 받으며, 아무 기능도 수행하지 않는다.
  • null을 리턴하는 대신, null을 대체할 클래스의 객체를 리턴한다.

널 객체 패턴을 사용할 때의 장점은 null 검사 코드를 사용할 필요가 없기 때문에 코드가 간결해진다는 점이다. 코드가 간결해진다는 것은 그만큼 코드 가독성을 높여주므로 향후에 코드 수정을 보다 쉽게 만들어준다.

출처

  • 책 <개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴> 7장 디자인 패턴