SOLID Principles
OOP 설계의 5원칙. 변경에 강하고 테스트 가능한 코드를 만드는 지침. SOLID는 목적이 아닌 수단. 과도한 추상화도 위반이다. 인터페이스 1개에 구현체 1개라면 추상화 비용이 이득을 초과.
Article
M
Me
hyunyoun's Blog
software-engineering-devopssoftware-engineeringdev-opsarchitecturedesignsolid-principlesoopddd11 min read
1. Overview
SOLID 원칙과 객체 지향 역학(SOLID Principles)은 기능 추가 요청이 들어올 때마다 수십 개의 파일에 if-else 폭탄을 터뜨리는 스파게티 코드를 분쇄하고, 블록 장난감(레고)처럼 코드를 갈아 끼울 수 있게 만드는 객체 지향 설계의 5가지 물리 법칙을 해부합니다.
학습자는 하나의 클래스에 수백 가지 잡일을 때려 박는 '신(God) 객체'를 쪼개어 단일 책임(SRP)의 작고 단단한 세포로 나누는 법을 뜯어봅니다. 나아가 기존 코드는 단 한 줄도 건드리지 않고(Closed) 무한히 새로운 기능을 확장(Open)해 내는 다형성(OCP)의 마법을 장악합니다. 마지막으로, 콘센트에 플러그를 꽂듯 구현체가 아닌 추상화(인터페이스)에 의존하게 만들어, 프레임워크와 비즈니스 로직의 결합을 끊어내는 의존성 역전(DIP) 역량을 확보합니다.
2. Scope & Boundaries
In-Scope
- Single Responsibility Principle (SRP): 변경의 이유가 단 하나뿐인 객체 설계. (응집도 극대화)
- Open-Closed Principle (OCP): 확장에 열려있고 수정에 닫혀있는 아키텍처. (다형성, Strategy Pattern)
- Liskov Substitution Principle (LSP): 자식 클래스가 부모의 자리를 100% 완벽하게 대체할 수 있는 상속의 계약.
- Interface Segregation Principle (ISP): 뚱뚱한 인터페이스를 쪼개어 클라이언트가 쓰지 않는 메서드에 의존하지 않게 분리.
- Dependency Inversion Principle (DIP): 고수준 모듈(비즈니스)이 저수준 모듈(DB, UI)에 의존하지 않고 추상화(인터페이스)에 의존하는 결합도 끊기.
Out-of-Scope
- 23가지 GoF 디자인 패턴: Strategy, Factory, Observer 등의 구체적인 패턴 모음 SOLID는 이 패턴들이 존재하는 '이유(원리)'이며, 패턴 자체의 문법은 생략함.
- 도메인 주도 설계(DDD): 비즈니스 경계(Bounded Context)를 나누는 거시적 설계 상위 모듈
03. Architecture & Design/ddd-bounded-context로 위임.
Boundaries
- SOLID vs 절차지향(Procedural):
C언어처럼 함수가 데이터를 직접 조작하는 절차지향은 요구사항이 바뀔 때 함수 내부를 뜯어고쳐야 합니다. SOLID 기반의 객체지향은 요구사항이 바뀔 때, 기존 함수를 고치는 게 아니라 "새로운 클래스(객체)를 만들어서 기존 플러그(Interface)에 갈아 끼우는" 방식으로 대응합니다. 코드의 '수정'을 '추가'로 치환해 버리는 근본적 패러다임의 전환임을 명확히 긋습니다.
3. Counterexample
- OCP 위반의 재앙 (끝없는 if-else): 결제 모듈을 짰습니다.
if (type == "CARD") { ... } else if (type == "CASH") { ... }. 3달 뒤 기획팀이 '카카오페이'와 '비트코인' 결제를 추가해 달라고 합니다. 개발자는 기존 결제 코드를 열어else if2줄을 추가합니다. 실수로 괄호 하나를 지웠다가 전체 결제 시스템이 멈췄습니다. 기존 코드를 '수정(Closed 위반)'해야만 확장이 가능한 끔찍한 강결합 구조의 말로입니다. - SRP 붕괴 (God Object): 주니어 개발자가
User라는 클래스를 만들었습니다. 이 안에는 로그인(Auth), 프로필 변경(Business), DB 저장(Data), 이메일 전송(Network) 함수가 다 들어있습니다. 이메일 템플릿 글자 하나를 고쳤는데, 뜬금없이 DB 저장 로직에 버그가 생깁니다. 모든 책임이 하나의 클래스에 떡칠(God Class)되어, 폭탄의 뇌관이 사방에 얽혀버린 유지보수 파산 상태입니다.
4. Prerequisites
- 객체 지향 프로그래밍 (Basic): 캡슐화, 상속, 다형성, 인터페이스의 개념. (05. Programming Languages)
- 응집도와 결합도 (Basic): Cohesion은 높이고 Coupling은 낮추는 설계 원칙. (09-03 Architecture Overview)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 변경의 이유를 격리하라 (Single Responsibility Principle, SRP)
- Why to Learn: 코드를 고칠 때 여기저기서 뜬금없는 에러가 뻥뻥 터지는 지뢰밭 코드를 피하고, 내가 수정하려는 딱 한 놈(모듈)만 안전하게 도려낼 수 있는 외과 수술적 응집도(Cohesion)를 갖기 위함입니다.
- What to Learn:
- Concepts: SRP, Cohesion (응집도), Reason to Change (변경의 이유), God Class (신 객체), Separation of Concerns (관심사 분리).
- Skills: 사원(Employee) 클래스 안에 있는
출근하기(),급여 계산하기(),DB에 저장하기()함수를 보고 "변경의 이유가 3개다!"라고 지적한 뒤, HR 부서, 회계 부서, DBA 부서의 3개 클래스로 쪼개버리기.
- How to Learn:
- 1단계: God Class의 탄생: 처음엔 클래스가 작았습니다. 요구사항이 붙으면서
User클래스가 1,000줄이 됩니다. 비즈니스 로직과 화면 그리는 로직이 한 몸이 되었습니다. - 2단계: 변경의 파도: UI 디자이너가 버튼 색을 바꿔달라고 해서
User클래스를 열어 UI 코드를 수정했습니다. 그런데 오타가 나서User클래스가 통째로 뻗고, 로그인(비즈니스)이 안 됩니다. 디자인 변경이 비즈니스를 죽인 겁니다. - 3단계: 관심사 분리 (SRP): "클래스가 변경되는 이유는 단 하나여야 한다." UI를 그리는 클래스와 데이터를 담는 클래스를 칼로 자르듯 분리합니다. UI가 폭발해도 비즈니스 데이터는 안전한 방화벽 설계를 해부합니다.
- 1단계: God Class의 탄생: 처음엔 클래스가 작았습니다. 요구사항이 붙으면서
- Implement: 책임 분리(Separation of Responsibilities) 리팩터링 모사.
Before:Report클래스 내에generateData(),formatToPDF(),printToPrinter()혼재.Action: 3개의 변경 축(데이터 추출, 포맷팅, 출력 매체)을 식별.After:ReportDataFetcher,PDFFormatter,PrinterDispatcher3개의 클래스로 분할. 보고서를 HTML로 출력하라는 요구가 와도formatToHTML()만 새로 만들면 끝나는 극강의 응집도 시각화.
Recommended
Core Topic 02: 수정하지 말고 추가하라 (Open-Closed Principle, OCP)
- Why to Learn: 새로운 결제 수단 하나 추가해 달라는 요청에 기존 코드를 열어
else if를 추가하다가 전체 시스템을 터뜨리는 무능함을 박살 내고, 플러그인(Plug-in)처럼 코드를 꽂아 넣는 아키텍처를 만들기 위함입니다. - What to Learn:
- Concepts: OCP, Polymorphism (다형성), Strategy Pattern (전략 패턴), Abstraction (추상화),
if-elseSwitch Smell. - Skills:
if (type == A) ... else if (type == B) ...형태의 냄새나는 조건문을 찾아내고, 공통 인터페이스를 도출하여 기존if블록을 통째로 날려버리기.
- Concepts: OCP, Polymorphism (다형성), Strategy Pattern (전략 패턴), Abstraction (추상화),
- How to Learn:
- 1단계: 수정(Modification)의 공포: 기존에 잘 돌고 있는 코드를 열어 수정(수술)하는 순간, 버그가 발생할 확률은 0에서 100 사이로 요동칩니다. 가장 좋은 코딩은 '기존 코드를 건드리지 않는 것'입니다.
- 2단계: 다형성의 마법:
Payment라는 인터페이스(추상화)를 만듭니다. 그 안에pay()함수 껍데기만 둡니다. 카카오페이, 네이버페이는 이 인터페이스를 상속(구현)받아 각자의pay()를 완성합니다. - 3단계: 플러그인 확장 (Extension): 핵심 결제 컨트롤러는 카카오인지 네이버인지 모릅니다. 그저 들어온 놈의
pay()를 누를 뿐입니다. 비트코인 결제가 추가되면?Bitcoin클래스만 하나 '추가(Open)'하면 됩니다. 컨트롤러 코드는 단 1줄도 '수정할 필요가 없습니다(Closed)'. 소프트웨어 공학 최고의 마법을 뜯어봅니다.
- Implement:
if-else지옥 분쇄기 렌더링.Before:CODEif (grade == "VIP") discount = 0.2; else if (grade == "GOLD") discount = 0.1;Action:DiscountPolicy인터페이스 생성VipDiscount,GoldDiscount구현체 생성.After:policy.getDiscount(). 다이아몬드 등급이 새로 나와도 기존 코드는 절대 수정(Closed)되지 않으며 무한히 확장(Open) 가능한 동치 변환 시각화.
Practical
Core Topic 03: 상속의 모순과 분리 (LSP & Interface Segregation, ISP)
- Why to Learn: "새는 날 수 있다 펭귄은 새다 고로 펭귄은 날 수 있다"는 멍청한 객체 지향 상속의 덫(에러)을 피하고, 뚱뚱한 인터페이스를 쪼개어 가벼운 플러그를 만들기 위함입니다.
- What to Learn:
- Concepts: Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), IS-A Relationship, Contract (계약), Fat Interface.
- Skills: 부모 클래스의 자리에 자식 클래스를 꽂아 넣었을 때 에러(예:
NotSupportedException)를 뱉는 자식 클래스를 색출하여 상속(Inheritance) 대신 합성(Composition)으로 구조 바꾸기(LSP). 안 쓰는 함수까지 강제로 구현하게 만드는 뚱뚱한 인터페이스를 역할별로 잘게 쪼개기(ISP).
- How to Learn:
- 1단계: LSP (상속의 계약 위반):
직사각형을 상속받아정사각형클래스를 만들었습니다. 직사각형의 "가로 길이를 늘린다" 함수를 정사각형에 호출하면? 가로만 늘어나면 정사각형이 아니게 되므로 버그가 터집니다. 수학적 IS-A 관계가 성립하지 않는 가짜 상속을 부숴버립니다. 부모가 할 수 있는 일은 자식도 100% 무조건 에러 없이 해내야(대체 가능해야) 진짜 상속입니다. - 2단계: ISP (뚱뚱한 플러그):
복합기인터페이스에프린트(),복사(),팩스()기능이 다 있습니다. 이걸 상속받아 단순 '프린터기' 클래스를 만드려니, 쓸데없는복사()와팩스()함수도 억지로 빈 껍데기로 구현해야 합니다. - 3단계: 인터페이스 쪼개기:
프린터,스캐너,팩스3개의 얇은 인터페이스로 쪼갭니다(Segregation). 클라이언트(클래스)는 자기가 쓰지도 않을 함수에 의존하느라 고통받지 않게 되는 인터페이스 다이어트를 해부합니다.
- 1단계: LSP (상속의 계약 위반):
- Implement: ISP 기반 플러그 다이어트 모사.
Fat Interface:IWorker { work(), eat(), sleep() }.문제:RobotWorker클래스가 이를 구현하려니eat()과sleep()에서throw new Exception()을 뱉음 (LSP까지 동시 위반).해결:IWorkable { work() }와IFeedable { eat(), sleep() }으로 쪼갬.RobotWorker는IWorkable만 가볍게 구현. 군더더기 없는 아키텍처 설계 시각화.
Advanced
Core Topic 04: 바퀴를 발명하지 마라 (Dependency Inversion Principle, DIP)
- Why to Learn: 내 비즈니스 핵심 로직(고수준)이 데이터베이스 툴이나 UI 라이브러리(저수준)에 꽁꽁 묶여서, DB를 MySQL에서 Oracle로 바꿀 때 내 비즈니스 코드까지 다 뜯어고쳐야 하는 파멸적 결합을 끊어내기 위함입니다.
- What to Learn:
- Concepts: Dependency Inversion Principle (DIP), High-level Module (비즈니스/도메인), Low-level Module (DB/네트워크), Abstraction (추상화), Dependency Injection (DI).
- Skills: "주문 서비스(고수준)가 MySQL DB(저수준)를 호출한다"는 기존의 의존성 화살표를 뒤집어, "주문 서비스는 인터페이스(추상화)를 호출하고, MySQL 구현체가 그 인터페이스를 의존하게" 화살표 방향 꺾어버리기.
- How to Learn:
- 1단계: 전통적인 폭포수 의존성: 자동차(고수준)는 스노우 타이어(저수준 구현체)에 직접 의존합니다. 여름이 와서 일반 타이어로 바꾸려면 자동차 바퀴 축을 통째로 뜯어고쳐야 합니다. 비즈니스가 인프라에 끌려다니는 최악의 꼬임입니다.
- 2단계: 추상화라는 완충재 (DIP): 자동차와 타이어 사이에 '타이어 인터페이스(표준 규격)'라는 추상화를 끼워 넣습니다. 이제 자동차는 스노우 타이어가 뭔지 모릅니다. 그저 17인치 타이어 표준(추상화)에만 의존합니다.
- 3단계: 의존성 역전 (화살표 꺾기): 놀랍게도 스노우 타이어(저수준) 역시 자동차가 만든 이 '표준 인터페이스(추상화)'에 자신을 맞춰서 구현해야 합니다. 고수준이 저수준을 바라보던 화살표가 역전되어, 둘 다 한가운데 떠 있는 '추상화'를 바라보게 되는 아키텍처의 혁명을 뜯어봅니다. 이것이 프레임워크(Spring, Nest)의 DI(의존성 주입)가 동작하는 핵심 원리입니다.
- Implement: DIP 화살표 역전 다이어그램 렌더링.
Before (강결합):OrderService(고수준)의존MySQLRepository(저수준). DB 바뀌면 OrderService 사망.After (역전):OrderService의존OrderRepository(인터페이스/추상화).MySQLRepository의존(구현)OrderRepository(인터페이스). 인프라(DB)가 도메인(Service)의 규격에 무릎을 꿇게 만드는 권력의 역전(Inversion) 시각화.
7. Terminology
8. References
Primary
- [P1] CS2023 - Software Engineering (SE) - Software Design and Architecture (SOLID principles)
- [P5] SFIA - Programming/Software Development (PROG) - Object-oriented design
Secondary
- [Clean Architecture] Robert C. Martin - Design Principles (SOLID)
- [Head First Design Patterns] Eric Freeman - Encapsulate what varies, Program to interfaces
Industry
- [MartinFowler.com] - Polymorphism, InversionOfControl
- [Microsoft Docs] - Architectural principles (SOLID)
9. Final Checklist
Primary
- 1,000줄이 넘어가는
User클래스 안에서 사용자 정보 업데이트(DB) 로직과 비밀번호 정규식 검증(Validation) 로직이 혼재된 코드를 마주했을 때, 단일 책임 원칙(SRP) 위반의 징후를 지적하고 2개의 클래스로 책임을 찢어낼 수 있는가? - 결제 모듈에서 할인 정책(할인 없음, 10% 할인, VIP 할인)을 적용하기 위해
if-else분기문을 끝없이 이어 붙이는 주니어의 코드에 대해, OCP(개방-폐쇄 원칙)와 다형성(Polymorphism)을 활용해 기존 코드 수정 없이 무한 확장이 가능한 아키텍처를 증명할 수 있는가?
Secondary
- '새(Bird)'를 상속받은 '펭귄(Penguin)' 클래스가
fly()메서드를 호출당했을 때 에러(Exception)를 뱉는 구조가, 왜 "부모 자리에 자식을 넣어도 완벽히 동작해야 한다"는 리스코프 치환 원칙(LSP)을 붕괴시키는지 논증할 수 있는가? -
복합기인터페이스에 인쇄, 스캔, 팩스 메서드가 모두 들어있어 단순 '프린터' 클래스가 팩스 메서드를 억지로 빈 껍데기로 구현해야 하는 뚱뚱한 인터페이스(Fat Interface) 문제를, ISP(인터페이스 분리 원칙)를 통해 어떻게 날씬하게 쪼개는지 설명할 수 있는가?
Industry
- 주문 비즈니스 로직(OrderService) 내부에서
new MySQLOrderRepository()를 직접 생성(강결합)하여 DB 변경 시 비즈니스 코드까지 폭발하는 구조를, 의존성 역전 원칙(DIP)을 적용해 둘 다IOrderRepository추상화에 의존하도록 화살표를 꺾어버릴 수 있는가? - 스프링(Spring)이나 네스트(NestJS) 같은 현대 백엔드 프레임워크가 제공하는 IoC(제어의 역전) 컨테이너와 생성자 의존성 주입(DI) 기능이, 근본적으로 SOLID 원칙 중 OCP와 DIP를 실현하기 위해 탄생한 공학적 도구임을 렌더링할 수 있는가?