Go 로 배우는 Solid 원칙

임동규·2022년 8월 1일
0
post-thumbnail

Solid 원칙이란?

객체지향 설계 5가지 원칙의 영문명 앞글자를 따서 만든 원칙

  1. 단일 책임 원칙 (SRP) single responsibility principle
  2. 개방 - 폐쇄 원칙 (OCP)open-closed principle
  3. 리스코프 치환 원칙 (LSP) liscov substitution principle
  4. 인터페이스 분리 원칙 (ISP) interface segregation principle
  5. 의존 관계 역전 원칙 (DIP) dependency inversion principle

Why Solid ?

현대 프로그래밍은 과거에 비해 매우 복잡하다. 여러 사람이 각자의 맡은 바 코드를 구현하기 때문이다. 맡은 바 코드는 모듈인데 모듈이 모여 프로그램을 이루다 보니까 나쁜 설계는 많은 버그가 창출 된다.

이러한 문제점을 해결하기 위해서는 모듈 간의 결합도는 낮추고 응집도를 높여서 프로그래밍 설계를 해야한다.

모듈간의 결합도는 낮추고 응집도를 높이는 방식의 프로그래밍이 Solid 원칙에 따르는 프로그래밍이다.

나쁜 설계란?

  1. 경직성이 강함 - 모듈 간의 결합도가 높아서 코드를 변경하기 매우 어려운 구조 (스파게티 코드)
  2. 부서지기 쉬움 - 한 부분을 건드렸더니 다른 부분 까지 망가지는 경우
  3. 부동성이 높음 - 모듈간의 결합도가 높아 옮길수 없는 경우, 재사용률 급격히 감소

1. 단일 책임 원칙 (SRP)

-모든 객체는 책임을 하나만 져야 한다.

  • 코드 재사용성을 높여줌
type FinanceReport struct{
	report string
}

func (f *FinanceReport) SendReport(email string){
...
}

Finance Report 는 회계 보고서 객체다.

그런데 마케팅 보고서가 추가됬다고 생각해보자.

type MarketingReport struct{
	report string
}

func (m *MarketingReport) SendReport(email string){
...
}

이런식으로 하게 된다면 새로운 보고서를 만들 때마다 SendReport 메서드를 정의해줘야한다.

그렇다면 인터페이스를 사용해서 각각의 객체에 하나의 책임만을 주게 된다면 어떨까?

type Report interface{

	Report() string
}

type FinanceReport struct{
	report string
}

func (f *FinanceReport) Report() string{
 return f.report
}

type MarketingReport struct{
	report string
}

func (m *MarketingReport) Report() string{
	return m.report
}

type ReportSender struct{}

func (r *ReportSender) SendReport(report Report){
	...
}

이런식으로 구현하게 된다면 FinanceReport, MarketingReport 는 각각 보고서의 책임만을 가지게 된다.
그리고 이 둘은 Report interface 를 구현하기 때문에 Report 타입을 사용할 수 있으며

ReportSender 구조체(Report 보내는 역할)의 메서드의 인수로 들어갈 수 있게 된다.
FinanceReport , MarketingReport 의 값을 통해 SendReport 를 할 수 있게 된다.

이 방식의 장점이 뭘까? 
ReportSender 구조체는 Report interface 에 의존하지만 FinanceReport, MarketingReport 구조체는
Report interface 를 구현함으로써 ReportSender 와의 커플링이 끊어진다는 점이 장점이다.

2.개방-폐쇄 원칙(OCP)

-확장에는 열려 있고, 변경에는 닫혀 있다.

  • 상호 결합도를 줄여 새 기능을 추가할 때 기존 구현을 변경하지 않아도 된다.
func SendReport(r *Report, method SendType, receiver string){
 switch method{
	case Email:
       //
  case Fax:
       //
  case PDF:
       //
  case Printer:
       // 
 }
}

뭔가 switching 할 method가 추가되면 SendReport 함수를 변경해줘야 한다., OCP 원칙이 위반 된다.

type ReportSender interface{
	Send(r *Report)
}

type EmailSender struct{}

func (e *EmailSender) Send(r *Report){
	 //
}

type FaxSender struct{}

func (f *FaxSender) Send(r *Report){
 //	
}

func SendReport(r *Report,rs *ReportSender){
	// rs.Send(r)
}

이제 SendReport 는 ReportSender 인터페이스를 구현하는 객체를 가지고 와서 Send() 함수만 실행시켜
주는 역할을 한다.

이런식으로 코드를 작성하게 된다면 SendReport 하는 객체에 Send() 메서드만 구현해주면 된다.

