Home 9. 5 solid principles
Post
Cancel

9. 5 solid principles

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
 구체화된 객체와 대상을 직접 연결하는 것이 아니라, 중간에 추상화된 모듈을 통해 각각이 통신하게 함으로서 결합도를 떨어트린다.
This post is licensed under CC BY 4.0 by the author.