일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 웹개발
- 자바
- 서버개발
- 프로그래밍
- 의존관계
- spring data jpa
- 봇
- 다자인패턴
- java
- 클래스다이어그램
- 객체지향
- redis
- 개발
- 코딩
- OOP
- 디자인패턴
- cache
- 연관관계
- IT
- 집합관계
- 웹
- Spring
- Sprign
- backend
- Programming
- 전략패턴
- caching
- 백엔드
- Web
- jpa
- Today
- Total
괴발나라
[ 3장 ] SOLID 원칙 본문
해당 포스트에는 주관적인 의견과 해석이 담겨있습니다.
내용이 허술하며, 정확하지 않을 수 있습니다.
오류나 다른 의견이 있다면 댓글 꼭꼭 부탁드립니다. 😊
SOLID 원칙이란?
SOLID 원칙은 객체 지향 프로그래밍이라는 패러다임 안에서 변경에 용이한 코드를 작성할 때 지켜야할 원칙들이다.
SOLID 는 5가지 원칙의 앞글자를 딴 용어다.
- SRP (Single Responsiblity Principle) - 단일 책임 원칙
- OCP (Open Closed Principle) - 개방 폐쇄 원칙
- LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
- DIP (Dependency Inversion Principle) - 의존 역전 원칙
- ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
개인적으로 원칙들의 이름이 굉장히 난해하여 기억하기 어려웠으나,
단순히 객체 지향 프로그래밍 언어로
토이 프로젝트라도 만들어 보았다면
다들 한번쯤 느꼈을 것들을 개념화시킨 것이었다.
온몸이 떨리고 심장이 빠르게 뛰는 것에 대하여 공포라는 이름을 지어줬듯이 말이다.
단일 책임 원칙 (Single Responsiblity Principle)
단일 책임 원칙은 관련된 기능들을 한 곳(객체)에 밀집시켜야 한다는 원칙이다.
예를 들어 "식당" 이라는 객체가 있다고 해보자.
"식당"은 손님이 입장하면
자리를 안내하고,
주문을 받고,
주문을 확인해 요리를 한 뒤,
완성된 요리를 손님에게 전달하고,
떠난 손님의 자리는 치우는 등 여러가지 일을 담당할 것이다.
이때 "식당"이라는 객체가 담당한 일들의 집합을 "책임"이라고 한다.
"식당" 이라는 객체를 생성하기 위한 "식당" 클래스를 코드로 작성하면 어떨까?
아마 다음과 같은 코드가 될 것이다.
class 식당 {
손님에게_인사한다()
자리를_안내한다()
주문을_받는다()
주문을_확인한다()
요리한다()
요리를_서빙한다()
계산한다()
// ...
}
짧은 코딩 경험이라도 있다면 본성에서 올라오는 답답함과 불쾌함을 즉시 느꼈을 것이다.
왜냐하면 너무 다양한 "책임"을 하나의 객체가 갖고 있기 때문이다.
손님에게 자리를 안내하고, 주문을 받는 등의 일은 손님과 직접 마주하는 사람들 서버(서빙하는 사람)와 관련되어 있다.
요리를 하는 사람인 쉐프는 손님에게 자리를 안내하고 주문을 받을 필요 없이,
주문을 확인하고 요리하여 서버에게 전달하기만 하면 된다.
따라서 손님에게 자리를 안내하고, 주문을 받는 등의 책임은 서버 객체에게 할당하고,
주문을 확인하고 요리를 완성하는 등의 책임은 쉐프 객체에게 할당해야 한다.
그래야 미래의 변경에 쉽게 대처할 수 있고, 코드를 이해하기도 쉬워지기 때문이다.
이렇게 관련된 책임은 한 곳으로 밀집시켜야 한다는 원칙이 단일 책임 원칙 (SRP)이다.
개방-폐쇄 원칙 (Open-Closed Principle)
개방 폐쇄 원칙은 기존 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계해야 한다는 원칙이다.
기능을 추가하는데 어떻게 기존 코드를 변경하지 않을 수 있을까?
힌트를 주자면, 인터페이스나 추상 클래스를 이용해 공통적인 부분을 추상화한다면 가능하다.
실세계의 예를 들어보자.
당신은 서울에 살고 부산에 있는 친구를 만나러 가야 한다.
서울에서 부산까지 가는 방법은 여러가지다.
자동차를 타고 갈 수 있고, 기차나 비행기를 타고 갈 수도 있다.
상황에 따라 어떤 방법이든 선택할 수 있다.
자동차를 타고 부산을 가는 코드를 작성하면 다음과 비슷할 것이다.
class 나 {
void 타고간다(자동차, 부산) {
자동차.간다(부산)
}
}
기차를 타고 싶다면?
"타고간다" 메서드의 "자동차" 타입을 "기차" 타입으로 바꿔줘야 한다.
비행기를 타고 싶다면?
"타고간다" 메서드의 "자동차" 타입을 "비행기" 타입으로 바꿔줘야 한다.
얼마나 귀찮은 일인가.
이렇게 하기 보다는 "자동차" 와 "기차", "비행기"를 추상화시켜
"탈것"이라는 인터페이스 뒤에 은닉(캡슐화)시키면
부산으로 가는 방법이 변경된다고 해도 "타고간다" 라는 메서드는 변경시킬 필요가 없어진다.
외부에서 인자를 전달할때, 부산으로 가는 방법에 맞는 탈것의 객체를 전달하면 되기 때문이다.
코드는 다음과 비슷할 것이다.
public class 메인 {
public static void 메인 () {
지역 붓산 = new 부산()
// 자동차를 타고 갈 경우
탈것 내차 = new 자동차()
나.타고간다(내차, 붓산)
// 비행기를 타고 갈 경우
탈것 부산행기차 = new 기차()
나.타고간다(부산행기차, 붓산)
}
}
class 나 {
// 부산 가는 방법은 외부에서 자유롭게 확장이 가능해졌다.
타고간다(탈것, 부산) {
탈것.간다(부산)
}
}
interface 탈것 {
간다(지역)
}
class 자동차 implements 탈것 {
간다(지역) {
//...
}
}
class 기차 implements 탈것 {
간다(지역) {
//...
}
}
class 비행기 implements 탈것 {
간다(지역) {
//...
}
}
이처럼 기존 코드의 수정에 대해서는 폐쇄적으로,
기능의 확장에는 개방적으로 코드를 작성해야 한다는 원칙이 개방-폐쇄 원칙 (OCP)이다.
리스코프 치환 원칙 (Liskov Substitution Principle)
리스코프 치환 원칙은 자식클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 원칙이다.
자식 클래스는 부모 클래스와 "is kind of" 관계다.
예를 들어 "포유류" 이라는 클래스의 자식 클래스중 하나가 "원숭이" 라면
"원숭이 is kind of 포유류"가 참이어야 한다.
"is kind of" 관계를 확인하는 방법은 간단하다.
참인 문장을 만들고, 단어만 바꿔주면 된다.
- 포유류는 새끼를 낳아 번식한다.
- 포유류는 젖을 먹여서 새끼를 키운다.
- 포유류는 폐를 통해 호흡한다.
"포유류"를 "원숭이"로 대체해보자.
- 원숭이는 새끼를 낳아 번식한다.
- 원숭이는 젖을 먹여서 새끼를 키운다.
- 원숭이는 폐를 통해 호흡한다.
- 모든 문장이 참이다.
"오리너구리" 는 어떨까?
- 오리너구리는 새끼를 낳아 번식한다.
- 오리너구리는 젖을 먹여서 새끼를 키운다.
- 오리너구리는 폐를 통해 호흡한다.
위 문장들은 거짓이다.
객체지향 관점에서 오리너구리와 포유류는 리스코프 치환 원칙을 만족하지 않는다.
리스코프 치환 원칙을 만족하려면 서브클래스가 부모클래스를 대체할 수 있어야 한다.
이렇게 부모클래스와 자식클래스 사이의 행위가 일관성이 있어야 한다는 원칙이 리스코프 치환 원칙이다.
의존 역전 원칙 (Dependency Inversion Principle)
의존 역전 원칙은 의존 관계를 맺을때 변화할 가능성이 높은 것 보다 변화할 가능성이 거의 없는 것에 의존해야 원칙히다.
변화할 가능성이 높은 것의 예로는 구체적인 로직을 담고있는 클래스이고
변화할 가능성이 거의 없는 것의 예로는 추상 클래스, 인터페이스이다.
느꼈을지 모르겠지만, 위에서 설명했던 개방-폐쇄 원칙과 겹치는 부분이 있다.
개방-폐쇄 원칙에서 설명했던 "탈것"과 그것을 구현한 "자동차"와 "기차", "비행기"를 생각해보자.
"나"가 "자동차"와 의존 관계를 맺고 있을때
자동차가 아닌 기차를 타고 부산을 가고 싶다면 "나"의 코드를 수정해야 하는 번거로움이 있었다.
"자동차"는 언제나 "기차"나 "비행기" 로 변경될 수 있는 부분이었기 때문이었다.
개선된 코드에서는 "나"는 "탈것"과 의존 관계를 맺고 있다.
"탈것"은 변화할 가능성이 적기 때문에 변경이 일어나도 "나"의 코드는 수정할 필요가 없었다.
왜냐하면 "간다" 라는 메서드는 구현이 없는 추상 메서드이기 때문이다.
추가를 하자면 여기서 리스코프 치환 원칙도 적용됐다.
"자동차"와 "기차" 그리고 "비행기" 모두 "탈것"과 "is kind of" 관계였기 때문에 변경에 용이한 코드로 개선시킬 수 있었던 것이다.
어쨋든, 이처럼 변경 가능성이 적은 인터페이스와 같은 것과 의존 관계를 맺어야 한다는 원칙이 의존 역전 원칙(DIP)이다.
인터페이스 분리 원칙 (Interface Segregation Principle)
인터페이스 분리 원칙은 인터페이스를 클라이언트의 요구에 맞게 분리해야 한다는 원칙이다.
인터페이스 분리 원칙은 단일 책임 원칙과 겹치는 부분이 있다.
단일 책임 원칙 (SRP) 에서 설명했던 "식당"을 인터페이스로 봐보자.
"식당" 은 "손님" 이라는 클라이언트의 요구와 관련되지 않은 기능들이 있었다.
굉장히 많은 기능들이 모여있었고, 인터페이스가 비대했다.
"주문서를 확인한다" 라던가, "요리한다" 같은 것들이다.
"손님"은 자리를 안내받고, 주문을 하고, 요리를 받기만 하면 되기 때문이었다.
그래서 "식당" 인터페이스는 "서버" 와 "쉐프" 인터페이스로 "손님"의 요구에 맞게 분리시켜야 했다.
이처럼 클라이언트의 요구에 맞게 인터페이스를 분리해야 한다는 원칙이 인터페이스 분리 원칙(ISP)이다.
하지만 단일 책임 원칙을 만족시키는 것이 항상 인터페이스 분리 원칙을 만족시키는 것은 아니다.
"게시판" 이라는 클래스에 글쓰기, 읽기, 수정, 삭제를 위한 메서드만 있다면 이는 단일 책임 원칙을 만족시킨 것이다.
관련된 기능들만 적절한 곳에 밀집시켰기 때문이다.
하지만 클라이언트에 따라 "읽기" 만 가능할 수도 있다.
이런 상황에서는 인터페이스 분리 원칙을 만족하지 못한다.
결론
지금까지 SRP, OCP, LSP, DIP, ISP 까지 모두 알아봤다.
객체지향 프로그래밍 언어로 프로그램을 만든다면 지켜야할 원칙들이고 그렇게 해야할 근거가 타당해보인다.
사실 이 원칙들을 하나하나 의식하면서 코드를 작성하기는 거의 불가능에 가까울 것 같지만
이 원칙들이 추구하는 공통의 목표인 "변경에 용이하게 코딩하는 것"을 의식하며 코드를 작성하다 보면
이 원칙들이 자연스레 지켜지고 코드의 품질도 개선시킬 수 있을 것 같다.
'도서 > JAVA 객체지향 디자인패턴' 카테고리의 다른 글
[GOF] 스테이트 패턴으로 슈팅게임 만들기 🔫 (0) | 2022.02.05 |
---|---|
[GOF] 전략 패턴으로 비행기 게임 만들기 🛫🛬 (0) | 2022.02.04 |
디자인 패턴이 뭐냐 🤔 (0) | 2022.02.03 |
클래스 다이어그램의 집합관계 (0) | 2022.02.01 |
연관 관계와 의존 관계의 차이 (5) | 2022.02.01 |