Go/Golang - 5 SOLID principles
좋은 설계와 나쁜 설계
좋은 설계란 상호 결합도
가 낮고, 응집도
가 높은 설계를 말한다.
상호결합도가 낮다
는 것은 모듈을 쉽게 떼어내서 다른곳에 붙여 사용할 수 있다는 뜻응집도가 높다
는 말은 하나의 모듈이 의존적이지 않고, 독립적으로 자립한다는 뜻- 이
좋은 설계
를 위한 가이드 중 하나가 SOLID 라고 할 수 있다.
1) 단일 책임 원칙 Single Responsibility Principle, SRP
An Object class should only be responsible for only one specific function, and only have one reason to change
- 정의: 모든 객체는 하나의 특정 기능만 책임진다. -> 수정이나 변경사항이 이루어지는 이유는
단 한가지
여야 한다. - 이점: 코드 재사용성을 높여준다.
EXAMPLE
1
2
3
4
5
6
7
8
9
10
11
| // 잘못된 설계
type FinanceReport struct { // 보고서 객체
report string
}
func (r *FinanceReport) SendReport(email string) { // 메서드
...
}
// 이게 왜 단일 책임 원칙을 위배한 경우인가?
// `FinanceReport` 가 아닌 새로운 `MarketingReport`가 생긴다면.. SendReport를 재활용할 수 없고 새로 만들어야 한다!
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 올바른 설계
// 올바른 설계에서는 FinanceReport가 Report인터페이스를 구현하고, ReportSender는 Report 인터페이스를 이용하는 관계를 형성한다.
type Report interface{
Report() string
}
type FinanceReport struct { // 경제 보고서만을 책임진다.
report string
}
func (r *FinanceReport) Report() string { // Report 인터페이스를 구현했다.
return r.report
}
type ReportSender struct { // 보고서 전송만을 책임진다.
...
}
func (s *ReportSender) SendReport(report Report){ // 전송하는 기능만을 담당한다.
// Report 인터페이스 객체를 인수로 전달받는다.
}
|
2) 개방-폐쇄 원칙 Open Closed Principle, OCP
Developers should be able to add new features, functions, and extensions to a class while leaving the rest of the existing codebase intact.
- 정의: 확장에는 열려있고 변경에는 닫혀있다.
- 이점: 상호 결합도를 줄여 새 기능을 추가할 때, 기존 구현을 변경하지 않아도 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 잘못된 설계
// 개방 폐쇄 원칙을 위반하는 경우는 다음과 같다.
func SendReport(r *Report, method SendType, receiver string) {
switch method {
case Email:
...
case Fax:
...
case PDF:
...
case Printer:
...
}
}
// 위와 같은 경우일때, case를 추가하게 되면 SendReport의 구현을 변경하게 된다.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 올바른 설계
// 따라서 이런 경우는 EmailSender와 FaxSender 모두 ReportSender라는 interface를 구현하게 된다.
// 그러면 새로운 객체를 추가하는 것 만으로 기능이 추가된다.
// 물론, 동시에 단일 책임 원칙 또한 들어맞게 된다.
type ReportSender interface {
Send(r *Report)
}
type EmailSender struct { //
}
func (e *EmailSender) Send(r *Report) {
}
type FaxSender struct {
}
func (f *FaxSender) Send(r *Report) {
}
|
3) 리스코프 치환 원칙 Liskov Substitution Principle, LSP
The Objects contained in a subclass must exhibit the same behavior as any higher-level superclass it’s dependent on.
- 정의: q(x)를 타입 T의 객체 x의 속성이라고 하자. S가 T의 하위타입이라면, q(y)는 타입S의 객체 y에 대해 증명할 수 있어야 한다.
- 이점: 예상치 못한 동작을 예방한다.
EXAMPLE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 결국, 상위 클래스에서 동작하는 상위클래스 함수는, 하위타입에 대해서도 똑같이 동작해야 하는 것이다.
// go는 상속을 지원하지 않음으로서, 상속을 잘못 사용해 리스코프 치환 원칙을 위반하는 일을 사전에 방지했다.
type T interface {
Something()
}
type S struct {
}
func (s *S) Something() { // S -> T interface 구현
}
type U struct {
}
func (u *U) Something() { // U -> T interface 구현
}
func q(t T) {
...
}
var y = &S{} // S타입 y
var u = &U{} // U타입 u
q(y)
q(u) // 두개 다 잘 동작해야 한다.
|
4) 인터페이스 분리 원칙 Interface Segregation Principle, ISP
Create a seperate client interface for each class within an application, even if those classes share some of the same methods
- 정의: 클라이언트는 자신이 이용하지 않는 메소드에 의존되면 안된다.
- 이점: 인터페이스를 분리하면 불필요한 메소드들과 의존관계가 끊어진다.
1
2
3
4
5
6
7
8
9
10
11
| // 잘못된 설계
type Report interface {
Report() string
Pages() int
Author() string
WrittenDate() time.Time
}
func SendReport(r Report) {
send(r.Report())
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 올바른 설계
// 아래와 같이, 인터페이스를 분리하게 되면 불필요한 구현이 사라져 더 가볍게 인터페이스를 이용할 수 있다.
type Report interface {
Report() string
}
type WrittenInfo interface {
Pages() int
Author() string
WrittenDate() time.Time
}
func SendReport(r Report) {
send(r.Report())
}
|
5) 의존관계 역전 원칙 Dependency Inversion Principle, DIP
When a subclass is dependent on a superior class, the higher-level class should not be affected by any changes made to the subclass.
- 정의: 상위 계층이 하위 계층에 의존하는 전통적인 의존관계를 반전시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
- rule 1) 상위 모듈은 하위 모듈에 의존하는 것이 아니라 둘 다 추상모듈에 의존한다.
1
2
| 예를 들어 키보드의 입력을 모니터의 출력으로 보내야 한다고 하면, 입력부분과 출력부분을 추상모듈로 만들고, 키보드<->입력모듈<->출력모듈<->키보드
와 같은 방식으로 추상화시키면 결합도를 떨어트릴 수 있다.
|
- rule 2) 추상 모듈은 구체화된 모듈에 의존하는 것이 아니라 그 반대가 되어야 한다.
1
2
| 마찬가지 구체화된 모듈은 추상모듈에 의존하도록 만든다. 추상 모듈인 Event와 Event-Listener가 있고, 각각에 해당하는 이벤트와 이벤트 발생시 수행할
작업들을 등록해주는 모양새이다.
|
- 이점:
- 추상모듈에 의존함으로써 확장성이 증가한다.
- 결합도가 낮아져서 이식성이 증가한다.
Questions
Q1. 좋은 설계란 무엇인가?
1
2
| `상호 결합도`가 낮고, `응집도`가 높은 설계를 말한다.
> 상호 결합도가 낮아 모듈을 쉽게 떼어내서 다른곳에 붙여 사용할 수 있고, 응집도가 높아 모듈이 의존적이지 않고 독립적이다
|
Q2 객체지향 설계의 5가지 원칙을 말하라
1
2
3
4
5
| 1) 단일책임원칙 (Single Responsibility Principle, SRP)
2) 개방폐쇄원칙 (Open Closed Principle, OCP)
3) 리스코프치환원칙 (Liskov Substitution Principle, LSP)
4) 인터페이스분리원칙 (Interface Segregation Principle, ISP)
5) 의존관계역전원칙 (Dependency Inversion Principle, DIP)
|
Q3. 다음 설계에서 위배된 원칙은 무엇인가? 수정해보라!
1
2
3
4
5
6
7
| type FinanceReport struct {
report string
}
func (r *FinanceReport) SendReport(email string) {
...
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 1) 단일책임원칙.
type Report interface{
Report() string
}
type FinanceReport struct { // 경제 보고서만을 책임진다.
report string
}
func (r *FinanceReport) Report() string { // Report 인터페이스를 구현했다.
return r.report
}
type ReportSender struct { // 보고서 전송만을 책임진다.
...
}
func (s *ReportSender) SendReport(report Report){ // 전송하는 기능만을 담당한다.
// Report 인터페이스 객체를 인수로 전달받는다.
}
|
Q4. 리스코프치환원칙(Liskov Substitution Principle, LSP)에 대해 설명하라.
1
| 상위 클래스에서 동작하는 상위클래스에 대한 기능(함수)는 하위타입에 대해서도 완전히 동일하게 동작해야 한다.
|
Q5. 의존관계 역전원칙(Dependency Inversion Principle, DIP)에 대해 설명하라.
1
| 구체화된 객체와 대상을 직접 연결하는 것이 아니라, 중간에 추상화된 모듈을 통해 각각이 통신하게 함으로서 결합도를 떨어트린다.
|