Computer Science/OOP

SOLID Principles

Ahn Paul 2020. 2. 25. 20:44

SOLID는 2000년대 초반에 로버트 마틴이 OOP 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것. SOLID 원칙을 지켜냄으로써 유지 보수와 확장이 쉬운 시스템을 구현할 수 있다.

 

 


S : Single Responsibility Principle (단일 책임 원칙)  

 - 하나의 클래스는 하나의 책임만 가져야 한다. 

  => Book에 대한 내용을 저장하는 클래스(BookSaver)가 있다고 할 때, BookSaver는 저장하는 역할 한 가지만 수행하는 클래스이어야 한다. BookSaver에 출력하는 기능이 추가가 된다면 SRP에 위배된다. 

public class Book {
	ArrayList<String> al;
	Book(){
		al = new ArrayList();
	}
	void saveBook(String name) {
		al.add(name);
	}
	
	void printBook() {
		for ( String book : this.al) {
			System.out.println(al);
		}
	}
}

 위 클래스는 한 클래스(Book) 안에 책을 저장하는 것과 출력하는 것을 동시에 가지고 있다. 따라서 SRP 원칙에 위배된다. SRP를 해결하기 위해서 저장하는 클래스와 출력하는 클래스를 별도로 두어야 한다.

 

public class Book {
	ArrayList<String> al;
	Book(){
		al = new ArrayList();
	}
	void saveBook(String name) {
		al.add(name);
	}
}

 

class BookPrinter extends Book{
	void showBookLists(ArrayList<String> al) {
		for ( String book : this.al) {
			System.out.println(al);
		}
	}
}

 두 개의 클래스로 구분할 수 있고 SRP 원칙을 고수하게 되면 해당 기능에 대한 확장이 용이해질 수 있다. 

 


O : Open/Closed Principle (개방·폐쇄 원칙) 

 - 확장(Extension)에는 개방되어야 하고, 변경(Modify)에는 폐쇄되어야 한다.

 간단한 예로 책에 버튼이 생겼다고 상상해 보자. 일반 책만 있는 것이 아니라 2020년이 되어 특별 출간된 소리가 나는 책이 출시되었다. 일반 책에 달린 버튼을 누르면 “푸~욱” 소리가 나고, 소리가 나는 책의 버튼을 누르면 “정체 모를 로봇의 소리가 나온다.”고 한다. 

public class book{
	public static void main(String args[]) {
		Book b = new Book("Normal");
		
		if (b.getName() == "Normal") {
			System.out.println("푸~욱")
		}
		else if (b.getName() == "Normal") {
			System.out.println("..시스템 가동 준비 완료")
		}
	}
}

 책을 인스턴스를 만들 때 책의 이름을 지정해 주고 책의 이름에 따라 소리가 나도록 설계되어 있는 모습이다. 책의 종류가 많아지면 코드는 점점 많아질 것이고 유지보수와 코드의 유연성은 두 말할 나위 없을 것이다. 

 위의 문제를 해결하기 위해 인터페이스를 이용할 수 있다.

interface Button {
	public void effect();
}

 위 인터페이스는 책에 부착될 버튼이 될 것이고 버튼을 클릭하게 되면 effect() 효과를 나게 할 수 있도록 한다. 

class NormalBook implements Button{
	public NormalBook() {}

	@Override
	public void effect() {
		// TODO Auto-generated method stub
		System.out.println("푸~욱");
	}
}

class SoundBook implements Button{
	public SoundBook() {}

	@Override
	public void effect() {
		// TODO Auto-generated method stub
		System.out.println("..시스템 가동 준비 완료");
	}
}

 일반 책과 소리가 나는 책을 선언해 준다. 

 

public class Book{
	public static void main(String args[]){
		pressButton(new NormalBook());
		pressButton(new SoundBook());
	}
	
	static void pressButton(Button b){
		b.effect();
	}
	
}

 그리고 책의 타입에 구애받지 않고 오버라이딩한 메소드를 통해 매개변수에 따라 원하는 소리를 낼 수 있도록 할 수 있다.


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

 - 하위 클래스(자식)은 상위 클래스(부모)를 대체할 수 있어야 한다. 즉, 업캐스팅을 할 경우에 문제가 발생하지 않아야 한다. 

 

interface Car {
	void accelate();
	void toggleEngine();
}

 만들어 볼 자동차는 엑셀 기능과 시동을 키고 끄는 기능이 있다.

 

class OilCar implements Car{

	@Override
	public void accelate() {
		// TODO Auto-generated method stub
		System.out.print("가속");
	}

	@Override
	public void toggleEngine() {
		// TODO Auto-generated method stub
	}
	
}

 흔히들 타는 휘발유를 가진 자동차를 설계했다. 엑셀을 밟는 것과 시동을 거는 것에 큰 문제는 없어 보인다. 시간이 흘러 전기 자동차가 출범하게 됐다. 

