[F-Lab 모각코 챌린지] 25일차 - 코드로 이해하는 SOLID (2)

F-Lab 모각코 챌린지 25일차 포스팅 입니다 SOLID 1 편에 이어, 리스코프 치환 원칙에 대한 예제를 추가로 제공하였으며 ISP, DIP 에 대한 예시를 통해 각 설계원칙을 설명하였습니다

[F-Lab 모각코 챌린지] 25일차 - 코드로 이해하는 SOLID (2)

이전 글에서 설명했던 리스코프 치환 원칙의 추가 예제 하나를 먼저 진행 한 뒤, 차례대로 ISP, DIP 를 설명 할 예정이다

SRP, OCP, LSP 각각의 내용을 이해하지 못했다면 이전 글을 확인하길 바란다

  • SRP (Single Responsibility Principle: 단일 책임 원칙)
  • OCP (Open-Closed Principle: 개방-폐쇄 원칙)
  • LSP (Liskov Substitution Principle: 리스코프 치환 원칙)
  • ISP (Interface Segregation Principle: 인터페이스 분리 원칙)
  • DIP (Dependency Inversion Principle: 의존성 역전 원칙)

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

상위 타입 P 를 구현한 P1, P2 가 있다.
P1 을 이용해 정상적으로 구동되는 프로그램에 P2 가 들어가도, 프로그램의 실행에는 영향을 주어서는 안된다

이전의 예시 에서는 올바르게 구현한 경우를 예제로 사용했지만, 올바르지 못한 구현 예제를 추가하여 이해를 돕는다

리스코프 치환 원칙은 결국 해당 메서드를 호출하는 대상에게 확장의 사이드 이펙트를 넘기지 말라는 이야기 이다

아래는 LSP 를 준수하지 않은 예제이다

public class LiskovExample {

  public static void main(String[] args) {
    // 정상적으로 사용
    Animal cat = new Cat();
    cat.greet();

    // 새로운 자식 객체의 추가. 프로그램은 의도한 대로 동작하지 않는다.
    Animal dog = new Dog();
    dog.greet();
  }
}
class Animal {
  void greet() {
    Cat target = (Cat) this;
    target.meow();
  }
}
class Cat extends Animal {
  void meow() {
    System.out.println("야옹");
  }
}
class Dog extends Animal {
  void woof() {
    System.out.println("멍멍");
  }
}

위와 같은 상황이 올 수 있는 과정은 이렇다

  1. 개발자가 초기 개발 당시, 하위 타입이 단 하나만 존재 할 것이라 생각하고 개발을 진행하였다
  2. 상위 타입에서 하위 타입의 함수를 사용하고 싶었으며 개발자는 LSP 를 알지 못한다
  3. 이윽고 상위 타입의 함수에서 하위 타입의 함수를 선언하기 위해 (Cat) this 형변환을 감행한다

이렇게 되면 Animal 의 하위타입 Cat 과 Dog 가 존재하고, Cat 의 자리에 Dog 를 넣게되면 프로그램이 정상적으로 동작하지 않게 된다

이 문제는 비단 '상속' 에서만 발생하는 문제는 아니다

  • Animal 과 Cat, Dog 를 변경 했을 때 사이드 이펙트는 해당 클래스를 사용하는 쪽으로 흐르게된다

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

클라이언트는, 자신이 이용하지 않는 메서드에 의존하지 않아야 한다

아래는 인터페이스 분리 원칙을 위배한 예제이다

interface Machine {
  void start();
  void fly();
  void sail();
}
class Airplane implements Machine {
  @Override
  public void start() {
    System.out.println("시스템 부팅 성공");
  }

  @Override
  public void fly() {
    System.out.println("이륙");
  }

  @Override
  public void sail() {
    throw new RuntimeException("비행기는 항해 할 수 없다");
  }
}
class Ship implements Machine {
  @Override
  public void start() {
    System.out.println("시스템 부팅 성공");
  }

  @Override
  public void fly() {
    throw new RuntimeException("배는 날 수 없다");
  }

  @Override
  public void sail() {
    System.out.println("항해 시작");
  }
}
  • Airplane 은 날 수 있지만 Ship 은 날지 못한다
  • Ship 은 항해 할 수 있지만 Airplane 는 항해할 수 없다

둘은 같은 인터페이스를 상속받았고, 불필요한 인터페이스를 공유하고 있다

  • Machine 인터페이스가 변경될 때 마다 '자신이 사용하지 않는' 메서드의 사이드 이펙트를 해결해야 한다

해결 하려면 아래 구조처럼 변경해야 한다

interface Machine {
  void start();
}
interface FlyingMachine extends Machine {
  void fly();
}
interface SailMachine extends Machine {
  void sail();
}
class Airplane implements FlyingMachine {
  @Override
  public void start() {
    System.out.println("시스템 부팅 성공");
  }

  @Override
  public void fly() {
    System.out.println("이륙");
  }
}
class Ship implements SailMachine {
  @Override
  public void start() {
    System.out.println("시스템 부팅 성공");
  }

  @Override
  public void sail() {
    System.out.println("항해 시작");
  }
}

DIP (Dependency Inversion Principle)
의존성 역전 원칙

의존성이 추상에 의존 하며 구체에는 의존 하지 않아야 한다

클래스에 의존하는 것 보다 인터페이스에 의존하는게 더 좋다는 말이다

아래는 DIP 를 위배 한 경우이다.

public class Dip {
  public static void main(String[] args) {
    MysqlRepositoryImpl mysqlRepository = new MysqlRepositoryImpl();
    mysqlRepository.save(new Object());
  }
}
class MysqlRepositoryImpl {
  void save(Object o) {
    System.out.println("저장합니다.");
  }

  void findById(Long id) {
    System.out.printf("id(%d) 를 통해 mysql 에서 데이터를 조회합니다", id);
  }
}

위 코드는 Mysql 이 아닌 다른 데이터베이스를 사용하기 위해서는 MysqlRepositoryImpl 클래스를 사용하는 모든 클래스에 변경이 이루어져야 한다

즉, 사이드 이펙트가 MysqlRepositoryImpl 를 호출하는 쪽으로 몰린다는 이야기 이다

이 코드를 개선하면 다음과 같이 변경할 수 있다

public class Dip {
  public static void main(String[] args) {
    BusinessService businessService = new BusinessService(
      new MysqlRepositoryImpl()
    );
    businessService.create(new Object());
  }
}
class BusinessService {
  private final DatabaseRepository databaseRepository;

  public BusinessService(DatabaseRepository databaseRepository) {
    this.databaseRepository = databaseRepository;
  }
  
  void create(Object o) {
    databaseRepository.save(o);
  }
}
interface DatabaseRepository {
  void save(Object o);
  void findById(Long id);
}
class MysqlRepositoryImpl implements DatabaseRepository{
  @Override
  public void save(Object o) {
    System.out.println("저장합니다.");
  }

  @Override
  public void findById(Long id) {
    System.out.printf("id(%d) 를 통해 mysql 에서 데이터를 조회합니다", id);
  }
}

이렇게 처리하면 BusinessService 는 MysqlRepositoryImpl 의 의존성을 갖지 않고 추상에 의존하므로 MysqlRepositoryImpl 이 아닌 다른 데이터베이스로 교체 하더라도 사이드 이펙트가 BusinessService 로는 향하지 않는다