상반기 내용 정리

  • cleanArchitecture
  • oop
  • hexagonal
  • DDD
  • designPattern
  • antiPattern

구성

아키텍쳐? 설계?

보통 아키텍쳐를 구성한다. 설계한다를 구분 없이 혼용해서 사용하는 경우가 있다. 물론 헷깔리긴 한다.

  • 아키텍쳐: 저수준의 세부 사항(‘외부에서 어떤 프로토콜로 호출하는가?’, ‘어떻게 기록할지?’)과는 별개의 코드 상의 고수준의 무엇인가를 의미한다.

  • 설계: 저수준의 구조 혹은 결정 사항 등을 의미한다.


개발자가 SW를 만들면서 마주하는 두 가치가 있다. 

첫 번째는 행위(behavior)다. SW는 수익을 창출하거나 비용을 절약하도록 만들기 위해서 만든다.  프로그래머는 이를 위해서 이해관계자가 기능 명세서나 요구 사항 문서를 구체화할 수 있도록 돕는다. 더 나아가 요구사항을 만족하도록 코드를 작성한다

두 번째는 아키텍쳐다. 문제를 해결하기 위해서 형태다. SW 개발 비용의 증가를 결정짓는 주된 요인이 되기도 한다. 아키텍쳐가 특정 형태에 치우치면 변화에 대응하기 어려워진다.


이 둘은 모두 중요하다. 예를 들어 완벽하게 동작하는데 기능 추가가 어려워도 힘들다. 반대로 동작하지 않고 변경에 쉽다고 해도 굉장히 부담이 된다. 만약 이 둘 중에 고르라면 그나마 후자가 낫다. 노력 여하에 따라서 변경할 수 있음을 의미하기 떄문이다. 결과적으로 결과물도 중요하지만 아키텍쳐가 중요하다.

개발자는 개발을 하는 것뿐만 아니라 SW를 안전하게 보호하는 역할을 하기도 한다. 따라서 그 역할에 최선을 다해야 한다.

 

패러다임

아키텍쳐를 살펴보면서 빼놓을 수 없는 것이 **패러다임이다. 기초 체력이 있어야 뼈대를 만든다. 프로그래밍 패러다임은 무엇인가를 **제한**하는 내용을 담고 있다.

1. 구조적 프로그래밍

  • 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있고 기능적으로 분해할 수 있음을 의미한다.

  • 모든 프로그램은 sequence, selection, iteration이 세 가지로 이뤄짐을 증명한다.

  • 제어 흐름의 직접적인 전환에 제한에 두는 패러다임이다.

2. 객체 지향

  • 여러 가지가 있지만 가장 설득력 있는 것은 **다형성**이다.

  • 다른 것들도 가진 것들을 소거하면 남는 특징이다.

  • DIP는 다형성을 기반으로 구현할 수 있으며 이를 통해서 의존성과 제어 흐름을 역전시킬 수 있다.

  • 이를 통해서 전체 시스템의 모든 소스 코드 의존성에 대한 절대적을 제어 권한을 갖을 수 있다.

  • 이를 통해서 제어 흐름을 간접적으로 제한하는 것에 방점을 두는 패러다임이다.

더 깊히 들어가보자.

객체지향은 단순히 클래스를 만드는 것이 아니다.  

  • 할 일을 정의하고 필요한 데이터 구조를 정의하고 만들 클래스를 저의
  • 공통적인 행동과 상태를 공유하는 객체를 추상화하는 과정
  • 객체 간의 컨텍스트 내에서 협력을 통해서 이뤄내는 것이다.  

    구현

  • 내부, 외부를 구분 짓는다. 
  • 어떤 역할로 어떤 모습으로 협업할지를 그리는 것이다.
  • 내, 외부 구분은 어떤 것을 공개하고 감출지를 결정하는 것이다.
  • 경계를 명확하게 두면서 객체의 역할과 한계를 정해서 오히려 자율성을 보장한다.  

    자율성

  • 경계를 그어주고 그 내부에서 스스로 판단하고 행동하는 존재다.
  • 이를 캡슐화라고 한다.
  • 흔들리지 않기 위해서 접근을 통제하고 이 선을 기준으로 협업을 한다.  

    협력

  • 객체 간 컨텍스트 내에서 서로가 서로에 요청을 해서 의사소통을 한다.
  • 요청을 해도 실패는 할 수 있다. 
  • 이러한 요청을 ‘메시지를 전송한다.’라고 한다.
  • 객체 간 협력, 메시지 수발신은 다형성에 의해서 ‘누가 수행할 것인가’를 정하게 할 수 있다. OOP의 특성이기도 하다.
  • 다형성은 상속, 인터페이스 구현으로 할 수 있다. 이를 지연 바인딩, 동적 바인딩이라고 한다.

