티스토리 뷰
어플리케이션의 복잡도를 다루기 위해 적절한 응집도와 결합도를 찾아야 한다.
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는 사이클을 예방한다.
.
├── 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)
- 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
응집도/결합도
- Why Product Development and Design needs Cohesion-Coupling
- Coupling and Cohesion: Failed Concepts.
- Cohesion vs Coupling
- [설계 용어] 응집도(Cohesion)와 결합도(Coupling)
- 명확한 코드 작성법
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
링크
TAG
- buildkit
- cka
- MSA
- user-agent
- Isolate level
- 위코드
- web_server
- database
- 창업
- 덕타이핑
- HTTP/3
- 프리온보딩
- HTTP/2
- GitHub
- inflearn
- 원티드
- Network
- Git
- docker-compose
- go
- Python
- direnv
- thetextbook
- gitignore
- http
- k8s
- no-op
- Complier
- pytest
- QUIC
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함