| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- 언어
- 로그인정책
- 대장동
- 장소의명사
- 설계원칙
- sim 스와핑
- 정교유착
- 스마트국민제보
- rdb
- Clean Architecture
- 양쪽 맞춤
- 정치와종교
- 저면관수
- 해킹 사건
- 안전신문고
- 법정명
- 아랍어
- 유심 해킹
- 클린아키텍처
- 두음현상
- Typesetting
- bpfdoor
- 카시다
- 손현보목사
- 식물집사
- 두음규정
- 언어와 권력
- 의무이행심판
- 법률상이익
- 통신사 보안
- Today
- Total
그루터기
Clean Architecture 스터디: 3부 설계 원칙 (SOLID 원칙) 본문
팀에서 로버트 C. 마틴의 <Clean Architecture>를 가지고 스터디를 하고 있습니다. 책을 읽어 나가면서 내용과 감상을 정리해 보려고 합니다.
클린 아키텍처
“살아있는 전설이 들려주는 실용적인 소프트웨어 아키텍처 원칙”소프트웨어 아키텍처의 보편 원칙을 적용하면 소프트웨어 수명 전반에서 개발자 생산성을 획기적으로 끌어올릴 수 있다. 《
book.naver.com
제3부: 설계 원칙
제3부에서는 소프트웨어를 설계할 때 따라야 할 원칙 다섯 가지를 설명하고 있습니다. 다섯 가지 원칙은 다음과 같습니다.
- 단일 책임 원칙 (SRP: Single Responsibility Principle)
- 개방-폐쇄 원칙 (OCP: Open-Closed Principle)
- 리스코프 치환 원칙 (LSP: Liskov Substitution Principle)
- 인터페이스 분리 원칙 (ISP: Interface Segregation Principle)
- 의존성 역전 원칙 (DIP: Dependency Inversion Principle)
이 다섯 가지 원칙은 영어 이름의 머릿글자를 따서 'SOLID'라고 부릅니다. SOLID는 '중간 수준'의 소프트웨어 구조를 설계하는 원칙입니다. 즉, 클래스와 모듈 수준에서 소프트웨어가 잘 만들어질 수 있도록 합니다. 하지만 이 책은 조금 더 거시적인 아키텍처를 다루는 책이기 때문에, 상대적으로 미시적인 수준에 관한 SOLID는 아주 간단하게만 다루어지고 있습니다. 자세한 내용은 같은 저자의 『클린 소프트웨어』에서 설명하고 있는 것 같습니다.
클린 소프트웨어
소프트웨어 개발을 위한 사려 깊은 조언으로 가득한 책! 《클린 코드》를 비롯한 베스트셀러 저자이자 소프트웨어 개발 전문가로 저명한 로버트 C. 마틴은 이 책을 통해 소프트웨어 개발자, 프
book.naver.com
단일 책임 원칙 (Single Responsibility Principle, SRP)
책에서도 이야기하다시피, 이 원칙은 그 의미가 자주 오해되는 것 같습니다. ‘단일 책임 원칙’이라는 이름에서 직관적으로 생각할 수 있는 의미는 ‘하나의 클래스/모듈은 하나의 역할(책임)만 가져야 한다’입니다. 그래서 클래스의 크기를 작게 유지해야 한다거나, 이질적인 두 종류의 함수를 같은 클래스에 배치하면 안된다는 주장의 근거로 사용되기도 했습니다. 저도 그런 식으로 이해했고, 인터넷을 찾아 보아도 그런 식의 설명이 많습니다. (사실 마틴의 전작인 『클린 소프트웨어』를 읽어보아도 이렇게 설명하는 듯합니다.) 하지만 『클린 아키텍처』를 읽으면서 제가 이해한 SRP의 의미는 조금 달랐습니다.
단일 책임 원칙은 콘웨이 법칙(Conway’s Law)의 따름정리(corollary)입니다. 콘웨이 법칙은 “소프트웨어의 구조는 그것을 설계하는 조직의 커뮤니케이션 구조를 따라간다”는 법칙입니다. 즉, 어떤 개발조직이 세 개의 팀으로 이루어져 있으면, 그 소프트웨어는 세 개의 컴포넌트 구조 혹은 3-tier 구조를 가지게 될 확률이 높다는 것입니다.
단일 책임 원칙은 이러한 개발 조직에서 각 팀이 하나의 컴포넌트만 책임져야 한다고 이야기합니다. 어떤 컴포넌트를 여러 팀이 책임진다면, 컴포넌트에 발생하는 변경이 여러 팀에 영향을 미친다는 의미이기도 합니다. 따라서 컴포넌트에 변경이 일어날 때마다 영향을 받는 모든 팀에게 확인을 받아야 하고, 결국 변화가 경직됩니다. 소프트웨어의 가장 큰 특징이자 가치는 '소프트'하다는 것, 즉 변경이 쉽다는 것이기 때문에, 소프트웨어의 변경을 방해하는 설계는 잘못된 설계입니다.
저는 이 원칙을 꼭 콘웨이 법칙을 따라 '개발 조직'으로 좁게 해석할 필요는 없다고 생각합니다. 소프트웨어는 개발 조직을 닮기도 하지만, 또한 소프트웨어를 사용하는 조직과 그 이해관계자의 관계를 반영합니다. 어떤 시스템을 두 개 이상의 팀에서 사용한다면, 당연히 각 팀에 관련된 컴포넌트는 분리되어야 하며, 각 컴포넌트의 변경의 원인은 하나의 팀에만 존재하도록, 즉 하나의 (사용자) 팀이 하나의 소프트웨어 컴포넌트를 '책임'지도록 하는 것이 바람직할 것입니다.
개방-폐쇄 원칙 (Open-Closed Principle, OCP)
개방-폐쇄 원칙이란, "확장에는 열려 있으되 수정에는 닫혀 있도록 소프트웨어를 설계하라"라는 원칙입니다. '확장에 열려 있다'는 말은 기능이 추가/변경될 수 있다는 의미입니다. 거시적으로는 서비스가 추가되는 것일 수도 있고, 미시적으로는 클래스에 기능이 추가되는 것일 수도 있습니다. 변경이 소프트웨어의 가장 중요한 가치라면, 확장에 열려 있어야 한다는 것은 당연한 말처럼 들립니다. 여기서 중요한 부분은 '수정에 닫혀 있다'는 점입니다. 기능에 확장이 일어나기 위해서 코드를 줄줄이 고쳐야 한다면 그것은 잘못 설계된 소프트웨어일 공산이 큽니다. 확장이 일어날 때 코드의 변경 범위가 최소화되디록 소프트웨어를 설계하라는 것이 OCP의 주요한 의미입니다.
OCP를 이해하기 위해서는 플러그인을 생각하면 좋겠습니다. 유럽 우주국(European Space Agency, ESA)에서 개발한 SNAP이라는 소프트웨어는 위성 데이터를 읽고 해석하고 가공하기 위한 전반적인 프레임워크와 도구들을 제공합니다. 바이너리 형태의 위성 영상을 읽어들이고, 적당한 보정 계수를 사용하여 영상을 보정하고, 지리 정보를 사용하여 여러 영상을 오버레이하고, 필요한 영역을 리샘플링하는 등의 작업이 가능합니다. SNAP의 특징 중 하나는 사용자가 개발한 플러그인을 SNAP 위에서 사용할 수 있다는 점입니다. 예를 들어, SNAP에서 원래 지원하지 않는 형식의 위성 데이터를 읽기 위해서는 해당 형식을 읽어들이는 모듈을 플러그인 형태로 개발한 다음, SNAP에서 플러그인을 추가하면 됩니다. 사용자는 SNAP을 다시 컴파일할 필요 없이 즉시 SNAP의 기능을 '확장'할 수 있습니다.
SNAP – STEP
A common architecture for all Sentinel Toolboxes is being jointly developed by Brockmann Consult, SkyWatch and C-S called the Sentinel Application Platform (SNAP). The SNAP architecture is ideal for Earth Observation processing and analysis due to the foll
step.esa.int
이처럼 변경을 최소화하면서 확장이 이루어질 수 있는 것은 SNAP이 OCP를 따라 설계·개발되었기 때문입니다. 이를 위해 SNAP은 SNAP의 플러그인을 개발하기 위한 일종의 '약속'을 플러그인 개발 지침이라는 문서로 제공합니다. 여기에는 플러그인 모듈의 이름을 표시하는 방법부터, SNAP의 영상이 모듈에 전달되는 프로토콜, 오류 처리 방법 등이 나와 있습니다. SNAP의 기능을 확장하고 싶으면 개발 지침에 따라 플러그인을 개발하여 SNAP에 연결하면 됩니다.
OCP는 더 미시적인 수준에서도 유효합니다. 클래스나 모듈 또한 기능이 쉽게 추가되고 변경될 수 있도록 하기 위해서 OCP를 따라야 합니다. 이때도 SNAP의 플러그인과 마찬가지로 '약속'을 정하고, 이 약속에 따라 기능이 확장될 수 있도록 합니다. 미시적인 수준에서는 인터페이스 혹은 추상 클래스가 그 역할을 합니다. 따라서, OCP를 잘 준수하는 설계란, 변경이나 확장의 여지가 있는 부분에 인터페이스를 사용하여, 그 인터페이스에 따라 구현된 구현부라면 얼마든지 쉽게 추가될 수 있도록 하는 설계를 의미합니다.
리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
세 번째 원칙인 리스코프 치환 원칙은 어떤 객체는 그 객체를 상속받는 객체로 치환될 수 있어야 한다는 원칙입니다. 즉, 어떠한 경우에 상속이 가능한가를 정하는 원칙이라고 할 수 있습니다.
책에서도 워낙 짧게 다루고 넘어가는 부분이라, 책의 예제만 짧게 소개하겠습니다. 대표적인 예가 정사각형-직사각형 문제입니다. 수학적으로 정사각형은 직사각형입니다. 따라서 정사각형을 나타내는 클래스 Square가 직사각형을 나타내는 클래스 Rectangle을 상속하도록 구현하는 것은 크게 이상하지 않습니다.
하지만 Rectangle에 SetWidth()와 SetHeight() 함수가 있다면 이야기가 달라집니다. SetWidth() 함수는 (당연하게도) 사각형의 높이를 변경시키지 않아야 합니다. 하지만 Square 클래스는 항상 너비와 높이가 같아야 하기 때문에, SetWidth()가 높이도 변경하게 됩니다. 따라서 Rectangle 객체를 쓰던 클라이언트 코드가 SetWidth() 함수를 호출하면서 당연히 높이가 변경되지 않을 거라고 기대했다면, 같은 자리에 Square 객체를 쓸 수 없습니다. 리스코프 치환 원칙에 따라서, Square 클래스는 Rectangle 클래스를 상속해서는 안 됩니다.
코드를 작성하다 보면 처음에는 당연하고 명확해 보였던 상속 관계가 이상한 분기와 예외 처리로 지저분해지는 걸 볼 때가 있습니다. 이럴 때 상속 관계에 대한 판단을 하기 위해 LSP는 좋은 기준이 될 것 같습니다.
인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
네 번째 원칙은 인터페이스 분리 원칙입니다. 인터페이스 분리 원칙은 쉽게 말해 분리할 수 있는 인터페이스는 분리해야 한다는 것입니다. 어떤 클래스를 여러 클라이언트 코드에서 사용한다고 해 봅시다. 만약 클라이언트 코드들이 동일한 함수들을 사용한다면 이 클래스의 인터페이스는 분리될 필요가 없습니다. 그러나 어떤 클라이언트 코드 Foo()는 함수 A()와 B()만 사용하고, 다른 클라이언트 코드 Bar()는 함수 C()와 D()만 사용한다면, A()와 B()를 포함하는 인터페이스를 하나 만들고, C()와 D()를 포함하는 인터페이스를 하나 만들어서 두 클라이언트 코드가 서로 다른 인터페이스를 통해 이 객체를 참조하도록 하는 것이 좋습니다. 이렇게 쪼개진 인터페이스를 역할 인터페이스라고 부릅니다. 클래스의 일부 역할만을 모아 인터페이스로 노출시켰다는 의미입니다.
인터페이스 분리 원칙의 주된 목표는, 다른 원칙들과 마찬가지로, 불필요한 의존성을 끊는 데에 있습니다. 만약 Foo()에서 A(), B(), C(), D() 모두를 노출하는 인터페이스를 사용한다면, 사용하지도 않는 C()와 D() 함수에도 의존성이 생깁니다. C() 함수의 명세에 변경이 생기면 인터페이스가 변경되고, Foo()도 영향을 받게 됩니다. 뿐만 아니라, Foo()를 테스트하기 위해 스텁 코드(stub code)를 만든다고 상상해 봅시다. Foo()에서는 A()와 B()만 사용하기 때문에 이 두 함수에 대한 스텁만 구현하면 충분하지만, 인터페이스에 C()와 D()가 포함되어 있기 때문에 불필요하게 C()와 D()에 대한 코드를 작성해야 합니다.
최근 리팩토링을 진행하면서 인터페이스를 분리한 경험이 있습니다. 특정한 위성과 지표 위의 피사체 간의 기하적인 관계를 모델링하고 계산하는 역할을 하는 클래스가 있었습니다. 이 클래스는 위성 기하를 모델링하기 위한 목적으로 작성되었기 때문에, 상당히 방대한 역할을 하고 있었습니다. 어떤 함수는 특정 시각에 위성이 지향하고 있는 지표 상의 지점을 찾는 역할을 하고 있었고, 어떤 함수는 위성의 실질 속도를 계산하는 역할을 하고 있었습니다. 두 함수는 밀접하게 연관되어 있기는 했지만, 서로 다른 클라이언트 코드에서 사용되고 있었습니다.

