[개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴] 설계 원칙: SOLID

·2022년 10월 27일
0
post-thumbnail
  • SOLID
    1. 단일 책임 원칙 (Single responsibility principle; SRP)
    2. 개방-폐쇄 원칙 (Open-closed principle; OCP)
    3. 리스코프 치환 원칙 (Liskov substitution principle; LSP)
    4. 인터페이스 분리 원칙 (Interface segregation principle; ISP)
    5. 의존 역전 원칙 (Dependency inversion principle; DIP)

[1] 단일 책임 원칙 (Single responsibility principle; SRP)

💡 클래스는 단 한 개의 책임을 가져야 한다.

단일 책임 원칙 위반이 불러오는 문제점

  • 코드를 절차 지향적으로 만들어 변경을 어렵게함
    • 한 책임의 변화가 다른 책임의 코드에 연쇄적으로 영향을 줌
  • 재사용을 어렵게함
    • 실제 사용하지 않는 기능이 의존하는 패키지까지 필요로함

책임이란 변화에 대한 것

  • 책임의 단위는 변화되는 부분과 관련된다
  • 단일 책임의 원칙을 지키는 방법
    • 메서드를 실행하는 것이 누구인지 확인해본다

[2] 개방 폐쇄 원칙 (Open-closed principle; OCP)

💡 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

이 말은 다음과 같다

  1. 기능을 변경하거나 확장할 수 있으면서
  2. 그 기능을 사용하는 코드는 수정하지 않는다.
  • 앞선 장에서의 FlowController / ByteSource 예시는 개방 폐쇄 원칙을 지킨다

    • 메모리에서 byte를 읽어 오는 기능을 추가해야 할 경우
      1. ByteSource 인터페이스를 상속받은 MemoryByteSource 클래스를 구현함으로써 기능 추가가 가능하다.
      2. 이 기능을 사용할 FlowController 클래스의 코드는 변경되지 않는다.
  • 개방 폐쇄 원칙을 구현하는 방법

    • 확장되는 부분(즉, 변화되는 부분)을 추상화해서 표현
    • 상속을 이용 (상위 클래스 기능 그대로 사용하면서 하위 클래스에서 일부 구현을 오버라이딩)
      public class ResponseSender {
      		...
      		public void send() {
      				sendHeader();
      				sendBody();
      		}
      		protected void sendHeader() {
      				// 헤더 데이터 전송
      		}
      		protected void sendBody() {
      				// 텍스트로 데이터 전송
      		}
      }
      public class ZippedResponseSender extends ResponseSender {
      		public ZippedResponseSender(Data data) {
      				super(data);
      		}
      		@Overide
      		protected void sendBody() {
      				// 데이터 압축 처리
      		}
      }
      ZippedResponseSender 클래스로 기존 기능에 압축 기능을 추가하면서, ResponseSender의 클래스 코드는 바뀌지 않았다.

개방 폐쇄 원칙이 깨질 때의 주요 증상

  • 다운 캐스팅을 한다
    • instanceof와 같은 타입 확인 연산자가 사용된다면 해당 코드는 개방 폐쇄 원칙을 지키지 않을 가능성이 높다
  • 비슷한 if-else 블록이 존재한다.
    • 추상화를 통해 해결하자

개방 폐쇄 원칙은 유연함에 대한 것

  • 개방 폐쇄 원칙은 변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 해준다

[3] 리스코프 치환 원칙 (Liskov substitution principle; LSP)

  • 개방 폐쇄 원칙을 받쳐주는 다형성에 관한 원칙 제공

💡 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다

public void someMethod(SuperClass sc) {
		sc.someMethod();
}

...

someMethod( new SubClass() );

이 때, someMethod는 상위 타입인 SuperClass 타입의 객체를 사용하는데, 하위 타입 SubClass 객체를 전달해도 someMethod()가 정상적으로 동작해야한다.

리스코프 치환 원칙을 지키지 않을 때의 문제

  • 리스코프 치환을 어기는 예시
    • 직사각형-정사각형 문제 https://en.wikipedia.org/wiki/Circle–ellipse_problem
      public void increaseHeight(Rectangle rec) {
      		if (rec instanceof Square)
      				throw new CantSupportSquareException();
      		if (rec.getHeight() <= rec.getWidth()) {
      				rec.setHeight(rec.getWidth() + 10);
      		}
      }
      • 개념적으로 상속 관계에 있는 것처럼 보일지라도 실제 구현에서는 상속 관계가 아닐 수도 있다
      • Square 클래스는 Rectangle 클래스를 상속받아 구현하기 보다 별개의 타입으로 구현해주어야 한다.
    • 상위 타입에서 지정한 리턴 값의 범위에 해당되지 않는 값을 리턴하는 것

리스코프 치환 원칙은 계약과 확장에 대한 것

  • 하위 타입은 상위 타입에서 정의한 명세를 벗어나지 않는 범위에서 구현해야 한다

[4] 인터페이스 분리 원칙 (Interface segregation principle; ISP)

💡 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

즉, 클라이언트는 자신이 사용하는 메서드에만 의존해야 한다.

  • 인터페이스를 각 클라이언트가 필요로 하는 인터페이스들로 분리함으로써, 각 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 만들어야 한다.
    • ex) 기존의 ArticleService → ArticleWriteService / ArticleListService / ArticleDeleteService
      • 게시글 쓰기 UI → ArticleWriteService
      • 게시글 목록 UI → ArticleListService
      • 게시글 삭제 UI → ArticleDeleteService
  • 인터페이스 분리 원칙 → 단일 책임 원칙 : 인터페이스와 클래스의 재사용성 UP

[5] 의존 역전 원칙 (Dependency inversion principle; DIP)

💡 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다.
저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

  • 고수준 모듈: 어떤 의미 있는 단일 기능을 제공하는 모듈
    • ex) 바이트 데이터를 읽어와 암호화하고 결과 바이트 데이터를 쓴다.
  • 저수준 모듈: 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
    • ex) 파일에서 바이트 데이터를 읽어온다.
    • AES 알고리즘으로 암호화한다.
    • 파일에 바이트 데이터를 쓴다.
  • 쉽게 말해, 구현 클래스에 의존하지 말고 인터페이스에 의존하라.
    • 즉, 역할에 의존해야 한다.

고수준 모듈이 저수준 모듈에 의존할 때의 문제

  • 프로그램이 어느정도 안정화되면서, 큰 틀에서 프로그램이 변경되기 보다는 상세 수준에서의 변경이 발생할 가능성 UP
  • 고수준 모듈이 저수준 모듈에 의존하면, 상세 수준의 구현이 추가되거나 변경될 때 마다 고수준 모듈이 변경된다.
  • 저수준 모듈이 변경되더라도 고수준 모듈은 변경되지 않아야함! 이것이 바로 DIP

의존 역전 원칙을 통한 변경의 유연함 확보

  • 저수준 모듈이 고수준 모듈을 의존하게 만드는 방법: 추상화
  • 추상타입 즉, 인터페이스에 의존하도록 하라!
    • 앞선 예시에서 ByteSource에 의존하듯..

소스 코드 의존과 런타임 의존

  • 소스 코드 상에서 의존은 역전되었지만, 런타임에서 의존은 고수준 모듈의 객체에서 저수준 모듈의 객체로 향함.
  • DIP는 소스코드의 의존을 역전시킴으로써 변경의 유연함을 확보할 수 있도록 만들어줄 뿐, 런타임에서 의존을 역전시키는 것은 아니다.

0개의 댓글