CS/언어

[JAVA] 객체지향설계 - 5대 원칙 (SOLID)

pythaac 2022. 6. 26. 22:40

https://khalilstemmler.com/articles/solid-principles/solid-typescript/#Liskov-Substition-Principle

 

SOLID Principles: The Software Developer's Framework to Robust & Maintainable Code [with Examples] | Khalil Stemmler

The SOLID principles are a set of software design principles that teach us how we can structure our functions and classes to be as robust, maintainable and flexible as possible.

khalilstemmler.com

 

SOLID 정의

  • SOLID
    • S: Single Responsibility Principle
    • O: Open-Closed Principle
    • L: Liskov-Substitution Principle
    • I: Interface Segregation Principle
    • D : Dependency Inversion Principle
  • 정의
    • 기능/클래스 구성을 안전하고, 유지보수에 좋고, 유연하게 할 수 있는 소프트웨어 설계 원칙
  • 배경
    • 과거에 작성된 코드가 현재 needs에 맞지 않을 경우, 코드 변경은 cost가 큼
    • 따라서, 우리는 코드를 변경할 수 있도록 작성해야함
  • 배우는 점
    • Test 가능한 코드 작성
    • 이해하기 쉬운 코드 작성
    • 찾기 쉬운 코드 작성
    • 의도한 동작만 하는 코드 작성
    • 빠르게 수정하고 확장하는 코드 작성 (+버그 없이)
    • Policy(Rule)와 Detail(Implementation)이 분리된 코드 작성
    • Swap이 가능한 코드 작성

 

Single Responsibility Principle (단일 책임 원칙)

Class 또는 function은 변경시 단 한가지 이유만 존재해야한다
  • 나쁜 예제
    • 3개 부서의 Employee가 존재
      - HR
      - Accounting
      - IT
    • SRP violation 예시
      - Employee 클래스의 각 메서드에서 HR / Accounting / IT 로직을 모두 구현
// https://khalilstemmler.com/articles/solid-principles/solid-typescript/#Liskov-Substition-Principle

class Employee {
  public calculatePay (): number {
    // implement algorithm for hr, accounting and it
  }
  public reportHours (): number {
    // implement algorithm for hr, accounting and it
  }

  public save (): Promise<any> {
    // implement algorithm for hr, accounting and it
  }
}

 

  • 안좋은 이유
    • 하나의 class에 여러 부서의 알고리즘이 위치
    • 한 부서의 알고리즘을 수정할 때, 다른 부서의 알고리즘에 영향을 줄 수 있음
    • 이러한 작업에서 if / switch가 남발할 수 있음
      - switch문의 코드가 길어질 경우, switch를 여러 class로 나눠 refactoring해야할 시그널일 수 있음
  • 좋은 예제
    • 추상클래스인 Employee를 각 부서가 상속받아 구현
    • 한 부서의 알고리즘 변경을 위한 독립적인 공간이 각각 존재
    • [중요] 애플리케이션을 사용하는 사용자(HR/Accounting/IT)에 따라 책임이 분리되어있음
// https://khalilstemmler.com/articles/solid-principles/solid-typescript/#Liskov-Substition-Principle

abstract class Employee {
  // This needs to be implemented
  abstract calculatePay (): number;
  // This needs to be implemented
  abstract reportHours (): number;
  // let's assume THIS is going to be the 
  // same algorithm for each employee- it can
  // be shared here.
  protected save (): Promise<any> {
    // common save algorithm
  }
}

class HR extends Employee {
  calculatePay (): number {
    // implement own algorithm
  }
  reportHours (): number {
    // implement own algorithm
  }
}

class Accounting extends Employee {
  calculatePay (): number {
    // implement own algorithm
  }
  reportHours (): number {
    // implement own algorithm
  }

}

class IT extends Employee {
  ...
}

 

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

소프트웨어는 확장에 열려있고 수정에 닫혀있어야한다
  • 정의
    • 새로운 기능을 추가할 때, 기존 코드의 수정을 필요로 하면 안됨
    • loose coupling을 위해 policy와 detail의 분리하여, high-level 컴포넌트를 low-level detail의 변경으로부터 지키는 것
  • 방법
    • interface / abstract class에서 구현해야할 High-level Policy 명시
    • concrete class에서 detail 구현
  • 나쁜 예제
    • SendGrid를 사용하여 email을 보내는 서비스 구현을 요청 받음
      - SendGridService라는 concrete class를 구현
    • 3달 뒤, SendGrid가 너무 비싸서 MailChimp로 변경 작업이 필요
      - 다시 MailChimpService라는 concrete class를 구현
      - 이 때, 이를 교체하는 cost가 큼
  • 좋은 예제
    • Mail service와 관련된 interface를 정의
    • 실제 구현은 각각에게 맡김

