이 글은 Clean Code, SOLID principle, 그리고 Clean Architecture의 핵심 개념을 이해하기 위해 작성되었습니다. 개인 학습 목적의 글이며 Gemin 2.5 Pro Deep Research를 사용한 결과를 필요한 부분만 정리하였습니다. 한 번 쭉 훑으며 내용을 수정하긴 하였지만 Deep Research의 결과물임을 감안하고 선별적으로 읽어주시기 바랍니다.
목차
-
Introduction: 왜 우리는 'Cleanliness'을 추구해야 하는가?
- 코드 가독성의 중요성
- 소프트웨어의 두 가지 가치
- 이 학습 가이드의 목표와 구성
-
Clean Code: 읽기 쉽고 유지보수하기 쉬운 코드 작성의 기술
- Meaningful Names: 코드의 의도를 명확하게
- Functions: 작게, 한 가지만, 그리고 명확하게 (Functions: Small, Do One Thing, and Clearly)
- Comments: 코드로 말하고, 필요할 때만 보충하라
- Formatting: 일관성과 가독성의 조화
- Objects and Data Structures: 올바른 균형 찾기
- Error Handling: 견고하고 예측 가능한 코드
- Boundaries: 외부 요소와의 깔끔한 상호작용
-
SOLID Principles: 견고하고 유연한 설계의 초석
- SRP (단일 책임 원칙 - Single Responsibility Principle)
- OCP (개방-폐쇄 원칙 - Open/Closed Principle)
- LSP (리스코프 치환 원칙 - Liskov Substitution Principle)
- ISP (인터페이스 분리 원칙 - Interface Segregation Principle)
- DIP (의존관계 역전 원칙 - Dependency Inversion Principle)
-
Clean Architecture: 시스템의 의도를 드러내는 설계
- 아키텍처의 본질: 도구를 넘어선 개념
- Use Case-Driven Architecture
- 의존성 규칙과 아키텍처 경계
- 주요 결정 지연의 가치
- 프로그래밍 패러다임의 역할
1. Introduction: 왜 우리는 'Cleanliness'을 추구해야 하는가?
1.1. 코드 가독성의 중요성
코드는 작성되는 시간보다 읽히는 시간이 훨씬 많습니다. 명확하고 이해하기 쉬운 코드는 동료 개발자, 미래의 자신, 그리고 유지보수 담당자 모두에게 중요합니다. 깨끗한 코드는 숙련된 엔지니어의 산물이며, 시스템 유지보수성을 극대화합니다. 가독성 높은 코드는 버그 수정, 기능 추가/변경을 용이하게 하여 팀 전체 생산성을 향상시키고, 신규 팀원의 적응을 돕습니다.
1.2. 소프트웨어의 두 가지 가치
소프트웨어는 두 가지 가치를 지닙니다.
- 행위 (Behavior): 현재 사용자의 요구사항을 충족시키는 기능 (부차적 가치)
- 변경 용이성 (Maintainability/Flexibility): 끊임없이 변화하는 요구사항과 환경에 유연하게 대응하는 능력 (일차적 가치)
대부분의 소프트웨어는 초기 요구사항은 잘 만족하지만, 시간이 지날수록 변경이 어려워집니다. 장기적 관점에서 '변경 용이성'을 확보하는 것이 프로젝트 성공의 열쇠이며, 깨끗한 코드는 이를 위한 가장 효과적인 수단입니다. 명확하고, 모듈화되고, 잘 구조화된 코드는 변경 비용과 위험을 줄여줍니다.
1.3. 이 학습 가이드의 목표와 구성
이 가이드는 깨끗한 코드 작성법, 견고하고 유연한 설계를 위한 SOLID 원칙, 시스템 의도를 명확히 드러내는 클린 아키텍처 구축 방법을 체계적으로 안내합니다.
- 제1부: Clean Code: 읽기 쉽고 유지보수하기 쉬운 코드 작성 기술
- 제2부: SOLID Principle: 객체 지향 설계의 핵심 원칙
- 제3부: Clean Architecture: 시스템 전체 구조와 의도를 명확히 하는 방법
2. Clean Code: 읽기 쉽고 유지보수하기 쉬운 코드 작성의 기술
클린 코드는 단순히 동작하는 코드를 넘어, 다른 사람이 쉽게 읽고 이해하며 수정할 수 있는 코드입니다.
2.1. Meaningful Names: 코드의 의도를 명확하게
변수, 함수, 클래스 등의 이름은 강력한 의사소통 도구이자 코드의 첫 번째 문서입니다.
- 검색 용이성:
MAX_CLASSES_PER_STUDENT
처럼 검색하기 쉬운 이름을 사용하세요. 숫자7
과 같은 매직 넘버는 검색을 어렵게 만듭니다. 예를 들어, API URL을/un/access
보다/unaccess
로 만들면unAccess
라는 변수명과 연관 지어 검색하기 쉽고, 이름 변경도 용이해집니다. - 명확성: 잘 지은 이름은 주석의 필요성을 줄이고 코드 이해도를 높입니다. 모호하거나 일반적인 이름(예: 단일 숫자 리터럴)은 유지보수를 방해합니다. 의미 있고, 구체적이며, 검색 가능한 이름 선택은 필수입니다.
2.2. Functions: 작게, 한 가지만, 그리고 명확하게
함수 설계는 코드 전체 품질을 좌우합니다.
[핵심 원칙: 크기, 단일 책임, 추상화 수준]
- 작게, 더 작게: 함수는 가능한 한 작게 만드세요. 이상적으로는 몇 줄 이내로 유지하고,
if
,else
,while
문 등의 블록은 한 줄짜리가 좋습니다. - 한 가지 일만 수행 (Single Responsibility): 함수는 이름 아래에서 추상화 수준이 하나인 단계만을 수행해야 합니다. 함수 내 모든 문장은 동일한 추상화 수준을 가져야 합니다. (예:
getHtml()
은 높은 수준,String pagePathName = PathParser.render(pagepath);
는 중간 수준,.append("\n")
는 낮은 수준). 여러 추상화 수준이 섞이면 코드를 이해하기 어렵습니다. - 함수 추출: 함수가 너무 길다면(예: 20줄 이상) 별도 클래스로 추출을 고려하세요. "Extract Till You Drop" 철학처럼 함수를 계속 분해하면 자연스럽게 단일 책임을 갖게 되고, 이름 짓기도, 테스트도 쉬워집니다. 이는 논리적 단위를 명확히 정의하는 과정입니다.
[함수 인수 가이드라인]
인수는 최소화하는 것이 이상적입니다.
- 이상적인 인수 개수: 0개(무항) > 1개(단항) > 2개(이항). 3개(삼항)는 피하고, 4개 이상은 특별한 이유 없이 사용하지 마세요. 인수는 인지적 부담을 가중시킵니다.
-
단항 함수 예시
- 질문:
boolean fileExists("MyFile")
- 변환 후 반환:
InputStream fileOpen("MyFile")
- 이벤트:
passwordAttemptFailedNtimes(int attempts)
(시스템 상태 변경)
- 질문:
- 플래그 인수 지양:
boolean
값을 넘기는 것은 함수가 여러 일을 한다는 신호이므로 피하고, 필요하면 함수를 분리하세요. - 이항 함수: 두 인수가 논리적 값 하나를 표현하고 자연스러운 순서가 있을 때 적절합니다 (예:
Point p = new Point(0,0)
).assertEquals(expected, actual)
처럼 순서가 자명하지 않으면 실수 유발 가능성이 있습니다. - 인수 객체: 인수가 2~3개 필요하면 독립 클래스(인수 객체)로 묶어 전달하는 것을 고려하세요. (예:
makeCircle(double x, double y, double radius)
대신makeCircle(Point center, double radius)
) - 입력 매개변수: 매개변수는 주로 입력(Innies)에 사용하고, 출력(Outies)을 위해 수정하는 방식은 지양하세요. 입력 매개변수 상태를 직접 변경하는 것은 좋지 않습니다.
각 인수의 목적이 명확해야 합니다. 플래그 인수나 출력으로 사용되는 인수는 함수 의도를 모호하게 만듭니다.
[부수 효과, 명령-조회 분리(CQS), Switch 문 처리]
- 부수 효과 (Side Effects) 최소화: 함수는 명시된 기능 외에 다른 일을 몰래 수행해서는 안 됩니다. (예:
checkPassword
함수가 내부적으로Session.initialize()
를 호출). 이는 시간적 결합(Temporal Coupling)을 유발할 수 있습니다. 함수 이름을 명확히 하거나 책임을 분리하세요. - 명령-조회 분리 (Command Query Separation, CQS): 객체 상태를 변경하는 함수(명령, Command)는 값을 반환하지 말고, 정보를 반환하는 함수(조회, Query)는 상태를 변경하지 마세요. (예:
set("username", "unclebob")
이boolean
을 반환하는 것은 CQS 위반. 대신if (attributeExists("username")) { setAttribute("username", "unclebob"); }
처럼 분리) - Switch 문 처리:
Switch
문은 작게 만들기 어렵고, 단일 책임 원칙(SRP)과 개방-폐쇄 원칙(OCP)을 위반하기 쉽습니다. 가능하면 다형성을 이용해Switch
문을 저수준 클래스에 숨기고 반복 사용을 피하세요. (예: 직원 유형별 급여 계산 시,Employee
추상 클래스와 각 유형별 구체 클래스의calculatePay
메서드, 추상 팩토리 사용)
[기타 함수 설계 원칙]
- 스텝다운 규칙 (Stepdown Rule): 코드는 위에서 아래로 이야기가 전개되듯 읽혀야 합니다. 공개 메서드는 상단에, 호출되는 비공개 메서드는 하단에 배치하여 중요 개념을 먼저, 세부 구현을 나중에 보여줍니다.
- 시간적 결합 (Temporal Coupling) 주의: 특정 순서로 호출되어야 하는 함수들(예:
open(), execute(), done()
)은 유연성을 저해하고 오류 가능성을 높입니다. 전략 패턴 등으로 순서 의존성을 관리하거나 제거하세요. - Tell, Don’t Ask: 객체에게 작업을 '명령(Tell)'하고, 객체 내부 상태를 '묻고(Ask)' 외부에서 결정하는 방식을 지양하세요. (예:
if(member.getExpiredDate().getTime() < System.currentTime())
대신if(member.isExpired())
) - 디미터 법칙 (Law of Demeter): "낯선 자에게 말하지 말라." 모듈은 직접 아는 이웃 객체와만 상호작용해야 합니다.
ctxt.getOptions().getScratchDir().getAbsolutePath()
같은 "기차 충돌(train wreck)" 코드는 피하세요. (단, ctxt, Options, ScratchDir이 단순 자료 구조라면 디미터 법칙이 적용되지 않을 수 있음) - 조기 반환 (Early returns): 조건 충족 또는 오류 발생 시 가능한 한 빨리 반환하여 코드 흐름을 명확히 합니다.
try
는 역할/함수:try
블록은 "예외 발생 가능 작업 시도"라는 하나의 역할을 나타내므로, 내부에 가급적 한 줄의 메서드 호출만 두어 명확히 합니다.
2.3. Comments: 코드로 말하고, 필요할 때만 보충하라
가장 좋은 주석은 주석이 필요 없도록 코드를 작성하는 것입니다. 코드 자체의 표현력을 강화하세요.
주석 있는 코드:
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
주석 없이 의도가 명확한 코드:
if (employee.isEligibleForFullBenefits())
후자가 훨씬 명확합니다. 닫는 중괄호 주석, 큰 배너 주석, 저자 정보 주석 등은 피해야 할 나쁜 주석입니다. 주석이 필요하다면 설명하는 코드에 최대한 가까이 위치시키세요.
주석은 때로 코드의 부족함을 나타내는 '냄새(smell)'일 수 있습니다. 주석 작성 전에 코드를 더 명확하게 개선할 방법을 먼저 고민하세요. '무엇을' 설명하는 주석은 종종 불분명한 코드를 가리기 위한 임시방편일 수 있습니다.
2.4. Formatting: 일관성과 가독성의 조화
일관된 코드 형식(코딩 컨벤션)은 의사소통의 일환이며 전문 개발자의 의무입니다. 코드 가독성과 일관된 스타일은 코드 품질에 지속적으로 영향을 미칩니다.
- 세로 공백: 논리적 코드 블록을 시각적으로 분리 (메서드 사이, 변수 선언부와 메서드 사이, 제어문 블록 주변 등)
- 세로 밀집도: 서로 관련된 코드는 세로로 가까이 배치하여 논리적 연관성을 표현합니다.
일관된 형식은 팀 생산성과 코드 품질에 기여합니다. 표준화된 형식(자동 포매터 도구 사용)은 마찰을 줄이고 로직에 집중하게 합니다.
2.5. Objects and Data Structures: 올바른 균형 찾기
[자료 추상화, 자료/객체 비대칭, DTO]
- 자료 추상화:
private
변수는 외부 의존성을 줄이기 위함입니다. 모든private
변수에public getter/setter
를 제공하는 것은 캡슐화를 퇴색시킬 수 있습니다. 자료를 세세하게 공개하기보다 추상적 개념으로 표현하세요. (예:getGallonsOfGasoline()
대신getPercentFuelRemaining()
) -
객체 vs. 자료 구조
- 객체: 내부 자료를 숨기고, 자료를 다루는 메서드만 공개.
- 자료 구조: 내부 자료를 그대로 공개하고, 별다른 행위(함수)는 제공하지 않음.
- 예시 (도형):
- 절차적:
Square
,Rectangle
,Circle
(자료 구조) +Geometry
클래스 (넓이 계산area()
함수). 새 도형 추가 시Geometry
의 모든 함수 수정 필요. - 객체 지향:
Shape
인터페이스 + 각 도형 클래스가area()
구현. 새 도형 추가는 용이하나, 새 함수(예:perimeter()
) 추가 시 모든 도형 클래스 수정 필요. - 상황에 맞게 분별력 있게 선택해야 합니다.
- DTO (Data Transfer Object): 공개 변수만 있고 함수가 없는 클래스. 데이터베이스 통신, 메시지 구문 분석 등에 유용. 빈(bean) 구조(private 변수 + public getter/setter)는 '사이비 캡슐화'일 수 있습니다. DTO에 DB 탐색 함수를 추가하는 활성 레코드(Active Record) 패턴은 잡종 구조를 만들 수 있어 주의가 필요합니다.
[클래스 응집도, Getter/Setter 사용]
- 높은 응집도 (Max Cohesive): 클래스 내 메서드들이 멤버 변수 대부분을 사용하며 밀접하게 관련되어야 합니다.
Getter/setter
자체는 응집도가 낮을 수 있습니다. - 무분별한
getter/setter
지양: "Tell, Don't ask" 원칙을 기억하세요. 과도한getter/setter
는 객체의 데이터를 가져와 외부에서 로직을 처리하게 만듭니다. 사용을 최소화하세요. - 자동차 객체의 연료량 정보를
gallonsOfGas
대신getPercentFuelRemaining
으로 제공하면 확장성 개선 및 의존관계 역전 원칙(DIP/IoC) 준수에 도움이 됩니다.
객체와 자료 구조 구분은 의도적이어야 합니다. 무분별한 getter/setter
는 객체를 자료 구조로 전락시킵니다. DTO는 명확한 '자료 전달' 목적을 가져야 합니다.
2.6. Error Handling: 견고하고 예측 가능한 코드
[예외 사용, Null 처리, 특수 사례 객체]
- 예외 사용: 오류 코드를 반환하는 것보다 예외(Exception)를 사용하는 것이 좋습니다. 오류 코드는 호출자에게 확인 및 분기 로직을 강요하고 중첩
if
문을 유발합니다. 예외는 정상 코드와 오류 처리 코드를 분리하여 깔끔하게 만듭니다.try/catch
블록 내용은 별도 함수로 추출하여 단순화하세요. null
반환 지양:null
반환은 호출자에게null
확인 작업을 떠넘깁니다. 확인 누락 시NullPointerException
발생 가능. 대신 예외를 던지거나 특수 사례 객체(Special Case Object/Null Object Pattern)를 반환하세요.null
전달 지양: API가 명시적으로null
을 기대하지 않는 한, 메서드에null
전달을 피하세요. 메서드 내에서null
매개변수 확인 후InvalidArgumentException
발생 또는assert
사용.Optional<T>
사용 주의:isPresent()
로null
체크처럼 사용하는 것은 큰 이점이 없을 수 있습니다.orElse(defaultValue)
나orElseThrow()
를 활용하세요. 컬렉션 반환 시Optional<Collection<T>>
보다 빈 컬렉션(Collection<T>
)을 반환하는 것이 간결합니다.null
이 오류가 아닌 경우: 컬렉션에서 요소 검색 실패 시null
반환은 유효한 결과일 수 있습니다.- 특수 사례 객체: 크기 0인 스택 생성 시, 오류 대신
isEmpty()
는true
,getSize()
는0
을 반환하고,push()/pop()
시 예외를 던지는 특별한 스택 객체를 반환할 수 있습니다.
예외는 흐름 분리에, null
반환 회피와 특수 사례 객체는 반복적 null
체크 감소에 도움을 줍니다. 일관된 오류 처리 전략이 중요합니다.
2.7. Boundaries: 외부 요소와의 깔끔한 상호작용
외부 라이브러리, 프레임워크 등과의 경계 관리는 시스템 안정성과 유지보수성에 영향을 줍니다.
- 외부 코드 캡슐화:
java.util.Map
같은 인터페이스를 직접 사용하는 대신, 경계 인터페이스를 시스템 내부로 숨겨 캡슐화하세요. (예:Sensors
클래스가 내부에Map
을 사용하되, 프로그램에 필요한 인터페이스만 제공). 이는 외부 인터페이스 변경 영향 최소화, 코드 이해도 향상, 오용 가능성 감소 효과가 있습니다. (모든Map
사용 시 캡슐화하라는 의미는 아님. 경계 인터페이스를 시스템 여기저기로 직접 넘기지 않도록 주의) - 존재하지 않는 코드와의 연동: 아직 인터페이스가 없거나 개발 중인 외부 모듈과 연동 시, 자체 필요 인터페이스를 먼저 정의하고 개발 진행 후, 어댑터 패턴(Adapter Pattern)으로 실제 외부 API와 연동할 수 있습니다.
경계 처리는 외부 변화로부터 시스템 내부를 보호합니다. 어댑터, 퍼사드(Facade) 패턴 등으로 외부 코드 결합도를 낮추면, 외부 라이브러리 변경/교체 시 파급 효과를 최소화합니다.
3. SOLID Principles: 견고하고 유연한 설계의 초석
SOLID 원칙은 로버트 C. 마틴(Robert C. Martin)이 대중화한 객체 지향 프로그래밍 및 설계의 5가지 기본 원칙으로, 이해하기 쉽고 유연하며 유지보수하기 좋은 소프트웨어를 만드는 데 도움을 줍니다.
잘못된 설계는 '설계의 악취(Design Smells)'를 풍깁니다.
- 경직성 (Rigidity): 변경이 어렵고, 작은 변경이 연쇄 수정을 요구.
- 취약성 (Fragility): 한 부분 수정이 관련 없어 보이는 다른 부분에 예기치 않은 문제 발생.
- 부동성 (Immobility): 특정 부분을 다른 시스템에서 재사용하기 어려움.
이는 잘못된 의존성 관리나 책임 분배에서 비롯됩니다. 객체 지향의 핵심은 제어의 역전(Inversion of Control, IoC)을 통해 상위 모듈을 하위 세부 구현으로부터 보호하고, 의존성을 효과적으로 관리하는 것입니다. SOLID는 이를 위한 지침입니다.
악취 (Smell) | 설명 (Description) | 주요 원인 (Common Cause) | SOLID 해결 방향 (SOLID Solution Direction) |
---|---|---|---|
경직성 | 변경이 어렵고, 하나의 변경이 연쇄적인 변경을 요구함 | 과도한 의존성, 긴 빌드/테스트 시간 | DIP, OCP |
취약성 | 한 모듈 변경이 다른 관련 없는 모듈에 예상치 못한 문제를 일으킴 | 모듈 간 강한 결합, SRP 위반 | SRP, ISP |
부동성 | 모듈을 다른 시스템에서 재사용하기 어려움 | 데이터베이스, UI, 프레임워크 등과의 강한 결합 | DIP, ISP |
3.1. SRP (단일 책임 원칙 - Single Responsibility Principle)
"하나의 모듈은 단 하나의 변경 이유만을 가져야 한다."
'모듈'(클래스/함수)은 특정 '액터(Actor)' 또는 이해관계자와 연결된 단 하나의 변경 이유, 즉 단 하나의 액터에 대해서만 책임을 져야 합니다.
- 예시:
Employee
클래스에 CFO용 급여 계산(calculatePay
), COO용 근무 시간 보고(reportHours
), CTO용 정보 저장(save
) 메서드가 모두 있다면 SRP 위반입니다. 한 팀의 요구사항 변경이 다른 팀 코드에 영향을 줄 수 있습니다. (예:calculatePay
와reportHours
가 공통 내부 메서드regularHours
사용 시, CFO 팀 결정으로regularHours
변경되면 COO 팀reportHours
도 문제 발생 가능) -
해결책
- 데이터와 메서드 분리 (예:
EmployeeData
클래스 + 각 책임을 맡는PayCalculator
,HourReporter
,EmployeeSaver
클래스) - 퍼사드 패턴 (Facade Pattern)
- 액터별로 의존 코드 분리
- 데이터와 메서드 분리 (예:
SRP는 "한 가지 일"을 "사용자(액터)" 관점에서 정의하여 변경 파급 효과를 최소화합니다.
3.2. OCP (개방-폐쇄 원칙 - Open/Closed Principle)
"소프트웨어 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다."
새 기능 추가/변경 시 기존 코드 수정 대신 새 코드 추가로 시스템 변경을 수용해야 합니다.
- 예시:
FinancialReportGenerator
모듈이FinancialDataGateway
인터페이스에 의존하고, 실제 데이터 접근은 이 인터페이스를 구현하는FinancialDataMapper
같은 구체 클래스가 담당. 새 데이터 소스(CloudDataMapper
) 추가 시, 새 구현체만 추가하고FinancialReportGenerator
코드는 수정 불필요. (FinancialDataMapper
변경으로부터FinancialReportGenerator
를 보호하려면,FinancialDataMapper
가FinancialDataGateway
에 의존하고FinancialReportGenerator
는FinancialDataGateway
사용) -
현실적 어려움
- 최상위 레벨(main 파티션)에서는 의존성 주입을 위한 분기문 필요 가능 (프레임워크가 자동화하기도 함).
- 미래 확장 완벽 예측의 어려움 ("수정 구슬 문제").
- 대안: 애자일 디자인 (최소 설계, 빠른 기능 전달, 피드백 기반 지속적 리팩토링 및 설계 발전).
OCP는 주로 추상화(인터페이스, 추상 클래스)를 통해 달성됩니다. 변경 가능성 있는 부분을 추상화하고, 구체 구현이 추상화에 의존하게 설계합니다.
3.3. LSP (리스코프 치환 원칙 - Liskov Substitution Principle)
"상위 타입(superclass 또는 interface)의 객체를 사용하는 코드에서 해당 객체를 그 하위 타입(subclass 또는 implementing class)의 객체로 치환하더라도 프로그램의 행위가 문제없이 그대로 유지되어야 한다."
LSP는 OCP가 요구하는 다형성을 올바르게 지원하는 기반입니다.
- 예시:
Billing
애플리케이션이License
인터페이스 타입 객체 사용.License
구현 하위 타입PersonalLicense
,BusinessLicense
는License
타입을 완벽히 치환 가능해야 함 (Billing
은 객체가 어느 하위 타입인지 관계없이 동일하게 동작). -
LSP 위반 신호
- 하위 타입에서 상위 타입 메서드를 오버라이드하며 아무 작업도 안 하거나, 계약을 위반하는 방식으로 동작 변경.
- 특정 하위 타입에서만 의미 있는 새 예외 발생.
instanceof
연산자로 타입 확인 후 다운캐스팅.
LSP는 하위 타입이 상위 타입의 '행위적 계약(behavioral contract)'을 준수해야 함을 의미하며, 신뢰할 수 있는 다형성을 보장합니다. (고전적인 정사각형/직사각형 문제는 LSP 위반의 흔한 예시)
3.4. ISP (인터페이스 분리 원칙 - Interface Segregation Principle)
"클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강요되어서는 안 된다."
인터페이스는 클라이언트 필요에 맞게 작고 응집력 있게 분리되어야 합니다.
- 예시:
OPS
인터페이스가op1, op2, op3
제공.User1
은op1
만 사용.OPS
가 단일 인터페이스면,User1
은op2, op3
에도 소스 코드 수준에서 의존.op2
변경 시User1
도 재컴파일/재배포 필요. - 해결책: 클라이언트별 인터페이스(
U1Ops
,U2Ops
,U3Ops
)로 분리.User1
은U1Ops
와op1
에만 의존.
"뚱뚱한 클래스(FatClass)"나 "뚱뚱한 인터페이스(Fat Interface)"는 클라이언트 필요에 따라 인터페이스를 분리하여 격리. HashMap
같은 광범위한 인터페이스를 인자로 직접 전달하는 것은 위험. 필요시 래핑 클래스로 필요한 기능만 노출.
ISP는 인터페이스를 클라이언트 관점에서 작고 응집력 있게 만들어 불필요한 의존성을 줄이고 유연성과 강건함을 높입니다.
3.5. DIP (의존관계 역전 원칙 - Dependency Inversion Principle)
- 고수준 모듈(High-level policies)은 저수준 모듈(Low-level details)에 의존해서는 안 된다. 둘 다 추상화(Abstractions)에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 한다.
유연성이 극대화된 시스템은 소스 코드 의존성이 구체 구현(Concretion)이 아닌 추상화(Abstraction, 예: 인터페이스)에만 의존합니다. (단, java.lang.String
같은 안정된 플랫폼 구체 클래스 의존은 허용) 피해야 할 것은 변동성이 큰 구체 요소에 대한 직접 의존입니다.
-
DIP 실천 코딩 지침
- 변동성 큰 구체 클래스 직접 참조 대신 추상 인터페이스 참조 (추상 팩토리 패턴 사용 강제 가능).
- 변동성 큰 구체 클래스로부터 상속 지양.
- 구체 함수 오버라이드 지양 (해당 구체 함수 의존성 제거 불가).
변동성 큰 구체 객체 생성 시 추상 팩토리 패턴이 자주 사용됩니다.
예시: Application 모듈이 Service 인터페이스를 통해 ConcreteImpl 구현 사용. Application은 ServiceFactory 인터페이스의 makeSvc 메서드 호출. ServiceFactoryImpl 클래스가 makeSvc에서 ConcreteImpl 인스턴스 생성 후 Service 타입으로 반환.
제어 흐름(Application -> ConcreteImpl)과 소스 코드 의존성 방향(Application -> Service 인터페이스, ServiceFactoryImpl -> ConcreteImpl)이 반대로 '역전'될 수 있습니다.
DIP 위배 완전 제거는 불가능할 수 있으나, main
함수나 팩토리 구현체 등 소수 구체 컴포넌트 내부로 모아 격리 가능합니다. 객체 지향 설계의 핵심은 IoC를 통해 고수준 모듈을 저수준 모듈 변경으로부터 보호하는 것입니다.
4. Clean Architecture: 시스템의 의도를 드러내는 설계
클린 아키텍처는 시스템의 '의도'(무엇을 하는지)를 명확히 드러내는 원칙과 패턴의 집합입니다. 특정 기술에 종속되지 않고, 핵심 비즈니스 로직과 사용 사례를 보호하고 강조합니다.
4.1. 아키텍처의 본질: 도구를 넘어선 개념
소프트웨어 아키텍처는 사용 언어, 프레임워크, 개발 도구 자체가 아닙니다. 이것들은 구현 도구일 뿐입니다. 진정한 아키텍처는 변경하기 어렵거나 변경해서는 안 되는 근본적 결정들의 집합이며, 시스템 개발의 기초입니다.
훌륭한 아키텍처는 시스템의 '사용 방식(usage)' (사용자에게 제공하는 가치, 상호작용 방식, 즉 사용 사례)을 명확히 드러내야 합니다. 아키텍처를 봤을 때 기술 스택이 아닌 시스템의 목적(예: "회계 처리 시스템")이 먼저 보여야 합니다. 기술적 세부사항이 아닌 '의도'가 아키텍처를 주도해야 합니다.
4.2. Use Case-Driven Architecture
전통적 MVC 웹 시스템은 종종 핵심 사용 사례를 숨기고 '전달 메커니즘(delivery mechanism)'만 노출하여 시스템의 비즈니스 가치를 불명확하게 만듭니다.
클린 아키텍처는 사용 사례를 중심에 둡니다. 사용 사례는 핵심 비즈니스 로직을 담고, UI, DB, 프레임워크와 독립적으로 존재해야 합니다. 그 자체로 완전한 단위로 기능해야 합니다. (예: 웹 회계 시스템 아키텍처는 회계 처리 관련 측면을 명확히 기술해야 함)
사용 사례는 입력 데이터를 해석하고 출력 데이터를 생성하는 시스템의 본질적 알고리즘입니다. 아키텍처는 사용 사례를 외부 변화로부터 보호하도록 설계되어야 합니다. 전달 메커니즘과 인프라스트럭처는 부차적이며, 핵심 사용 사례 로직을 오염시키지 않고 플러그인 형태로 결합되어야 합니다.
4.3. 의존성 규칙과 아키텍처 경계
- 의존성 규칙 (The Dependency Rule): 모든 소스 코드 의존성은 외부에서 내부를 향해야 합니다. (저수준 세부 구현 -> 고수준 추상 정책). DIP의 아키텍처 수준 확장입니다
-
계층 구조
- 내부: 도메인 모델 (Entities), 사용 사례 (Use Cases) - 핵심 비즈니스 로직
- 중간: 인터페이스 어댑터 (Interface Adapters: Presenters, Controllers, Gateways 등)
- 외부: 프레임워크 및 드라이버 (Frameworks & Drivers: UI, DB, External Interfaces 등) - 구체 기술 요소
내부 원(도메인, 사용 사례)은 외부 원(프레젠테이션, DB 등)을 알지 못해야 합니다. 비즈니스 로직은 UI 종류나 DB 종류에 의존하지 않아야 합니다. 외부 원 요소는 내부 원 인터페이스에 의존합니다.
데이터 형식은 경계를 넘을 때 격리되어 각 계층에 적합한 형태로 변환되어야 합니다. DB 객체가 UI 계층까지 그대로 전달되어서는 안 됩니다.
의존성 규칙은 도메인 로직과 사용 사례를 안정적이고 독립적으로 유지하여 테스트 용이성 및 유지보수성을 향상시킵니다.
4.4. 주요 결정 지연의 가치
좋은 아키텍처는 특정 프레임워크, WAS, UI 기술, DB 시스템 선택 같은 중요한 기술적 결정을 가능한 한 오랫동안 미룰 수 있도록 설계되어야 합니다.
프로젝트 초기에 DB 스키마 설계부터 시작하는 관행 대신, 핵심 사용 사례와 비즈니스 로직에 먼저 집중하세요.
-
기술적 결정 지연의 이점
- 더 많은 정보 바탕으로 최적 기술 선택 가능.
- 특정 기술 조기 종속 방지로 장기적 적응성과 유연성 향상.
- 핵심 시스템이 세부 사항과 독립적으로 개발되어, 결정이 정말 필요하고 정보가 충분할 때까지 미룰 수 있음.
4.5. 프로그래밍 패러다임의 역할
구조적, 객체 지향, 함수형 프로그래밍 패러다임은 아키텍처의 제어 흐름 관리, 의존성 관리, 데이터 불변성 유지 등에 기여합니다.
- 함수형 프로그래밍: 순수 함수, 불변 데이터, 부수 효과 최소화 원칙은 예측 가능하고 테스트 용이하며 동시성 처리에 강한 시스템 구축에 도움.
- 테스팅의 한계: 에츠허르 다익스트라(Edsger Dijkstra)는 "테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없다"고 말했습니다. 테스트만으로 완벽한 품질 보장은 불가능하며, 견고한 설계 원칙과 아키텍처로 오류 발생 가능성을 줄이고 신뢰성을 높여야 합니다.
각 패러다임은 아키텍처 설계 시 강력한 도구와 원칙을 제공합니다. 구조적 프로그래밍은 제어 흐름 규율을, 객체 지향 프로그래밍은 DIP를 통한 의존성 관리를, 함수형 프로그래밍은 불변성과 부수 효과 감소 이점을 제공합니다.