(수정중) SOLID, SRP(Single Responsibility)

14738 단어 디자인패턴OOPOOP

🚨면접 중 발생한 불상사!🚨

면접관 : SOLID 원칙 중에 ... 어떻게 생각하시나요?
나 : (머리가 하얘져서 아무것도 안들림)

이런 젠장.. 어제 겪은 창피함에 다시 얼굴이 벌개진다.
하나씩 다시 살펴보려한다.

객체지향적 설계 원칙

객체지향 설계에는 다섯가지 원칙이 존재한다.

1. SRP(Single Responsibility Principle) : 단일 책임 원칙
2. OCP(Open-Closed Principle) : 개방-폐쇄 원칙
3. LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
4. ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
5. DIP(Dependency Inversion Principle) : 의존 역전 원칙

이 글에서는 SRP에 대해서 알아본다.


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

"A Class should have one, and only one, reason to change."

클래스는 단 하나의 책임을 가져야하고, 클래스를 변경하는 이유는 단 하나의 이유이어야 한다. - Robert C. Martin

여기서 책임이라는 말은, '기능' 정도로 해석한다. 설계를 잘한 프로그램은 새로운 요구사항과 프로그램 변경에 영향을 받는 부분이 적다. 즉, 응집도는 높은데 결합도는 낮은 프로그램을 뜻한다. 만약 한 클래스가 수행할 수 있는 기능(책임)이 많아지면 클래스 내부의 함수끼리 강한 결합을 발생할 가능성이 높아진다. 이는 유지보수 비용의 증가를 의미하므로 책임을 분리시킬 필요가 있다. 방금 언급한 Coupling(결합), Cohesion(응집력)은 비슷해보일 수 있는 단어지만 중요한 차이점이 있다.

💡 차이점
결합? 어떤 한 이유로 두 가지 이상이 함께 변한다는 것을 나타내는 표현
응집? 서로 분리되어 있지만 밀접한 관련성을 가진 것을 모아둔다는 표현

결합도(Coupling)를 좀 더 쉽게 설명하자. 만약 하나의 기능을 개선하기 위해서 1개의 클래스만 고쳐야한다면 결합도가 낮은 것이고, 100개의 클래스를 고쳐야 하면 결합도가 높은 것이다.

응집도(Cohesion)를 설명할 때 자주 사용되는 방법은 모듈이다. Go에서 모든 코드는 패키지(모듈) 안에 있는데 패키지는 유사한 목적과 기능을 가진 집합이다. 잘 설계된 패키지는 본인의 목적을 드러낼 수 있는 이름으로 시작한다. 즉, 패키지의 이름은 목적에 대한 명확한 설명이 된다.

  • net/http? http 클라이언트와 서버를 제공한다.
  • os/exec? 외부 명령을 실행한다.
  • encoding/json? JSON 문서의 인코딩 및 디코딩을 구현한다.

응집도를 설명하기 위해 패키지 네이밍을 언급한 것이 그저 현학적인 이야기를 하는건 아니다. 잘못 네이밍된 패키지는 실제 그 목적을 가지고 있더라도 목적을 나타낼 기회를 놓치는 것이기 때문이다.

  • server? 서버.. 인건 알겠는데 어떤 프로토콜을 사용하지?
  • private? 무엇을 private하게 나타내지? 내가 보면 안되는 것인가?
  • common?, utils? etc..

이처럼 Go의 패키지는 그 자체가 작은 Go 프로그램이며 단일 책임을 가진 단일 변경 유닛이다. 결국 단일 기능을 가진 프로그램이라면 결합도는 낮을 것이고, 잘 설계된 프로그램이라면 각 기능이 서로 연관성 있게 짜여지며, 네이밍을 통해 프로그램의 목적을 명확하게 드러내야 한다.


확인 사항

Go에서 RSP 법칙을 지키기 위해 확인해야 할 3가지 질문이 있다.

  1. 구조체의 메서드가 구조체와 관련이 있는 행위인가?
  2. 구조체가 다른 구조체와 단단하게 결합되어 있나?
  3. 메서드를 다르게 적용할 수 있나?

위 3가지 관점에서 아래의 Go 코드를 점검해보자.
UML을

type Area interface {
	Area() int
	ViewPanorama() string
}
type shape struct {
	rooms    int
	roomSize int
}
func (s *shape) Area() int {
	return s.rooms * s.roomSize
}
type House struct {
	name string
	shape
}
func (h *House) Name() string{
	return h.name
}
func (h *House) ViewPanorama() string {
	return h.name + "의 뷰 입니다."
}
type Apartment struct {
	House
}
func NewApartment(name string, room, roomSize int) Area {
	return &Apartment{House{name, shape{room, roomSize}}}
}
func (a *Apartment) ViewPanorama() string {
	return a.name + ", 야경 지리네!"
}
type Villa struct {
	House
}
func NewVilla(name string, room, roomSize int) Area {
	return &Villa{House{name, shape{room, roomSize}}}
}
func (v *Villa) ViewPanorama() string {
	return v.name + ", 벽뷰 지리네..."
}
type Empty struct {
	House
}
func NewEmpty(name string) Area {
	return &Empty{House{name, shape{}}}
}
package main
import "fmt"
func main() {
	someHouses := []Area{
		NewApartment("한남더힐", 5, 40),
		NewVilla("빌라촌A", 2, 20),
		NewEmpty("집없음"),
	}

	for _, h := range someHouses {
		fmt.Println(h, h.Area(), h.ViewPanorama())
	}
}

1. 구조체의 메서드가 구조체와 관련이 있는 행위인가?

위 예제에서 House의 name을 알아내는건 구조체와 관련이 있는 행위이지만, House를 저장하는건 관련이 있는 행위라고 보기 어렵다. 이런 경우, 별도의 구조체와 메서드로 분리시켜야 한다. ("Single structure, Single responsibility")

2. 구조체가 다른 구조체와 강하게 결합되어 있는가?

House 구조체는 shape 구조체를 임베딩하고 있다. (일종의 상속 관계)
어떤 구조체에 임베딩된다는건 그 구조체에 의존한다는 뜻이다. 따라서 의존하는 클래스의 코드가 변경되면 영향을 받게 된다. 다만

3. 메서드를 다르게 적용할 수 있나?

다른 포맷으로 저장하고 싶을 경우

변경 후

type House struct {
    name string
}
func (h House) Name(){
    return h.name
}

type HousePersistence struct{ }
func (hp HousePersistence) Save(h House){
    fmt.Println("Saving in the Terminal .... " + h.name)
}





참고 자료

좋은 웹페이지 즐겨찾기