티스토리 뷰

Development

SOLID/Cohesion/Coupling

신잼 2022. 3. 19. 23:40

어플리케이션의 복잡도를 다루기 위해 적절한 응집도와 결합도를 찾아야 한다.

SOLID는 높은 응집도와 낮은 결합도를 위한 OOD의 설계 원칙이다.

SOLID

  • SOLID는 지향해야 될 목표로 이상향에 가까움
    • 현실적인 문제로 Trade off는 항상 있다.

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

하나의 객체는 하나의 책임을 가져야 한다.

ex) 예금 잔고 객체

  • 단일책임은 정해진 게 아니라 어디까지 하나의 책임으로 볼 건지 고민 필요
    • 입금, 출금이 있을 때 입출금 하나의 책임인지 따로따로가 단일 책임인지에 대한 고민
  • 하나의 객체가 있는데 두개의 책임이 있다면 분리
package main
​
type FinanceReport struct {
​
}
​
func (r *FinanceReport) MakeReport() *Report {
  ...
}
​
func (r *FinanceReport) SendReport(email string) *Report {
  ...
}
  • 만약에 프로그램이 확장되면서 email이 아니라 다른 형태(http, file 등등..)라면 함수가 계속 추가 되어야 한다.
    • SendRportFile, SendRportHttp..
  • 확장하려면 switch를 사용하게 된다.
func (r *FinanceReport) SendReport(method int) *Report {
  switch (method) {
  case 1:
    //send email
  case 2:
    //make file
  case 3:
    // http
  }
}
  • SendReport와 MakeReport는 분리되어야 한다.
package main
​
type FinanceReport struct {
​
}
​
func (r *FinanceReport) MakeReport() *Report {
  ...
}
​
type ReportSender interface {
  SendReport(*Report)
}
​
type EmailReportSender struct {
​
}
​
func (r *EmailReportSender) SendReport(r *Report) {
  ...
}
​
type FileReportSender struct {
​
}
​
func (r *FileReportSender) SendReport(r *Report) {
  ...
}
​

OCP(Open/closed Principle)::개방 폐쇄 원칙

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

변경이 열려있다=새로운 방식이 추가 되려면 기존 코드를 바꿔야 하는 것

func (r *FinanceReport) SendReport(method int) *Report {
  switch (method) {
  case 1:
    //send email
  case 2:
    //make file
  case 3:
    // http
  }
}

확장이 열려있다=아래 처럼 따로 추가적인 확장이 가능한 것

type ReportSender interface {
  SendReport(*Report)
}
​
type EmailReportSender struct {
​
}

LSP(Liskov Substitution Principle)::리스 코프 치환 원칙

원래 하기로 정한 것을 바꾸지 말자

OOD 중 가장 어려운 개념

  • x는 T의 인스턴스, O(x) 함수
  • T의 확장 타입인 S, y는 S의 인스턴스, O(y) 함수

위와 같은 상황일 때 O(x)와 O(y)의 동작이 같아야 하고 O(y)에 x를 넘겨도 동작 방식이 같아야 한다

즉, Base Type의 기종 함수(동작)를 바꾸지 말아야 한다. 이는 overiding을 하지 말라는 뜻으로 다루기 힘든 부분이 된다.

  • Go언어에서는 LSP를 걱정 안 해도 된다. 상속을 지원하지 않기 때문이다.
    • 그러면 상속이 안 되는 Go는 OOP가 안 된다? -> OOP는 상속과 관련이 없고 오히려 상속이 LSP를 위반하기도 한다.

ISP(Interface Segregation Principle)::인터페이스 분리 원칙

하나의 인터페이스를 여러 객체가 공유하지 말자

여러 개의 관계를 모아놓은 인터페이스보다 관계 하나씩 정의하는 게 더 좋다.

  • 묶여 있으면 의존성을 발생시킨다
type Actor interface {
  Move()
  Attack()
  Talk()
}
​
// 단일 책임에 어긋난다.
func MoveTo(a *Actor) {
  a.Move()
  a.Attack()
}
  • 따로따로 인터페이스를 분리시켜야 한다.
type Movable interface {
  Move()
}
​
type Attackable interface {
  Attackable()
}
​
type Talkable interface {
  Talkable()
}
​
// SRP에 어긋나지 않도록 강제 할 수있다.
func MoveTo(a *Movable) {
  a.Move()
  a.Attack() // error
}

DIP(Dependency Inversion Principle)::의존관계 역전 원칙

관계는 인터페이스에 의존해야지 객체에 의존하면 안 된다(더 좋다)
  • 상위 모듈은 하위 모듈에 의존 x
  • 상위 모듈과 하위 모듈 모두 추상화에 의존 o
  • 추상화는 세부 사항에 의존 x, 세부 사항이 추상화에 의존 o
  • DIP는 사이클을 예방한다.

예제: abcdefuji/go-dip-sample

.
├── domain
│   ├── user.go
│   └── user_repository.go
├── go.mod
├── infrastructure
│   └── mysql_user_repository.go
└── server.go
  • user_repository.go
