디자인 패턴(구조 패턴)
작성일: 2021-08-28 16:36
구조 패턴은 더 큰 구조를 형성하기 위해 어떻게 클래스와 객체를 합성하는가와 관련된 패턴이다.
# 적응자(Adapter) 패턴
특정 클래스의 인터페이스를 사용자가 기대하는 인터페이스 형태로 변환 시켜주는 패턴
# 활용성
- 기존 클래스를 사용하고 싶은데 인터페이스가 맞지 않을 때
- 이미 만든 것을 재사용하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없을 때
# 구조
- Target: 클라이언트가 사용하는 인터페이스
- Adapter: Adaptee를 Target 인터페이스에 적응 시켜주는 Adapter
- Adaptee: Target 인터페이스로 변환이 필요한 Adaptee
# 결과
- 기존 코드를 변경하지 않고 기존 구현체들을 재사용하여 클라이언트가 원하는 인터페이스를 제공해줄 수 있다.
- 인터페이스 변환의 책임을
Adapter
가 담당하여 관심사를 분리할 수 있다. - Adapter를 적용하기 위한 작업량은 Target과 Adaptee간의 유사성에 달려 있다.
- 단순한건 연산의 이름만 수정해도 되겠지만, 복잡한 경우엔 새로운 연산을 추가하여 새로운 기능을 정의해야 할 수도 있다.
Adapter
클래스가 추가되는것이 실용적이지 않다면Adaptee
가 직접Target
을 구현하도록 하여 구조를 단순화 시킬 수 있다.
# 관련 패턴
- Bridge, Decorator, Proxy패턴과 클래스 구조가 유사하나 사용 목적이 다르다.
# 사용예
Arrays.asList(1, 2, 3)
는[1, 2, 3](Adaptee)
배열을 Arrays 내부에 구현된ArrayList(Adapter)
를 이용하여List(Target)
로 변환해준다.- Spring MVC의
HandlerAdapter
인터페이스는hanlder(Adaptee)
를 파라미터로 받아ModelAndView(Target)
로 변환해주는 어댑터로 볼 수 있다.
# 가교(Bridge) 패턴
- 추상과 구현을 분리하여 각각이 독립적으로 확장 가능하도록 하는 패턴
# 활용성
- 추상적 개념과 구현 사이의 지속적인 종속 관계를 피하고 싶을 때
- 추상적 개념과 구현이 모두 독립적으로 확장되어야 할 필요가 있을 때
# 구조
- Abstraction: 추상적 개념에 대한 인터페이스를 제공하고
Implementor
를 참조하여 인터페이스의 구현에 사용함 - RefinedAbstraction: 추상적 개념에 대한 인터페이스를 확장하는 클래스
- Implementor: 추상적 개념에 대한 구현을 위한 인터페이스를 정의
Abstraction
에 정의된 인터페이스와 정확하게 대응될 필요가 없으며 서로 다른 형태일 수 있다.
- ConcreteImplementor:
Implementor
에 정의된 인터페이스를 구현하는 구현체
Abstraction
과Implementor
가 연결된 관계를 bridge라고 한다.
# 결과
- 추상과 구현이 분리되어 독립적으로 확장 가능해진다.
Implementor
의 구현체가 변경되어도Abstraction
가 변경되지 않으므로Implementor
를 런타임에 변경할 수 있다.
- 구조적으로 복잡성이 증가한다.
# 사용예
- 자바의 JDBC에서 데이터 접근을 위한 코드를 작성할 때
Connection
,Statement
,ResultSet
등의 인터페이스를 사용하고 어떤 데이터베이스 기술을 사용하는지 신경쓰지 않는다. - 실제 사용되는 데이터베이스 기술에 대한 설정은
java.sql.Driver
를 통해 수행된다. 여기서java.sql.Driver
인터페이스가 bridge 패턴의Implementor
가 되고 데이터베이스 기술별로 구현된Driver(com.mysql.cj.jdbc.Driver)
가ConcreteImplementor
가 된다. - 우리가 데이터베이스 기술을 변경하여 새로운
Driver
구현체를 사용하더라도Connection
,Statement
,ResultSet
를 사용하여 작성된 데이터 접근 코드는 영향이 없다. 이들이 bridge 패턴의Abstraction
이 된다.- 이러한
Abstraction
들도RefiendAbstraction
형태로 확장되어 사용된다.
- 이러한
# 복합체(Composite) 패턴
객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 동일하게 다룰 수 있도록 하는 패턴
# 활용성
- 부분-전체의 객체 계통을 표현하고 싶을 때
- 클라이언트에서 개별, 복합 객체의 차이를 알지 못하고 동일한 인터페이스로 취급하도록 하고 싶을 때
# 구조
- Component: 개별, 복합 객체에 대한 인터페이스를 정의
- Composite: 자신이 복합하는 요소들을 이용하여 Component를 구현하는 구현체
- Leaf: 자식이 없고 operation만을 구현하는 기본 구현체
# 결과
Leaf
,Composite
객체는 캡슐화되어 클라이언트는 일반화된 상위 개념인Component
에만 의존할 수 있어 코드가 단순해지고 확장이 가능해진다.- 클라이언트가
Component
에만 의존할 수 있게 하기 위해선Leaf
는 실제 구현을 제공하지 않는 메서드도 구현이 필요하다. 이는 상속 규칙인리스코프 치환 원칙
을 위배한다. - 이를 해결하려면 클라이언트는
Leaf
,Composite
를 개별 대상으로 간주해야 하고 이는Composite
패턴의 장점을 잃는다. 상황에 따른 트레이드오프가 필요하다.
- 클라이언트가
# 데코레이터(Decorator) 패턴
위임을 활용하여 객체에 새로운 책임(기능)을 동적으로 추가할 수 있도록하는 패턴
# 활용성
- 다른 객체에 영향을 주지 않고 개별 객체에 새로운 책임을 추가하기 위해 사용한다.
# 구조
- Component: 동적으로 책임을 추가할 가능성이 있는 객체들의 인터페이스
- ConcreteComponent: 동적으로 책임이 추가되어야 할 객체
- Decorator: Component를 구현하면서 다른 Component를 참조하여 동적으로 책임을 추가할 수 있도록 하는 추상 클래스
- ConcreteDecorator: 동적으로 추가할 책임을 구현하는 구현체
Decorator가 적거나 하나라면 추상 클래스를 정의하지 않는게 더 깔끔해보인다.
# 결과
- Decorator를 활용하면 런타임에 새로운 책임을 추가 및 제거가 가능해진다.(런타임에 다양한 조건에 따라 특정 데코레이터(책임)을 추가할지 말지를 결정할 수 있다.)
- 상속을 통해 책임을 추가하기 위해선 가능한 모든 경 우의 수에 대한 클래스가 필요하므로 현실성이 없다.
- 데코레이터는 추가하고 싶은 책임의 수 만큼만 데코레이터 클래스가 있으면 된다.
- 동적으로 추가할 책임마다 Decorator를 정의해야 하므로 작은 규모의 객체들이 많아져 구조가 복잡해질 수 있다.
# 사용예
Collections.synchronizedList(list)
는 리스트를 동기화할 수 있도록 데코레이터를 활용한다.Collections.unmodifiableList(list)
는 리스트를 수정할 수 없도록 데코레이터를 활용한다.
# Decorator 패턴과 Strategy 패턴
Decorator
패턴을 활용하기 위해선 Component를 가볍게 유지해야 한다. Component가 무거워지면 Decorator도 무거워져 새로운 Decorator를 추가하기 어려워진다.- 만약 Component가 본질적으로 매우 복잡하고 무거운 특성을 갖는다면 Strategy 패턴이 더 좋은 해결 방안이다.
Decorator
패턴은 객체의 외관을 변경하므로 Component는 Decorator에 대해 모른다.Strategy
패턴은 Component의 내부 구성요소에 대한 확장을 위한 것이므로 Component는 Strategy를 참조한다.
# 관련 패턴
- Decorator 패턴은 한 구소용소만을 갖는 Composite로 볼 수 있으므로 Composite와 관련된다. 하지만 Decorator의 목적은 객체의 합성이 아니라 새로운 책임을 추가하기 위함이다.
- Decorator 패턴은 객체의 외관을 변경하지만, Strategy 패턴은 객체의 내부를 변경한다. 이 둘은 객체를 변경하는 두 가지 다른 대안이다.
# 퍼사드(Facade) 패턴
- 서브시스템을 사용하기 쉽도록 상위 수준의 획일화된 하나의 인터페이스를 제공하는 패턴
# 활용성
- 복잡한 서브시스템에 대한 단순한 인터페이스 제공이 필요할 때
- 클라이언트 혹은 다른 서브시스템 간의 결합도를 줄일 필요가 있을 때
- 서브시스템에 정의된 모든 인터페이스를 공개하지 않고, 단순한 퍼사드만을 공개하여 결합도를 줄일 수 있다.
- 서브시스템을 계층화시킬 때
- 퍼사드 패턴을 사용하여 각 서브 시스템의 계층에 대한 접근점을 제공한다.
# 구조
- Facade: 서브시스템을 어떻게 사용해야하는지 잘 알고 있어 단순하고 일관된 통합 인터페이스를 제공
- 클라이언트는 서브시스템을 사용하기 위해 Facade만 참조하면 된다.
# 결과
- 서브시스템의 구성요소를 보호하고 클라이언트간의 결합도를 약하게 만든다.
- 클라이언트는 퍼사드만 알기 때문에 내부 구성요소를 보호하고, 결합도를 약하게 만들어 서브시스템 내부 설계 변경에 자유로울 수 있다.
# 사용예
- layered architecture에서의
application layer
나 hexagonal architecture에서의inbound(primary) port
의 역할을 하는 객체들이 내부 도메인 로직을 숨기고 유즈케이스를 위한 인터페이스를 제공하기 때문에 퍼사드 패턴을 사용한다고 볼 수 있다.
# 관련 패턴
- 추상 팩터리 패턴은 서브시스템에 독립적인 방법으로 서브시스템 객체를 생성하는 인터페이스를 제공하기 위해 퍼사드를 함께 사용할 수 있다.
- 중재자 패턴은 퍼사드 패턴과 비슷하다.
- 중재자 패턴은 여러 객체 사이의 협력관계를 추상화하여 중재자를 통해서만 상호작용할 수 있도록 한다.
- 퍼사드 패턴은 서브시스템 인터페이스 자체를 추상화하여 사용을 용이하게 하려는 목적을 갖는다.
# 프록시(Proxy) 패턴
대상 객체에 대한 접근을 제어하기 위한 대리자 역할을 하는 객체를 두는 패턴
# 활용성
remote proxy
로 활용- 서로 다른 주소 공간에 존재하는 객체를 가리키는 로컬 환경에 존재하는 proxy
virtual proxy
로 활용- 요청이 있을 때 필요한 고비용 객체를 생성하는 proxy
protection proxy
로 활용- 대상 객체의 접근을 제어하기 위한 proxy
# 구조
- Subject: Target과 Proxy에 대한 공통 인터페이스를 정의하여 Target이 필요한 곳에 Proxy를 대체할 수 있도록 한다.
- Target: 프록시가 대리자 역할을 하기 위한 대상 객체
- Proxy: Target을 참조하여 Proxy 특성에 따라 적절한 기능을 수행한다.
# 결과
- 클라이언트는 Subject에만 의존하므로 클라이언트는 Proxy의 존재를 모른채 Proxy의 기능들을 제공할 수 있다.
# 관련 패턴
Proxy
패턴과Decorator
패턴은 프록시 객체를 활용하기 때문에 구현 방법이 비슷하나 사용 목적이 다르다.Proxy
패턴은 객체에 대한 접근을 제어하는 목적으로 사용된다.Decorator
패턴은 동적으로 책임(기능)을 추가하기 위한 목적으로 사용된다.
- 즉, 프록시 객체가 객체에 대한 접근을 제어하기 위한 목적이라면
Proxy
패턴을 활용했다고 볼 수 있고 기능을 추가하기 위한 목적이라면Decorator
패턴을 활용했다고 볼 수 있다.
# Composite vs Decorator vs Proxy
- Composite 패턴과 Decorator 패턴은 재귀적 합성 기법을 사용하기 때문에 구조가 비슷하다. 하지만 목적은 다르다.
Decorator 패턴의 목적은
상속 없이 객체에 새로운 기능을 추가하려는 것이다.Composite 패턴의 목적은
클래스 구조화에 초점을 맞춘것으로 어떻게 관련 객체들을 하나의 인터페이스로 다룰 수 있도록 일관성을 부여하는 것이다.- 이런 목적들을 상충되는 것이 아닌, 상호보완적이므로 두 패턴이 함께 사용할 때가 많이 있다.
- Decorator 패턴과 Proxy 패턴은 대상 객체에 대한 참조자를 관리하므로 구조가 비슷하다. 하지만 목적은 다르다.
- Proxy는 Decorator와 달리 동적으로 어떤 기능을 추가 및 제거하지 않는다.
Proxy 패턴의 목적은
대상 객체에 대한 접근을 제어하는 것이다. (캐시, 지연 로딩 또한 접근 제어 중 하나라고 볼 수 있다.)- Proxy 패턴은 참조하는 Target이 실제 기능을 제공하지만 Decorator 패턴은 Decorator 자체에서도 기능적인일을 담당한다.
- 그러므로 Decorator는 재귀적 합성이 중요하지만 Proxy에서는 단순이 메시지를 전달하므로 재귀적 합성은 전혀 의미가 없다.
- 각 패턴들은 구조가 비슷해보이지만, 서로 다른 객체지향 설계의 문제들을 해결하려는 목적이 있으므로 이러한 차이는 매우 중요하다.