Kotlin 객체지향 디자인 패턴
Java 객체지향 디자인 패턴 책을 보고 Kotlin으로 변환하면서 공부한 내용입니다
1장. 객체지향 모델링
-
모델링
-
소프트웨어 모델을 통해 서로의 해석을 공유해 합의를 이루거나 해석의 타당성을 검토할 수 있을 뿐만 아니라 현재의 소프트웨어 시스템 또는 앞으로 개발할 소프트웨어의 원하는 모습을 가시화 하는데 도움을 준다
-
소프트웨어 시스템의 구조와 행위를 명세할 수 있으며 시스템을 구축하는 틀과 구출된 소프트웨어의 문서화 기능을 제공할 수도 있다
-
모델은 추상화에 바탕을 두고 만들어져야 한다
- 추상화할 때 특정 관점에서 관련이 있는 점은 부각시키고 관련이 없는 면은 무시하는것이 필요하다
-
UML (Unified Modeling Language)
- 시스템을 모델로 표현해주는 언어
- 요구분석, 시스템 설계, 시스템 구현 등의 시스템 개발 과정에서 개발자 사이의 의사소통이 원활하게 이루어지도록 표준화한 통합 보델링 언어이다
분류 | 다이어그램 유형 | 목적 |
---|---|---|
구조 다이어그램 | 클래스 다이어그램 | 시스템을 구성하는 클래스들 사이의 관계 표현 |
객체 다이어그램 | 객체 정보를 보여준다 | |
복합체 구조 다이어그램 | 복합 구조의 클래스와 컴포넌트 내부구조 표현 | |
배치 다이어그램 | 소프트웨어, 하드웨어, 네트워크를 포함한 실행 시스템의 물리 구조를 표현 | |
컴포넌트 다이어그램 | 컴포넌트 구조 사이의 관계를 표현 | |
패키지 다이어그램 | 클래스나 유즈 케이스 등을 포함한 여러 모델 요소들을 그룹화해 패키지를 구성하고 패키지들 사이의 관계를 표현한다 |
|
행위 다이어그램 | 활동다이어그램 | 업무 처리 과정이나 연산이 수행되는 과정을 표현 |
상태 머신 다이어그램 | 객체의 생명주기를 표현 | |
유즈케이스 다이어그램 | 사용자관점에서 시스템 행위를 표현 | |
상호작용 다이어그램 | 순차 다이어그램 : 시간 흐름에 따른 객체 사이의 상호작용 표현 | |
상호작용 개요 다이어그램 : 상호작용 다이어그램 사이의 제어 흐름 표현 | ||
통신 다이어그램 : 객체 사이의 관계를 중심으로 상호작용 표현 | ||
타이밍 다이어그램 : 객체 상태 변화와 시간 제약을 명시적으로 표현 |
-
클래스 다이어그램
- 시간에 따라 변하지 않는 시스템의 정적인 면을 보여주는 대표적인 UML 구조 다이어그램
- 시스템을 구성하는 클래스와 그들 사이의 관계를 보여준다
클래스
- 동일한 속성과 행위를 수행하는 객체의 집합
- 객체를 생성하는 설계도로 간주
UML으로 클래스 표현
- 가장 윗부분에는 클래스의 이름
- 중간부분에는 클래스의 특징을 나타내는 속성
- 마지막 부분에는 클래스가 수행하는 연산들을 기술한다
접근 제어자
접근 제어자 | 표시 | 설명 |
---|---|---|
public | + | 어떤 클래스의 객체에서든 접근 가능 |
private | - | 이 클래스에서 생성된 객체들만 접근 가능 |
protected | # | 이 클래스와 동일 패키지에 있거나 상속관계에 있는 하위 클래스의 객체들만 접근 가능 |
package | ~ | 동일 패키지에 있는 클래스의 객체들만 접근 가능 |
속성과 연산 표기
표기 방법 | |
---|---|
속성 | [+|-|#|~]이름: 타입[다중성 정보][=초기값] |
연산 | [+|-|#|~]이름(인자: 타입1, ..., 인자n:타입n): 반환타입 |
관계
- 다수의 클래스가 모인 시스템이 훨씬 더 효율적이기 때문에 객체지향 시스템도 여러 개의 클래스가 서로 긴밀한 관계를 맺어 기능을 수행한다
-
연관 관계
- 클래스들이 개념상 서로 연결되었음을 나타낸다
- 실선이나 화살표로 표시하며 보통은 한 클래스가 다른 클래스에서 제공하는 기능을 사용하는 상황일 때 표시
-
일반화 관계
- 상속 관계라고 한다
- 한 클래스가 다른 클래스를 포함하는 상위 개념일 때 이를 IS-A 관계라고 하며 UML 에서는 일반화 관계로 모델링한다
- 속이 빈 화살표를 사용해 표시한다
-
집합 관계
-
클래스들 사이의 전체 또는 부분 같은 관계를 나타낸다
-
집약 관계와 합성 관계가 존재한다
-
집약
- 전체 객체와 부분 객체의 생명 주기가 다르다
- 부분 객체를 여러 전체 객체가 공유할 수 있다
-
합성
- 전체 객체가 없어지면 부분객체도 없어진다
- 부분객체를 여러 전체 객체가 공유할 수 없다
-
-
-
의존 관계
- 연관 관계와 같이 한 클래스가 다른 클래스에서 제공하는 기능을 사용할 때를 나타낸다
- 매우 짧은 시간만 유지되며 점선 화살표를 사용해 표시한다
-
실체화 관계
- 책임들의 집합인 인터페이스와 이 책임들을 실제로 실현한 클래스들 사이의 관계를 나타낸다
- 상속과 유사하게 빈 삼각형을 사용하여 머리에 있는 실선 대신 점선을 사용해 표시한다
2장. 객체지향 원리
-
추상화
- 어떤 영역에서 필요로 하는 속성이나 행동을 추출하는 작업을 의미한다
-
구체적인 사물들의 공통적인 특징을 파악해서 이를 하나의 개념으로 다루는 수단
- 각 개체의 구체적인 개념에 의존하지 않고 추상적 개념에 의존해야 설계를 유연하게 변경할 수 있다
1
2
3
fun changeEngineOil(c: Car) {
c.changeEngineOil()
}
- 구체적인 자동차 대신 이들의 추상화 개념인 자동차를 이용한 예
-
캡슐화
- 요구사항 변경에 대처하는 고전적인 설계 원리로는 응집도와 결합도가 있다
- 응집도
- 클래스나 모듈 안의 요소들이 얼마나 밀접하게 관련되어 있는지를 나타낸다
- 결합도
- 어떤 기능을 실행하는데 다른 클래스나 모듈들에 얼마나 의존적인지를 나타낸다
- 높은 응집도와 낮은 결합도를 유지할 수 있도록 설계해야 요구사항을 변경할 때 유연하게 대처할 수 있다
- 캡슐화는 낮은 결합도를 유지할 수 있도록 해주는 객체지향 설계 원리이다
- 정보 은닉을 통해 높은 응집도와 낮은 결합도를 갖도록 한다
-
일반화 관계
일반화는 또 다른 캡슐화
-
일반화 관계는 객체지향 프로그래밍 관점에서는 상속 관계라고 한다
-
일반화 관계는 자식 클래스를 외부로부터 은닉하는 캡슐화의 일종이다
일반화 관계와 위임
-
일반화 관계를 속성이나 기능의 상속, 즉 재사용을 위해 존재한다고 오해하고 있다
-
두 자식 클래스 사이에 ‘is a kind of 관계’가 성립되지 않을 때 상속을 사용하면 불필요한 속성이나 연산(빚)도 물려받게 된다
-
어떤 클래스의 일부 기능만 재사용하고 싶은 경우 위임을 사용하면 된다
-
위임을 사용해 일반화(상속)를 대신하는 과정
-
자식 클래스에 부모 클래스의 인스턴스를 참조하는 속성을 만든다. 이 속성 필드를 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) }
-
서브 클래스에 정의된 각 메서드에 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) }
-
서브 클래스에서 일반화 관계 선언을 제거하고 위임 속성 필드에 슈퍼 클래스의 객체를 생성해 대입한다
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) }
-
서브 클래스에서 사용된 슈퍼클래스의 메서드에도 위임 메서드를 추가한다
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 }
-
컴파일하고 잘 동작하는지 확인한다
-
-
다형성
- 서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 동작하는 능력
- 일반화 관계와 함께 자식 클래스를 개별적으로 다룰 필요 없이 한 번에 처리할 수 있게 하는 수단을 제공한다
- 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("안녀엉")
}
}
-
피터 코드의 상속 규칙
- 상속의 오용을 막기 위해 상속의 사용을 엄격하게 제한하는 규칙들을 만들었다
- 다음 규칙 중 어느 하나라도 만족하지 않으면 상속을 사용해서는 안된다
- 자식 클래스와 부모 클래스 사이는 ‘역할 수행’ 관계가 아니어야 한다
- 한 클래스의 인스턴스는 다른 서브 클래스의 객체로 변환할 필요가 절대 없어야 한다
- 자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행해야 한다
- 자식 클래스가 단지 일부 기능을 재사용할 목적으로 유틸리티 역할을 수행하는 클래스를 상속하지 않아야 한다
- 자식 클래스가 ‘역할’, ‘트랜잭션’, ‘디바이스’ 등을 특수화 해야 한다
3장. SOLID 원칙
SOLID는
SRP // OCP // LSP // ISP // DIP
-
단일 책임 원칙 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 클래스는 변경되어야 한다
책임 분리
- 단 하나의 책임만 수행하도록 해 변경 사유가 될 수 있는 것을 하나로 만드는 것
회귀 테스트
시스템에 변경이 발생할 때 기존의 기능에 영향을 주는지를 평가하는 테스트
- 클래스들이 책임을 적절하게 분담하도록 변경하면 어떤 변화가 생겼을 때 영향을 최소화할 수 있다
산탄총 수술
- 하나의 책임이 여러개의 클래스들로 분산되어 있는 경우에도 단일 책임 원칙에 입각해 설계를 변경해야 하는 경우
- 횡단 관심에 속하는 기능은 대부분 시스템 핵심기능 안에 포함되는 부가 기능이다
- 부가 기능에 변경사항이 발생하면 해당 부가 기능을 실행하는 모든 핵심 기능에도 변경사항이 적용되어야 한다
-
개방-폐쇄 원칙(Open-Closed Principle, OCP)
- 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻
- OCP를 위반하지 않은 설게를 할 때 가장 중요한 것은 무언이 변하는 것인지, 무엇이 변하지 않는 것인지를 구분해야 한다
- 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할 수 있는 설계가 되어야 한다
- 단위 텟트를 수행할 때 매우 중요
- 테스트를 위해 실제 데이터베이스 기능을 대체하는 가짜 객체를 만들 필요가 있다
-
리스코프 치환 원칙(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는 일반화 관계에 대한 이야기이며, 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다
-
즉 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 의미이다
-
의존 역전 원칙(Dependency Inversion Principle, DIP)
- 객체 사이에 서로 도움을 주고받으면 의존 관계가 발생하는데 의존 역전 원칙은 그러한 의존관계를 맺을 때의 가이드라인이다
- 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 원칙이다
- 객체지향 관점에서는 변하기 어려운 추상적인 것들으 표현하는 수단으로 추상 클래스와 인터페이스가 있다
- 따라서 DIP를 만족하려면 어떤 클래스가 도움을 받을 때 구체적인 클래스보다는 인터페이스나 추상 클래스와 의존 관계를 맺도록 설계해야 한다
- DIP를 만족하면 의존성 주입이라는 기술로 변화를 쉽게 수용할 수 있는 코드를 작성할 수 있다
-
인터페이스 분리 원칙(Interface Segregation Principle, ISP)
- 클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다는 내용이 담겨있다
- 인터페이스를 클라이언트에 특화되도록 분리시키라는 설계 원칙
SRP와 ISP 사이의 관계
- SRP를 만족한다면 ISP도 만족할 수 있나?
- ISP를 만족한다면 SRP도 만족할 수 있나?
- 반드시 그렇다고 볼 수 없다
4장. 디자인 패턴
-
디자인 패턴의 이해
-
패턴이란?
- 비슷하거나 동일한 양식 또는 유형들이 반복되어 나타난다는 의미
- 문제와 해결책도 동일한 유형이나 양식을 통해 쉽게 찾을 수 있다
-
디자인 패턴의 구조
- 콘텍스트
- 문제가 발생하는 여러 상황을 기술한다
- 패턴이 적용될수 있는 상황을 나타낸다
- 경우에 따라서는 패턴이 유용하지 못한 상황을 나타내기도 한다
- 문제
- 패턴이 적용되어 해결될 필요가 있는 여러 디자인 이슈들을 기술한다
- 이때 여러 제약사항과 영향력도 문제 해결을 위해 고려해야 한다
- 해결
- 문제를 해결하도록 설계를 구성하는 요소들과 그 요소들 사이의 관계, 책임, 협력 관계를 기술한다
- 해결은 반드시 구체적인 구현 방법이나 언어에 의존적이지 않으며 다양한 상황에 적용할 수 있는 일종의 템플릿이다
- 콘텍스트
-
아키텍처 패턴
- 시스템을 구성하는 컴포넌트의 구성과 컴포넌트 사이의 협조 방법을 패턴화 한 것
- 레이어 패턴, 파이프 & 필터 , 브로커 패턴 등이 대표적인 예
-
디자인 패턴
- 아키텍처 패턴에서 컴포넌트의 내부 구조를 대상으로 한 클래스 / 객체의 구조와 협업 방법을 패턴화 한것
-
관용구
- 각각의 프로그램 언어 특유의 패턴
- 프로그래밍에서 자주 사용하는 기술 방법(코딩 방법)을 패턴화 한 것
-
GoF 디자인 패턴
- 생성 패턴
- 객체 생성에 관련된 패턴, 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공
- 추상 팩토리, 빌더, 팩토리 메서드 , 프로토타입, 싱글턴
- 구조 패턴
- 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴
- 어댑터, 브리지, 컴퍼지트, 데커레이터, 퍼사드, 플라이웨이트, 프록시
- 행위 패턴
- 객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴
- 책임 연쇄, 커맨드, 인터프리터, 이터레이터, 미디에이터, 메멘토, 옵서버, 스테이트, 스트래티지, 템플릿 메서드, 비지터
-
UML과 디자인 패턴
컬레보레이션
- UML 2.0에서 디자인 패턴을 표현하는 도구
- UML에서는 객체들이 특정 상황에서 수행하는 역할의 상호작용을 컬레보레이션이라는 요소로 작성한다
-
담보 대출 관계를 보여주는 컬레보레이션으로 대출자, 대출인, 담보라는 역할이 필요하고 그들 사이의 협력이 요구되므로 이 역할들을 커넥터로 연결한다
-
컬레보레이션은 역할들의 상호작용을 추상화한 것으로, 특별한 상황에 적용하면 많은 시스템 개발에 재사용할 수 있다
- 컬레보레이션 어커런스는 더 구적인 상황에서의 컬레보레이션 적용을 표현해준다
- 컬레보레이션 어커런스는 컬레보레이션에 참가하는 응용 클래스에 의존하며 의존 관계에 붙은 레이블은 응용 클래스가 컬레보레이션에 수행하는 역할을 나타낸다
순차 다이어그램
- 객체들의 상호작용을 나타내는 다이어그램 중 하나이다
- 객체들 사이의 메시지 송신과 그들의 순서를 나타낸다
- 기본적으로 하나의 시나리오에 관한 객체 사이의 상호작용을 보여주는 데 사용되어야 한다
순차 다이어그램과 클래스 다이어그램의 관계
- 순차 다이어그램은 객체 사이의 메시지 흐름과 순서를 알려주는 행위 측면에 중점을 두는 모델
- 클래스 다이어글매은 시스템의 구조적인 측면에 중점을 두는 모델