> 구현 상속, 인터페이스 상속

  • 구현 상속은 서브 클래싱이며 서브 클래싱이라고 한다. 순수한 재사용을 목적으로 한다.
  • 인터페이스 상속은 서브 타이핑이다. 다형적인 협력을 위해서 부모-자식 간의 인터페이스 공유를 목적으로 한다.
  • 보통 상속을 재사용을 위해서 사용한다고 생각한다. 음. 반은 맞고 반은 틀리다.

    >   - 일단 상속은 부모 자식 간 강결합이 생긴다. => 설계에서 유연성이 떨어진다.

  - 서로 너무 많이 알게 된다. => 캡슐화 위반   - 서브 타이밍을 위해서 한다면 문제는 없다.

  • 만약 재사용을 위한 거라면 composition이 나을 수 있다.

    > 메시지

    1. 메시지: 객체가 다른 객체와 협력하기 위해서 사용하는 의사소통 메커니즘
    2. 오퍼레이션: 객체가 다른 객체에 제공하는 추상적인 서비스, 메시지를 수신하는 객체의 인터페이스를 강조
    3. 메소드: 메시지에 응답하기 위해서 실행되는 코드 블록
    4. 퍼블릭 인터페이스: 객체가 협력에 참여하기 위해서 외부에서 수신할 수 있는 메시지 묶음
    5. 시그니쳐: 오버레이션, 메소드의 명세

> 디미터 법칙

  • ‘인접한 이웃과만 의사소통하라’를 기치로 한다.
    1. 묻지 말고 시켜라: 내부에 대해서 파헤치지 말고 바깥에 물어보도록 한다.
    2. 의도를 드러내는 인터페이스: 메소드가 어떻게가 아니라 무엇을 하는지를 나타내도록 한다.
    3. 함께 모으기: ISP를 준수하면 된다. 내부 구조를 묻는 대신 직접 자신의 책임을 수행하도록 시킨다.
  • 이를 바탕으로 메시지를 보낼 수 있는 대상을 좁힌다. 
  • 추가로 체이닝을 막도록 한다. (알 필요가 없는 경우를 밖으로 내보내지 말고 안에서 처리할 수 있도록 한다.)  

    책임

  • 협력에 참여하기 위해서 객체가 수행하는 행동을 책임이라고 한다.
  • 책임은 객체에 의해 정의되는 응집도 있는 행위의 집합니다.
  • 목표는 필요한 정보를 가장 잘 아는 전문가에 책임을 할당하는 것이다.

책임을 할당하는 방법 

  • GRASP(General Responsibility Assignment Software Pattern)이 있다.  
    1. InformationExpert: 책임에 걸맞는 객체에 데이터와 처리 로직을 묶는 것. 이를 통해서 스스로 처리하는 자율적인 존재가 된다.
    2. Creator: A,B 간 결합도가 높으면 서로의 생성을 책임지는 것
    3. Controller: 시스템 이벤틀를 처리할 객체를 만드는 것, antiCorruption과 유사하다. (물론 약간의 차이가 있긴 하다.)
    4. LowCoupling: 객체들간, 서브 시스템들간의 상호의존도가 낮게 책임을 부여
    5. HighCohesion: 각 객체가 밀접하게 연관된 책임들만 가지도록 구성
    6. Polymorphism: 객체 종류 역할 개념을 부여하여 다른 일을 하도록 하는 것이다. 동적 바인딩을 이용한다.
    7. PureFabrication: 도메인 관련이 아니면 책임을 별도로 한 곳으로 관리하는 객체를 만드는 것이다. 
    8. Indriection: 두 객체 사이의 직접적인 커플링을 피하기 위해서 중간에 하나의 층을 두는 것이다. 보통 interface가 그 역할을 한다.
    9. 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 개발에서 발생하는 반복적인 문제를 해결하고 유지보수성과 확장성을 높이기 위해서 고안됐다.

  • 경험을 바탕으로 귀납적인 방식으로 구체적 문제를 해결하기 위한 해법을 제시한다.