https://khalilstemmler.com/articles/solid-principles/solid-typescript/#Liskov-Substition-Principle

  • 참고1
    • Dependency Inversion Principle과 밀접한 관련
      - concretion 대신 interface에 의존한다는 점에서
    • Liskov-Substitution Principle과 밀접한 관련
      - type/interface에 의존하면 swap이 가능하다는 점에서
  • 참고2
    • 아키텍쳐 관점에서 business logic을 실행하는 Use Case가 가장 high-level 컴포넌트

https://khalilstemmler.com/articles/solid-principles/solid-typescript/#Liskov-Substition-Principle

 

Liskov-Substitution Principle (리스코브-치환 법칙)

하나의 implementation을 다른 것으로 swap이 가능해야한다
  • 좋은 예제
    • 위 IMailService interface를 SendGridService, MailChimpService 등이 구현
    • "Dependency Injection"을 통해, 우리 class에서 하나의 concrete class가 아닌 interface를 참조
      - 다양한 concretion으로 swap 가능

 

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

class가 필요없는 것에 의존하지 말아야한다
  • 정의
    • 고유한 기능으로 인터페이스를 나누어야함
    • High-level policy를 분리하고, 이를 결합하여 low-level detail을 구현

https://blog.itcode.dev/posts/2021/08/16/interface-segregation-principle

 

[OOP] 객체지향 5원칙(SOLID) - 인터페이스 분리 원칙 (Interface Segregation Principle) - 𝝅번째 알파카의

인터페이스 분리 원칙이란 객체는 자신이 호출하지 않는 메소드에 의존하지 않아야한다는 원칙이다. 구현할 객체에게 무의미한 메소드의 구현을 방지하기 위해 반드시 필요한 메소드만을 상속

blog.itcode.dev

  • 나쁜 예제
    • 스마트폰을 정의하는 abstract class(SmartPhone)
      - 통화 (call)
      - 메시지 (message)
      - 무선충전 (wirelessCharge)
      - AR (ar)
      - 생체인식 (biometrics)
    • 갤럭시 S20은 위 기능들을 모두 지원
    • 그런데 스마트폰인 갤럭시 S2에는 "무선충전/AR/생체인식" 기능을 포함하지 않음
      - 상속할 경우 불필요한 구현/Overriding이 필요
      - "통화/메시지" 기능은 포함
// https://blog.itcode.dev/posts/2021/08/16/interface-segregation-principle

/**
 * 스마트폰 추상 객체
 *
 * @author RWB
 * @since 2021.08.16 Mon 16:48:03
 */
abstract public class SmartPhone
{
    /**
     * 통화 함수
     *
     * @param number: [String] 번호
     */
    public void call(String number)
    {
        System.out.println(number + " 통화 연결");
    }
    
    /**
     * 문자 메시지 전송 함수
     *
     * @param number: [String] 번호
     * @param text: [String] 내용
     */
    public void message(String number, String text)
    {
        System.out.println(number + ": " + text);
    }
    
    /**
     * 무선충전 함수
     */
    public void wirelessCharge()
    {
        System.out.println("무선 충전");
    }
    
    /**
     * AR 함수
     */
    public void ar()
    {
        System.out.println("AR 기능");
    }
    
    /**
     * 생체인식 추상 함수
     */
    abstract public void biometrics();
}
  • 좋은 예제 (해결 방법)
    • SmartPhone에 스마트폰의 공통된 최소한의 기능(통화/메시지)만 남김
    • 나머지 기능(무선충전/AR/생체인식)을 interface로 정의
    • 갤럭시 S20에서 SmartPhone을 포함한 모든 기능을 implement
// https://blog.itcode.dev/posts/2021/08/16/interface-segregation-principle

/**
 * 무선충전 인터페이스
 *
 * @author RWB
 * @since 2021.08.16 Mon 18:23:33
 */
public interface WirelessChargable
{
    /**
     * 무선충전 추상 함수
     */
    void wirelessCharge();
}
/**
 * AR 인터페이스
 *
 * @author RWB
 * @since 2021.08.16 Mon 18:24:29
 */
public interface ARable
{
    /**
     * AR 추상 함수
     */
    void ar();
}
/**
 * 생체인식 인터페이스
 *
 * @author RWB
 * @since 2021.08.16 Mon 18:25:08
 */
public interface Biometricsable
{
    /**
     * 생체인식 추상 함수
     */
    void biometrics();
}

 

Dependency Inversion Principle (의존 역전 원칙)

Abstraction은 detail을 의존하면 안된다.
Detail은 Abstraction을 의존해야한다.
  • 정리
    • Abstraction
      - interface
      - abstract class
    • detail
      - concrete class
  • 나쁜 예제
    • interface의 메서드 파라미터에 concrete class(PrettyEmail)를 포함
interface IMailService {
  // refering to concrete "PrettyEmail" and "ShortEmailTransmissionResult" from an abstraction
  sendMail(email: PrettyEmail): Promise<ShortEmailTransmissionResult>
}
  • 좋은 예제
    • 위 PrettyEmail의 interface로 수정
class SendGridEmailService implements IMailService {
  // concrete class relies on abstractions
  sendMail(email: IMail): Promise<IEmailTransmissionResult> {
  }
}