객체지향 프로그래밍의 설계 원칙, SOLID

강승구·2023년 2월 19일
0

SOLID 원칙이란 로버트 마틴이 소개한 객체 지향 프로그래밍 및 설계의 5가지 기본 원칙을 말하며, 5가지의 원칙의 앞 글자를 따서 “SOLID”라고 부른다.

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

SOLID 원칙을 철저히 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 된다.


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

어떤 클래스던지 단 하나의 기능만을 책임을 갖는다. 이는 클래스를 변경해야하는 이유는 단 하나여야 한다는 뜻이다.

class Person {
	void cook();     //요리하기 - 요리사
	void plate();    //플레이팅 - 요리사
	void order();    //주문하기 - 손님
	void pickup();   //픽업하기 - 손님
	void eat();      //먹기    - 손님
}

위와 같이 Person 클래스는 요리사의 역할과 손님의 역할을 모두 가진 클래스로 하나의 책임만을 가지고 있지 않다.

따라서 Person 클래스는 요리사의 역할과 관련한 기능이 추가될 때마다 코드의 변경이 불가피하다.

class Cook {
	void cook();
	void plate();
}

class Customer {
	void order();
	void pickup();
	void eat();
}

Person 클래스를 단 하나의 책임만 갖도록 Cook 클래스와 Customer 클래스로 구분해 주었다.

이렇게 단일 책임 원칙에 맞게 클래스를 수정한다면, 요리사의 역할과 관련한 기능이 추가될 때 Customer 클래스가 변경되지 않아도 된다.

즉, 클래스 변경으로 인한 다른 클래스에게 영향을 줄일 수 있기 때문에 응집도가 높아지고 결합도가 낮아지는 장점을 가진다.


개방-패쇄 원칙(ORP, Open-Closed Principle)

소프트웨어 엔티티 (클래스, 모듈, 함수 등)는 확장에는 열려있고, 변경에는 닫혀있어야 한다는 원칙이다.

기존 소프트웨어 엔티티 기능에 대한 변경이 있더라도 변경없이 새롭게 확장하여 기능을 추가할 수 있어야한다는 것을 의미한다.

객체 지향의 특징인 “추상화"와 “다형성”을 이용해 OCP를 가능하게 한다.

interface Car {
	void accel();
	void brake();
}

class Bus implements Car {
	void accel() { //속도 10 증가 };
	void brake() { //속도 5 감소 };
}

class Truck implements Car {
	void accel() { //속도 5 증가 };
	void brake() { //속도 3 감소 };
}

위에서 클라이언트(운전자)는 자동차가 변경 되더라도 엑셀을 밟거나 브레이크를 밟는 것에 대해 영향을 받지 않는다. 이는 클라이언트가 변경에는 닫혀있는 것을 의미한다.

반대로, 자동차는 Bus나 Truck 이외에도 다른 자동차 종류로 클래스를 인터페이스를 통해 확장할 수 있고, 클라이언트(운전자)의 역할에는 영향을 끼치지 않는다. 이는 확장에는 열려있는 것을 의미한다.


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

하위 타입이 상위 타입이 지정한 제약조건들을 지키고, 상위 타입에서 하위 타입으로의 변동이 일어나도 상위타입의 역할을 문제없이 제공해야하는 것을 의미한다.

class Car {
	int speed;
	void drive() { 
		this.speed += 10;
	}
}

class Bus extends Car {
	int speed;
	int km;
	
	@Override 
	void drive() {
		// 부모 기능 그대로 수행
		this.speed += 10;
		
		// 하위 타입 기능 추가
		this.km += 1;
	}
}

위와 같이 Bus 클래스는 Car 클래스가 제공하는 drive 메소드의 speed += 10기능을 그대로 수행하고 있고, 하위 타입에서 추가적으로 구현한 km += 1을 수행한다.

이는 상위 타입인 Car클래스가 제공하던 speed += 10기능을 하위타입에서 제대로 수행할 수 있기 때문에 해당 원칙을 잘 지킨것이다.

리스코프 치환 원칙을 지키기 위해서는 하위 타입에서 상위 타입은 상속을 하지만 무분별한 메소드 오버라이딩을 줄이는 것이다. 메소드 오버라이딩을 하더라도 상위 타입이 구현한 기능을 변경없이 구현 후 추가적인 기능을 구현하는 것이 바람직하다.


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

SRP (단일 책임 원칙)가 클래스에 대해서 단일 책임을 갖도록 하는 원칙이었다면, ISP (인터페이스 분리 원칙) 인터페이스를 최대한 변경을 하지 않도록 구체적으로 구분하는 원칙이다.

interface Person {
	void cook();
	void plate();
	void order();
	void pickup();
	void eat();
}

위와 같이 Person 인터페이스를 요리사 인터페이스의 기능과 손님 인터페이스를 모두 구현할 수 있다.

interface Cook {
	void cook();
	void plate();
}

interface Customer {
	void order();
	void pickup();
	void eat();
}

따라서, Cook 인터페이스와 Customer 인터페이스로 구분해 만들어 준다면, Cook 인터페이스에서는 Customer 인터페이스의 기능을, Customer 인터페이스에서는 Cook 인터페이스의 기능을 구현하지 않아도 된다.


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

구현체보다 인터페이스나 추상 클래스에 의존하는 것이 좋다는 원칙이다.

class ComicBook {
	void read();
	void save();
} 

위와 같이 클라이언트가 ComicBook 클래스에 의존한다면, Magazine이나 Poem 클래스를 이용하고 싶을 때 클라이언트의 코드를 전격 수정해주어야 한다.

interface Book {  
	void read();
	void save();
}

class ComicBook implements Book{
	void read() {...}
	void save() {...}
}

class Magazine implements Book{
	void read() {...}
	void save() {...}
}

class Poem implements Book{
	void read() {...}
	void save() {...}
}

따라서, 클라이언트는 구현체에 의존하기 보다는 ComicBook, Magazine, Poem을 추상화한 Book 인터페이스를 의존한다면, 클라이언트의 코드 변경 없이 여러가지 구현체를 별다른 코드의 변경없이 사용이 가능해진다.

profile
강승구

0개의 댓글