이방식의 장점이 뭘까? 바로 기존 코드의 변경을 최소화 할 수 있다는 것이다. 

3. 리스코프 치환 원칙(LSP)

-q(x) 를 타입 T의 객체 x 에 대해 증명할 수 있는 속성이라면 S 가 T의 하위 타입이라면 q(y) 는 타입 S 의 객체 y에 대해 증명할 수 있어야 한다.

  • 예상치 못한 작동을 예방
type T interface{
	Something()
}

type S struct{}

func (s *S) Something(){
 //
}

type U struct{}

func (u *U) Something(){
 //
}

func q (t T){
...
}

s:= &S{}

u := &U{}

q(s)
q(u)
// 함수 두개 다 작동 가능해야함.

잘못된 예시

type Report interface{

	Report() string
}

type FinanceReport struct{
	report string
}

func (f *FinanceReport) Report() string{
 return f.report
}

func SendReport(r Report) {
	if _, ok := r.(*MarketingReport); ok{
		panic("Can't send MarketingReport")
	}
  ...
}

report := &MarketingReport{}

SendReport(report) // 패닉 발생

상위타입 Report interface 에 대해서 작동하는 SendReport 함수에서 하위 타입인 MarketingReport
일 경우 코드는 작동하지 않기 때문에 이는 원칙을 위반한 코드가 됨.

4. 인터페이스 분리 원칙 (ISP)

-클라이언트는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

  • 인터페이스를 분리하면 불필요한 메서드들과 의존 관계가 끊어져 더 가볍게 인터페이스 이용가능
type Report interface{
	Report() string
  Pages() int
  Author() string
  WrittenDate() time.Time
}

func SendReport(r Report){
	send(r.Report())
}

쉽게 생각해보자. interface 를 사용하려면 interface 안에 메서드들을 구현해야한다. 
그런데 불필요한 메서드들을 구현하면서 까지 인터페이스를 써야할까? 코드 낭비가 될 것이다.

차라리 각각 필요한 메서드를 구현하는 interface 로 나누면 어떨까?

type Report interface{
	Report()
}

type WrittenInfo interface{
	Pages() int
  Author () string
  WrittenDate() time.Time
}

만약 이 두 인터페이스를 공유하는 타입을 쓰고 싶다면?

type Book interface{
	Report
  WrittenInfo
}

5. 의존 관계 역전 원칙

-상위 계층이 하위 계층에 의존하는 전통적인 의존 관계를 반전시킴으로ㅓ써 상위 계층이 하위 계층의 구현으로부터 독립되게 하는 것

원칙 1

  • 상위 모듈은 하위 모듈에 의존해서는 안된다. 둘 다 추상 모듈에 의존해야 한다.

원칙 2

  • 추상 모듈은 구체화된 모듈에 의존해서는 안됨. 구체화된 모듈은 추상 모듈에 의존해야 한다.
type mail struct{
 alarm Alarm
}

type Alarm struct{}

func (a *Alarm) Alarm(){
 //
}

func (m *Mail) OnRecv(){
  m.alarm.Alarm()
}

이런식으로 구체화된 모듈이 구체화된 모듈을 불러오는 것은 커플링이 되어 있다.
추상 모듈이 추상모듈을 의존하는 것은 괜찮지만 구체화된 모듈이 규체화된 모듈과 결합되면 Solid 원칙에 위배된다.

수정 버전
type Event interface{
 Register(EventListener)
}

type EventListener interface{
	OnFire()
}

현재 Event 와 EventListener 는 결합되어 있다.

type Mail struct{
	listenr EventListener
}

func (m *Mail) Register(listener EventListener){ //Event 인터페이스 구현
	m.listener = listener
}

func (m *Mail) OnRecv(){
	m.listener.Onfire()
}

type Alarm struct{}

func (a *Alarm) OnFire(){ // EventListener 인터페이스 구현
	fmt.Println("알람")
}

mail := &Mail{}

listener := &Alarm{}

mail.Register(listener)

mail.OnRecv()

위의 코드를 보면 mail 은 Event 인터페이스를 구현하고 있고 EventListener 인터페이스와 결합되어 있다.
그러나 이렇게 추상모듈끼리 결합되어 있으면 확장성은 확실해진다. 
만약 Alarm 말고 다른 이벤트가 있다면 그 객체를 가지고 Event 인터페이스가 사용할 수 있게 될 것이다.
그렇게 된다면 결합도는 낮춰지고 응집도는 높아지게 되는 것이다.
profile
I will be Blockchain Core Developer

0개의 댓글