[디자인 패턴] 3. SOLID

lhs's avatar
Nov 13, 2024
[디자인 패턴] 3. SOLID
 

S - 단일 책임 원칙 (Single Responsibility Principle, SRP)

💡
“클래스는 단 하나의 책임만 가져야 한다.”는 원칙
  • 한 클래스는 하나의 기능만을 수행해야 하며, 이 기능을 변경해야 할 이유도 하나여야 한다.
  • 클래스의 변경 이유가 하나뿐이므로, 유지보수성이 향상된다.
  • 클래스가 여러 가지 책임을 가지게 되면, 하나의 책임이 변경되었을 때 다른 책임도 영향을 받을 수 있다. 이는 코드 변경 시 예기치 않은 오류를 발생시킬 수 있고, 유지보수를 어렵게 만든다.
  • 예제 코드
    • class Plus{ int plus(int a, int b){ return a + b; } int minus(int a, int b){ return a - b; } }
      SRP를 지키지 않는 코드
       
      class Plus{ int plus(int a, int b){ return a + b; } }
      SRP를 지키는 코드
    • 첫 번째 예제에서 Plus 클래스는 더하기 뿐만 아니라 빼기 기능도 가지고 있어 SRP를 어긴다.
    • 두 번째 예제에서 Plus 클래스는 더하기만 수행하여 SRP를 지킨다.

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

  • "소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다"는 원칙
  • 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 한다.
  • 코드의 유지보수성과 확장성을 높여준다.
  • 기존 코드의 변경을 최소화하면서 기능을 확장할 수 있기 때문에, 기존 기능에 영향을 미치지 않고 새로운 기능을 추가할 수 있다.
  • 예제 코드
    • class A{ void print(){ System.out.print("A"); } } class Print{ void print(A a){ a.print(); } } class B{ void print(){ System.out.print("B"); } }
      OCP를 지키지 않는 코드
       
      interface I{ void print(); } class A implements I{ void print(){ System.out.print("A"); } } class Print{ void print(I a){ a.print(); } } class B implements I{ void print(){ System.out.print("B"); } }
      OCP를 지키는 코드
    • 첫 번째 예제에서는 B를 추가하면 Print클래스를 수정해야해 OCP를 어긴다.
    • 두 번째 예제에서는 인터페이스 I를 활용해 B를 추가해도 기존 코드를 수정하지 않아도 되어 OCP를 지킨다.

L - 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

  • "자식 클래스는 언제나 부모 클래스로 교체할 수 있어야 한다"는 원칙
  • 자식 클래스가 부모 클래스를 대신해서 사용될 수 있어야 하며, 부모 클래스의 기능을 잘못 변경해서는 안 된다.
  • 이 원칙을 어기면, 코드에서 예기치 않은 동작을 할 수 있으며, 시스템의 예측 가능성이 낮아진다.
  • 자식 클래스가 부모 클래스의 계약을 어기지 않도록 하여, 다형성을 안전하게 사용할 수 있게 만든다.
  • 예제 코드 1
    • abstract class Bird { abstract void fly(); } class Penguin extends Bird { @Override public void fly() { throw new UnsupportedOperationException("펭귄은 날 수 없음"); } } public class Main{ public static void main(String[] args) { Bird penguin = new Penguin(); penguin.fly(); } }
      잘못된 상속 관계
       
      abstract class Bird{} interface Fliable{ void fly(); } class Penguin extends Bird{}
      수정된 코드
    • Penguin은 날 수 없지만 Bird를 상속받아 fly() 메서드를 강제로 구현해서 예외로 처리했다.
    • main에서 fly()를 실행했지만 예상치 않은 예외가 발생할 수 있어 LSP를 어긴다.
    • 이를 수정하기 위해 fly() 메서드를 인터페이스로 빼내서 정의한다.
  • 예제 코드 2
    • class A{ void print(){ System.out.print("A"); } } class B extends A{ void print(int b){ System.out.print("B" + b); } } public class Main{ public static void main(String[] args) { A b = new B(); b.print(1); } }
      잘못된 메서드 오버로딩
    • B 클래스에서 print()를 오버라이딩하지 않고 오버로딩하여 새로운 메서드를 추가했다.
    • A 클래스에는 print(int b) 가 존재하지 않아 실행이 안된다.
    • A 클래스의 print() 메서드가 B 클래스에서 호출되지 않기 때문에 일관성 없는 동작이 발생해서 LSP를 어긴다.
    • 이를 수정하려면 부모 클래스에서 오버로딩 메서드를 구현해야 한다.
  • 예제 코드 3
    • class A a{ void hi(){ System.out.print("A hi"); } } class B extends A{ @Override void hi(){ System.out.print("B bye"); } }
      의도와 다른 메서드 오버라이딩
    • A 클래스에서 hi() 메서드는 “hi"를 출력하도록 구현했다.
    • B 클래스에서 hi() 메서드는 “bye”를 출력하는 의도와는 다른 출력을 해서 LSP를 어긴다.

I - 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

  • "하나의 커다란 인터페이스보다는 여러 개의 작은 인터페이스가 더 좋다"는 원칙
  • 사용하지 않는 메서드를 포함하는 큰 인터페이스를 피하고, 클라이언트가 자신에게 필요한 메서드만을 제공하는 작은 인터페이스를 사용해야 한다.
  • 유지보수성을 높이고, 응집도를 강화할 수 있다.
  • 불필요한 메서드를 구현하지 않게 하여, 클라이언트가 필요하지 않은 메서드를 구현하지 않도록 할 수 있다.
  • 예제 코드
    • interface Movable{ void run(); void fly(); }
      ISP를 지키지 않는 코드
       
      interface Runable{ void run(); } interface Flyable{ void fly(); }
      ISP를 지키는 코드
    • Movablefly() 메서드와 run() 메서드 두 가지 기능을 담고 있어 ISP를 위반한다.
    • RunableFlyable로 분리해 ISP를 지켰다.

D - 의존 역전 원칙 (Dependency Inversion Principle, DIP)

  • "고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다"는 원칙
  • 구체적인 구현이 아니라 인터페이스나 추상 클래스에 의존해야 한다는 것이다.
  • 의존 관계를 역전시켜, 의존성을 쉽게 교체하거나 테스트할 수 있게 한다.
  • 구체적인 클래스에 의존하지 않고 인터페이스나 추상 클래스에 의존함으로써, 시스템의 유연성과 확장성을 높인다.
  • 예제 코드
    • class A{ void print(){ System.out.print("A"); } } class B{ void print(){ System.out.print("B"); } } class High{ A a; High(A a){ this.a = a; } void print(){ a.print(); } }
      DIP를 지키지 않는 코드
       
      interface I{ void print(); } class A{ void print(){ System.out.print("A"); } } class B{ void print(){ System.out.print("B"); } } class High{ I i; High(I i){ this.i = i; } void print(){ i.print(); } }
      DIP를 지키는 코드
    • 위의 예제에서는 고수준 모듈인 High 클래스가 구체적인 A 클래스에 의존하여 DIP를 어긴다.
    • 아래의 예제에서는 인터페이스 I를 구현하여 High 클래스가 I에 의존하게 만들어 DIP를 지키게 수정되었다.
Share article

LHS's Study Space