스프링 공부/인프런 김영한 스프링 핵심 원리 - 기본편 노트정리

1-4. 좋은 객체 지향 설계의 5가지 원칙(SOLID)

모항 2022. 8. 20. 20:49

면접에서 물어볼 수도 있는 내용이라고 한다.

 

 

 

SOLID란

저서 《클린 코드》로 유명한 로버트 마틴이 정리한 좋은 객체 지향 설계의 5가지 원칙.

좋은 객체 지향 설계가 어떤 것인지에 대한 개념은 이전에도 있었지만, 로버트 마틴이 이를 SOLID로 깔끔하게 정리해주었다.

 

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

2. OCP (Open/Closed Principle): 개방-폐쇄 원칙

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

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

5. DIP (Dependency Inversion Principle): 의존관계 역전 원칙

 

 

 

1. SRP, 단일 책임 원칙

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

 

그러나 하나의 책임이라는 것이 뭔지가 모호하다. 어느 범위까지를 '하나'라고 할 수 있는가?

 

이 원칙의 핵심은 '하나'의 정의를 내리는 것이 아니다.

 

특정 클래스가 너무 많은 역할과 파급 효과를 가지고 있어서, 그것을 변경하는 순간 프로그램의 여러 부분에서 큰일이 난다면 이 원칙을 지키지 못한 것이다.

변경이 있을 때 파급 효과가 적어 관리하기가 용이하다면 이 법칙을 잘 지킨 것이다.

 

무조건 책임을 잘게 쪼개어 부여하는 게 좋은 것도 아니다.

쓸데없이 너무 조금씩의 책임을 많은 클래스에 분배하면 오히려 효율이 떨어진다.

합의점을 잘 찾아서 책임을 적절히 분배해야 한다.

 

 

2. OCP, 개방-폐쇄 원칙

소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

가장 중요한 원칙이다.

 

그리고 언뜻 들으면 말도 안 되는 원칙이다. 애초에 확장을 하려면 기존의 코드에 손을 대야 하는데, 어떻게 변경 없이 확장을 하라는 말인가?

 

 

 

다음의 예시를 보자.

 

인터페이스 A가 있다.

인터페이스 A를 바탕으로 AOne과 ATwo라는 구현체를 만들었다.

우리는 개발 초기에 AOne을 사용하다가 후에 AOne를 빼고 그 자리에 ATwo를 갈아끼울 것이다.

 

이 사항만 본다면, A라는 인터페이스와 AOne이라는 구현체를 전혀 변경하지 않고, ATwo라는 새로운 구현체를 만들기만 했으므로 확장은 되었지만 변경이 되지 않았다. OCP를 만족한다. 객체 지향 프로그래밍의 다형성 덕분이다.

 

 

 

문제는 '갈아끼우는' 부분이다.

 

X라는 다른 클래스가 있다.

이 클래스는 A를 가져다 쓴다.

지금은 AOne을 가져다 쓰고 있다. 그래서 X의 내부에는 다음과 같은 코드가 들어있다.

class X (...) {
	private final A a = new AOne();

	...
}

이렇게 X 본인이 사용할 AOne 객체를 직접 선언한다.

 

그런데 AOne 객체가 아닌 ATwo 객체를 사용해야 하는 상황이 온다면?

class X (...) {
	private final A a = new ATwo();

	...
}

X의 코드 자체를 위와 같이 변경해야만 한다.

 

 

 

 

우리는 다형성을 활용하여 최선을 다해 OCP를 지키려 하였지만, X의 코드를 변경하지 않고 확장을 이루는 데 실패했다.

 

변경 없는 확장이란 정녕 불가능한 것일까?

아니다!

 

스프링은 변경 없이 확장을 할 수 있게 해주는 방법을 제공한다.

객체를 생성하고 연관관계를 맺어주는 별도의 조립, 생성자들이다.

저번에 다른 강의에서 배운 DI도 그 중 하나이고, 이번에 처음 듣는 IoC 컨테이너? 도 그렇다고 한다.

 

 

3. LSP, 리스코프 치환 원칙

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

매우 기본적인 원칙이다.

 

상위에서 정해놓은 사항을 하위 인스턴스가 마음대로 바꾸지 말라는 뜻이다. 지킬 건 지켜야 한다.

그래야 프로젝트 코드 전체에 일관성이 생겨 사용과 관리가 편리해진다. 프로젝트에 참여하는 개발자들 사이 혼선이 생기는 일도 막을 수 있다.

 

 

4. ISP, 인터페이스 분리 원칙

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

 

인터페이스 하나가 여기저기 다양한 사용처에 다 쓰이게 하지 말고,

각 클라이언트별로(역할별로) 각자 다른 인터페이스가 담당하도록 잘 쪼개라는 뜻이다.

 

그러면 클라이언트 A와 관련된 변경사항이 생겨 인터페이스 A의 코드를 고치더라도, 클라이언트 B에는 아무 영향이 없도록 할 수 있다.

 

인터페이스가 명확해지고, 대체 가능성이 높아진다. -> 유지보수하기 좋다!

 

 

5. DIP, 의존관계 역전 원칙

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

 

구현체에 의존하지 말고, 인터페이스에만 의존하라는 뜻이다.

 

비유하자면 다음과 같다.

로미오와 줄리엣이라는 공연을 올린다고 하자.

이 공연에서는 로미오와 줄리엣 역에 각각 3명의 배우가 배정되어있다. 번갈아가며 다양한 조합으로 무대에 올라간다.

그런데 로미오 역의 김철수라는 배우가, 줄리엣 역의 김미나라는 배우 한 명과만 연기 연습을 했다. 심지어 원래 정해진 대본은 무시해버리고 김미나와만 독자적으로 연습을 했다.

그럼 김철수는 김미나가 아닌 다른 줄리엣과는 연기를 할 줄 모르는 쓸모없는 배우가 되어버린다.

줄리엣이라는 상대역 인터페이스에만 의존했어야 한다. 김미나라는 구현체에 의존하지는 말았어야 했다.

 

앞에서 OCP를 설명할 때 예로 들었던 코드도 한 번 더 보자.

class X (...) {
	private final A a = new AOne();

	...
}
class X (...) {
	private final A a = new ATwo();

	...
}

 

이 코드는 DIP도 위반하고 있다.

 

인터페이스 A에만 의존하는 것이 아니라,

코드 상에서 자신이 사용할 구현체가 무엇인지를 명확히 적음으로써, 구현체에도 의존하고 있기 때문이다.

 

이 문제도 해결할 방법이 다 있다.

의존성 주입(DI)는 이 원칙을 따르는 방법 중 하나다.

DI의 사용은 스프링 입문 강의에서 보았던 것이다. 생성자가 외부에서 A의 구현체 객체를 주입받으면, X가 무슨 구현체를 사용할지는 X 코드 내에 전혀 명시하지 않아도 된다.

 

 

 

 

 

 

 

 

정리

객체 지향 프로그래밍의 핵심 성질은 다형성이다.

그러나 예시 코드에서 보았듯이 다형성만으로는 OCP와 DIP를 만족할 수 없다.

다섯 가지 원칙을 모두 만족하려면 무언가 더 필요하다.

그 무언가를 스프링이 제공해줄 것이다.

스프링을 사용하면, 마치 레고 블럭을 갈아끼우듯이 각 요소를 편리하게 넣었다 뺐다 하는 진정한 객체 지향 프로그래밍을 할 수 있다.