package domain
​
type Repository interface {
  Store(u *User)
  FindById(id int) *User
}
  • mysql_user_repository.go
package infrastructure
​
import (
  "dip-golang/src/domain"
  "fmt"
)
​
type TestRepository struct{}
​
func (repo TestRepository) FindById(id int) *domain.User {
  return &domain.User{1, "test_user"}
}
​
func (repo TestRepository) Store(u *domain.User) {
  fmt.Printf("userId: %v \n", u.ID)
  fmt.Printf("userName: %v \n", u.Name)
  fmt.Println("stored success")
  return
}
​
func NewTestRepository() TestRepository {
  return TestRepository{}
}
  • server.go
package main
​
import (
  "dip-golang/src/domain"
  "dip-golang/src/infrastructure"
  "fmt"
)
​
var userRepository domain.Repository
​
func main() {
​
  userRepository = infrastructure.NewTestRepository()
  user := getUserById(1)
  fmt.Printf("Find: %v  \n", user)
  createUser(1, "hoge")
}
​
func getUserById(id int) *domain.User {
  user := userRepository.FindById(id)
  return user
}
​
func createUser(id int, name string) {
  defer userRepository.Store(&domain.User{id, name})
}

Beyond OOP

  • 설계 발전 과정: 절차적 → oop →?
  • oop 문제점
    • 잘 쓰기 어렵다
    • object 개수가 많다 → 새로운 프로그래머가 모든 객체의 역할과 관계를 파악하기 오래 걸림
    • 빨리 만들기 어렵고 빨리 만들다 보면 설계 원칙이 깨진다(설계 원칙을 깨는 것: Tech debt, 당장을 위해 엉망인 코드를 만드는 것)
    • 성공했을 경우 기반이 좋지 않으면 견딜 수 없기 때문에 빠르게 기술 부채를 없애야 한다.
    • 하지만 성공할지 모르기 때문에 오래 시간 쏟기 쉽지 않다.

어떻게 각자 맡은 부분만 독립적으로 만들 수 있을까?

상태는 없애고 기능만 만들자, 상태는 외부에서 만들어서 들어가질 수 있도록 하자

  • oop 다음은 stateless?
    • object는 상태와 기능을 갖는다
    • 상태와 기능을 분리시켜야 빠르게 만들 수 있다
    • 레고 방식으로 조립되는 프로그램이 적합(100명의 프로그래머 퀄리티를 모두 올리는 것보다 최대한 분리시키기)
    • MSA, Serverless, Functional Language, 게임 쪽에서는 ECS, MVC(MVVC) 등

응집도(Cohesion), 결합도(Coupling)

(좌)bootcamp.uxdesign.cc/why-product-development-and-design-needs-cohesion-coupling-87731c84aaa7(우)devopedia.org/cohesion-vs-coupling

 

  • Components
    • 단일 업무를 수행하는 각각의 요소들.
    • RAM, ROM, Lens, Image Processor, CPU, GPU 등이 컴포넌트
  • Modules
    • 기능적으로 비슷한 컴포넌트들의 집합
    • Memory(RAM, ROM), Camera(Lens, Image Processor), System-on-Chip(CPU, GPU) 등이 모듈

응집도(Cohesion)

모듈의 요소들이 기능적으로 얼마나 연관되어 있는지에 대한 척도
  • 높은 응집
    • 모듈의 컴포넌트들이 하나의 역할과 관련이 있을 때
  • 낮은 응집
    • 컴포넌트들이 여러 작업이 가능해서 모듈이 여러 작업이 가능해질 때

결합도(Coupling)

모듈 간에 얼마나 독립적인지에 대한 척도
  • 높은 결합
    • 두 모듈이 각자의 역할 수행에 있어 서로에게 의존적일 때
    • 디스플레이 모듈과 시스템 모듈은 서로 의존적(디스플레이가 손상되면 GPU는 기능을 수행할 수 없다.)
  • 낮은 결합
    • 두 모듈이 각자 역할 수행에 있어 서로에게 의존적이지 않을 때
    • 배터리와 배터리 충전기(배터리 충전기가 고장 나면 다른 배터리 충전기로 바꿔 사용 가능하다)

High Cohesion & Low Couping

높은 응집도와 낮은 결합으로 프로그래밍을 해야 한다.

  • 쉬운 트러블슈팅: 문제가 됐을 때 문제가 되는 모듈을 쉽게 찾을 수 있다.
  • 쉬운 수정: 다른 모듈을 건드리지 않고 쉽게 교체가 가능하다
  • 쉬운 테스트: 모듈 전체에 대한 테스드가 아닌 필요한 모듈들만 테스트가 가능해진다.

Reference

응집도/결합도

SOLID

반응형

'Development' 카테고리의 다른 글

MySQL vs PostgreSQL  (0) 2022.04.08
Database 고르기  (0) 2022.04.07
Network command line  (0) 2022.03.01
k8s 공부 하면서 헷갈렸던 용어 정리  (0) 2022.02.21
docker compose VS docker-compose  (0) 2022.02.17
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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 27 28
29 30 31
글 보관함