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라는 클래스를 새로 만듦
  • 그런데 어느 날 스펙 일부가 바뀜
  1. A를 고쳐 사용
  2. 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 정신이 그때부터 도움이 될 수 있음

필요하면 사용한다

  • 필요 없는 걸 굳이 하려는 사람은 민폐
  • 성능이 불필요한 곳에서 모든 걸 기계어로 작성하는 사람도 민폐
  • 뭔가 새롭도 대단해 보이는 것은 필요할 때만 사용하라는 이야기