ABOUT ME

먹는걸 좋아하고 다양한 일을 하며 살고 싶어하는 파워J 개발자 블로그

Today
Yesterday
Total
  • 객체지향 (LID)
    Engineering WIKI/Docs 2021. 1. 17. 17:24

    2023.05.27 - [Engineering WIKI/Docs] - SOLID 5가지 설계 원칙

    2021.01.17 - [Engineering WIKI/Docs] - 객체지향 (LID)


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

    • 자식 클래스는 부모 클래스에서 가능한 행위를 수행할 수 있어야 한다.
      • 자식 클래스가 부모 클래스의 기본 의미를 해치지 않는지!
    • 부모클래스와 자식클래스 사이의 행위에는 일관성이 있어야 한다.
      • (부모 클래스의 인스턴스 대신 자식 클래스의 인스터스를 사용해도 문제가 없어야 한다는 것을 의미한다.)
    • 상속 관계에서는 일반화 관계(IS - A)가 성립해야 한다. 일반화 관계에 있다는 것은 일관성이 있다는 것이다. 따라서 리스코프 치환 원칙은 일반화 관계에 대해 묻는 것 이다.
      • Ex) 도형 클래스와 사각형 클래스가 있고, 사각형 클래스는 도형 클래스의 상속을 받는다고 가정하자.
        • (1) 도형은 둘레를 가지고 있다.
        • (2) 도형은 넓이를 가지고 있다.
        • (3) 도형은 각을 가지고 있다.
        • 일반화 관계(일관성인지 확인하는 방법은 단어를 교체해 보면 알 수 있다.  (1) ~ (3)의 도형이란 단어 대신 사각형을 넣어보자.
        • (1) 사각형은 둘레를 가지고 있다.
        • (2) 사각형은 넓이를 가지고 있다.
        • (3) 사각형은 각을 가지고 있다.
        • (1) ~ (3) 모두 딱히 이상한 부분이 보이지 않는다. 따라서 도형과 사각형 사이에는 일관성이 있다고 할 수 있다.
        • 여기서 원(Circle) 이라는 도형에 대해 생각해보자. 원 클래스 역시 도형 클래스의 상속을 받는다고 가정하자. 앞에서 언급한 (1) ~ (3)의 도형 단어 대신 원을 대입해보자.
        • (1) 원은 둘레를 가지고 있다.
        • (2) 원은 넓이를 가지고 있다.
        • (3) 원은 각을 가지고 있다.
        • 문장을 읽어보면 (3)번 문장이 어색하다는 것을 알 수 있다. 따라서 도형 클래스는 LSP을 만족하지 않은 설계라 할 수 있다. 따라서 (3)문장에 대해서는 일반화 관계가 성립하도록 수정되어야 한다.

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

    • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스 보다는 여러개의 구체적인 인터페이스가 낫다.
    • 이는 다시 말해서, 자신이 사용하지 않는 기능(인터페이스)에는 영향을 받지 말아야 한다.
    • 인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.
    • SRP는 객체의 단일 책임을 뜻한다면, ISP는 인터페이스의 단일 책임을 의미한다고 보면 됩니다.

    • ISP를 만족하려면 Phone 인터페이스에 call(), sms(), alarm(), calculator() 함수를 모두 정의하는 것보다, Call, Sms, Alarm, Calculator 인터페이스를 각각 정의하여, 3G폰과 스마트폰 클래스에서 4개의 인터페이스를 구현하도록 설계되어야 합니다.

    • 이렇게 설계를 하면, 각 인터페이스의 메서드들이 서로 영향을 미치지 않게 됩니다. 즉, 자신이 사용하지 않는 메서드에 대해서 영향력이 줄어들게 됩니다.

     

    • Bad Code
    interface SmartPrinter {
      print();
      fax();
      scan();
    }
    
    class AllInOnePrinter implements SmartPrinter {
      print() {
        // ...
      }
    
      fax() {
        // ...
      }
    
      scan() {
        // ...
      }
    }
    
    class EconomicPrinter implements SmartPrinter {
      print() {
        // ...
      }
    
      fax() {
        throw new Error('Fax not supported.');
      }
    
      scan() {
        throw new Error('Scan not supported.');
      }
    }

     

    • Good Code
    interface Printer {
      print();
    }
    
    interface Fax {
      fax();
    }
    
    interface Scanner {
      scan();
    }
    
    class AllInOnePrinter implements Printer, Fax, Scanner {
      print() {
        // ...
      }
    
      fax() {
        // ...
      }
    
      scan() {
        // ...
      }
    }
    
    class EconomicPrinter implements Printer {
      print() {
        // ...
      }
    }

     


     

    Dependency Inversion Principle (의존 역전 원칙)

    • 의존 관계를 맺을 떄, 변화하기 쉬운것 보단 변화하기 어려운 것에 의존해야 한다는 원칙.
    • 저수준 모듈이 고수준 모듈에 의존하게 되는 것
      • Ex) 계층구조 예시
        • 표현 계층 : 사용자의 요청을 받아 응용 영역에 전달함과 동시에 처리 결과를 사용자에게 표시
        • 응용 영역 : 사용자에게 제공해야 할 기능 구현
        • 도메인 영역 : 도메인의 핵심 로직 & 도메인 모델 구현
        • 인프라 계층 : 구현 기술을 다룸 (ex. DB 연동)
        • 각 계층은 위와 같은 특징을 가지고 있으며, 상위 계층은 하위 계층에게 의존하지만, 하위 계층은 상위 계층에 의존하지 않습니다. 하지만 그렇게 되면 인프라 계층에게 종속적인 현상이 많이 일어나게 됩니다.
    • 여기서 말하는 변화하기 쉬운 것 이란 구체적인 것을 말하고, 변화하기 어려운 것이란 추상적인것을 말한다.
      • 변화하기 쉽고 구체적인 것 → 클래스
      • 변화하기 어려운 것 → 인터페이스, 추상 클래스

    • Client 객체는 Cat, Dog, Bird의 crying() 메서드에 직접 접근하지 않고, Animal 인터페이스의 crying() 메서드를 호출함으로써 DIP를 만족할 수 있습니다.

     

    • 기존 코드의 문제
    /* A사의 알람 서비스 */
    public class A {
      public String beep() {
        return "beep!";
      }
    }
    /* 서비스 코드*/
    public class AlarmService {
    
      private A alarm;
    
      public String beep() {
        return alarm.beep();
      }
    
    }
    • 문제점 : 테스트의 어려움
      • 위 코드에서 AlarmService만 온전히 테스트할 수 없습니다. 인프라 계층에 속하는 A가 완벽하게 동작해야만 AlarmService를 테스트할 수 있습니다.
    • 문제점 : 확장 및 변경이 어려움
      • 만약 알람 서비스에 B사가 추가된다면 어떻게 될까요? 아래와 같이 서비스 코드를 변경해야합니다.
    /* B사의 알림 서비스 */
    public class B {
      public String beep() {
        return "beep";
      }
    }
    /* 서비스 코드 */
    public class AlarmService {
    
    	private A alarmA;
      	private B alarmB;
    
      	public String beep(String company) {
            if (company.equals("A")) {
                return alarmA.beep();
            } else {
                return alarmB.beep();
            }
        }
    }
    • 해결책 : 방금 설명한 기능을 고수준 모듈과 저수준 모듈로 분리하면 아래와 같습니다.
      • 고수준 모듈 : 알림
      • 저수준 모듈 : A사의 알림 서비스, B사의 알림 서비스
      • 지금까지 사용한 방법은 고수준 모듈이 저수준 모듈에 의존하는 방법이지만, DIP를 적용하게 되면 저수준 모듈이 고수준 모듈에 의존해야 합니다. 그러기 위해 사용하는 것이 추상 타입(ex. 인터페이스, 추상 클래스)입니다.
    public interface Alarm {
        String beep();
    }
    • 저수준 모듈이 상속 받을 Alarm 인터페이스를 만들어줍니다.
    /* A사의 알람 서비스 */
    public class A implements Alarm {
        @Override
        public String beep() {
        	return "beep!";
        }
    }
    /* B사의 알림 서비스 */
    public class B implements Alarm {
        @Override
        public String beep() {
        	return "beep";
        }
    }
    • 그 후에 저수준 모듈들이 Alarm을 구현하게 하면 됩니다. 그렇게 되면 서비스 코드를 아래와 같이 변경할 수 있습니다.
    /* 서비스 코드 */
    public class AlarmService {
        private Alarm alarm;
        
        public AlarmService(Alarm alarm) {
        	this.alarm = alarm;
        }
        
        public String beep() {
        	return alarm.beep();
        }
    }
    • 더 이상 AlarmService는 알람 서비스가 추가된다고 코드를 변경하거나 추가해야 하는 일이 없어집니다.
    • 또한, 알람 관련 객체는 무조건 인터페이스인 Alarm에 의존하기 때문에 Alarm을 Mock 객체로 만들어 다양한 시나리오로 AlarmService 기능을 온전히 테스트할 수 있다는 장점도 가져갈 수 있습니다.
Designed by Tistory.