객체지향의 설계 원칙
우리가 좋은 코드를 작성하고자 하는 가장 큰 이유는 서비스를 잘 운영하기 위해서라고 생각합니다. 결국 돈과 관련되어 있기 때문에 유지 관리하기 쉽고 확장성을 위해 유연한 소프트웨어를 만들고자 하는게 아닌가 싶습니다.
많은 사람들이 객체지향을 하면서 좋은 코드를 작성하기 위해 항상 말하는 SOLID원칙에 대해서 알아보겠습니다.
SRP (Single Responsibility Principle : 단일 책임 원칙)
하나의 클래스는 한 가지 책임만 가져야 한다. 또한 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나
책임?
저는 책임이라는 말을 '행동' 이라는 단어에서 힌트를 얻었습니다. '너가 한 행동에 대한 책임을 가져라'와 같이 말이죠.
결국 여기서 말하는 책임은, 클래스가 어떤 동작(행동)을 하게 되었을 때 문제가 발생하여도 해당 부분만 찾아서 고치면 된다라고 생각했습니다.
예를 들어, 팀프로젝트를 하는데 팀원이(클래스) [자료 조사, 발표, 피피티, 설문조사, 통계]와 같이 파트를 나눠서 해온다고 하면, 각자 맡은 일(책임)이 있기 때문에 이를 수행해와야 합니다. 그래야 한 파트를 안해오면 어디가 잘못됐는지 알고 고치기가 쉽겠죠(유지보수) , 만약 팀원이 여러 책임을 가지게 된다면 고치기도 어려울 것입니다. 또한 설문 조사를 맡은 사람이 통계까지 한다고 했을 때, 한 책임을 변경하게 된다고 하면, 다른 책임까지 변경해야하는 연쇄작용이 발생할 수 있습니다.
OCP (Open Close Principle : 개방폐쇄의 원칙)
확장을 위해서는 열려 있어야 하지만 수정을 위해서는 닫혀 있어야 한다.
저는 최근 꾸삐라는 레고 유튜버를 보는데, 이 개방 폐쇄 원칙을 설명해주는 것이 레고의 브릭이라 생각합니다.
레고는 여러 브릭으로 어떤 모형을 만들 수가 있는데(확장성이 좋다), 만약 일반 브릭이 아니라 특수한 브릭이나 다른 3D프린팅으로 만든 브릭중 일부의 경우 사용할 수 있는 부분이 제한(낮은 결합도)되게 됩니다. 결국 브릭이 부서지거나 고장나도 다른 브릭으로 쉽게 대체가 가능하므로 유지보수성이 좋다.
LSP(리스코프 치환 원칙)
객체는 프로그램의 정확성을 깨지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

기본 A라는 클래스와 B라는 인터페이스가 있다하자. A와 B가 관계가 있을 때 B를 implment한 C나 D클래스로 관계를 변경했을 때에도 문제가 없어야 한다는 것이 LSP원칙이다. 결국 B인터페이스에 public으로 추상 메서드가 존재하여 공통의 메서드로 하나의 창구 역할을 해줘야 한다.
'상속을 통한 재사용은 기반 클래스와 서브 클래스 사이에 IS-A 관계가 있을 경우에만 가능하다. 그 외에는 합성을 이용한 재사용을 해야한다.' 는 결국 인터페이스와 구현체간 인과관계가 있어야 한다.
ISP(Interface Segragation Principle)
클라이언트가 사용하지 않는 인터페이스를 구현하도록 강요해서는 안 되며, 클라이언트가 사용하지 않는 메서드에 의존하도록 강요받아서는 안 됩니다.
인터페이스라는 것이 제 생각에는 내부적으로 본다고 하면, '내가 꼭 필요한 메서드는 알려줄게 이거는 무조건 사용하고 나머지는 알아서 해' 라는 느낌이 있습니다. 하지만 외부적으로 보면, 클라이언트와의 입장에서는 객체가 내부적으로 어떻게 구현되는지 모르기 때문에 자신의 기준으로 할 수 밖에 없다고 생각합니다.
예를들어, 은행에서 개발을 한다고 생각해보겠습니다. A는 DB에 커넥션을 하는 기능을 맡고 있고 B는 결제 모듈을 관리하고 있다고 할때, B가 트랜잭션이 끝난 후 DB에 업데이트 할때(클라이언트) A가 구현한 DB커넥션에 대해 모를 뿐만 아니라 알 필요도 없습니다. 따라서 B는 A가 사용하기 편하도록 인터페이스를 구현해야 합니다.
근데 문제는 이러한 클라이언트가 많다고 가정해보겠습니다. A뿐만 아니라 C, D, E도 있고 요구사항도 조금씩 다르다 한다면 이에 맞게 인터페이스를 구현해야하는데 이를 위해서 인터페이스를 분리하여 각 클라이언트에 맞게 세부적으로 나눠야 한다는 내용입니다. 만약 인터페이스를 분리하지 않게 된다면 , 즉 범용적으로 상요하게 한다면 모든 코드에 알맞게 하기 위해 원하는 정보들을 파라미터로 가져올 것이고, 이는 많은 파라미터로 인해 사용하기가 힘들다.
하지만 반드시 생각해야 할 것은 인터페이스를 변화하고 클래스를 변화한다고 했을 때 이를 통해 클라이언트 코드가 변해서는 안된다.
SPR와 같이 ISP또한 '단일 책임'에 대해 중점인 원칙입니다.
DIP(Dependency Inversion Principle : 의존관계 역전 원칙)
실체는 구체화가 아닌 추상화에 의존해야 합니다. 상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 되지만 추상화에 의존해야 한다.
예를들어, 이메일을 통해 메일을 받게 되는데 이메일이 Naver(구체화)에만 오는 것은 아니기 때문에 Naver, Gamil, Yahoo, Daum등 다양한 사이트의 이메일을 추상화하고 있는 이메일 추상클래스에 의존해야한다.
결국 인터페이스를 통해 추상화를 하여야 하위 수준의 모듈마다 대응하지 않아도 되며, 유지보수가 쉽다고 이해했습니다.
결론
사실 여러 사람들의 포스트나 의견들을 한번 봤었습니다. 전부 비슷하지만 조금씩 해석의 차이가 있었다 생각이 드네요.
저는 위에서도 언급했지만 결국 서비스를 유지보수하고 확장시키기 위해 위와 같은 SOLID라는 원칙을 내세웠다고 생각합니다.
SRP의 경우 하나의 책임만을 가지게 하여 유지보수를 쉽게 하고, 하나를 변경할 때 연쇄작용을 없앴습니다.
OCP는 유지보수와 확장성을 위한 전제 조건으로, 변경에는 폐쇄적이고, 확장에는 열려있어야 한다는 것이었고
LSP는 인터페이스를 통한 다형성으로 확장과 상속은 확장성과 재사용성이 있는데 재사용성의 경우 IS-A관계가 성립되어야만하고 만약 성립하지 않는다면 합성을 통해 재사용성을 늘려야한다.
ISP는 SRP와 같이 단일 책임에 대한 내용입니다. 다만 ISP는 클라이언트가 인터페이스의 세부적인 내용까지 알 필요가 없는 만큼 인터페이스는 클라이언트에 맞춰서 작성을해야하며, 인터페이스가 변경되거나 하위 클래스가 변경되더라도 클라이언트의 코드가 변경이 되면 안된다는 전제 조건이 있었습니다.
DIP의 경우 인터페이스를 통한 추상화를 해야 유지보수가 쉽다
라고 정리할 수 있을 거 같네요. 이에 대한 생각이 변할 때마다 지속적으로 업데이트 할 예정입니다.