SOLID
꼭 따라야하는 원칙은 아니지만, 이 원칙들을 따랐을 때, 코드 변경에 따른 리소스 소모를 줄일 수 있습니다. 변경사항, 기능 등이 적은 애플리케이션을 만들 때는 이런 원칙을 따르지 않는 것이 생산성이나 가독성에 더 좋을 수 있습니다.
Single Responsibility Principle(SRP)
하나의 모듈은 하나의, 오직 하나의 액터(actor)에 대해서만 책임져야 한다.
의미상 각 기능이 한 모듈에 있는 것이 어색하다면 처음부터 분리하는 것이 좋습니다. 하지만 개발하다보면 하나의 모듈로 만들어야 할 것 처럼 느껴지는 상황들이 있습니다.
상황 하나를 가정해보겠습니다.
- 의미상 각 기능이 한 모듈에 있어도 될 것 같은 모듈이 있다.
- 모듈에 다른 목적을 가진 A, B 액터가 의존하고 있다.
- A 액터가 자주 사용하는 함수, 테스트 코드가 있다.
- B 액터가 자주 사용하는 함수, 테스트 코드가 있다.
- 두 액터에 의해 사용되는 함수를 구성하기 위한 공통 함수가 있다.
이 상황에서 A 액터의 목적 변화에 따라 공통으로 사용되는 함수를 변경할 수 있습니다. 모든 테스트를 통과했지만 논리적으로 B 에게는 맞지 않는 상태가 될 수 있다는 문제를 가지고 있습니다. 그나마도 테스트 코드가 없다면, 문제가 있더라도 언어에 따라 그냥 지나갈 수도 있습니다.
이 문제를 해결하기 위해 공용 데이터 클래스가 있다면 분리해내고, 각 액터에 맞는 메서드만으로 구성된 모듈을 만들면 됩니다.
이 때 공통으로 사용되었던 함수는 각각의 모듈로 분리되면서 중복된 코드가 될 수 있습니다. 하지만 문제 상황으로 돌아가보면 언젠가 이 코드는 서로 다른 코드가 된다는 것을 알 수 있습니다. 어떤 코드를 진짜 중복으로 받아들일 지 일시적인 중복으로 받아들일 지 고민을 잘 해야합니다.
의미상 한 모듈에 있는것이 좋아보였다는 것은, 사용할 때, 한 모듈에 두고 사용하는 것이 편한 상황이 있었다는 뜻일 수 있습니다. 이 때 분리된 코드는 개발자가 쉽게 추적하지 못할 수 있습니다. 이런 경우에는 Facade 패턴을 사용하여 각 객체를 생성하고 요청된 메서드를 각 객체로 위임해주거나 실행 순서에 맞춰 호출만 해주는 클래스를 만들어 주면 됩니다.
또는 가장 중요한 액터를 위한 기능만 남기고 나머지 기능에 대한 퍼사드로 사용해도 됩니다. 이 때 분리되어 나온 기능은 서로 의존적이지 않도록 유지하는 것이 좋습니다.
Open-Closed Principle(OCP)
Classes should be open for extension, but closed for modification.
기능 추가를 위해 기존 코드 수정을 많이 해야한다면, 기능 추가에 큰 장벽이 됩니다. 이 문제를 해결하기 위해서는
- SRP를 지키면서 기능을 컴포넌트 단위로 분리한다. ex) View, Controller, ...
- 기능이 어떻게(how), 왜(why), 언제(when) 발생하는지에 따라 기능을 분리한다.
- DIP를 활용하여 컴포넌트간 의존 성을 한 방향으로 흐르도록 유지한다.
- A 컴포넌트의 변경으로부터 B 컴포넌트를 보호하려면 A 컴포넌 트가 B 컴포넌트에 의존해야한다.(A 컴포넌트가 B 컴포넌트를 사용)
- 도메인에서 중요한 로직, 정책 등을 담당하는 컴포넌트가 보호되도록 한다.
- 보호받는 컴포넌트가 상대적으로 고수준 컴포넌트이므로 저수준 컴포넌트는 고수준 컴포넌트에 의존해야한다.
- 저수준 컴포넌트가 고수준 컴포넌트 변경에 너무 취약하지 않도록 고수준 컴포넌트의 캡슐화 또한 중요하다.
- 입출력관련 컴포넌트는 저수준 컴포넌트로 만든다.(데이터의 흐름에 따라 수준을 결정하면 안된다)
컴파일 언어에서 컴포넌트 단위는 런타임에 플러그인 형태로 결합할 수 있는 동적 링크 파일에 대한 소스코드 묶음으로 볼 수 있습니다. 이러한 관점에서 SOLID원칙을 바라보면 컴파일 언어에서의 중요성과 인터프리터형 언어에서의 중요성이 조금 달라질 수 있습니다.
Liskov Substitution Principle(LSP)
If S is a subtype of T, then objects of type T in program P may be replaced with objects of type S without altering any of the desirable properties of that program.
프로 그램 P에서 사용할 수 있는 T와 S가 다른 인터페이스를 갖는다면 P에서는 if 문을 사용하여 T를 사용할 때와 S를 사용할 때를 구분해줘야 합니다. 하지만 같은 인터페이스를 가져도 상관 없다면 P의 수정 없이 S를 사용할 수 있습니다. 마찬가지로 같은 인터페이스를 갖지만 구현이 다른 새로운 타입 S1, S2, ...를 추가하여 P에 기능을 확장할 수 있습니다.
LSP에서 같은 인터페이스란 덕 타이핑을 쓴다면 같은 메서드 시그니처를 공유한다는 의미일 수도 있고, 같은 REST 인터페이스가 될 수도 있습니다.
Interface Segregation Principle(ISP)
Clients should not be forced to depend on methods that they do not use.
컴파일을 하는 지 안하는 지, 컴파일 방식은 어떻게 되는 지에 따라 영향을 받을 수도 있고 아닐 수도 있는 원칙입니다.
Dependency Inversion Principle(DIP)
High-level modules should not depend on low-level modules. Both should depend on the abstraction. Abstractions should not depend on details. Details should depend on abstractions.
운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 DIP를 적용하지 않아도 됩니다. 하지만 개발중인 코드는 언제 변경될 수 있을지 모르기 때문에 DIP를 사용하여 의존성 흐름을 관리하는 것이 좋습니다.
인터페이스와 구현체를 생각해보면 당연히 구현체의 변경이 잦을 수 밖에 없습니다. 따라서 아래와 같은 사항을 지키는 것이 좋습니다.
- 변동성이 큰 구현체를 참조하지말고, 추상 인터페이스를 참조한다.
- 변동성이 큰 구현체 클래스를 최대한 상속받지 않는다.
- 상속받더라도 구현체 함수를 오버라이드 하지 않는다. 필요하면 차라리 인터페이스를 공유하는 각각의 구현체를 만든다.
- 변동성이 크다면 어떻게 해서든 직접 사용을 피한다.
Reference
- Robert C. Martin, Clean Architecture(번역본)