SOLID 원칙은 객체지향 프로그래밍(OOP) 및 설계의 다섯 가지 기본 원칙이다. 반드시 해야한다는 것도 아니고, 하기도 어렵지만 이렇게 만들었을 경우, 유지보수와 확장이 쉬운 시스템이 만들어진다고 한다. 물론 초보 중의 왕초보인 필자로써는 못하는게 당연하지만 연차가 쌓여가면서 이런 효율적인 시스템을 만드는 방법을 자연스럽게 깨우치게 되는데 그 때 도움이 될 수도 있으니 정리해보자.
객체지향을 잘하는 법은 SOLID를 잘하는 것이고, SOLID를 잘하는 법은 스프링을 하는 것이라고 한다.(아직은 이해못함)
각 SOLID는 크게 분류와 교체로 나눠볼 수 있다.(반드시 맞는다는 것은 아니고 이해를 돕기 위한 분류)
1. SRP(분류) : 단일 책임 원칙(Single Reponsibility Principle)
SRP는 한 클래스는 단일(하나)의 책임만을 가져야 한다는 것을 의미한다. 여기서 하나의 책임을 가져야 한다는 말의 의미가 잘 이해가 안갈 수 있다.
예를 들어, 처음 프로젝트를 시작해서 클래스를 만들었을 때는 클래스에 메소드가 몇개 없다. 하지만 시간이 지나 프로젝트 규모가 커지면서 팀원들이 들어오고 함께 작업을 하다보면 처음에는 괜찮았던 한 클래스 안에 많은 메소드가 들어오면서 해당 클래스 하나가 다양한 역할을 수행하게 된다. 그렇게 되면, 서로 다른 부분을 개발하던 팀원들끼리 같은 부분을 수정하거나 건드리는 경우가 생기게 되어, 충돌(conflict)이 일어나게 되고, 이는 나중에 프로젝트 병합(merge)을 진행할 때 심각한 문제를 초래할 수 있게된다.
그래서 하나의 클래스가 다양한 역할을 하지 않고 하나의 클래스는 하나의 역할만을 맡도록 처음엔 하나의 클래스에서 진행하던 것도 클래스를 나눠서 단일화를 시켜줌으로써 충돌을 방지하자는 것이다.
즉, 충돌을 방지하기 위해 각 클래스는 단일의 책임만을 가지게 해주자는 것이다.
2. OCP(교체) :단일 책임 원칙(Open Closed Principle)
OCP는 확장에는 열려있고, 변경에는 닫혀있다는 원칙이다. 보통 원래 클래스에서 수정하거나 변경해야할 것이 생겼을 경우, if-else문을 넣어서 수정하지만 이런 수정(변경)을 하지말고, 새로운 클래스를 만들어서(확장) 진행하라는 뜻이다.
이것도 말이 좀 어려운데, 예외케이스, 즉 if-else를 반복적으로 사용하여 처리해야 하는 일이 많아지는 경우, 이걸 인터페이스로 만들고 확장해서 관리하면 좀더 편하다는 뜻인 것 같다.
해당 클래스의 기존 동작을 변경하지 않고 클래스의 동작을 확장하는 것을 목표로 한다. 그리고 유지 관리 및 수정이 쉬운 코드를 작성하는데 필수적이다. 하지만 OCP에 대한 과도한 설계는 불필요한 복잡성을 유발하기 때문에 적절히 사용해야 한다.
3. LSP(분류) : 리스코프 치환 원칙(Liskov Substitution Principle)
LSP는 치환, 교체를 쉽게 하기 위한 원칙이다. 서브타입은 언제나 기본타입으로의 교체가 가능하게 만들어야 한다는 뜻이다.
예를 들어, 자동차를 부모 클래스로 해서 자동차, 경찰차 이런건 만들 수 있지만 헬리콥터는 만들 수 없듯이 상속받은 클래스는 부모 클래스와 동일한 동작을 해야 재활용 가능성이 높아진다는 의미이다. (부모타입을 인터페이스처럼 생각하라는 뜻이다) 즉, 상속받은 클래스는 항상 부모 클래스로 교체할 수 있는 가능성을 항상 열어둬야한다는 뜻이다. (재활용성이 떨어지고, 클래스 사용에 혼란을 줄 수 있기 때문이다)
현업에서는 여러 이유로 상속을 많이 쓰지는 않기 때문에 상속보다는 인터페이스를 고려하고, 상속을 해도 이와 비슷하게 만들어야 교체가 쉽다.
부모클래스를 인터페이스로 추출해서 껍데기를 만들어놓고 부모 클래스 자체도 해당 인터페이스를 구현한 것처럼 만들기도 한다고 한다.
4. ISP(분류) :인터페이스 분리 원칙(Interface Segregation Principle)
ISP는 인터페이스도 단일 책임을 갖도록 분리해야 한다는 뜻이다. 이것도 SRP와 같이 너무 큰 메서드를 만들면 빈 메서드를 만드는 경우가 발생한다고 한다. 필요한 기능만 구현하도록 만들라는 뜻이다. 케이스별로 나눴는데 사용하지 않는 빈 메서드가 생길 수가 있는데 이런 메소드를 과감하게 잘라버려야 한다는 것이다. 인터페이스를 잘게 나눠서 사용하는 기능만 구현하여 제공한다는 뜻이다.
극단적으로 볼 때 하나의 메소드만 들어있는 인터페이스도 있으니 괜찮다고 한다.
인터페이스도 OCP를 따라야 구현이 편리하고 재활용성이 올라간다.
5. DIP(분류) :의존성 역전 원칙(Dependency Inversion Principle)
DIP는 하위 모듈의 변경이 상위 모듈의 변경을 요구하는 의존성을 끊어내야 한다는 뜻이다.
개발을 하다보면 내가 사용하던 라이브러리를 다른 라이브러리로 변경하면 다 뜯어고쳐야 하는 경우가 있는데 그렇게 라이브러리에 직접적으로 의존하면 교체가 어렵다는 의미이다. 그런식으로 라이브러리에 극단적으로 의존하면 유지보수가 어렵다고 한다.
하위 모듈이 상위 모듈에 의존하도록 만들게 되면 이런 문제를 해결할 수 있다고 한다. 이게 어떻게 가능하냐면 인터페이스를 만들어서 모두가 그쪽(인터페이스)을 다 바라보도록 만들어주는 것이다. 어뎁터도, 서비스도 인터페이스를 기준으로 구현해주면 된다. 이렇게 되는 경우, 라이브러리 변경이 필요할 때 어뎁터 부분만 바꿔주면 된다고 한다.
이렇게 하지 않을경우, 서비스부터 어뎁터까지 일일이 다 변경해줘야한다.
이런식으로 외부 라이브러리에 의존하는 것을 의존성 역전 원칙을 지켜주면 쉽게 변경이 가능하다고 한다. 약간이라도 변경 가능성이 있다면 이렇게 하는 것이 좋다고 한다.
하위 모듈에 너무 의존하면 변경이 어려움, 중간 IF(인터페이스)를 둬야 하위 모듈 변경이 쉽다.
억지로 적용하려 하지말고 시스템이 성장하면서 복잡해질 때 적용해보는 것이 좋다.
이외에도 개발쪽에는 여러 원칙들이 있다고 한다.
KISS(Keep it Simple Stupid!) - 코드를 단순하게 해라.
YAGNI(You Ain't Gonna Need it) - 안쓰는건 지워라.
개발자로써 성장하다보면 또 접할일이 생기지 않을까 싶다.