생성 

  • 객체 생성에 대한 문제를 해결하는데 집중한다.

  • 객체 생성의 독립성, 유연성, 객체 생성 로직의 일원화 등에 중점을 둔다.

  • 단순히 생성자를 사용해서 만드는 것을 넘어서 더 효율적인 방법을 제시한다.

싱글톤
  • 하나의 객체 인스턴스만을 보장하기 위해서

  • 하나의 인스턴스를 애플리케이션 전역에서 공유해야 하는 경우

  • 중복된 인스턴스 생성하는 비효율을 방지하고 전역 상태 관리에 유용하다.

팩토리 메소드
  • 생성 로직을 클라이언트 코드에서 숨긴다.

  • 객체 생성 로직을 서브클래스에게 위임한다.

  • 구체적인 명세를 몰라도 객체를 생성할 수 있도록 한다.

  • 여러 종류의 객체가 필요할 때, 생성 방법을 일관되게 처리할 수 있다.

  • 코드가 변경에 강한 구조를 가지게 할 수 있다.

추상 팩토리
  • 서로 관련이 있는 객체들을 일관되게 생성할 수 있는 인터페이스를 제공한다.

  • 다양한 제품군을 쉽게 교체하거나 추가할 수 있게 한다.

  • 교체 가능한 설계를 가능하게 한다.

  • 여러 종류가 있을 경우 하나의 인터페이스로 생성할 수 있게 한다.

빌더
  • 복잡한 객체를 만들 때, 구성 요소를 점진적으로 설정하면서 만들어야 할 때 사용한다.

  • 복잡한 객체를 쉽게 만들수 있게 하면서, 구성 요소들을 재사용하고, 객체 생성을 유연하게 처리할 수 있다.

프로토타입
  • 객체를 복제해서 생성할 필요가 있을 때 사용

  • 객체 생성 비용이 클 때 사용할 수 있다.

  • 객체를 직접 복제해서 객체 생성 비용을 절감할 수 있다.

  • 정리
    1. 하나만 쓰거나
    2. 만드는 방법을 감추거나
    3. 비슷한 유형의 만드는 것, 어떤 유형을 만들 것인지를 감추거나
    4. 만드는 과정을 점진적으로 가져가며, 상황에 맞게 만들거나
    5. 리소스가 많이 드는 과정을 줄이기 위해 복사하거나  

      객체 생성 제어 및 캡슐화

    6. 팩토리 메소드: 객체 생성 로직을 서브클래스에 위임 -> 상위 클래스는 타입만 알고 생성은 모름 
    7. 추상 팩토리: 관련 객체군을 생성하는 인터페이스를 제공 -> 제품군 간 일관성 유지
    8. 빌더: 복잡한 객체 생성을 단계별로 분리 -> 동일 생성 로직으로 다양한 표현 가능
    9. 프로토타입: 복제로 객체 생성 -> 원형 객체로부터 복사하여 비용 절감

      객체 생명주기 조절

    10. 싱글톤 : 클래스 인스턴스를 하나만 보장 -> 글로벌 접근 지점 제공, 상태 공유 필요 시 사용 

구조

  • 객체 간의 관계를 정의하고 클래스나 객체들이 어떻게 결합되도록 할지에 대해서 다루는 패턴이다.
어댑터(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);

    }

프록시
  • 실제 객체에 대한 접근을 제어한다.

  • 객체에 대한 접근을 지연시키거나 권한을 제어하거나 비용이 큰 작업을 미루기 위해서 사용한다.

구조 캡슐화 및 계층화

  1. 어댑터: 기존 인터페이스를 원하는 형태로 변환 -> 호환성 확보
  2. 파사드: 복잡한 하위 시스템을 단순 인터페이스로 감싸서 사용 -> 클라이언트 단순화
  3. 브릿지: 구현과 추상을 분리하여 독립적인 확장 가능하게 설계 -> 런타임 조합 유연화
  4. 데코레이터: 기능을 계층적으로 확장 -> 기존 객체 변경 없이 동적 기능 추가

    복합 구조/ 계층 표현

  5. 컴포지트: 트리 구조를 동일 인터페이스로 처리 -> 전쳬-부분 일관성 유지
  6. 프록시: 실제 객체 대신 대리 객체를 통해서 접근 제어 -> 접근 제어, 지연 로딩, 캐싱 등에 활용
  7. 플라이 웨이트: 공유 가능한 상태를 분리하여 메모리 절약 -> 동일 객체 다수 사용에 유리 