class ElectricCar implements Car{

	@Override
	public void accelate() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void toggleEngine() {
		// TODO Auto-generated method stub
		System.out.print("엔진이 없습니다.");
	}
	
}

 전기 자동차도 마찬가지로 시동을 걸고 악셀를 밟는 기능을 주었다. 그러자 문제가 생겼다. 엔진이 없기 때문에 전기차는 시동을 일반 자동차와 같이 걸 수 없다. 

 => LSP 위반.

 

 위의 문제를 해결하기 위해서는 Car에서는 Car를 상속받는 오브젝트들이 동일하게 가지고 있는 것을 추상화하고 개별적으로 필요한 기능은 오브젝트 자체에서 구현할 수 있도록 해야한다. 


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

 - 하나의 인터페이스에 여러 기능이 들어가 있는 것보다, 여러 인터페이스로 나누어 구현한다.

 

 이 세상의 한 ”아들“이 되었다고 가정한다. 아들은 먹을 수 있고, 잘 수 있고, 엄마의 잔소리를 들을 수 있다. 해당 역할을 구현한다. 

interface Son{
	void listeningSermon();
	void eating();
	void sleeping();
}

 해당 인터페이스를 상속 받는 아들을 만들어 보자.

class Son_ implements Son{

	@Override
	public void listeningSermon() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void eating() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void sleeping() {
		// TODO Auto-generated method stub
		
	}
	
}

 엇, 듣기 싫은 엄마의 잔소리 또한 들어야 한다. 잔소리를 피하기 위해서 잔소리를 듣는 기능만 누군가에게 넘겨줄 수 있으면 좋겠다. 이를 위해서 만들어진 인터페이스를 분리한다. (ISP) 

 

interface listeningSermon {
	void listening();
}

interface eating {
	void eating();
}

interface sleeping {
	void sleeping();
}

 불쌍한 아들을 하고 싶은 것만 할 수 있게 만들어 줄 수 있다.

class Son_ implements eating, sleeping{



	@Override
	public void eating() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void sleeping() {
		// TODO Auto-generated method stub
		
	}
	
}

 잔소리는 잠깐 친구가 들을 수 있도록 해 주자.

class BestFriend implements listeningSermon{
	@Override
	public void listening() {
		// TODO Auto-generated method stub
		
	}
}

 짠, 이제 아들은 잔소리를 들어야 하는 것을 친구가 들을 수 있도록 해 주었다. 이것이 인터페이스를 분리해서 얻을 수 있는 유연함이라고 할 수 있다. 또한, 인터페이스를 분리하면서 카풀링 되어 있는 것을 디카풀링 하는 효과도 얻을 수 있다. 


D : Dependency Inversion Principle (의존관계 역전 원칙)

 - 구체화가 아닌 추상화에 의존해야 한다. 대표적으로 의존성 주입(DI, Dependency Injection)이 DIP를 따른다.

 

 우리는 컴퓨터 한 대를 장만했는데 컴퓨터에는 키보드와 마우스가 있다. 

class Computer {
	final Mouse mouse;
	final Keyboard keyboard;
	public Computer() {
		mouse = new Mouse();
		keyboard = new Keyboard();
	}
}

class Mouse{
	
}

class Keyboard{
	
}

 짜잔, 멋있게 컴퓨터에 마우스와 키보드를 하나 씩 주었다. 근데 위의 설계를 자세히 보고 있으니 마우스와 키보드가 고정이 되어 있기 때문에 다양한 종류의 마우스와 키보드를 사용할 수 없게 되어 있다. 즉, 마우스, 키보드, 컴퓨터 세 개의 클래스가 단단히 카풀링 되어 있다. 이를 해결해 보자.

 

interface Keyboard{
	void input();
}

interface Mouse{
	void move();
}

 키보드와 마우스를 인터페이스로 구현한다.

 

마우스는 빛이 나게, 키보드는 전자식 키보드를 사용하고 싶다. 빛이 나는 마우스에 마우스 인터페이스를, 전자식 키보드에 키보드 인터페이스를 부여해 주자.

class BlinkMouse implements Mouse {

	@Override
	public void move() {
		// TODO Auto-generated method stub
		
	}
	
}

class ElectricKeyboard implements Keyboard{

	@Override
	public void input() {
		// TODO Auto-generated method stub
		
	}
	
}

 멋있는 키보드와 마우스 한 쌍을 만들어 냈다. 이를 컴퓨터랑 연결해 주자.

class Computer {
	final Mouse mouse;
	final Keyboard keyboard;
	public Computer(Keyboard k, Mouse m) {
		this.mouse = m;
		this.keyboard = k;
	}
}

 짠, 멋진 컴퓨터에 어떠한 종류의 키보드와 마우스라도 사용할 수 있게 되었다. 

위의 과정을 통해 종속성 문제를 해결하고 테스팅에 자유롭도록 문제를 해결할 수 있다.