[Kotlin] 디자인패턴 (1)

Kotlin 디자인 패턴

Posted by 동식이 블로그 on June 1, 2020

Kotlin 객체지향 디자인 패턴

Java 객체지향 디자인 패턴 책을 보고 Kotlin으로 변환하면서 공부한 내용입니다

Java객체지향 디자인패턴

1장. 객체지향 모델링

  1. 모델링

  • 소프트웨어 모델을 통해 서로의 해석을 공유해 합의를 이루거나 해석의 타당성을 검토할 수 있을 뿐만 아니라 현재의 소프트웨어 시스템 또는 앞으로 개발할 소프트웨어의 원하는 모습을 가시화 하는데 도움을 준다

  • 소프트웨어 시스템의 구조와 행위를 명세할 수 있으며 시스템을 구축하는 틀과 구출된 소프트웨어의 문서화 기능을 제공할 수도 있다

  • 모델은 추상화에 바탕을 두고 만들어져야 한다

    • 추상화할 때 특정 관점에서 관련이 있는 점은 부각시키고 관련이 없는 면은 무시하는것이 필요하다
  1. UML (Unified Modeling Language)

  • 시스템을 모델로 표현해주는 언어
  • 요구분석, 시스템 설계, 시스템 구현 등의 시스템 개발 과정에서 개발자 사이의 의사소통이 원활하게 이루어지도록 표준화한 통합 보델링 언어이다
분류 다이어그램 유형 목적
구조 다이어그램 클래스 다이어그램 시스템을 구성하는 클래스들 사이의 관계 표현
  객체 다이어그램 객체 정보를 보여준다
  복합체 구조 다이어그램 복합 구조의 클래스와 컴포넌트 내부구조 표현
  배치 다이어그램 소프트웨어, 하드웨어, 네트워크를 포함한 실행 시스템의 물리 구조를 표현
  컴포넌트 다이어그램 컴포넌트 구조 사이의 관계를 표현
  패키지 다이어그램 클래스나 유즈 케이스 등을 포함한 여러 모델 요소들을
그룹화해 패키지를 구성하고 패키지들 사이의 관계를 표현한다
행위 다이어그램 활동다이어그램 업무 처리 과정이나 연산이 수행되는 과정을 표현
  상태 머신 다이어그램 객체의 생명주기를 표현
  유즈케이스 다이어그램 사용자관점에서 시스템 행위를 표현
  상호작용 다이어그램 순차 다이어그램 : 시간 흐름에 따른 객체 사이의 상호작용 표현
    상호작용 개요 다이어그램 : 상호작용 다이어그램 사이의 제어 흐름 표현
    통신 다이어그램 : 객체 사이의 관계를 중심으로 상호작용 표현
    타이밍 다이어그램 : 객체 상태 변화와 시간 제약을 명시적으로 표현
  1. 클래스 다이어그램

  • 시간에 따라 변하지 않는 시스템의 정적인 면을 보여주는 대표적인 UML 구조 다이어그램
  • 시스템을 구성하는 클래스와 그들 사이의 관계를 보여준다

클래스

  • 동일한 속성과 행위를 수행하는 객체의 집합
  • 객체를 생성하는 설계도로 간주
UML으로 클래스 표현

1-1

  • 가장 윗부분에는 클래스의 이름
  • 중간부분에는 클래스의 특징을 나타내는 속성
  • 마지막 부분에는 클래스가 수행하는 연산들을 기술한다
접근 제어자
접근 제어자 표시 설명
public + 어떤 클래스의 객체에서든 접근 가능
private - 이 클래스에서 생성된 객체들만 접근 가능
protected # 이 클래스와 동일 패키지에 있거나 상속관계에 있는 하위 클래스의 객체들만 접근 가능
package ~ 동일 패키지에 있는 클래스의 객체들만 접근 가능
속성과 연산 표기
  표기 방법