행위 

  • 객체 간의 책임 분리, 커뮤니케이션에 초점을 둔 디자인 패턴이다.

  • “누가 무엇을 언제 어떻게 수행할 것인가?”가 중점이다.

전략
  • 알고리즘(행위)을 객체로 캡슐화해서 런타임에 바꿔지기할 수 있게 한다.

  • 조건문 분기 대신, 동일한 작업을 수행하는 다양한 방식을 교체 가능한 객체로 분리한다.(OCP 위반 우회)

템플릿 메소드
  • 상위 클래스에 알고리즘 구조를 정의하고, 일부 단계는 하위 클래스에서 구현

  • 알고리즘의 흐름은 고정하되, 세부 로직은 확장 가능하게 만들기 위해서

  • IOC 구조로 공통 알고리즘 틀을 유지하면서 다형성 도입

옵저버
  • 한 객체의 상태가 여러 구독자에게 자동 전달되는 구조

  • 이벤트 중심 구조나 데이터 바인딩이 필요할 떄, 느슨한 결합으로 반응성 구현

커맨드
  • 요청을 객체로 캡슐화

  • 메소드 호출을 객체로 바꿈 (명령의 재사용)

책임 연쇄
  • 여러 객체가 순차적으로 요청을 처리할 수 있게 한다.

  • 요청 처리자의 책임을 분산하거나, 유연한 필터링이 필요한 경우

상태
  • 상태마다 행위가 다를 때 상태를 객체로 분리한다.

  • 조건문 기반 상태 관리를 상태 객체로 역할 분리

방문자
  • 객체 구조 변경 없이 새로운 기능을 외부에서 주입

  • 요소 구조가 복잡하고 기능이 자주 바뀌는 경우

  • 객체가 자신을 처리하는 것에서 벗어나 외부가 객체를 방문해서 처리 

중재자
  • 객체 간 직접 통신을 막고, 중재자를 통해서 간접 소통

  • 객체 간 관계가 복잡해질 때, 의존성 축재가 필요

  • N:M -> 1:M으로 변경

반복자
  • 컬렉션 내부 구조에 상관없이 순회할 수 있게 한다.

  • 데이터 구조와 순회 로직의 분리

해석자
  • 언어나 표현식을 해석하는 구조 제공

  • 복잡한 문법이나 규칙 기반 해석이 필요할 때 (DSL)

  • if, switch로 하던 것을 구문 트리로 추상화 한다.

메멘토
  • 객체의 상태를 저장하고 복원 가능하게 한다.

  • 캡슐화를 해치지 않으면 undo를 제공해야 할 떄

  • 내부 상태를 외부에서 변경 불가능하게 보존

책임의 위임

  1. 전략: 행위를 인터페이스로 분리 -> 런타임에 교체
  2. 커맨드: 요청을 객체화
  3. 책임 연쇄: 책임을 하나의 객체가 아닌 연쇄된 객체들에 넘김
  4. 상태: 상태가 객체로 분리되며, 상태에 따라 행위가 바뀜

    행위의 캡슐화 및 재사용

  5. 템플릿 메소드: 알고리즘 골격을 상위 클래스에서 정의, 하위 클래스가 일부만 구현
  6. 방문자: 객체 구조 변경 없이 기능을 외부 방문자로 캡슐화하여 유연한 확장 가능

    상태 기반 행위 변화

  7. 상태: 상태에 따라 객체의 행위가 변경
  8. 메멘토: 상태의 저장과 복원을 위해서 스냅샷 제공

    객체 간 커뮤니케이션 조정

  9. 옵저버: 주체의 상태가 변화가 등록된 관찰자에 제공
  10. 중재자: 객체 간 직접 통신 대신 중재자를 통해서 메시지 제공

    컬렉션/문법 처리 지원

  11. 반복자: 컬랙션 내부 표현을 외부에 노출하지 않고 요소 순회
  12. 해석자: DSL을 클래스로 표현하여 문장 해석