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

F-Lab 모각코 챌린지 24일차를 진행하며 정리 한 내용입니다 일반적으로는 이해하기 힘든 SOLID 개념을 코드와 함께 풀어 냈습니다.

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

객체지향 5대 원칙이라고 불리는 SOLID 는 유연성, 재사용성, 유지보수성을 향상시키기 위해 사용된다

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

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

객체는 하나의, 오직 단 하나의 이해 관계자에 대해서만 책임져야 한다

객체가 변경되어야 하는 이유는 오직 하나여야 한다는 말이다

아래의 클래스를 보자

public class Employee {
	private Long id;
	private String name;
    ...
	
	// 회계팀에서 기능을 정의, CFO 보고를 위해 사용
	public Double calculatePay() {
		return regularHours();
	}

	// 인사팀에서 기능을 정의, COO 보고를 위해 사용
	public Double reportHours() {
		return regularHours();
	}

	// 코드의 중복을 피하기 위해 알고리즘을 공유
	public Double regularHours() {
		return null;
	}
    ... line: 300
}

이 예제 에서는 단일 책임의 원칙을 위배하여 우발적 중복 현상이 발생 한 경우인데, 아래의 문제점들을 가지고 있다

  • 코드 중복이 자주 발생 할 가능성이 있고, 개발자는 두 클래스가 다른 요구사항을 가진지 알기 힘들다.
  • 공유 알고리즘을 사용하고 있다. COO 의 요구사항이 변경되면 CFO 는 변경을 알아차리지 못할 가능성이 크다
  • 클래스가 변경 될 요인이 2가지 이상이 되었다. (calculatePay, reportHours)
    Conflict 를 자주 발생 시킬 수 있고 개발자의 실수를 유도한다

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

소프트웨어 개체(Artifact) 는 확장에는 열려있어야 하고 변경에는 닫혀있어야 한다.

소프트웨어가 변경하는데 드는 비용을 최소화 하기 위한 원칙이다

class BadOcp {
	public static void main(String[] args) {
		Computer computer = new Computer();
		computer.boot();
	}
}
class Computer {
	private final SKeyboard sKeyboard = new SKeyboard();
	
	public void boot() {
		System.out.println("부팅 시작");
		sKeyboard.connect();
	}
}
class SKeyboard {
	public void connect() {
		System.out.println("S 사 키보드가 연결 되었습니다.");
	}
}

위 코드는 OCP 를 위배한 경우 이다.

  • 이 컴퓨터가 다른 키보드를 사용하려면 Computer 클래스의 변경이 불가피하다
    결국, 추가 하려는 키보드가 있다면 컴퓨터의 코드는 계속해서 변경되어야 한다
class Computer {
	private final SKeyboard sKeyboard = new SKeyboard();
	private final AKeyboard aKeyboard = new AKeyboard();
	private final LKeyboard lKeyboard = new LKeyboard();
	private final RKeyboard rKeyboard = new RKeyboard();
	private final HKeyboard hKeyboard = new HKeyboard();
	private final KKeyboard kKeyboard = new KKeyboard();

	public void boot() {
		System.out.println("부팅 시작");
		sKeyboard.connect();
		aKeyboard.connect();
		lKeyboard.connect();
		rKeyboard.connect();
		hKeyboard.connect();
		kKeyboard.connect();
	}
}

SRP 와 OCP 를 준수하여 다시 만들어진 컴퓨터

class Ocp {
	public static void main(String[] args) {
		Computer computer = new Computer();
		computer.setKeyboard(new SKeyboard());
		computer.boot();
	}
}
class Computer {
	private Keyboard keyboard;

	public void setKeyboard(Keyboard keyboard) {
		this.keyboard = keyboard;
	}

	public void boot() {
		System.out.println("부팅 시작");
		keyboard.connect();
	}
}
interface Keyboard {
	void connect();
}
class SKeyboard implements Keyboard {
	public void connect() {
		System.out.println("S 사 키보드가 연결 되었습니다.");
	}
}

이제는 키보드가 새로 추가되어도 컴퓨터는 정상적으로 동작하며 Computer 코드의 변경은 닫히고 확장에는 열리게 되었다

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

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

상위 타입을 구현한 하위 타입에, 같은 상위 타입을 구현한 다른 하위 타입이 오더라도 프로그램의 동작에는 영향을 주어선 안된다는 말이다

아래는 LSP 를 준수한 코드이다

public class LiskovRunner {
	public static void main(String[] args) {
		Car combinationCar = new Car(
			new AudiEngine(),
			new BenzTire()
		);

		combinationCar.drive();
	}
}
class Car {
	private Engine engine;
	private Tire tire;

	public Car(Engine engine, Tire tire) {
		this.engine = engine;
		this.tire = tire;
	}
	
	public void drive() {
		engine.start();
		tire.rotate();
	}
}
interface Engine {
	void start();
}
interface Tire {
	void rotate();
}

class AudiEngine implements Engine {
	@Override
	public void start() {
		System.out.println("아우디 엔진 가동");
	}
}
class BenzEngine implements Engine {
	@Override
	public void start() {
		System.out.println("벤츠 엔진 가동");
	}
}

class AudiTire implements Tire {
	@Override
	public void rotate() {
		System.out.println("아우디 타이어 회전");
	}
}
class BenzTire implements Tire {
	@Override
	public void rotate() {
		System.out.println("벤츠 타이어 회전");
	}
}

LSP 를 준수하여 짜여진 코드이다.

타이어가 교체 되어도, 엔진이 교체 되어도 프로그램의 실행에는 이상이 없으며 둘을 교체하더라도 문제가 생기지 않는다.

하위 타입에 대한 의존성을 가지고 있지 않기 때문인데, 이것은 2번째 글에서 설명 할 DIP 에서 추가로 설명 할 예정이다.


마무리

리스코프 치환 원칙은 올바른 예제는 아니라고 생각은 든다. 왜냐하면 리스코프 치환 원칙을 위배하는 사례에 대한 예시와 함께 인사이트를 전달해야 하는데 그러한 예시가 쉽게 떠오르지 않았기 때문이다

다음 글에서 리스코프 치환 원칙에 대한 예시와 함께 나머지(ISP, DIP) 에 대한 이야기를 추가로 진행하겠다.