이번 게시글에서는 객체지향 설계 5대원칙에 대해 정리합니다.
SOLID란
1980년대 유즈넷(구 페이스북)에서 설계 원칙을 토론하며 이들 원칙을 모으기 시작했습니다.
2004년 무렵 마이클 페더스가 해당 원칙을 재배열하며 각 원칙의 첫글자(SPR, OCP, LSP, ISP, DIP)를 따서
SOLID라는 단어를 만들며 탄생하게 되었습니다.
- 단일책임원칙(SPR: Single Response Principle)
- 개방-패쇄 원칙(OCP: Open Closed Principle)
- 리스코프 치환 원칙(LSP: Liskoc Substituion Principle)
- 인터페이스 분리 원칙(ISP: Interface Segregation Principle)
- 의존성 역전 원칙(Dependency Inversion Principle)
객체지향 설계원칙을 적용하면 변경 및 유지보수가 쉬워집니다.
단일책임원칙(SPR: Single Response Principle)
Conway 법칙에 따름 정리: 소프트웨어 모듈은 변경의 이유가 단 하나여야만 한다.
가장 의견이 분분한 원칙인것 같습니다.
애매모호한 이름에 걸맞게 해석하는 방향도 제각각인것 같습니다.
제가 읽은 아키텍쳐 책에서는 이렇게 해석해줬습니다.
해당원칙은 모듈이 단 하나의 일만 해야 한다는 의미가 아닙니다.(함수가 하나의 일만 해야한다는 원칙은 따로 있습니다.)
하나의 모듈은 오직 하나의 액터(사용자)에 대해서만 책임져야한다는 의미입니다.
단일 액터를 책임지는 코드를 묶어 응집성을 높이라는 의미이기도 합니다.
놀이공원를 설계하는데 Actor가 누구냐에 따라 클래스를 분리하라는 뜻입니다. (꼬마가 바이킹을 못타게 하는 규정을 추가하는데 회전목마까지 타지 못하게 되면 안됩니다.)
클래스 따로 분리해 버리거나 응집도를 높이고자하면 퍼사드를 구현하여 묶어버려야 합니다.
인터페이스 분리 원칙과 혼용되기도 하며 역할과 책임이 직접적으로 엮여 있으므로 어려운 원칙인 것 같습니다.
개방패쇄원칙(OCP: Open Closed Principle)
확장에는 열려있고 변경에는 닫혀있어야 한다는 원칙입니다.
행위를 확장하는건 되지만 산출물이 변경되지 않게 설계하라는 뜻 입니다.
사실 아키텍쳐를 공부하는 가장 근본적인 이유입니다.
OCP가 지켜지지 않는다면 요구사항이 살짝 변경되었는데 프로젝트를 갈아 엎어야 될 수도 있습니다.
언제 어떻게 왜 발생하는지에 따라 기능과 책임을 분리하고, 계층구조로 조직화 해야한다는 원칙입니다.
저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 원칙이며 처리 과정을 클래스 단위로 분할하고, 클래스는 컴포넌트 단위로 구분해서 구현하라는 원칙입니다.
말이 어렵지만
spring을 해봤거나 layered architecture에 대해 고민해봤다면 어렵지 않게 답을 얻을 수 있습니다.
표현계층(@Controller), 유스케이스(@Service) 계층 등으로 나누는건 컴포넌트를 나눈것이고(스프링에서 괜히 어노테이션 이름이 component가 아닙니다)
도메인별로 나눈것을 처리과정을 기능 단위로 나눠 책임을 분리시켰다고 생각할 수 있습니다.
또한 인터페이스는 열려있고, 구현체는 닫혀있다는것이 핵심입니다.
아키텍쳐의 기본은 구현체에 의존하지 않는것에서 시작되며 구현체에 의존하지 않으므로 고수준의 컴포넌트르 보호 할 수 있습니다.
spring은 태어날때부터 구현체에 의존하지 않게 만들어져있어서 spring만 사용한다면 크게 문제가 없으나(@Service계층, @Repository 계층 등 @Component들은 이미 인터페이스로 열려있습니다.)
spring처럼 block된 구조의 프레임워크를 사용하지 않는다면 Layered Architecture를 적용하여 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호해야 합니다.
리스코프 치환 원칙(LSP: Liskoc Substituion Principle)
리스코프 치환 법칙은 interface로 추상화된 컴포넌트를
치환할 수 있도록 설계하라는 뜻입니다.
이렇게만 보면 오해하기 쉽습니다.
interface에 구현체를 끼워 사용할때 애초에 interface에 method들을 구현하지 않으면
적어도 제가 본 모든 언어는 에러를 뱉었습니다.
아키텍쳐를 공부하는데 컴파일 에러 잡는방법을 배우는게 아닙니다.
interface의 메서드가 select일때 user구현체에서는 DB에서 select를 하지만
animal구현체로 갈아 끼웠을때에서는 DB에서 insert를 하면 말라는 뜻입니다.
인터페이스가 구현은 되있으니 컴파일에러는 나지 않겠지만 이런 설계를 하지 말라는 뜻입니다.
치환 가능성을 위배하면 시스템 아키텍처가 오염되어 무수한 2~3중 if문을 구현해야 할 지도 모릅니다.
인터페이스 분리 원칙(ISP: Interface Segregation Principle)
인터페이스를 분리하라는 원칙입니다.
말그대로이며 이해하기 쉽습니다.
ISP를 지키지 않았을때 발생할 수 있는 예시입니다.
최초에는 GateWay의 구현체가 DBMS일수도, 다른 API와의 통신일수도 있는걸 고려하여
다음과 같이 하나의 인터페이스에 설계한 듯 싶지만
추후 개발할때는 왜 이렇게 설계했는지 파악하지 못한 예 입니다. (주석처리된 부분은 추가된 부분)
interface GateWay{
getAllUser()
getUserById()
// getAllAnimal()
// getAnimalById()
}
주석처리된 부분은 추가된 부분으로
Animal관련 부분에서 오류가 발생하면 User 부분에도 영향이 생깁니다.
물론 작은 시스템이라면 상관없지만 관련 도메인이 20~30개가 되는순간
하나의 인터페이스에 메서드가 몇십 몇백개가 되는 경우도 충분히 생길 수 있습니다.
유지보수는 물론 테스트 코드를 작성 하기에도 굉장히 껄끄러워집니다.
아래와 같이 인터페이스의 분리를 해줘야 합니다.
interface UserGateWay{
getAllUser()
getUserById()
}
interface AnimalGateWay{
getAllAnimal()
getAnimalById()
}
의존성 역전 원칙(Dependency Inversion Principle)
추상에 의존한후 의존성을 외부에서 주입해 의존관계를 제어의 흐름과 반대로 가져가라는 의미입니다.
ocp와 함께 가장 기본이 되는 원칙입니다.
import, use, include 등은 구상화된 인터페이스를 불러올 때 사용하는것이지
구현체를 불러와 의존하기 위해 사용하는것이 아닙니다.
물론 운영체제나 언어 자체가 제공하는 안정성이 보장된 환경이라면 import 해도 상관없지만
현재 개발중인 구현체에 의존하게되면 요구사항이 변경됐을때 아규먼트가 급속도로 늘어나고 2~3중 if문이 생기게 될 것입니다.
프로필, 아규먼트, 환경변수 등을 적당히 활용하여 의존성을 역전시키면 요구사항의 변화에 유연하게 대처할 수 있습니다.
'개발상식' 카테고리의 다른 글
[개발상식] 데이터 모델링(스키마설계 하는법, DB설계 하는법) (6) | 2021.11.22 |
---|---|
[개발상식] tdd란 (tdd 예제, tdd하는법) (1) | 2021.11.16 |
[개발상식] 프로세스간 통신(IPC) (1) | 2021.08.25 |
[개발상식] 멀티 프로세스와 멀티 스레드(파이썬은 멀티스레드 언어일까?) (0) | 2021.08.17 |
[개발상식] 인코딩과 binary (0) | 2021.08.16 |
댓글