[개체지향] SOLID 설계 정신(원칙)
SOLID 설계 정신(원칙)
- Single-responsibility principle
- Open closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
SOLID 정신의 목표 : 소프트웨어 설계를 이해하기 쉽고, 유연하고, 유지보수가 쉽게 만들기 위해 나온 원칙
굳이 정신이라 한 이유
- 원칙도 규칙도 아닌 방향성을 제시할 뿐
- 알아두면 좋은 정도의 주관적인 내용
- 이미 기존에 존재하던 베스트 프랙티스를 이름만 바꾼 것도 있음
- 심지어 좀 극단적인 주장을 한 것도 있음
정확히 어느 경우에 어떻게 적용하는지를 설명하지 않음
- 도움되는 점도 있음
- 개체 모델링할때 이 가이드를 한 번씩 생각하면 좀 더 모듈화가 잘 됨
- 모든 코드를 맞춰서 작성하면 유지보수가 힘든 코드가 됨
SOLID 정신에서 유일하게 맞는 이야기
- 설계가 유연해짐 추상적인 설계로 커플링을 제거할 수 있음
- 이해하기 쉽고 : 일수도있고 아닐수도있음
- 유지보수가 쉽다 : 일수도있고 아닐수도있음
- SOLID는 디커플링을 중요하게 여기니 대규모 프로젝트일수록 유용
- 따라서 모든 프로젝트에 적용할 수 있다 생각치 말것
- 직접적 구체적인 게 더 이해하기 쉽다는 사실을 잊지 말것
- 많은 프로젝트의 시작은 직접적 구체적인 설계
- 규모가 커지면서 유연성이 필요 해지면 그때부터 바꿔갈 때 도움이 될 정신
과거에는 SOLID 정신이 조금더 말이 되는 이유
- 모든걸 외주로 개발하던 시절
- 외주 프로세스
- 계약시 스펙에 따라 금액 산정
- 따라서 처음부터 스펙을 잘 준비하고 그 후 변경이 별로 없음
- 개발은 주로 계약시 합의한 스펙대로(재사용성이 높은대로 설계가능)
- 완성후 납품
- 자체 개발팀의 프로세스
- 스펙에 많은 시간을 퍼붓지 않음 그시간에 첫 버전을 만듦
- 필요에 따라 요구사항을 고치고 그에 따라 종종 기능도 바뀜
- 툭하면 요구사항이 바뀌기에 재사용성 높은 설계가 힘듦
SOLID 원칙(정신)
- SPR : 단일 책임 원칙 Single-responsibility principle
- OCP : 개방 폐쇄 원칙 Open closed principle
- LSP : 리스코프 치환 원칙 Liskov substitution principle
- ISP : 인터페이스 분리 원칙Interface segregation principle
- DIP : 의존 역전 원칙 Dependency inversion principle
SPR : 단일 책임 원칙 Single-responsibility principle
- Only do one thing
- 한 함수에서 너무 많은 일을 하지 말라는 말과 비슷
- 그냥 클래스 하나에서 너무 많은 일을 하지 말라는 이야기
- 기본적으로 좋은 이야기이자 좋은 정신
- One reason to change
- Only do one thing
클래스의 존재 이유는 하나여야만 한다는 의미
단일책임정신에 따라 클래스를 설계해보아라
- 하나란 의미가 정확히 뭘 의미하는지 주관적임
- 단일 책임 정신에 대한 가장 큰 비판
- 극단적인 주장이 클래스에 있는 함수가 4개를 넘으면 안 된다 실제로 진영을 주장하는 사람 이유는 내가 이해하기 힘들기 때문
단일 책임 개념의 의의
` 이 코드를 보는 대부분의 사람이 이해할 수 있는 크기로 클래스를 만들자`
- 물론 주관적
- 객관적으로 만들겠다고 메서드 몇 개 따위의 규칙을 정하지 말자
- 포프가 만들어 낸 정의(해석)
- 따라서 대부분의 사람이 이해할 수 있는 크기를 어느정도로 봐야하는지를 따져야함
- 주관적인 부분이라 팀과 동료에 따라 달라짐 대부분 정규분포를 따름
- 이 정신을 어겼음을 보여주는 객관적 지표 한가지
- class이름에 and가 들어가는 경우
- 메서드 이름에도 적용되는 이야기
아인슈타인의 명언
Everything should be made as simple as possible, but no simpler
One reason to change
- 클래스가 바꿀 상황이 생긴다면 바꾸려는 이유가 하나여야한다.
- 말도안되는 소리
- 그 이유가 뭐가 될지 어떻게 알지
-
바꾸려는 이유가 몇 개가 될지 예측하기 어려움
- 각 클래스의 책임을 분명하게 정의
- 각 함수의 책임을 분명하게 정의하자고 말했던 것과 마찬가지
- 이러면 어떤 오류 상황이 있을 때 대응이 쉽다.
- 그 오류를 검사하고 처리해야 하는 곳이 명확함
- 앞에서 말했던 입력값 검증이좋은예
OCP : 개방 폐쇄 원칙 Open closed principle
Open for extension Closed for modification
- 클래스 내부 수정 없이 동작을 확장할 수 있어야 한다는 의미
- 상속이 그 좋은 예
개방 폐쇄 정신에 어긋나는 경우
- if switch문을 사용하여 기존 클래스를 변경
이 정신을 지키며 확장하는 법
- 상속 + 다형성
- 단일 책임 정신도 더불어 이룰 가능성이 높음
극단적인 주장
` 한번 만든 클래스는 절대로 바꾸면 안 됨`
- 안 바꾸면 좋은 점은 있음
- 이 클래스를 사용하는 코드가 망가지진 않음
- 이 정신의 방향성 자체는 훌륭
- 클래스를 바꿔야 할 상황도 충분히 많음
언젠가는 다 뜯어 고쳐야 한다
-
기존코드를 고쳐서 breaking change(호환성 문제가 발생할 수 있는 수정사항)를 만드는게 유지보수가 용이
-
부품수가 많아질수록 고잘 날 수 있는 곳이 많아짐 유지보수가 힘들어짐
스펙이 바뀌는 경우
- 스펙에 맞게 A라는 클래스를 새로 만듦
- 그런데 어느 날 스펙 일부가 바뀜
- A를 고쳐 사용
- AA를 새로 만듦
A를 고쳐사용
- 상식적인 사람들의 선택
- 더이상 기존 A를 사용할 일이 없음
A를 둔채 AA를 새로 만듦
- 이사모의 선택
- 혹시라도 예전 스펙으로 되돌아가도 안점
- AA를 따로 만들었기에 A가 이미 존재
사용 안하는 옛 A를 유지할 필요가 없다.
- 예전 버전으로 되돌리는 건 버전 관리 시스템(git)의 책임
- 모든 버전이 다 저장되어 있으니 클릭 한 번으로 되돌릴 수 있음
- 사용 안하는 코드가 남아있는 것이 유지보스에 더 힘듦
LSP : 리스코프 치환 원칙 Liskov substitution principle
1. 부모 클래스의 개체를 사용하는 코드 A가 있음
2. 나중에 자식 클래스의 개체를 거기에 대신 사용함(치환)
3. 이때 A가 아무 문제없이 작동해야한다.
- 즉 부모가 할 수 있었던 일은 자식도 다 할 수 있어야 함
- 당연히 지키면 좋은 정신
- 안 지키면 호출자 코드가 어느 순간 작동 안 할 수 있음
- 선언된 자료형만 보면 실제 개체가 뭔지 모르기 때문에
100% 이렇게 되지는 않음
- 모든 자식을 알기 전에 부모를 완벽히 추상화하기 어려움
- 자식을 추가할 때 부모의 동작을 바꾸는 일이 꽤 있음
추상화란
- 일반적인 부모부터 만든 뒤, 자식을 하나씩 만드는 과정이 아님
- 자식을 먼저 만든 후, 자식 간의 공통점을 찾아 추상 클래스를 만듦
- 자식을 추가하면, 또 다른 공통점을 찾아 추상 클래스를 수정 이런 과정을 여러 번 반복하는 것
리스코프 치환과 직사각형/정사각형
- 리스코프 치환정신을 극단적으로 따르는 사람들은 이렇게 주장
직사각형은 정사각형의 부모가 될 수 없다.
정사각형은 직사각형을 상속할 수 있는가?
- 찬성(다수)
- 기하학적 이유
- 정사각형은 직사각형 중 너비==높이
- 모든 동작이 직사각형과 동일
- 반대
- setWidth/Height 호출이 오작동
- 정사각형을 직사각형 대신 사용 불가능
- 리스코프 치환 정신 위배
소수설의 주장이 모순인 이유
- 정사각형이 직사각형의 자식인 게 리스코프 정신에 어긋나는 경우
- 생성 후 setter로 너비나 높이를 바꿀 때
- 그런데 이 진영에서 꽤 강하게 주장하는 다른 주장과 모순
- 일단 생성된 개체는 상태는 절대 바꿀 수 없어야 한다.
- 개체의 상태를 외부에서 마음대로 바꾸는 건 OO가 아니라면서
setter가 없으면 리스코프 치환 정신을 만족
- 구체 클래스의 직사각형 또는 정사각형을 만듦
- 생성자에서 받는 매개변수에서 그 차이를 명백히 알 수 있음
- setter가 없으면 생성 후 너비, 높이를 바꿀 수 없음
- 이제 정사각형을 직사각형 대신 사용해도 모든 동작이 제대로 작동
- 사실 setter를 동작이라 보기도 힘듦
- 리스코프 치환 정신도 만족하고 올바른 OO의 원칙을 따르고 있음
ISP : 인터페이스 분리 원칙Interface segregation principle
큰 인터페이스가 몇 개 있는 것 보단 작은 인터페이스가 많이 있는 게 좋다
- IMammal하나에 eat(), makeNoise()메서드를 다 넣는 대신 IEat, IMakeNoise로 분리하자는 이야기
- 극단적으로 가져가면 이상한 이야기들이 나옴
- 구형이 하나만 있어도 인터페이스를 만들어라
- 1메서드당 1인터페이스로 해야한다
밸런스
- 결국 인터페이스에 메서드 하나면 C의 함수 포인터
- 무조건 이렇게 해야 했다면 모든 걸 함수 포인터로 만드는 게 맞음
- 따라서 역시 중간 어디에선가 밸런스를 맞춰야 할 문제
DIP : 의존 역전 원칙 Dependency inversion principle
- 개체끼리 통신할 때 구체적인 것 말고 추상적인 것에 의존하란 이야기
- 구체적인 것에 의존하면 좋은 상황들을 이미 충분히 봤음
정리: 소프트웨어 품질
- 소프트웨어 품질의 시작은 개발자의 실수를 줄이는 환경 구축
- 모두가 이해하기 쉬운 코드를 작성할 것
- 언제부터 디커플링을 고려해야 하는가?
- 협업 환경에서 잦은 변경으로 서로 일 못하는 상황이 생길 때
- SOLID 정신이 그때부터 도움이 될 수 있음
필요하면 사용한다
- 필요 없는 걸 굳이 하려는 사람은 민폐
- 성능이 불필요한 곳에서 모든 걸 기계어로 작성하는 사람도 민폐
- 뭔가 새롭도 대단해 보이는 것은 필요할 때만 사용하라는 이야기