속성 [+|-|#|~]이름: 타입[다중성 정보][=초기값]
연산 [+|-|#|~]이름(인자: 타입1, ..., 인자n:타입n): 반환타입

관계

  • 다수의 클래스가 모인 시스템이 훨씬 더 효율적이기 때문에 객체지향 시스템도 여러 개의 클래스가 서로 긴밀한 관계를 맺어 기능을 수행한다
  1. 연관 관계

    • 클래스들이 개념상 서로 연결되었음을 나타낸다
    • 실선이나 화살표로 표시하며 보통은 한 클래스가 다른 클래스에서 제공하는 기능을 사용하는 상황일 때 표시

    1-2

  2. 일반화 관계

    • 상속 관계라고 한다
    • 한 클래스가 다른 클래스를 포함하는 상위 개념일 때 이를 IS-A 관계라고 하며 UML 에서는 일반화 관계로 모델링한다
    • 속이 빈 화살표를 사용해 표시한다

    1-3

  3. 집합 관계

    • 클래스들 사이의 전체 또는 부분 같은 관계를 나타낸다

    • 집약 관계와 합성 관계가 존재한다

      • 집약

        • 전체 객체와 부분 객체의 생명 주기가 다르다
        • 부분 객체를 여러 전체 객체가 공유할 수 있다

        1-4

      • 합성

        • 전체 객체가 없어지면 부분객체도 없어진다
        • 부분객체를 여러 전체 객체가 공유할 수 없다

        1-5

  4. 의존 관계

    • 연관 관계와 같이 한 클래스가 다른 클래스에서 제공하는 기능을 사용할 때를 나타낸다
    • 매우 짧은 시간만 유지되며 점선 화살표를 사용해 표시한다

    1-6

  5. 실체화 관계

    • 책임들의 집합인 인터페이스와 이 책임들을 실제로 실현한 클래스들 사이의 관계를 나타낸다
    • 상속과 유사하게 빈 삼각형을 사용하여 머리에 있는 실선 대신 점선을 사용해 표시한다

1-7

2장. 객체지향 원리

  1. 추상화

  • 어떤 영역에서 필요로 하는 속성이나 행동을 추출하는 작업을 의미한다
  • 구체적인 사물들의 공통적인 특징을 파악해서 이를 하나의 개념으로 다루는 수단

  • 각 개체의 구체적인 개념에 의존하지 않고 추상적 개념에 의존해야 설계를 유연하게 변경할 수 있다
1
2
3
fun changeEngineOil(c: Car) {
    c.changeEngineOil()
}
  • 구체적인 자동차 대신 이들의 추상화 개념인 자동차를 이용한 예
  1. 캡슐화

  • 요구사항 변경에 대처하는 고전적인 설계 원리로는 응집도와 결합도가 있다
  • 응집도
    • 클래스나 모듈 안의 요소들이 얼마나 밀접하게 관련되어 있는지를 나타낸다
  • 결합도
    • 어떤 기능을 실행하는데 다른 클래스나 모듈들에 얼마나 의존적인지를 나타낸다
  • 높은 응집도와 낮은 결합도를 유지할 수 있도록 설계해야 요구사항을 변경할 때 유연하게 대처할 수 있다
  • 캡슐화는 낮은 결합도를 유지할 수 있도록 해주는 객체지향 설계 원리이다
    • 정보 은닉을 통해 높은 응집도와 낮은 결합도를 갖도록 한다
  1. 일반화 관계

일반화는 또 다른 캡슐화

  • 일반화 관계는 객체지향 프로그래밍 관점에서는 상속 관계라고 한다

    1-3

  • 일반화 관계는 자식 클래스를 외부로부터 은닉하는 캡슐화의 일종이다

일반화 관계와 위임

  • 일반화 관계를 속성이나 기능의 상속, 즉 재사용을 위해 존재한다고 오해하고 있다

  • 두 자식 클래스 사이에 ‘is a kind of 관계’가 성립되지 않을 때 상속을 사용하면 불필요한 속성이나 연산(빚)도 물려받게 된다

  • 어떤 클래스의 일부 기능만 재사용하고 싶은 경우 위임을 사용하면 된다

  • 위임을 사용해 일반화(상속)를 대신하는 과정

    1. 자식 클래스에 부모 클래스의 인스턴스를 참조하는 속성을 만든다. 이 속성 필드를 this로 초기화 한다

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      class MyStack<String> : ArrayList<String>() {
          private val arList: ArrayList<String> = this
               
          fun push(element: String) {
              add(element)
          }
               
          fun pop() : String = removeAt(size - 1)
      }
      
    2. 서브 클래스에 정의된 각 메서드에 1번에서 만든 위임 속성 필드를 참조하도록 변경한다

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      class MyStack<String> : ArrayList<String>() {
          private val arList: ArrayList<String> = this
               
          fun push(element: String) {
              arList.add(element)
          }
               
          fun pop() : String = arList.removeAt(size - 1)
      }
      
    3. 서브 클래스에서 일반화 관계 선언을 제거하고 위임 속성 필드에 슈퍼 클래스의 객체를 생성해 대입한다

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      class MyStack<String> {
          private val arList: ArrayList<String> = arrayListOf()
               
          fun push(element: String) {
              arList.add(element)
          }
               
          fun pop() : String = arList.removeAt(arList.size - 1)
      }
      
    4. 서브 클래스에서 사용된 슈퍼클래스의 메서드에도 위임 메서드를 추가한다

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      class MyStackDelegation<T> {
          private val arList: ArrayList<T> = arrayListOf()
               
          fun push(element: T) {
              arList.add(element)
          }
               
          fun pop() : T = arList.removeAt(arList.size - 1)
               
          fun isEmpty() : Boolean = arList.isEmpty()
               
          fun size() : Int = arList.size
      }
      
    5. 컴파일하고 잘 동작하는지 확인한다

  1. 다형성

  • 서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 동작하는 능력
  • 일반화 관계와 함께 자식 클래스를 개별적으로 다룰 필요 없이 한 번에 처리할 수 있게 하는 수단을 제공한다
  • Pet 클래스에 talk 메서드를 정의하고 Cat, Dog, Parrot 클래스에서 여러가지 울음 방식에 맞게 재정의를 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Pet {
    abstract fun talk()
}

class Cat : Pet() {
    override fun talk() {
        println("ㅇㅑ옹")
    }
}

class Dog : Pet() {
    override fun talk() {
        println("멍~멍")
    }
}

class Parrot : Pet() {
    override fun talk() {
        println("안녀엉")
    }
}
  1. 피터 코드의 상속 규칙

  • 상속의 오용을 막기 위해 상속의 사용을 엄격하게 제한하는 규칙들을 만들었다
  • 다음 규칙 중 어느 하나라도 만족하지 않으면 상속을 사용해서는 안된다
    • 자식 클래스와 부모 클래스 사이는 ‘역할 수행’ 관계가 아니어야 한다
    • 한 클래스의 인스턴스는 다른 서브 클래스의 객체로 변환할 필요가 절대 없어야 한다
    • 자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행해야 한다
    • 자식 클래스가 단지 일부 기능을 재사용할 목적으로 유틸리티 역할을 수행하는 클래스를 상속하지 않아야 한다
    • 자식 클래스가 ‘역할’, ‘트랜잭션’, ‘디바이스’ 등을 특수화 해야 한다

3장. SOLID 원칙

SOLID는

SRP // OCP // LSP // ISP // DIP

  1. 단일 책임 원칙 SRP(Single Responsibility Principle)

  • 단 하나의 책임만을 가져야 한다는 의미

책임의 의미

  • SRP에서 말하는 책임의 기본 단위는 객체를 지칭한다
    • 즉 객체는 단 하나의 책임만 가져야 한다는 의미
  • 객체에 책임을 할당할 때는 어떤 객체보다도 작업을 잘 할 수 있는 객체에 책임을 할당해야 한다
  • 객체는 책임에 수반되는 모든 일을 자신만이 수행할 수 있어야 한다
1
2
3
4
5
6
7
8
class Student {
    fun getCourses() {...}
    fun addCOurse(c: Course) {...}
    fun save() {...}
    fun load() : Student {...}
    fun printOnReportCard() {...}
    fun printOnAttendanceBook() {...}
}
  • Student 클래스는 너무나 많은 책임을 수행해야 한다
  • Student 클래스는 수강 과목을 추가하고 조회하는 책임만 수행하도록 하는 것이 SRP를 따르는 설계이다

변경

  • SRP를 따르는 실효성 있는 설계가 되려면 책임을 좀 더 현실적인 개념으로 파악해야 한다
  • 좋은 설계란 기본적으로 시스템에 새로운 요구사항이나 변경이 있을 때 가능한 한 영향 받는 부분을 줄어야 한다
  • 그렇다면 위의 Student 클래스는 언제 변경되어야 하는지 알아보자
    • 데이터베이스의 스키마가 변경된다면 Student 클래스는 변경되어야 한다
    • 학생이 지도 교수를 찾는 기능이 추가되어야 한다면 Student 클래스는 변경되어야 한다
    • 학생 정보를 성적표와 출석부 이외의 형식으로 출력해야 한다면 Student 클래스는 변경되어야 한다

책임 분리

  • 단 하나의 책임만 수행하도록 해 변경 사유가 될 수 있는 것을 하나로 만드는 것

회귀 테스트

시스템에 변경이 발생할 때 기존의 기능에 영향을 주는지를 평가하는 테스트

  • 클래스들이 책임을 적절하게 분담하도록 변경하면 어떤 변화가 생겼을 때 영향을 최소화할 수 있다

산탄총 수술

  • 하나의 책임이 여러개의 클래스들로 분산되어 있는 경우에도 단일 책임 원칙에 입각해 설계를 변경해야 하는 경우
  • 횡단 관심에 속하는 기능은 대부분 시스템 핵심기능 안에 포함되는 부가 기능이다
    • 부가 기능에 변경사항이 발생하면 해당 부가 기능을 실행하는 모든 핵심 기능에도 변경사항이 적용되어야 한다
  1. 개방-폐쇄 원칙(Open-Closed Principle, OCP)

  • 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻
  • OCP를 위반하지 않은 설게를 할 때 가장 중요한 것은 무언이 변하는 것인지, 무엇이 변하지 않는 것인지를 구분해야 한다
  • 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할 수 있는 설계가 되어야 한다
    • 단위 텟트를 수행할 때 매우 중요
    • 테스트를 위해 실제 데이터베이스 기능을 대체하는 가짜 객체를 만들 필요가 있다
  1. 리스코프 치환 원칙(Liskov Substitution Principle, LSP)

Data Abstraction and Hierarchy - Liskov1987

A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a eubtype is one whose objects provide all the behavior of objects of another type (th e supertype) plus something extra. What is wanted here is something like the following substitution property : If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for oz, then S is a subtype of T.

  • LSP는 일반화 관계에 대한 이야기이며, 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다

  • 즉 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 의미이다

  1. 의존 역전 원칙(Dependency Inversion Principle, DIP)

  • 객체 사이에 서로 도움을 주고받으면 의존 관계가 발생하는데 의존 역전 원칙은 그러한 의존관계를 맺을 때의 가이드라인이다
  • 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 원칙이다
  • 객체지향 관점에서는 변하기 어려운 추상적인 것들으 표현하는 수단으로 추상 클래스인터페이스가 있다
    • 따라서 DIP를 만족하려면 어떤 클래스가 도움을 받을 때 구체적인 클래스보다는 인터페이스나 추상 클래스와 의존 관계를 맺도록 설계해야 한다
  • DIP를 만족하면 의존성 주입이라는 기술로 변화를 쉽게 수용할 수 있는 코드를 작성할 수 있다
  1. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)

  • 클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다는 내용이 담겨있다
  • 인터페이스를 클라이언트에 특화되도록 분리시키라는 설계 원칙
SRP와 ISP 사이의 관계
  • SRP를 만족한다면 ISP도 만족할 수 있나?
  • ISP를 만족한다면 SRP도 만족할 수 있나?
  • 반드시 그렇다고 볼 수 없다

4장. 디자인 패턴

  1. 디자인 패턴의 이해

  • 패턴이란?

    • 비슷하거나 동일한 양식 또는 유형들이 반복되어 나타난다는 의미
    • 문제와 해결책도 동일한 유형이나 양식을 통해 쉽게 찾을 수 있다
  • 디자인 패턴의 구조

    • 콘텍스트
      • 문제가 발생하는 여러 상황을 기술한다
      • 패턴이 적용될수 있는 상황을 나타낸다
      • 경우에 따라서는 패턴이 유용하지 못한 상황을 나타내기도 한다
    • 문제
      • 패턴이 적용되어 해결될 필요가 있는 여러 디자인 이슈들을 기술한다
      • 이때 여러 제약사항과 영향력도 문제 해결을 위해 고려해야 한다
    • 해결
      • 문제를 해결하도록 설계를 구성하는 요소들과 그 요소들 사이의 관계, 책임, 협력 관계를 기술한다
      • 해결은 반드시 구체적인 구현 방법이나 언어에 의존적이지 않으며 다양한 상황에 적용할 수 있는 일종의 템플릿이다
  • 아키텍처 패턴
    • 시스템을 구성하는 컴포넌트의 구성과 컴포넌트 사이의 협조 방법을 패턴화 한 것
    • 레이어 패턴, 파이프 & 필터 , 브로커 패턴 등이 대표적인 예
  • 디자인 패턴
    • 아키텍처 패턴에서 컴포넌트의 내부 구조를 대상으로 한 클래스 / 객체의 구조와 협업 방법을 패턴화 한것
  • 관용구
    • 각각의 프로그램 언어 특유의 패턴
    • 프로그래밍에서 자주 사용하는 기술 방법(코딩 방법)을 패턴화 한 것
  1. GoF 디자인 패턴

  • 생성 패턴
    • 객체 생성에 관련된 패턴, 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공
    • 추상 팩토리, 빌더, 팩토리 메서드 , 프로토타입, 싱글턴
  • 구조 패턴
    • 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴
    • 어댑터, 브리지, 컴퍼지트, 데커레이터, 퍼사드, 플라이웨이트, 프록시
  • 행위 패턴
    • 객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴
    • 책임 연쇄, 커맨드, 인터프리터, 이터레이터, 미디에이터, 메멘토, 옵서버, 스테이트, 스트래티지, 템플릿 메서드, 비지터
  1. UML과 디자인 패턴

컬레보레이션

  • UML 2.0에서 디자인 패턴을 표현하는 도구
  • UML에서는 객체들이 특정 상황에서 수행하는 역할의 상호작용을 컬레보레이션이라는 요소로 작성한다

4-1

  • 담보 대출 관계를 보여주는 컬레보레이션으로 대출자, 대출인, 담보라는 역할이 필요하고 그들 사이의 협력이 요구되므로 이 역할들을 커넥터로 연결한다

  • 컬레보레이션은 역할들의 상호작용을 추상화한 것으로, 특별한 상황에 적용하면 많은 시스템 개발에 재사용할 수 있다

4-2

  • 컬레보레이션 어커런스는 더 구적인 상황에서의 컬레보레이션 적용을 표현해준다
  • 컬레보레이션 어커런스는 컬레보레이션에 참가하는 응용 클래스에 의존하며 의존 관계에 붙은 레이블은 응용 클래스가 컬레보레이션에 수행하는 역할을 나타낸다

순차 다이어그램

  • 객체들의 상호작용을 나타내는 다이어그램 중 하나이다
  • 객체들 사이의 메시지 송신과 그들의 순서를 나타낸다
  • 기본적으로 하나의 시나리오에 관한 객체 사이의 상호작용을 보여주는 데 사용되어야 한다

순차 다이어그램과 클래스 다이어그램의 관계

  • 순차 다이어그램은 객체 사이의 메시지 흐름과 순서를 알려주는 행위 측면에 중점을 두는 모델
  • 클래스 다이어글매은 시스템의 구조적인 측면에 중점을 두는 모델