개념지식

객체 지향 설계의 5원칙 : SOLID

GRITZ 2021. 11. 9. 21:12

최근 Spring Boot에 대한 강의를 수강하면서 동시에 여러 객체 지향 설계를 위한 중요한 개념들을 같이 배우고 있어서 기분이 좋다. 퀄리티 높은 인터넷 강의를 들으면서 코드를 어떻게 설계해야될지에 관한 생각도 많이 들고 있다.

 

객체지향 언어를 사용한다면 한번 쯤은 들어봤을 SOLID 5원칙을 복습 겸 정리해보려고 한다.

 

SOLID는 로버트 미틴이 2000년 대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더사가 두문자어 기억술로 소개한 것이다... 라고 위키에 쓰여있다. SOLID가 등장하게된 이유는 한마디로 유지보수를 쉽게하기위해서다. 다른 개발자가 나의 개발 코드를 보더라도 쉽게 이해할 수 있고, 문제가 있는 부분을 바로 수정할 수 있도록 코드를 설계하는 데 필요한 원칙을 5가지로 명명한 것이다. 

 

SOLID에 대해 알아보기 전에, 역할과 구현이라는 개념을 먼저 정의하고 넘어가려고 한다. 객체 지향의 특징인 추상화,상속, 캡슐화, 다형성은 객체지향언어를 공부한 사람이라면 대부분 알고 있을 것이다. 여기서 주목해야할 부분은 '다형성'이다. 다형성은 객체지향 프로그래밍의 핵심 기능으로, 레고를 조립하듯이 유연하고 빠르게 변경할 수 있다는 장점을 가지고 있다. 

 

다형성은 역할과 구현을 통해서 이야기할 수 있는데, 예를 들어 우리가 벤츠 자동차를 샀다고 생각해보자. 벤츠를 1년동안 탔는데 질려서 BMW 자동차를 구매했다. 그렇다고 운전자가 BMW 자동차를 타기 위해 운전을 다시 배울 필요는 없다. 그냥 벤츠자동차를 탔던 것 처럼 BMW 자동차를 타고 운전을 하면 된다. 여기서 자동차는 역할, 벤츠와 BMW는 구현이라고 볼 수 있다.

 

다시 객체지향으로 돌아와보자, 자동차는 역할이고, BMW, 벤츠자동차는 구현이다. 이를 인터페이스와 클래스에 빗대어 표현할 수 있다. 자동차는 인터페이스이고, 차체인 BMW, 벤츠는 자동차를 상속받은 구현클래스이다. 운전자가 자동차를 사용하는 방법을 알고 있다면, 즉 자동차의 역할을 알고 있다면 구현체인 벤츠나 BMW 어떤 것을 사용하더라도 문제없이 사용이 가능하다. 역할을 알고 있기 때문에 어떤 구현체가 오더라도 사용이 가능하게 되는 확장성 면에서 큰 장점을 가지게 되는 것이다.

 

이를 토대로 SOLID 원칙을 알아보자.

 

 

위키백과에 나와있는 SOLID

 

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

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

말 그대로 하나의 클래스는 하나의 책임을 가진다. 근데 여기서 책임의 범위가 모호하다. 얼마나 책임을 져야하는지가 문맥과 상황에따라서 달라질 수 있기 때문이다.

책임의 범위기준은 '변경'이다. 만약 기능을 변경할 일이 생겼을 때 파급혀과가 적으면 이 원칙을 잘 따른 것이다.

 

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

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

말로만 보았을 때는 이해가 잘 되지 않을 것이다. 확장을 하기위해서는 변경이 필요할텐데, 도대체 무슨 소리인지를 모르겠다.

아까의 역할과 구현을 설명했을 때로 돌아가보자. 자동차라는 역할이 있고, 디젤차라는 구현체가 있다. 만약 디젤차를 전기차로 바꾼다고해서 운전자가 운전을 못해서 운전을 다시 배워야할까? 아니다. 운전자에게 영향을 미치지 않고 운전자는 전기차를 바로 운전할 수 있을 것이다.

예시를 하나 더 들어보자, 공연을 한다고 했을 때, 남주인공과 여주인공이 있는데 남주인공 배역이 갑자기 일이 생겨서 공연을 할 수 없게 되었다. 그렇다고 공연을 아예 못하는 것이 아니다. 다른 배역을 찾아서 남주인공 역할을 수행하게 하면 된다.

 

위의 예시가 OCP이다. 자동차가 가지고 있는 역할을 변경하지않고 다형성을 적극적으로 활용하여 확장을 했을 뿐이다. 덕분에 사용자는 역할만 알고 있다면 구현체를 사용하는데 전혀 이상이 없다.

 

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

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

인터페이스에는 기능을 설계할 때 기능이 어떤식으로 사용되야하고, 어떤 의도로 만들어진 기능인지에 대한 규약을 세워야한다. 자동차 엑셀의 기능을 생각해보자. 엑셀은 사용자가 밟으면 차가 앞으로 나가고, 앞으로 나가도록 의도하여 만들어진 기능이다. 하지만 엑셀을 밟으면 뒤로 가도록 기능을 구현하더라도 딱히 문제가 발생하지는 않는다. 코드상으로만 보면 말이다.

하지만 우리가 생각하는 엑셀은 차가 앞으로 나가게 하기위한 장치이지, 뒤로가도록 의도하여 설계하지는 않은 기능이다. LSP는 인터페이스가 기능에 대한 규약에 대해 보장이 되어있어야한다는 원칙을 말하는 개념이다.

 

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

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

한 인터페이스에서 담당하는 기능의 범위가 커지면 일부분을 바꾸기 위해 여러가지 작업을 해야한다. 이는 효율성의 저하로 이어진다. 따라서 각 기능의 범위별로 인터페이스를 분리한다면 인터페이스가 명확해지고, 대체 가능성도 높아질 뿐더러, 서로의 인터페이스에 영향을 주지 않게된다. ISP는 이 원칙을 따라야한다는 것을 의미한다.

 

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

프로그래머는 추상화에 의존해야한다. 구체화에 의존해선 안된다.

구현 클래스에 의존하면 안된다. 인터페이스에 의존해야한다. 

운전자가 자동차에 가지고 잇는 역할, 기능에 대해서 알아야지 BMW 기종만 잘 알고 있따면 다른 차종을 운전하기 어려울 것이다. 자동차의 역할을 모르기 때문이다.

공연도 마찬가지이다. 배우가 공연 내용과 대본에 대해서 알고 있어야하는데 상대 배우에 대해서만 알고있으면 자신의 역할에 대해서 알지 못하므로 성공적인 공연이 될 수 없을 것이다.

즉, DIP는 클라이언트가 인터페이스, 역할에 의존해야 유연하게 변경과 확장이 가능해진다는 의미를 가지고 있다.

 

객체지향의 핵심은 다형성이지만, 다형성만으로는 쉽게 부품을 갈아 끼우듯 개발하기가 어렵다. 이 말은 OCP와 DIP 원칙을 준수하기가 어렵다는 뜻이다. 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경되고, SOLID 원칙의 위반을 막을 수 없다.

 

이러한 SOLID 원칙을 위반하지않고 웹개발을 할 수는 없을까? 에서 나오게된 프레임워크가 바로 Spring이다. Spring은 다양한 의존관계를 만들어 줄 수 있는 기능들을 제공하기 때문에 개발자 입장에서 SOLID 원칙을 위반하지않고 개발을 진행할 수 있게 된다.