상반기 내용 정리
- cleanArchitecture
- oop
- hexagonal
- DDD
- designPattern
- antiPattern
구성
아키텍쳐? 설계?
보통 아키텍쳐를 구성한다. 설계한다를 구분 없이 혼용해서 사용하는 경우가 있다. 물론 헷깔리긴 한다.
-
아키텍쳐: 저수준의 세부 사항(‘외부에서 어떤 프로토콜로 호출하는가?’, ‘어떻게 기록할지?’)과는 별개의 코드 상의 고수준의 무엇인가를 의미한다.
-
설계: 저수준의 구조 혹은 결정 사항 등을 의미한다.
개발자가 SW를 만들면서 마주하는 두 가치가 있다. 첫 번째는 행위(behavior)다. SW는 수익을 창출하거나 비용을 절약하도록 만들기 위해서 만든다. 프로그래머는 이를 위해서 이해관계자가 기능 명세서나 요구 사항 문서를 구체화할 수 있도록 돕는다. 더 나아가 요구사항을 만족하도록 코드를 작성한다 두 번째는 아키텍쳐다. 문제를 해결하기 위해서 형태다. SW 개발 비용의 증가를 결정짓는 주된 요인이 되기도 한다. 아키텍쳐가 특정 형태에 치우치면 변화에 대응하기 어려워진다. 이 둘은 모두 중요하다. 예를 들어 완벽하게 동작하는데 기능 추가가 어려워도 힘들다. 반대로 동작하지 않고 변경에 쉽다고 해도 굉장히 부담이 된다. 만약 이 둘 중에 고르라면 그나마 후자가 낫다. 노력 여하에 따라서 변경할 수 있음을 의미하기 떄문이다. 결과적으로 결과물도 중요하지만 아키텍쳐가 중요하다. 개발자는 개발을 하는 것뿐만 아니라 SW를 안전하게 보호하는 역할을 하기도 한다. 따라서 그 역할에 최선을 다해야 한다.
패러다임
아키텍쳐를 살펴보면서 빼놓을 수 없는 것이 **패러다임이다. 기초 체력이 있어야 뼈대를 만든다. 프로그래밍 패러다임은 무엇인가를 **제한**하는 내용을 담고 있다.
1. 구조적 프로그래밍
-
모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있고 기능적으로 분해할 수 있음을 의미한다.
-
모든 프로그램은 sequence, selection, iteration이 세 가지로 이뤄짐을 증명한다.
-
제어 흐름의 직접적인 전환에 제한에 두는 패러다임이다.
2. 객체 지향
-
여러 가지가 있지만 가장 설득력 있는 것은 **다형성**이다.
-
다른 것들도 가진 것들을 소거하면 남는 특징이다.
-
DIP는 다형성을 기반으로 구현할 수 있으며 이를 통해서 의존성과 제어 흐름을 역전시킬 수 있다.
-
이를 통해서 전체 시스템의 모든 소스 코드 의존성에 대한 절대적을 제어 권한을 갖을 수 있다.
-
이를 통해서 제어 흐름을 간접적으로 제한하는 것에 방점을 두는 패러다임이다.
더 깊히 들어가보자.
객체지향은 단순히 클래스를 만드는 것이 아니다.
- 할 일을 정의하고 필요한 데이터 구조를 정의하고 만들 클래스를 저의
- 공통적인 행동과 상태를 공유하는 객체를 추상화하는 과정
- 객체 간의 컨텍스트 내에서 협력을 통해서 이뤄내는 것이다.
구현
- 내부, 외부를 구분 짓는다.
- 어떤 역할로 어떤 모습으로 협업할지를 그리는 것이다.
- 내, 외부 구분은 어떤 것을 공개하고 감출지를 결정하는 것이다.
- 경계를 명확하게 두면서 객체의 역할과 한계를 정해서 오히려 자율성을 보장한다.
자율성
- 경계를 그어주고 그 내부에서 스스로 판단하고 행동하는 존재다.
- 이를 캡슐화라고 한다.
- 흔들리지 않기 위해서 접근을 통제하고 이 선을 기준으로 협업을 한다.
협력
- 객체 간 컨텍스트 내에서 서로가 서로에 요청을 해서 의사소통을 한다.
- 요청을 해도 실패는 할 수 있다.
- 이러한 요청을 ‘메시지를 전송한다.’라고 한다.
- 객체 간 협력, 메시지 수발신은 다형성에 의해서 ‘누가 수행할 것인가’를 정하게 할 수 있다. OOP의 특성이기도 하다.
- 다형성은 상속, 인터페이스 구현으로 할 수 있다. 이를 지연 바인딩, 동적 바인딩이라고 한다.
> 구현 상속, 인터페이스 상속
- 구현 상속은 서브 클래싱이며 서브 클래싱이라고 한다. 순수한 재사용을 목적으로 한다.
- 인터페이스 상속은 서브 타이핑이다. 다형적인 협력을 위해서 부모-자식 간의 인터페이스 공유를 목적으로 한다.
- 보통 상속을 재사용을 위해서 사용한다고 생각한다. 음. 반은 맞고 반은 틀리다.
> - 일단 상속은 부모 자식 간 강결합이 생긴다. => 설계에서 유연성이 떨어진다.
- 서로 너무 많이 알게 된다. => 캡슐화 위반 - 서브 타이밍을 위해서 한다면 문제는 없다.
- 만약 재사용을 위한 거라면 composition이 나을 수 있다.
> 메시지
- 메시지: 객체가 다른 객체와 협력하기 위해서 사용하는 의사소통 메커니즘
- 오퍼레이션: 객체가 다른 객체에 제공하는 추상적인 서비스, 메시지를 수신하는 객체의 인터페이스를 강조
- 메소드: 메시지에 응답하기 위해서 실행되는 코드 블록
- 퍼블릭 인터페이스: 객체가 협력에 참여하기 위해서 외부에서 수신할 수 있는 메시지 묶음
- 시그니쳐: 오버레이션, 메소드의 명세
> 디미터 법칙
- ‘인접한 이웃과만 의사소통하라’를 기치로 한다.
- 묻지 말고 시켜라: 내부에 대해서 파헤치지 말고 바깥에 물어보도록 한다.
- 의도를 드러내는 인터페이스: 메소드가 어떻게가 아니라 무엇을 하는지를 나타내도록 한다.
- 함께 모으기: ISP를 준수하면 된다. 내부 구조를 묻는 대신 직접 자신의 책임을 수행하도록 시킨다.
- 이를 바탕으로 메시지를 보낼 수 있는 대상을 좁힌다.
- 추가로 체이닝을 막도록 한다. (알 필요가 없는 경우를 밖으로 내보내지 말고 안에서 처리할 수 있도록 한다.)
책임
- 협력에 참여하기 위해서 객체가 수행하는 행동을 책임이라고 한다.
- 책임은 객체에 의해 정의되는 응집도 있는 행위의 집합니다.
- 목표는 필요한 정보를 가장 잘 아는 전문가에 책임을 할당하는 것이다.
책임을 할당하는 방법
- GRASP(General Responsibility Assignment Software Pattern)이 있다.
- InformationExpert: 책임에 걸맞는 객체에 데이터와 처리 로직을 묶는 것. 이를 통해서 스스로 처리하는 자율적인 존재가 된다.
- Creator: A,B 간 결합도가 높으면 서로의 생성을 책임지는 것
- Controller: 시스템 이벤틀를 처리할 객체를 만드는 것, antiCorruption과 유사하다. (물론 약간의 차이가 있긴 하다.)
- LowCoupling: 객체들간, 서브 시스템들간의 상호의존도가 낮게 책임을 부여
- HighCohesion: 각 객체가 밀접하게 연관된 책임들만 가지도록 구성
- Polymorphism: 객체 종류 역할 개념을 부여하여 다른 일을 하도록 하는 것이다. 동적 바인딩을 이용한다.
- PureFabrication: 도메인 관련이 아니면 책임을 별도로 한 곳으로 관리하는 객체를 만드는 것이다.
- Indriection: 두 객체 사이의 직접적인 커플링을 피하기 위해서 중간에 하나의 층을 두는 것이다. 보통 interface가 그 역할을 한다.
- ProtectedVariations: 변경될 여지가 있는 것과 아닌 것을 분리하고 변하는 개념을 캡슐화하는 것이다.
- 이를 바탕으로 ‘책임 주도 설계’가 있다. - 데이터보다 행동을 먼저 정하고 그에 필요한 데이터를 지정 - 협력이라는 문맥 안에서 책임을 결정
역할
- 협력아래서 만들어진 책임들을 모으면 역할이 된다.
- 역할이 생기면 유연하고 재사용 가능한 협력을 할 수 있다.
추상화
- 불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 것을 의미한다.
- 프로시저 추상화: 내부 상세 구현을 몰라도 인터페이스만 알면 프로시저를 사용할 수 있게 한다.
- 데이터 추상화: 시스템의 상태를 저장할 데이터를 표현한다.
의존성
- 협력을 하기 위해서 다른 객체를 필요로 하면 생긴다.
- 삼단논법과 유사하게 A->B B->C ==> A -> C 와 같이 전이 될 수 있다.
- 의존성은 의존 자체가 문제가 아니라 얼마만큼 의존하는가가 주용하다.
3. 함수형 프로그래밍
-
가변 변수를 상정하지 않는다. 한 번 초기화하면 절대로 변경하지 않는다.
-
이를 통해서 아키텍쳐에서 변수의 가변성을 배제한다. 이로써 경합, 교착, 동시성 문제에서 자유로와 진다.
-
함수형 프로그래밍은 변수 할당에 대한 규칙을 부과한다.
설계 원칙
-
패러다임을 바탕으로 몇 가지 설계 원칙을 잡을 수 있다.
-
그 중에 살펴보면 SOLID를 볼 수 있다.
SRP
- 하나의 모듈이 하나의 일만 하는 것이 아니다.
- 단일 모듈의 변경의 이유가 하나, 오직 하나뿐이어야 한다는 것이다.
- 즉, 하나의 모듈은 하나의 actor에 대해서만 책임져야 한다.
- 이는 변경되는 상황에서의 전파도를 낮출 수 있어야 한다는 것, 변경될 것과 아닌 것을 분리해야 한다는 의미로까지 해석할 수 있다.
- 이 원칙을 위반하는 징후들은 아래와 같다.
OCP
- 개채의 행위는 확장할 수 있어야 하지만, 이 때 개체를 변경하면 안 된다는 원칙이다.
- 작은 수정에 기존이 흔들이면 연쇄적으로 이에 의존하고 있는 부분도 흔들리게 될 것이다.
- 이를 위해서는 책임에 대한 분리, 의존성 역전 등을 해둬야 한다.
- 이는 아키텍쳐를 떠받치는 원동력으로 작용하며, 시스템을 확장하기 쉬운 동시에 변경에 대한 파급력이 적을 수 있는 방향으로 유도해준다.
LSP
- ‘S 타입의 객체와 T 타입에 대응하는 객체가 있을 때 S 타입에 대응하는 객체 O1이 T 타입의 객체 O2 대신 놓였을 때도 행위가 변하지 않는다면 S는 T의 하위 타입이다.’는 의미다.
- 자주 나오는 주제로 상속, 구현이 있다. 결론부터 말하면 서브 타이핑이 필요하며, 서브 클래싱을 위한 상속은 큰 의미가 없다는 것이다.
- 처음에는 LSP는 상속을 사용하도록 유도하는 장치 정도로 간주됐었다.
- LSP는 시간이 지남에 따라서 인터페이스와 구현체에도 적용되는 설계원칙으로 변모됐다.
- LSP를 준수하도록 만들어져 있다면 서브타이핑 하나로 새로운 기능을 추가할 수 있지만 아니라면 새롭게 코드를 작성해야 하는 안타까운 경우가 발생할 수도 있다.
- LSP는 아키텍쳐 레벨까지 확장해야만 한다. 그게 아니라면 아키텍쳐가 오염되어 위의 언급과 같이 별도 메커니즘을 추가해야 할 수 있기 때문이다.
ISP
- 정적 타입 언어는
import등의 타입 선언문을 강제한다.- 소스코드에 선언된 선언문으로 소스 코드 의존성이 발생하고, 이로 인해 재컴파일, 재배포가 강제되는 상황이 무조건 초래된다.
- ISP를 사용하는 근본적인 동기는 필요 이상으로 많은 것을 포함하는 모듈에 의존하는 것에 대한 인지로부터 시작된다.
- 불필요한 재컴파일, 재배포를 강제하기 때문이다.
DIP
- ‘유연성이 극대화된 시스템’이란 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.
- 의존에서 피하고자 하는 것은 변동성이 큰 구체적 요소다. 이는 인터페이스보다 변동성이 높다.
- 인터페이스를 변경하지 않고 구현체를 수정할 수 있으며, 외부에서는 인터페이스에 의존하면 안정된 아키텍쳐를 얻을 수 있다.
컴포넌트 원칙
-
컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 배포 단위다.
-
SW의 재사용성의 중요성, 필요성을 위해서 나온 개념이다.
응집도
-
재사용/릴리즈 등가 원칙: 재사용 단위는 릴리즈 단위와 같다. -> 포함 원칙
-
공통 폐쇄 원칙(OCP의 연장): 동일한 이유로 동일한 시점에 변경되는 클래스를 같은 컴포넌트로 묶는다. -> 포함 원칙
-
공통 재사용 원칙: 컴포넌트 사용자들을 필요하지 않는 것에 의존하게 강요하지 말라 -> 배제 원칙
결합
-
의존성 비순환 원칙
-
안정된 의존성 원칙: 안정된 방향으로 의존하라
-
안정된 추상 원칙: 컴포너틑는 안정된 정도만큼만 추상화되어야 한다.
아키텍쳐
-
시스템 생명 주기를 지원하는 것이다.
-
시스템을 쉽게 이해할 수 있게 해주고, 쉽게 개발하며, 쉽게 유지보수하고, 쉽게 배포하게 해준다.
-
시스템의 비용을 줄이고 생산성을 끌어올리는 것에 있다.
-
제약 사항의 모음이다. 처음에는 모래주머니 같지만 시간이 지나면서 진가를 발휘한다.
-
정책과 세부 사항으로 나뉜다.
-
정책을 공고히 할 수 있게 하는 것이 목표다.
-
세부사항은 열어두고 최후에 결정해도 문제가 없을 수 있도록 하는데 있다.
- UseCase는 시스템의 의도를 표현한다.
- 세부 사항을 열어 놓는다.
- 계층간 분리해서 단일 책임, 공통 폐쇄 원칙을 적용해서 다른 이유로 변경되는 것을 분리한다.
- 중복이 생기는 경우 ‘의도된 것’인지 ‘그냥 중복’인지 고민해봐야 한다.
경계
-
아키텍쳐는 선을 긋는다.
-
핵심 업무 로직을 오염되는 것을 방지한다.
-
세부 사항 결정을 늦출 수 있게 한다.
-
선과 선 사이는 추상화를 통해서 DIP를 한다.
-
plugInArchitecture를 할 수 있다.
-
변경의 축이 있는 지점에 그어진다.
정책
-
SW는 정책의 모음이다.
-
하나의 정책, 이를 구성하는 다수의 정책으로 이뤄진다.
-
변경되는 시점에 따라서 포함되는 정책은 동일 수준에 둔다.
수준
-
입-출력 간 거리다.
-
시스템의 입-출력에서 멀수록 정책 수준이 높다.
-
시스템의 입-출력에서 멀수록 덜 변경되고 변경되면 크리티컬하다.
업무 규칙
-
규칙을 자동화하는 핵심 업무를 의미한다.
-
보통 데이터를 필요로 한다.
-
UseCase로 애플리케이션에 특화된 업무 규칙을 설명한다.
Hexagonal
-
위의 아키텍쳐에서 필요한 것들을 준수하면 나오는 결과물이다.
-
비즈니스 코드를 기술 코드로부터 분리하는 것이 주된 아이디어다.
-
이런 설계 덕분에 비즈니스, 기술 의존 사항에 구애받지 않고 개발을 진행할 수 있다. 이는 의도적으로 기술적 결정을 뒤로 미룰 수 있도록 해준다.
Domain Hexagon
-
SW가 해결해야 하는 핵심 문제를 설명하는 요소들을 결합한다.
-
DDD에서 차용해서 entity, valueObject를 사용한다. 이를 통해서 풍부한 도메인 객체를 목표로 한다.
- active record 패턴을 의미한다.
- 도메인 헥사곤은 비즈니스 규칙을 정하고 이를 통해서 문제를 해결한다. 이러한 과정은 실세계의 문제를 이해하고 모델링하는 활동을 나타낸다.
DDD
- 도메인을 이해하고 모델링하는 것에 중점을 둔 아키텍쳐 접근 방식이다. - 비즈니스 도메인과 일치시켜 실제 문제를 더 잘 표현하고 시스템 요구 사항을 더 명확히 하는 것을 목표로 한다.
Ubiquitous Language
- 개발, 도메인 전문가 간의 공통 언어 필요성을 역설한다.
- 기술 도메인, 비즈니스 도메인 간의 간극을 메운다.
- SW가 실제 비즈니스를 더 잘 반영하도록 한다.
도메인
- 해결하고나 하는 문제의 영역이다.
- 도메인은 다시 하위 도메인으로 구성될 수 있다.
엔티티
- 수명 주기가 있고 고유한 ID를 가진다.
- 자기 상태와 동작을 유지할 책임이 있다.
- 도메인 모델에서 가장 복잡한 객체가 될 수 있다.
- 개발 객체에 특정한 비즈니스 규칙, 로직을 캡슐화한다.
값 객체
- 고유한 ID가 없다.
- 속성 값으로 정의된다.
- 개념적 정체성이 없으며 불변이다.
- 속성을 기반으로 비교할 수 있는 개념을 캡슐화 한다.
repository, domainService
- repository는 도메인 객체에 엑세스하고 저장할 수 있느 ㄴ방법을 제공한다.
- domain model과 persistence 간 가교 역할을 한다.
- domainService는 엔티티, 값 객체에 없는 애플리케이션 레벨의 오퍼레이션을 표현한다.
- 도메인 개념이지만 엔티티, 값 객체에 맞지 않는 작업을 그린다.
- 복잡한 비즈니스 로직을 캡슐화 한다.
애그리거트
- 데이터 변경 및 일관성을 위해서 단일 단위로 취급되는 도메인 객체의 클러스터다.
- 트랜잭션 일관성 경계를 나타낸다.
- 애그리거트에 대한 모든 변경이 함께 수행되도록 보장한다.
- 비즈니스 규칙을 적용하고 해당 경계 내애서 데이터의 무결성을 유지한다.
- 도메인 로직을 캡슐화하고 도메인 모델과 상호 작용하기 위한 명확한 진입점이 된다.
바운디드 컨택스트
- 애플리케이션의 여러 부분이 특정 컨텍스트에 따라 서로 다른 모델과 용어를 가질 수 있음을 의미한다.
- 컨텍스트 간 통신은 이벤트, API, 메시지 큐 등을 통해서 처리한다.
- 경계를 나눠서 독립성, 유지보수성, 배포 편의성을 확보한다.
도메인 이벤트
- 느슨한 결합을 가능하게 하고 시스템의 여러 부분 간 비동기 통신을 위한 메커니즘 제공
- 도메인 이벤트는 도메인 내에서 발생해서 시스템의 다른 부분에서 관심을 가질 만한 중요한 일을 나타낸다.
- 다른 애그리거트에서 변경을 트리거하거나 경계가 있는 컨텍스트에서 데이터를 동기화하는 데 사용할 수 있다.
Application Hexagon
-
도메인 헥사곤에 정의된 비즈니스 규칙으로 전체적인 처리를 담당한다.
-
비즈니스, 기술 측면 사이에서 조율한다. 또한 애플리케이션 특화 작업을 처리한다.
-
도메인 비즈니스 규칙에 기반한 SW 사용자의 의도와 기능을 표현한다.
-
Usecase, InputPort, OutputPort로 구성된다.
- UseCase : 애플리케이션의 특화 오퍼레이션을 나타낸다.
- InputPort: UseCase를 구현한다. SW의 의도를 구현한다.
- OutputPort: 외부 리소스에서 데이터를 가져와야 하는 상황에서 역할을 한다.
Framework Hexagon
-
외부 인터페이스를 제공한다. 입력, 출력 인터페이스를 담당한다.
-
애플리케이션 오퍼레이션을 외부로 노출한다.
-
Driving, Driven으로 나뉜다.
장·단점
장점
-
SW 구성 원칙 수립에 도움을 준다.
-
변경에 용이하며 기술 변화를 흡수할 때 큰 장애물을 만들지 않을 수 있다.
-
변경에 강점이 생기면서 유지보수성도 올라간다.
- 비즈니스 규칙 변경은 도메인 헥사곤에 캡슐화되어 있으므로 크게 문제가 되지 않는다.
- 기술, 프로토콜 도입을 위해서는 어댑터만 추가하면된다.
- 테스트 용이성이 높다.
단점
-
보일러플레이트 코드가 는다.
-
불필요한 오버헤드가 는다.
더 나아가서?
디자인 패턴
-
SW 개발에서 발생하는 반복적인 문제를 해결하고 유지보수성과 확장성을 높이기 위해서 고안됐다.
-
경험을 바탕으로 귀납적인 방식으로 구체적 문제를 해결하기 위한 해법을 제시한다.
생성
-
객체 생성에 대한 문제를 해결하는데 집중한다.
-
객체 생성의 독립성, 유연성, 객체 생성 로직의 일원화 등에 중점을 둔다.
-
단순히 생성자를 사용해서 만드는 것을 넘어서 더 효율적인 방법을 제시한다.
싱글톤
-
하나의 객체 인스턴스만을 보장하기 위해서
-
하나의 인스턴스를 애플리케이션 전역에서 공유해야 하는 경우
-
중복된 인스턴스 생성하는 비효율을 방지하고 전역 상태 관리에 유용하다.
팩토리 메소드
-
생성 로직을 클라이언트 코드에서 숨긴다.
-
객체 생성 로직을 서브클래스에게 위임한다.
-
구체적인 명세를 몰라도 객체를 생성할 수 있도록 한다.
-
여러 종류의 객체가 필요할 때, 생성 방법을 일관되게 처리할 수 있다.
-
코드가 변경에 강한 구조를 가지게 할 수 있다.
추상 팩토리
-
서로 관련이 있는 객체들을 일관되게 생성할 수 있는 인터페이스를 제공한다.
-
다양한 제품군을 쉽게 교체하거나 추가할 수 있게 한다.
-
교체 가능한 설계를 가능하게 한다.
-
여러 종류가 있을 경우 하나의 인터페이스로 생성할 수 있게 한다.
빌더
-
복잡한 객체를 만들 때, 구성 요소를 점진적으로 설정하면서 만들어야 할 때 사용한다.
-
복잡한 객체를 쉽게 만들수 있게 하면서, 구성 요소들을 재사용하고, 객체 생성을 유연하게 처리할 수 있다.
프로토타입
-
객체를 복제해서 생성할 필요가 있을 때 사용
-
객체 생성 비용이 클 때 사용할 수 있다.
-
객체를 직접 복제해서 객체 생성 비용을 절감할 수 있다.
- 정리
- 하나만 쓰거나
- 만드는 방법을 감추거나
- 비슷한 유형의 만드는 것, 어떤 유형을 만들 것인지를 감추거나
- 만드는 과정을 점진적으로 가져가며, 상황에 맞게 만들거나
- 리소스가 많이 드는 과정을 줄이기 위해 복사하거나
객체 생성 제어 및 캡슐화
- 팩토리 메소드: 객체 생성 로직을 서브클래스에 위임 -> 상위 클래스는 타입만 알고 생성은 모름
- 추상 팩토리: 관련 객체군을 생성하는 인터페이스를 제공 -> 제품군 간 일관성 유지
- 빌더: 복잡한 객체 생성을 단계별로 분리 -> 동일 생성 로직으로 다양한 표현 가능
- 프로토타입: 복제로 객체 생성 -> 원형 객체로부터 복사하여 비용 절감
객체 생명주기 조절
- 싱글톤 : 클래스 인스턴스를 하나만 보장 -> 글로벌 접근 지점 제공, 상태 공유 필요 시 사용
구조
- 객체 간의 관계를 정의하고 클래스나 객체들이 어떻게 결합되도록 할지에 대해서 다루는 패턴이다.
어댑터(adapter)
-
호환되지 않는 인터페이스를 가진 두 대상의 상호작용을 위해서 변환하는 것
-
기존 상태를 변경하지 않고 두 계층 사이 새로운 계층을 둬서 통합하는 것
-
보통 외부와 통합할 때 사용한다.
브릿지
- 기능의 계층 구조와 구현의 계층 구조를 분리해서 독립적으로 변경할 수 있게 한다.
- 구현의 변경이 추상화에 영향을 끼치지 않는다.
- 추상화의 변경이 구현에 영향을 주지 않는다.
- 다양항 기능을 가진 객체들을 처리할 때, 구현이 자주 변할 수 있는 경우에 사용한다.
-
기능, 구현을 독립적으로 확장한다.
-
서로를 분리해서 유연성을 높인다.
- 추상 클래스는 동작에 집중한다.
- 구현 클래스는 어떻게 그 동작을 수행하는지 집중한다.
- 보통의 상태와 행위를 결합한 객체의 형태를 비트는 방식
- 조합 가능한 확장의 수가 많아지는 경우 (상속 폭발) 진행한다.
// 구현 계층
interface DrawingAPI {
fun drawCircle(x: Float, y: Float, radius: Float)
}
class DrawingAPI1 : DrawingAPI {
override fun drawCircle(x: Float, y: Float, radius: Float) {
println("API1.circle at ($x, $y), radius $radius")
}
}
class DrawingAPI2 : DrawingAPI {
override fun drawCircle(x: Float, y: Float, radius: Float) {
println("API2.circle at ($x, $y), radius $radius")
}
}
// 추상화 계층
abstract class Shape(protected val drawingAPI: DrawingAPI) {
abstract fun draw()
}
class Circle(
private val x: Float,
private val y: Float,
private val radius: Float,
drawingAPI: DrawingAPI
) : Shape(drawingAPI) {
override fun draw() {
drawingAPI.drawCircle(x, y, radius)
}
}
컴포지트
-
객체를 트리 구조로 구성해서 하위와 상위를 동일한 방식으로 다룰 수 있게 한다.
-
부분, 전체 계층을 표현하며, 개별 객체의 객체들의 집합을 일관되게 다룰 수 있도록 한다.
- 부분과 전체의 계층적 구조를 동일한 방식으로 처리할 수 있게 한다.
- 복합 객체를 단일 객체처럼 다룰 수 있도록 한다.
- 부분과 전체를 동일하게 처리해야 하는 경우에 사용한다.
- 부모, 자식이 공통 인터페이스를 같고 계층형태로 분포되어 처리 시 해당 인터페이스를 기준으로 해서 처리 방법을 동일하게 가져간다.
- 목적은 부분-전체 계층 구조를 모델링하려는 의도에서 시작됐다.
- 자식은 부모의 일부로 행위의 위임의 대상이 되기도 한다.
데코레이터
-
기존 객체에 추가적인 기능을 동적으로 부여할 수 있게 한다.
-
상속을 사용하지 않는다.
-
객체에 기능을 추가하는 방식으로 작동한다.
-
기능을 확장하되, 클래스를 상속하지 않고 동적으로 기능을 추가할 수 있도록 한다.
-
객체를 감싸는 방식으로 확장한다.
파사드
-
복잡한 서브 시스템을 단순화해서 외부에 제공하는 간단한 인터페이스를 제공한다.
-
복잡한 시스템 기능을 숨기고 간단한 인터페이스를 제공해서 사용하기 쉽게 한다.
플라이웨이트
-
다수의 객체가 공유할 수 있는 부분을 저장하고 재사용하며, 메모리 사용을 절감한다.
-
객체가 많을 경우 공통 속성을 공유하고, 메모리 비용을 절약한다.
//java 표준 라이브러리 Integer.valueOf
private static final class IntegerCache {
static final int low = -128;
static final int high;
@Stable
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
프록시
-
실제 객체에 대한 접근을 제어한다.
-
객체에 대한 접근을 지연시키거나 권한을 제어하거나 비용이 큰 작업을 미루기 위해서 사용한다.
구조 캡슐화 및 계층화
- 어댑터: 기존 인터페이스를 원하는 형태로 변환 -> 호환성 확보
- 파사드: 복잡한 하위 시스템을 단순 인터페이스로 감싸서 사용 -> 클라이언트 단순화
- 브릿지: 구현과 추상을 분리하여 독립적인 확장 가능하게 설계 -> 런타임 조합 유연화
- 데코레이터: 기능을 계층적으로 확장 -> 기존 객체 변경 없이 동적 기능 추가
복합 구조/ 계층 표현
- 컴포지트: 트리 구조를 동일 인터페이스로 처리 -> 전쳬-부분 일관성 유지
- 프록시: 실제 객체 대신 대리 객체를 통해서 접근 제어 -> 접근 제어, 지연 로딩, 캐싱 등에 활용
- 플라이 웨이트: 공유 가능한 상태를 분리하여 메모리 절약 -> 동일 객체 다수 사용에 유리
행위
-
객체 간의 책임 분리, 커뮤니케이션에 초점을 둔 디자인 패턴이다.
-
“누가 무엇을 언제 어떻게 수행할 것인가?”가 중점이다.
전략
-
알고리즘(행위)을 객체로 캡슐화해서 런타임에 바꿔지기할 수 있게 한다.
-
조건문 분기 대신, 동일한 작업을 수행하는 다양한 방식을 교체 가능한 객체로 분리한다.(OCP 위반 우회)
템플릿 메소드
-
상위 클래스에 알고리즘 구조를 정의하고, 일부 단계는 하위 클래스에서 구현
-
알고리즘의 흐름은 고정하되, 세부 로직은 확장 가능하게 만들기 위해서
-
IOC 구조로 공통 알고리즘 틀을 유지하면서 다형성 도입
옵저버
-
한 객체의 상태가 여러 구독자에게 자동 전달되는 구조
-
이벤트 중심 구조나 데이터 바인딩이 필요할 떄, 느슨한 결합으로 반응성 구현
커맨드
-
요청을 객체로 캡슐화
-
메소드 호출을 객체로 바꿈 (명령의 재사용)
책임 연쇄
-
여러 객체가 순차적으로 요청을 처리할 수 있게 한다.
-
요청 처리자의 책임을 분산하거나, 유연한 필터링이 필요한 경우
상태
-
상태마다 행위가 다를 때 상태를 객체로 분리한다.
-
조건문 기반 상태 관리를 상태 객체로 역할 분리
방문자
-
객체 구조 변경 없이 새로운 기능을 외부에서 주입
-
요소 구조가 복잡하고 기능이 자주 바뀌는 경우
-
객체가 자신을 처리하는 것에서 벗어나 외부가 객체를 방문해서 처리
중재자
-
객체 간 직접 통신을 막고, 중재자를 통해서 간접 소통
-
객체 간 관계가 복잡해질 때, 의존성 축재가 필요
-
N:M -> 1:M으로 변경
반복자
-
컬렉션 내부 구조에 상관없이 순회할 수 있게 한다.
-
데이터 구조와 순회 로직의 분리
해석자
-
언어나 표현식을 해석하는 구조 제공
-
복잡한 문법이나 규칙 기반 해석이 필요할 때 (DSL)
-
if, switch로 하던 것을 구문 트리로 추상화 한다.
메멘토
-
객체의 상태를 저장하고 복원 가능하게 한다.
-
캡슐화를 해치지 않으면 undo를 제공해야 할 떄
-
내부 상태를 외부에서 변경 불가능하게 보존
책임의 위임
- 전략: 행위를 인터페이스로 분리 -> 런타임에 교체
- 커맨드: 요청을 객체화
- 책임 연쇄: 책임을 하나의 객체가 아닌 연쇄된 객체들에 넘김
- 상태: 상태가 객체로 분리되며, 상태에 따라 행위가 바뀜
행위의 캡슐화 및 재사용
- 템플릿 메소드: 알고리즘 골격을 상위 클래스에서 정의, 하위 클래스가 일부만 구현
- 방문자: 객체 구조 변경 없이 기능을 외부 방문자로 캡슐화하여 유연한 확장 가능
상태 기반 행위 변화
- 상태: 상태에 따라 객체의 행위가 변경
- 메멘토: 상태의 저장과 복원을 위해서 스냅샷 제공
객체 간 커뮤니케이션 조정
- 옵저버: 주체의 상태가 변화가 등록된 관찰자에 제공
- 중재자: 객체 간 직접 통신 대신 중재자를 통해서 메시지 제공
컬렉션/문법 처리 지원
- 반복자: 컬랙션 내부 표현을 외부에 노출하지 않고 요소 순회
- 해석자: DSL을 클래스로 표현하여 문장 해석