Single Responsibility Principle#
Single Responsibility Principle(단일 책임 원칙, SRP) 은 SOLID 설계 원칙 중 하나로, " 클래스, 모듈, 함수 등은 오직 하나의 책임만을 가져야 하며, 단 하나의 변경 이유만을 가져야 한다 " 는 원칙이다. SRP 를 적용하면 각 클래스가 명확한 역할을 갖고, 코드의 응집도가 높아지며, 유지보수성과 테스트 용이성이 크게 향상된다. SRP 는 대규모 시스템, 마이크로서비스, 도메인 주도 설계 등 다양한 환경에서 핵심적으로 적용된다.
핵심 개념#
정의 및 기본 개념#
- 책임 (Responsibility): 모듈이 수행해야 하는 기능이나 역할을 의미한다.
- 단일 책임: 클래스나 모듈이 오직 하나의 기능이나 역할만을 담당해야 함
- 변경의 이유: 클래스가 변경되어야 하는 이유가 단 하나여야 함
- Actor: 변경을 요구하는 이해관계자 그룹
- 응집성 (Cohesion): 관련된 기능들이 하나의 모듈에 집중되는 정도
- 결합도 (Coupling): 모듈 간의 의존성 정도
핵심 원리#
- 분리의 원칙: 서로 다른 이유로 변경되는 기능들을 분리
- 단일 목적: 각 구성요소는 명확하고 단일한 목적을 가져야 함
- 변경 격리: 한 기능의 변경이 다른 기능에 영향을 주지 않아야 함
graph TD
A[단일 책임 원칙] --> B[하나의 책임]
A --> C[하나의 변경 이유]
A --> D[하나의 Actor]
B --> E[명확한 목적]
B --> F[기능적 응집성]
C --> G[변경 격리]
C --> H[영향 범위 최소화]
D --> I[이해관계자 분리]
D --> J[요구사항 분할]
배경 및 필요성#
발전 배경#
- 1970 년대: Tom DeMarco 의 응집성 개념에서 시작
- 1979 년: Structured Analysis and System Specification 에서 응집도 원리 제시
- 2002 년: Robert C. Martin 이 SOLID 원칙의 일부로 공식화
- 2003 년: “Agile Software Development, Principles, Patterns, and Practices” 저서에서 구체화
- 2014 년: Martin 이 " 변경의 이유 " 개념을 명확히 정의
목적 및 필요성#
- 코드 복잡성 감소: 각 구성요소의 역할을 명확히 하여 이해하기 쉬운 코드 작성
- 유지보수성 향상: 변경 사항의 영향 범위를 제한하여 안전한 수정 가능
- 재사용성 증대: 단일 목적의 구성요소는 다른 컨텍스트에서 재사용 용이
- 테스트 용이성: 각 구성요소를 독립적으로 테스트 가능
- 병렬 개발: 서로 다른 팀이 독립적으로 개발 가능
주요 기능 및 역할#
- 역할 분리: 각 클래스/모듈이 한 가지 책임만 담당.
- 변경 영향 최소화: 한 책임의 변경이 다른 책임에 영향을 주지 않음.
- 응집도 향상: 관련 기능이 한 곳에 모여 있어 코드 관리가 쉬움.
설계 차원의 역할#
- 아키텍처 정리: 시스템을 논리적이고 일관성 있는 구조로 조직화
- 의존성 관리: 모듈 간 불필요한 의존성 제거
- 인터페이스 정의: 명확한 책임 경계를 통한 인터페이스 설계
개발 차원의 역할#
- 코드 품질 향상: 가독성, 유지보수성, 확장성 개선
- 버그 감소: 변경 사항의 부작용 최소화
- 개발 효율성: 명확한 책임 분할로 개발자 간 협업 개선
설계적 특징#
- 단순성: 각 구성요소가 하나의 명확한 목적을 가짐
- 예측가능성: 변경의 영향 범위를 예측하기 쉬움
- 모듈성: 독립적으로 동작하는 구성요소들의 조합
구현적 특징#
- 높은 응집성: 관련 기능들이 하나의 모듈에 집중
- 낮은 결합도: 모듈 간 의존성 최소화
- 명확한 인터페이스: 각 모듈의 역할이 인터페이스를 통해 명확히 드러남
핵심 원칙#
SRP 는 모듈이 하나의 책임만을 가지도록 설계하여, 변경의 이유를 하나로 제한한다. 이를 통해 코드의 응집도를 높이고, 변경에 따른 영향을 최소화한다.
기본 원칙#
- 단일 책임: 클래스는 오직 하나의 책임만을 가져야 함
- 단일 변경 이유: 클래스가 변경되는 이유는 오직 하나여야 함
- 단일 Actor: 클래스는 하나의 이해관계자 그룹에만 책임을 져야 함
적용 원칙#
flowchart TD
A[요구사항 분석] --> B{여러 책임 존재?}
B -->|Yes| C[책임 분리]
B -->|No| D[단일 책임 유지]
C --> E[별도 클래스 생성]
E --> F[인터페이스 정의]
F --> G[의존성 주입]
D --> H[구현 완료]
G --> H
작동 원리#
식별 과정#
- 책임 식별: 클래스나 모듈이 수행하는 모든 기능 나열
- 변경 이유 분석: 각 기능이 변경될 수 있는 이유 파악
- Actor 구분: 변경을 요구하는 이해관계자 그룹 식별
- 분리 결정: 서로 다른 이유로 변경되는 기능들을 별도 모듈로 분리
리팩토링 과정#
sequenceDiagram
participant Dev as 개발자
participant Code as 기존 코드
participant Analysis as 분석
participant Refactor as 리팩토링
Dev->>Code: 다중 책임 클래스 식별
Code->>Analysis: 책임 분석
Analysis->>Analysis: 변경 이유 파악
Analysis->>Refactor: Extract Class 적용
Refactor->>Refactor: 인터페이스 정의
Refactor->>Dev: 단일 책임 클래스들
아키텍처 다이어그램#
classDiagram
class UserController {
+createUser(request)
+updateUser(id, request)
+deleteUser(id)
}
class UserService {
+createUser(userData)
+updateUser(id, userData)
+deleteUser(id)
}
class UserRepository {
+save(user)
+findById(id)
+delete(id)
}
class UserValidator {
+validateUserData(userData)
+validateEmail(email)
}
class EmailService {
+sendWelcomeEmail(user)
+sendNotification(user, message)
}
UserController --> UserService : uses
UserService --> UserRepository : uses
UserService --> UserValidator : uses
UserService --> EmailService : uses
구분 | 항목 | 기능 | 역할 | 특징 |
---|
설계 원칙 | 책임 경계 (Responsibility Boundary) | 클래스나 모듈의 책임 범위 정의 | 책임 있는 부분과 없는 부분을 명확히 구분 | 이해하기 쉬운 경계 설정 |
구성 요소 | 인터페이스 (Interface) | 구성요소 간 상호작용 방식 정의 | 구현 세부사항은 숨기고 계약만 제공 | 변경에 강하고 안정적인 설계 |
구현 요소 | 구현체 (Implementation) | 실제 비즈니스 로직 수행 | 구체 기능 제공 및 단일 책임 집중 | 테스트 가능하고 유연한 변경 구조 |
설계 기법 | 의존성 주입 컨테이너 (DI Container) | 의존성 관리 자동화 | 런타임에 적절한 구현체 주입 | 느슨한 결합 및 구성 유연성 확보 |
디자인 패턴 | 팩토리 패턴 (Factory Pattern) | 객체 생성 로직 캡슐화 | 생성 책임을 클라이언트로부터 분리 | 생성과 사용의 명확한 분리 |
구현 기법#
구현 기법 | 정의/구성 | 목적/예시 |
---|
역할별 클래스 분리 | 각 책임별로 클래스를 분리 | 서비스/저장소 분리, 컨트롤러/비즈니스 로직 분리 등 |
인터페이스/추상 클래스 활용 | 공통 기능 추상화, 구현체 분리 | 저장소 인터페이스, 출력 인터페이스 등 |
유틸리티 클래스 분리 | 공통 기능을 별도 유틸리티 클래스로 분리 | 날짜 처리, 포맷 변환 등 |
- 정의: 하나의 클래스에서 여러 책임을 별도 클래스로 분리
- 구성: 원본 클래스, 추출될 클래스, 인터페이스
- 목적: 과도한 책임을 가진 클래스를 단일 책임으로 분리
- 예시: 사용자 관리 클래스에서 이메일 발송 기능을 별도 클래스로 분리
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
29
30
31
32
33
34
| // 기존 - SRP 위반
class UserManager {
public void createUser(String name, String email) {
// 사용자 생성 로직
User user = new User(name, email);
database.save(user);
// 이메일 발송 로직 (다른 책임)
Email email = new Email();
email.setTo(user.getEmail());
email.setSubject("환영합니다!");
emailSender.send(email);
}
}
// 개선 - SRP 적용
class UserService {
private EmailService emailService;
public void createUser(String name, String email) {
User user = new User(name, email);
database.save(user);
emailService.sendWelcomeEmail(user);
}
}
class EmailService {
public void sendWelcomeEmail(User user) {
Email email = new Email();
email.setTo(user.getEmail());
email.setSubject("환영합니다!");
emailSender.send(email);
}
}
|
- 정의: 큰 메소드에서 특정 기능을 별도 메소드로 분리
- 구성: 원본 메소드, 추출된 메소드들, 주 로직
- 목적: 메소드 레벨에서 단일 책임 원칙 적용
- 예시: 복잡한 계산 로직을 여러 단계로 분리
Interface Segregation 기법#
- 정의: 큰 인터페이스를 작은 단위로 분리
- 구성: 원본 인터페이스, 분리된 인터페이스들, 구현체
- 목적: 클라이언트가 필요하지 않은 기능에 의존하지 않도록 함
- 예시: 다기능 프린터 인터페이스를 출력, 스캔, 팩스 인터페이스로 분리
Dependency Injection 기법#
- 정의: 의존성을 외부에서 주입받아 결합도 감소
- 구성: 의존성 인터페이스, 구현체, 주입 메커니즘
- 목적: 구성요소 간 느슨한 결합으로 단일 책임 유지
- 예시: 서비스 클래스에 리포지토리 인터페이스를 주입
장점과 단점#
구분 | 항목 | 설명 |
---|
✅ 장점 | 코드 가독성 향상 | 각 클래스의 목적이 명확하여 이해하기 쉬움 |
| 유지보수성 개선 | 변경 사항의 영향 범위가 제한되어 안전한 수정 가능 |
| 테스트 용이성 | 단일 책임으로 인해 독립적인 테스트 작성 가능 |
| 재사용성 증대 | 명확한 목적의 구성요소는 다른 컨텍스트에서 재사용 용이 |
| 병렬 개발 지원 | 서로 다른 팀이 독립적으로 개발 가능 |
⚠ 단점 | 복잡성 증가 | 동일한 기능을 위해 더 많은 클래스 필요 |
| 과도한 추상화 | 불필요한 추상화로 인한 코드 복잡성 증가 |
| 책임 정의의 어려움 | 적절한 책임 범위 결정의 주관성 |
| 성능 오버헤드 | 구성요소 간 통신으로 인한 성능 저하 가능성 |
| 초기 개발 비용 | 설계 단계에서 더 많은 시간과 노력 필요 |
단점 해결 방법#
과도한 분할 방지
- 비즈니스 도메인 기준으로 적절한 추상화 수준 유지
- KISS(Keep It Simple, Stupid) 원칙과의 균형 고려
성능 최적화
- 필요시 구성요소 간 통신 최소화
- 캐싱, 배치 처리 등 성능 개선 기법 적용
명확한 가이드라인 수립
- 팀 내 일관된 책임 정의 기준 마련
- 코드 리뷰를 통한 지속적인 개선
도전 과제#
도전 과제 | 설명 | 해결책 |
---|
책임 정의의 모호성 | 무엇이 단일 책임인지 정의하기 어려움 | 도메인 전문가와 협업, 변경 빈도 기준으로 책임 범위 정의 |
역할 분리 기준의 불명확성 | 역할의 경계가 불분명하면 설계가 복잡해짐 | 도메인 분석 기반 역할 정의, 코드 리뷰 강화 |
과도한 역할 분할 | 분할이 지나치면 관리할 클래스와 모듈이 지나치게 많아짐 | 실용적 관점에서 점진적 리팩토링 적용 |
클래스/파일 수 증가 | 분리로 인해 클래스 및 파일 수가 급증 | 패키지 구조 체계화, 네이밍 규칙 준수 |
중복 코드 발생 위험 | 동일한 기능을 여러 구현체에서 반복할 가능성 | 공통 기능을 유틸리티 클래스로 추출 및 재사용 |
레거시 시스템 적용의 어려움 | 기존 시스템에 원칙 적용 시 리팩토링 비용과 리스크가 큼 | Strangler Fig 패턴을 활용한 점진적 전환 |
팀 간 커뮤니케이션 오버헤드 | 모듈 분리로 인한 팀 간 협업 복잡성 | 명확한 인터페이스 정의, 설계 문서화 및 아키텍처 가이드 공유 |
분류 기준에 따른 종류 및 유형#
분류 기준 | 유형 | 설명 | 예시 |
---|
적용 레벨 | 클래스 레벨 | 개별 클래스의 단일 책임 | UserService, EmailService |
| 모듈 레벨 | 패키지/모듈의 단일 책임 | 인증 모듈, 결제 모듈 |
| 서비스 레벨 | 마이크로서비스의 단일 책임 | 사용자 서비스, 주문 서비스 |
책임 유형 | 데이터 처리 | 데이터 변환, 검증, 포맷팅 | DataValidator, DataFormatter |
| 비즈니스 로직 | 핵심 업무 규칙 처리 | OrderCalculator, InventoryManager |
| 외부 연동 | 외부 시스템과의 통신 | PaymentGateway, EmailSender |
| 인프라 관리 | 기술적 관심사 처리 | DatabaseConnection, Logger |
분리 전략 | 수직 분리 | 계층별 책임 분리 | Controller, Service, Repository |
| 수평 분리 | 기능별 책임 분리 | UserManager, ProductManager |
| 시간적 분리 | 생명주기별 책임 분리 | Factory, Destroyer |
실무 적용 예시#
영역 | 적용 사례 | 분리 전 구조 | 분리 후 구조 | 기대 효과 |
---|
웹 개발 | MVC 패턴 | 모든 로직이 하나의 클래스 | Controller, Service, Repository 계층 분리 | 계층별 책임 명확화, 유지보수 용이 |
데이터 처리 | ETL 파이프라인 분리 | 추출·변환·적재 통합 처리 | Extractor, Transformer, Loader 모듈 분리 | 각 단계별 독립적 개발 및 테스트 가능 |
인증 시스템 | 인증/인가/세션 분리 | 인증 기능 통합 | AuthService, AuthorizationService, SessionService | 보안 책임 분리, 기능별 테스트 용이 |
결제 시스템 | 결제 프로세스 분리 | 검증·처리·알림 통합 | PaymentValidator, PaymentProcessor, NotificationService | 단계별 안정성 확보 및 기능 확장 용이 |
로깅 시스템 | 로그 수집/처리/저장 책임 분리 | 로그 기능이 하나의 클래스에 집중 | LogCollector, LogFormatter, LogStorage | 성능 최적화 및 포맷 다양성 대응 가능 |
사용자 관리 시스템 | 사용자 정보 관리 vs 인증 분리 | 단일 모듈에 모든 사용자 기능 포함 | UserProfileService, AuthenticationService | 책임 명확화, 보안 기능 독립화 |
백엔드 서비스 | 비즈니스 로직과 저장소 책임 분리 | 단일 서비스 클래스 | Service, Repository 인터페이스 및 구현 분리 | DIP 적용으로 테스트 용이성 증가 |
프론트엔드 컴포넌트 | 역할별 UI 분리 | UI 처리 통합 | ViewComponent, EventHandlerComponent 등 역할 기반 분리 | 재사용성 향상, 유지보수 용이 |
유틸리티 관리 | 공통 기능 유틸 클래스 분리 | 기능 중복 존재 | DateUtil, FormatUtil 등 독립 클래스 구성 | 중복 제거, 코드 일관성 확보 |
활용 사례#
사례 1: 전자상거래 주문 시스템#
시나리오: 온라인 쇼핑몰의 주문 처리 시스템을 SRP 를 적용하여 설계하는 사례
시스템 구성:
graph TB
A[OrderController] --> B[OrderService]
B --> C[PaymentService]
B --> D[InventoryService]
B --> E[ShippingService]
B --> F[NotificationService]
C --> G[PaymentGateway]
D --> H[InventoryRepository]
E --> I[ShippingProvider]
F --> J[EmailService]
F --> K[SMSService]
H --> L[(Database)]
G --> M[외부 결제 API]
I --> N[배송업체 API]
각 구성요소의 단일 책임:
- OrderController: HTTP 요청/응답 처리만 담당
- OrderService: 주문 비즈니스 로직 조율
- PaymentService: 결제 처리 로직
- InventoryService: 재고 관리
- ShippingService: 배송 처리
- NotificationService: 알림 발송
Workflow:
sequenceDiagram
participant Client
participant OrderController
participant OrderService
participant PaymentService
participant InventoryService
participant ShippingService
participant NotificationService
Client->>OrderController: 주문 요청
OrderController->>OrderService: 주문 처리 요청
OrderService->>InventoryService: 재고 확인
InventoryService-->>OrderService: 재고 상태
OrderService->>PaymentService: 결제 처리
PaymentService-->>OrderService: 결제 결과
OrderService->>ShippingService: 배송 요청
ShippingService-->>OrderService: 배송 정보
OrderService->>NotificationService: 알림 발송
NotificationService-->>OrderService: 발송 완료
OrderService-->>OrderController: 처리 결과
OrderController-->>Client: 응답
SRP 적용 효과:
- 유지보수성: 결제 로직 변경 시 PaymentService 만 수정
- 확장성: 새로운 결제 수단 추가 시 기존 코드 영향 없음
- 테스트 용이성: 각 서비스를 독립적으로 테스트 가능
- 재사용성: NotificationService 를 다른 기능에서도 활용 가능
사례 2: 온라인 학습 플랫폼#
시나리오: 온라인 학습 플랫폼
시스템 구성:
CourseManager
: 강좌 생성, 수정, 삭제 (→ SRP 위반)- SRP 준수 시 다음과 같이 분리:
CourseCreator
CourseEditor
CourseDeleter
시스템 아키텍처 다이어그램
classDiagram
class CourseCreator {
+createCourse()
}
class CourseEditor {
+editCourse()
}
class CourseDeleter {
+deleteCourse()
}
class Frontend {
+handleUserInput()
}
Frontend --> CourseCreator
Frontend --> CourseEditor
Frontend --> CourseDeleter
Workflow:
- 사용자가 강좌 생성 요청
CourseCreator
가 요청 처리- 강좌 편집 요청 시
CourseEditor
가 처리 - 삭제 요청은
CourseDeleter
가 처리
역할 분리 효과:
- 코드 테스트가 용이해짐
- 요구사항 변경에 유연하게 대응
- DevOps 관점에서 기능별 CI/CD 적용 가능
실무 적용 고려사항 및 권장사항#
구분 | 고려사항 | 설명 | 권장사항 |
---|
설계 단계 | 책임 정의 명확화 | 모듈 또는 클래스의 역할을 혼동하지 않도록 함 | 도메인 전문가와 협업하여 명확한 책임 경계 설정 |
| 변경 빈도 분석 | 자주 바뀌는 기능을 기준으로 책임을 분리 | 과거 변경 이력 분석, 요구사항 중심 설계 |
| 적절한 추상화 수준 | 과도한 추상화는 유지보수를 어렵게 할 수 있음 | 기능 중심의 실용적 추상화 적용 |
구현 단계 | 인터페이스 우선 설계 | 구현체보다 추상화 중심의 구조를 먼저 정의함 | 인터페이스 기반 의존성 설계로 DIP 준수 |
| 점진적 리팩토링 | 초기 도입 시 전체 재설계보다 점진적 적용이 효과적 | 기능 단위로 단계적 구조 개선 |
| 테스트 주도 개발 (TDD) | 구조적 책임 분리를 테스트와 병행할 수 있음 | 단위 테스트 기반 개발로 자연스럽게 SRP 유도 |
운영 단계 | 코드/파일 증가 | 역할 분리로 인해 클래스 및 파일 수 증가 가능성 | 패키지 구조 체계화, 네이밍 표준화, 문서화 |
| 코드 중복 | 공통 로직이 여러 모듈에 반복될 가능성 | 유틸리티 클래스 또는 공통 인터페이스 추출 |
| 모듈 간 결합도 | 모듈 간 강한 의존은 유지보수 비용을 높임 | 인터페이스/DI 활용하여 결합도 최소화 |
| 지속적 모니터링 | 리팩토링 시점이나 책임 범위 재조정 타이밍 판단 필요 | 변경 빈도, 코드 복잡도 등의 지표 기반으로 개선 계획 수립 |
팀 협업 | 팀 내 일관성 확보 | SRP/DIP 등 원칙 적용에 대한 이해도 차이 발생 가능성 | 정기적 교육, 코드 리뷰 기준 설정, 아키텍처 가이드 공유 |
최적화 고려사항 및 주의할 점#
구분 | 고려사항 | 설명 | 권장사항 |
---|
성능 최적화 | 구성요소 간 통신 비용 | 모듈 간 과도한 호출로 인한 오버헤드 발생 가능 | 인터페이스 통한 일괄 처리, 캐싱 전략 적용 |
| 메모리 사용량 | 객체 과다 생성 시 GC(Garbage Collection) 부담 증가 | 객체 풀링 (Pooling), 싱글톤 (Singleton) 등으로 관리 |
| 네트워크 호출 최적화 | 마이크로서비스, 분산 환경에서 네트워크 비용 발생 | 배치 호출, GraphQL, API Gateway 활용 등으로 호출 최소화 |
구조 최적화 | 의존성 그래프 관리 | 순환 의존 발생 시 구조 불안정 | 의존성 방향 명확화, 계층 아키텍처 또는 헥사고날 아키텍처 적용 |
| 인터페이스 안정성 | 자주 바뀌는 인터페이스는 의존 모듈에 영향을 줌 | 도메인 안정성 기준으로 인터페이스 설계, 의사결정 기록 (ADR) 문서화 |
| 모듈 경계 최적화 | 과도한 모듈화 시 복잡도 증가 | 도메인/기능 기준으로 합리적 분할, 역할/경계 명확화 |
개발 최적화 | 코드 중복 | 책임 분리 중 반복 로직 발생 가능 | 공통 기능을 유틸리티 또는 공유 서비스로 모듈화 |
| 개발 생산성 | 과도한 보일러플레이트 (boilerplate) 로 개발 속도 저하 | DI 프레임워크 (Spring, NestJS 등) 적극 활용, 코드 생성 도구 사용 |
| 문서화 자동화 | 구조가 복잡해질수록 문서 미비로 협업 어려움 | README, ADR 문서, Swagger, Typedoc 등 자동 문서화 도구 활용 |
설계 최적화 | 역할 정의/경계 불분명 | SRP(단일 책임 원칙) 적용 시 경계 설정이 모호할 수 있음 | 도메인 분석 기반 역할 정의, 표준화 및 코드 리뷰 강화 |
| 변경 용이성 | 요구사항 변화 시 유연하게 대처 가능한 구조 필요 | OCP(개방 - 폐쇄 원칙), SRP 원칙 기반 설계 적용 |
| 테스트 용이성 | 모듈 간 결합도가 높으면 테스트 어려움 | 의존성 주입 (DI) 기반 테스트 구조 구성, 단위 테스트 우선 설계 |
운영/협업 최적화 | 모듈 경량화 | 지나친 분할로 인한 유지보수 및 배포 복잡성 증가 | 기능 단위로 경량화, 디렉토리 구조 명확화 |
| 모듈 재사용성 | 비슷한 책임의 모듈을 반복 개발하게 될 수 있음 | 일반화된 인터페이스 설계 및 공유 모듈화 |
| 의사소통 명확성 | 구조가 복잡할수록 팀원 간 책임/경계 혼란 발생 가능 | 책임 및 경계 문서화, 정기적인 아키텍처 공유 및 리뷰 |
SRP 관련 주요 문제와 해결 방안#
문제 유형 | 원인 | 영향 | 탐지/진단 방법 | 예방 방안 | 해결 전략 |
---|
책임 모호화 | 책임 정의가 불분명하거나 도메인 분석 부족 | 변경 충돌, 설계 오해, 테스트 난이도 상승 | 설계 리뷰, 변경 이력 분석, 코드 리뷰 | 도메인 전문가 협업, 명세 기반 책임 문서화 | 책임 재정의, 역할 분할, 클래스 분리 |
과도한 역할 분리 | 모든 기능을 억지로 분리하려는 잘못된 SRP 해석 | 클래스/파일 수 증가, 보일러플레이트 증가, 설계 복잡도 증가 | 클래스 수 증가 모니터링, 정적 분석 툴 (Coupling/Complexity) | 실용적 SRP 해석 적용 (비즈니스/변경 단위 기준) | 일부 모듈 통합, 리팩토링 |
God Class | 여러 책임이 한 클래스에 집중되어 있음 | 높은 결합도, 낮은 응집도, 테스트/유지보수 복잡성 | 클래스 크기 (라인/메서드 수), 의존성 수, 복잡도 메트릭 (Cyclomatic) | 정기적 코드 리뷰, 모듈 응집도 평가 | Extract Class , Extract Method 리팩토링 |
Shotgun Surgery | 단일 책임이 제대로 분리되지 않아 다양한 곳에서 동시에 변경 발생 | 유지보수 리스크 증가, 생산성 저하 | 변경 이력 분석 (버전 관리 시스템), 변경 빈도 추적 | 연관 기능은 하나의 클래스/모듈로 집중 | Move Method , Move Field 리팩토링 |
Feature Envy | 기능이 다른 클래스의 데이터에 과도하게 의존 | 캡슐화 위반, 불필요한 결합도 증가 | 메소드 호출 패턴 분석, 정적 분석 도구 사용 | 데이터와 책임의 일치 유지, 정보 은닉 원칙 준수 | Move Method 를 통해 기능 이동 |
Large Class | 장기간 책임이 누적되어 클래스가 비대화됨 | 테스트 어려움, 변경 충돌 위험 증가 | 클래스 라인/필드/메소드 수 분석, 응집도 (Cohesion) 분석 | 관심사 분리 기반 설계, 기능별 구조 정의 | Extract Class , Extract Subclass 패턴 |
Divergent Change | 클래스가 다양한 이유로 자주 변경됨 | 예측 불가한 변경 전파, 유지보수 난이도 증가 | 변경 이력 분석, 이유별 변경 빈도 분류 | 변경 이유 기준 책임 분리 (기능/도메인 중심) | Extract Class 로 변경 이유별 클래스 분리 |
Middle Man | 지나치게 위임만 하는 클래스가 생김 (SRP 적용의 부작용) | 성능 오버헤드, 코드 복잡성, 설계 이해도 저하 | 위임만 하는 클래스 탐지, 로직 비율 분석 | 위임 필요성 명확히 판단 후 설계 결정 | Remove Middle Man 리팩토링 |
모듈 간 의존성 증가 | 책임 분리 이후 모듈 간 공유 데이터/행동 증가 | 결합도 증가, 변화 전파 위험성 | DI 사용 로그, 영향도 분석 도구 | 이벤트 기반 아키텍처, 안정적 인터페이스 정의 | Pub/Sub, 메시지 큐, 인터페이스 개선 등으로 통신 구조 재설계 |
코드 중복 발생 | 유사한 책임을 분산하면서 공통 기능이 중복됨 | 중복 로직, 유지보수 비용 증가, 버그 유발 가능성 | 중복 탐지 도구 (e.g., SonarQube), 코드 리뷰 | 공통 로직 유틸리티화, 인터페이스 기반 공통 책임 관리 | Extract Utility Class , Template 패턴 등 재사용 구조 설계 |
주제와 관련하여 주목할 내용#
주제 | 항목 | 설명 |
---|
설계 원칙 | SRP (단일 책임 원칙) | 클래스/모듈은 하나의 책임만 가져야 하며, 변경 이유도 하나여야 함 |
| OCP (개방/폐쇄 원칙) | 확장에는 열려 있고, 기존 코드는 수정하지 않아야 함 |
| LSP (리스코프 치환 원칙) | 하위 타입은 상위 타입을 완전히 대체할 수 있어야 함 |
| ISP (인터페이스 분리 원칙) | 클라이언트는 사용하지 않는 인터페이스에 의존하지 않아야 함 |
| DIP (의존성 역전 원칙) | 고수준, 저수준 모듈 모두 추상화에 의존해야 하며 구체 구현에 의존하면 안 됨 |
설계 패턴 | Strategy Pattern | 알고리즘을 캡슐화하여 런타임에 동적으로 교체 가능 |
| Command Pattern | 요청을 객체로 캡슐화하여 요청 매개변수화 및 실행 큐 지원 |
| Observer Pattern | 상태 변화가 발생하면 관련 객체 (구독자) 에게 자동 통보 |
| Factory Pattern | 객체 생성을 전담하는 클래스를 통해 생성 책임과 사용 책임을 분리 |
코드 품질 | Code Smell | 유지보수에 불리한 구조나 설계상의 문제점을 암시하는 코드 패턴 |
| Refactoring | 기능은 동일하게 유지하면서 코드 구조를 개선하는 작업 |
| Clean Code | 읽기 쉽고, 예측 가능하며, 변경에 강한 고품질 코드 작성 원칙 |
| Technical Debt | 빠른 개발을 위해 축적된 구조적 부채, 장기적으로 유지보수 비용 증가 원인 |
아키텍처 | Microservices | 단일 책임 원칙에 기반한 작고 독립적인 서비스 간 통신 구조 |
| Domain-Driven Design (DDD) | 도메인 모델을 기반으로 책임과 경계를 명확히 나누는 설계 전략 |
| Hexagonal Architecture | 내부 비즈니스 로직과 외부 시스템 (데이터베이스, UI 등) 의 의존성을 명확히 분리 |
| Clean Architecture | 의존성 규칙을 적용하여 계층별로 책임과 변경 가능성을 통제 |
실무 적용 | 책임 분리 기반 계층 구조 | 예: Controller / Service / Repository 로 계층 분리 |
| 유틸리티 및 공통 모듈 분리 | 공통 기능을 분리해 재사용성 증대 및 코드 중복 최소화 |
| 테스트 가능 구조 | SRP + DIP 기반으로 Mocking 이 쉬운 구조 설계로 테스트 용이성 확보 |
심화학습 및 연계 주제#
카테고리 | 주제/패턴 또는 기법 | 설명 |
---|
설계 원칙 | OCP (개방/폐쇄 원칙) | SRP 기반 구조는 OCP 적용의 기초가 되며, 변경 없이 기능 확장 가능 |
| ISP (인터페이스 분리 원칙) | 책임 단위에 맞춘 인터페이스 설계를 통해 SRP 와 시너지 생성 |
| Law of Demeter (데메테르 법칙) | 낮은 결합도 유지를 위한 최소 지식 원칙, SRP 기반 설계에 부합 |
| DRY / YAGNI | 중복 제거와 불필요한 책임 방지를 통해 SRP 보조 |
리팩토링 기법 | Extract Class / Method | 책임 분리를 위한 대표적 기법, 하나의 클래스가 두 개 이상의 책임을 질 경우 유용 |
| Move Method / Move Field | 책임에 따라 메소드나 필드를 적절한 클래스로 이동하여 응집도 개선 |
| Replace Conditional with Polymorphism | 역할에 따라 조건문을 다형성으로 대체하여 단일 책임 구현 |
| Introduce Parameter Object | 연관된 매개변수들을 묶어 책임 단위 객체로 추출 |
테스트 전략 | Unit Testing / Mock Objects | SRP 구조에서는 단위 테스트가 단순하며 모의 객체 사용 용이 |
| Test-Driven Development (TDD) | SRP 에 적합한 설계를 자연스럽게 유도 |
| Behavior-Driven Development (BDD) | 행위 중심 테스트를 통해 책임 명확화 |
아키텍처 및 시스템 | Microservices Architecture | 서비스 단위로 SRP 적용, 각 서비스는 독립된 단일 책임 수행 |
| Layered Architecture | Controller, Service, Repository 계층 간 책임 분리 적용 |
| Hexagonal Architecture / Clean Architecture | 내부 도메인 로직과 외부 의존성 분리를 통해 SRP 구조화 |
| Event-Driven Architecture | 모듈 간 강한 결합을 제거하고 이벤트 단위로 책임 분리 |
| CQRS / Event Sourcing | 읽기/쓰기 또는 상태 변경을 별도 책임으로 분리 |
도메인 설계 | DDD (Domain-Driven Design) | Bounded Context 기반으로 책임 단위 도출 |
| Repository Pattern / Unit of Work | 데이터 액세스 책임을 별도 계층으로 분리하여 SRP 적용 |
함수형 프로그래밍 | Pure Function / Immutability | 부작용 없는 함수와 불변성을 활용해 단일 책임 함수 설계 |
| Function Composition | 작은 책임 함수 조합으로 복잡한 로직 구성 |
문서화 및 관리 | ADR (Architecture Decision Record) | 책임 분리 기준과 설계 이유를 기록 |
| README / UML / 설계 리뷰 | SRP 적용 결과 및 책임 단위 명세 공유 |
DevOps/자동화 | Infrastructure as Code / CI-CD Pipeline | 빌드, 테스트, 배포 단계를 모듈별로 책임 분리 |
| Container Orchestration (K8s 등) | 컨테이너 단위로 책임 단일화, 독립 배포 가능 구조 설계 |
용어 정리#
핵심 개념#
용어 | 설명 |
---|
Cohesion (응집도) | 클래스나 모듈이 하나의 목적을 위해 얼마나 밀접하게 관련되어 있는지를 나타내는 정도 |
Coupling (결합도) | 모듈 간 상호 의존성의 정도. 낮을수록 변경에 강하고 유지보수가 쉬움 |
Actor (액터) | 변경을 요구하는 이해관계자나 시스템 외부 주체 |
Code Smell | 코드에서 더 깊은 문제를 암시하는 구조적 결함의 징후 |
Utility Class | 공통 기능 (날짜 처리, 문자열 포맷 등) 을 제공하는 재사용 가능한 클래스 |
Domain Analysis | 시스템의 요구사항을 분석하고 역할 및 책임을 식별하는 과정 |
객체지향 설계 원칙#
용어 | 설명 |
---|
SRP (Single Responsibility Principle) | 클래스나 모듈은 하나의 책임만을 가져야 한다는 설계 원칙 |
SOLID | 객체지향 설계 5 대 원칙의 집합: SRP, OCP, LSP, ISP, DIP |
OCP (Open-Closed Principle) | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다는 원칙 |
LSP (Liskov Substitution Principle) | 하위 타입은 상위 타입을 대체할 수 있어야 한다는 원칙 |
ISP (Interface Segregation Principle) | 클라이언트는 사용하지 않는 인터페이스에 의존하지 않아야 한다는 원칙 |
DIP (Dependency Inversion Principle) | 상위 모듈과 하위 모듈은 구체화가 아닌 추상화에 의존해야 한다는 원칙 |
설계 및 구현 기법#
용어 | 설명 |
---|
Interface (인터페이스) | 객체 간 상호작용의 계약을 정의하는 추상화 계층 |
Dependency Injection (DI) | 객체의 의존성을 외부에서 주입받아 결합도를 낮추는 기법 |
Modularization (모듈화) | 책임 단위별로 코드를 분리해 독립성과 재사용성을 높이는 구조화 전략 |
Refactoring (리팩토링) | 코드의 외부 동작을 유지하면서 내부 구조를 개선하는 과정 |
UML (Unified Modeling Language) | 시스템 설계를 시각화하기 위한 표준화된 다이어그램 언어 |
ADR (Architecture Decision Record) | 아키텍처 및 설계 결정 사항을 문서화해 추적 가능하게 하는 형식화된 기록 |
디자인 패턴#
용어 | 설명 |
---|
Strategy Pattern | 알고리즘을 캡슐화하여 동적으로 교체할 수 있도록 설계하는 패턴 |
Command Pattern | 요청을 객체로 캡슐화하여 실행, 큐잉, 로그 등의 기능을 유연하게 처리하도록 하는 패턴 |
Observer Pattern | 객체의 상태 변화가 있을 때 등록된 관찰자에게 자동으로 통지하는 패턴 |
Factory Pattern | 객체 생성 로직을 분리하여 객체 생성을 캡슐화하고 확장에 유리하게 만드는 패턴 |
Chain of Responsibility | 요청을 여러 처리 객체 중 하나가 처리할 수 있도록 연결한 패턴 |
아키텍처 및 구조#
용어 | 설명 |
---|
Microservices | 애플리케이션을 독립적으로 배포 가능한 작은 서비스 단위로 분리한 아키텍처 |
Hexagonal Architecture | 애플리케이션 핵심 도메인을 외부 의존성과 분리하고, 포트/어댑터 구조를 따르는 아키텍처 패턴 |
Clean Architecture | 의존성 규칙을 기반으로 계층 간 분리를 엄격히 지켜 유연성과 테스트 용이성을 확보하는 아키텍처 |
Domain Driven Design (DDD) | 도메인 모델을 중심으로 문제를 설계하고 구현하는 방법론 |
테스트 및 개발 방법론#
용어 | 설명 |
---|
Unit Test (단위 테스트) | 클래스 또는 함수 같은 작은 단위의 로직을 독립적으로 검증하는 테스트 방식 |
Test-Driven Development (TDD) | 테스트를 먼저 작성하고 구현하는 테스트 주도 개발 방법론 |
Behavior-Driven Development (BDD) | 사용자 행위 중심으로 테스트 케이스를 작성하고 검증하는 개발 방식 |
Mock Object (모의 객체) | 테스트에서 실제 객체를 대체하기 위해 사용하는 테스트 더블 객체 |
참고 및 출처#