오랜만에 신규 프로젝트를 생성하여 개발하고 있는데, 설계단에서 조금 어려움을 겪고 있다. 기존 프로젝트보다 더 나은 구현을 해보고 싶은데 생각보다 손이 나가질 않고 있다. 그래서 '개발자가 반드시 정복해야 할 객체 지향과 디자인패턴' 책을 펼쳤고, 책 내용 중 SOLID 부분을 다시금 정리하고 가보려 한다. 

 

 

단일책임원칙 (Single Responsibility Principle)

  • 클래스는 단 한 개의 책임을 가져야 한다. 
  • 예를 들면, 
    • 어떤 클래스에 HttpClinet() 클래스에서 데이터를 로드하는 메소드, 로드된 데이터로 파싱하는 메소드가 있다고 치자.
    • 그런데 HTTP 프로토콜에서 소켓 시반의 프로토콜로 변경되었다.
    • 그렇다면 데이터를 로드하는 메소드, 그리고 파싱하는 메소드 두개 다 변경을 해야 하는 상황인 것이다
    • 이러한 연쇄적은 코드 수정은 두 개의 책임이 한 클래스에 있기 떄문이라고 볼 수 있다. 
    • 데이터를 읽는 것과 데이터를 파싱해서 화면에 보여주는 책임을 분리해야 한다. 
  • 단일 책임 원칙을 잘 지키려면? 
    • 메서드를 실행하는 것이 누구인지 확인해 보는 것 
    • 어떤 클래스에 두개의 메서드가 있는데 각각의 메서드가 A,B 클래스 즉 2개의 클래스에서 사용되는 것이라면 책임 분리 후보가 될 수 있다.

 

개방폐쇄원칙 (Open-closed Principle)

  • 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.  
    • 기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않는다. 
  • 한 인터페이스를 사용하는 클래스는 인터페이스를 구현한 클래스가 추가되더라도 변경되지 않을 것이다. 

 

리스코프 치환 원칙 (Liskov Substitution Principle)

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다. 
  • 리스코프 치환 원칙이 지켜지지 않은 대표적인 예  
    •  이런 코드가 있는데 특수 Item은 무조건 할인을 해주지 않는 정책이 추가되었다고 하자. 이를 반영하기 위해 Coupon 클래스를 아래와 같이 수정할 수 있을 것이다.
    • public class Coupon { public int claculateDiscountAmount(Item item) { return item.getPrice(). * discountRate; } }
    •  위 코드는 아주 흔한 리스코프 치환 원칙 위반 사례이다. Item 타입을 사용하는 코드는 SpecialItem 타입이 존재하는지 알 필요 없이 오직 Item 탗입만 사용해야 하는데 SpecialItem 타입인지의 여부를 확인하고 있다는 것은 SpecialItem이 상위 타입인 Item을 완벽하게 대체하지 못하는 상황이라고 볼 수 있는 것이다. 
    • public class Coupon { public int calculateDiscoutnAmount(Item item) { if (item instanceof SpecialItem) // LSP 위반 발생 return 0; return item.getPrice() * discountRate; } }
    • 타입을 확인하는 기능 (instanceof연산자 같은..)을 사용하는 것은 전형적인 리스코프 치환 언칙을 위반할 때 발생하는 증상이다. 새로운 종류의 하위 타입이 생길 때마다 상위 타입을 사용하는 코드를 수정해줘야 할 가능성을 높이는 것은 개방 폐쇄 원칙을 지킬 수도 없게 하는 것이다. 
    • public class Item { 
      	public boolean isDiscountAbailable() {
          	return true;
          }
      }
      
      public class SpecialItem extends Item {
      	@Override
          public boolean isDiscountAbailable() {
          	return false;
          }
      }
      Item 클래스에 가격 할인 가능 여부를 판단하는 기능을 추가하고, SpecialItem 클래스는 이 기능을 알맞게 재정의 했다. 이렇게 함으로써 Item 클래스만 사용하도록 구현할 수 있게 되었다.
    • public class Coupon {
      	public int calculateDiscountAmount(Item item) {
          	if (!item.isDiscountAvailable()) // instanceof 연산자 사용 제거 
              	return 0;
                  
              return item.getPrice() * discountRate;
          }
      }
      리스코프 치환 원칙이 지켜지지 않으면 쿠폰 예제에서 봤듯이 개방 폐쇄 원칙을 지킬 수 없게 된다. 개방 폐쇄 원칙을 지키지 않으면 기능 확장을 위해 더 많은 부분을 수정해야 하므로, 리스코프 치환 원칙을 지키지 않으면 기능을 확장하기가 어렵게 된다.

 

인터페이스 분리 원칙 (Interface Segregation Priciple) 

  • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다. 
    • 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙으로 말할 수 있다.
    • 예를 들어
      • AServiceInterface에 읽기, 쓰기, 삭제가 구현되어 있다고 치자. 그런데 읽기 부분에 변경이 발생했다고 치면 쓰기/삭제 등 변경이 필요 없는 소스 코드도 다시 컴파일해야 하는 경우가 생기는 것이다. 이럴 때에는 쓰기/읽기/삭제를 각각의 인터페이스들로 분리함으로써 각 클라이언트가 사용하지 않는 인터페이스에는 변경이 발생하더라도 영향을 받지 않도록 해야 한다. 
      • 자바의 경우 사용하지 않는 인터페이스 변경에 의해 발생하는 소스재컴파일 문제가 발생하진 않지만 인터페이스 분리 원칙은 재컴파일 문제만 관련된 것이 아니라 용도에 맞게 인터페이스를 분리하는 것, 즉 단일 책임 원칙과도 연결된다.

 

의존 역전 원칙 (Dependency Inversion Priciple)

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
  • 저수준 모듈이 변경되더라도 고수준 모듈은 변경되지 않는 것! 
    • 고수준 모듈 : 어떤 의미 있는 단일 기능을 제공하는 모듈
    • 저수준 모듈 : 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현 
    • 예를 들어, 
      • 1)
        • 암호화 예의 경우 바이트 데이터를 암호화한다는 것이 이 프로그램의 의미 있는 단일 기능으로서 고수준 모듈에 해당된다. 고수준 모듈은 데이터 읽기, 암호화, 데이터 쓰기라는 하위 기능으로 구성되는데, 저수준 모듈은 이 하위 기능을 실제로 어떻게 구현할지에 대한 내용을 다룬다.
      • 2) 
        • '쿠폰을 적용해서 가격 할인을 받을 수 있다.' '쿠폰은 동시에 한 개만 적용 가능하다' --> 고수준
        • '금액 할인 쿠폰', '비율할인쿠폰' 등 다영한 쿠폰이 존재 --> 저수준 
        • 쿠폰을 이용한 가격 계산 모듈이 개별적인 쿠폰 구현에 의존하게 되면 새로운 쿠폰이 추가되거나 변경될 때마다, 가격 계산 모듈이 변경되는 상황이 초래된다. 
  • 의존 역전 원칙은 앞서 리스코프 치환 원칙과 함께 개방 폐쇄 원칙을 따르는 설계를 만들어 주는 기반이 된다. 

 

 

출처 

책 - 개발자가 반드시 정복해야 할 객체 지향과 디자인패턴 (최범균 지음)

+ Recent posts