설명을 위해서 크게 단순화시켰지만, 실제로는 대여섯 개 정도의 역할을 가진 거대한 클래스/인터페이스를 수많은 클라이언트 코드에서 참조하는 구조였습니다. 이런 구조 하에서 불필요한 의존성이 크게 늘어난 건 둘째치고, IGeometry 인터페이스 자체가 커다란 블랙박스처럼 여겨져서 정확히 이 인터페이스가 어떤 역할을 수행하는지 파악하고 있는 사람이 아무도 없을 지경이었습니다. 테스트가 거의 불가능한 건 당연했습니다.
이런 문제를 해결하기 위해, ISP에 따라서 인터페이스를 두 개(실제로는 n개)로 분리하였습니다.

의존성 역전 원칙 (Dependency Inversion Principle, DIP)
소프트웨어 설계에서 의존성은 변경을 방해합니다. A가 B에 의존할 때 (A → B) B가 변경되면 A의 변경도 불가피합니다. 그래서 함부로 B를 변경할 수 없게 되고, 소프트웨어는 경직됩니다. 만약 A가 훨씬 자주 변경되고 B가 변경되는 경우는 상대적으로 적다면, 이런 설계는 큰 문제가 없습니다. 따라서, 의존성 역전 원칙은 의존의 대상이 되는 B의 변동 가능성을 줄여야 한다고 이야기합니다.
일반적으로, 구체 클래스(concrete class)는 추상 클래스(abstract class) 혹은 인터페이스에 비해서 변동 가능성이 큽니다. 따라서, DIP의 일반 원칙은 "모든 의존성은 추상 클래스를 향해야 한다"라고 말합니다. 책에서는 이렇게 설명합니다.
자바와 같은 정적 타입 언어에서 이 말은 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻이다. 구체적인 대상에는 절대로 의존해서는 안 된다.
하지만, 구체 클래스라고 하더라도 변동 가능성이 현저히 낮은 경우에는 의존의 대상이 되어도 괜찮습니다. string 클래스가 대표적인 예로 소개되고 있습니다. 결국 DIP의 궁극적인 목적은, 의존성의 방향이 변동 가능성의 크고 작음에 의해 결정되도록 하는 것입니다.
'개발' 카테고리의 다른 글
| SK텔레콤 유심 정보 유출 사건 정리: 무엇이 문제였고, 어떻게 대응해야 할까? (0) | 2025.04.27 |
|---|---|
| MySQL REPLACE와 ON DUPLICATE KEY UPDATE 구문 (0) | 2023.01.03 |
| Wrap-around된 수열을 정렬하는 방법 (0) | 2021.07.30 |
| Clean Architecture 스터디: 1, 2부 (0) | 2021.05.05 |