개발하며 '이 코드는 좀 꼬인 것 같은데...'라는 느낌, 다들 한 번쯤 받으셨을 텐데요. 오늘은 클린 코드의 핵심, SOLID 원칙 중에서도 단일 책임 원칙(SRP)을 집중적으로 파헤쳐 보겠습니다. 복잡한 보고서 생성 과정을 포함해 실제 상황에서 SRP를 어떻게 적용하고 리팩토링하는지 3가지 예시와 함께 알아볼게요.
📑 목차
1. 클린 코드, 왜 SOLID 원칙이 중요할까?
소프트웨어 개발에서 클린 코드는 가독성, 유지보수성, 확장성이 뛰어난 코드를 의미합니다. 클린 코드는 협업을 용이하게 하고, 버그 발생 가능성을 줄이며, 변화하는 요구사항에 빠르게 대응할 수 있도록 돕습니다. 이를 달성하기 위한 방법론 중 하나가 SOLID 원칙입니다.
SOLID는 객체 지향 프로그래밍의 다섯 가지 기본 원칙을 묶어 부르는 약어입니다. 각 원칙은 모듈화된 코드 설계를 통해 코드의 유연성과 재사용성을 높이는 데 기여합니다. SOLID 원칙을 준수하면 코드의 결합도를 낮추고 응집도를 높여 변경에 강한 시스템을 구축할 수 있습니다.
본 글에서는 SOLID 원칙 중에서도 단일 책임 원칙(SRP)에 집중하여 설명합니다. SRP는 "클래스는 변경해야 하는 이유가 오직 하나여야 한다"는 원칙입니다. 즉, 하나의 클래스는 오직 하나의 책임만을 가져야 한다는 의미입니다. 이를 위반할 경우 클래스가 여러 이유로 변경될 수 있어 유지보수가 어려워집니다.
이 글에서는 SRP를 위반하는 구체적인 상황별 예시를 3가지 제시하고, 이를 리팩토링하는 전략을 소개합니다. 예시를 통해 독자는 SRP를 실제 코드에 적용하는 방법을 이해하고, 클린 코드 작성 능력을 향상시킬 수 있습니다. 더불어, SRP를 준수함으로써 얻을 수 있는 이점을 명확히 제시하여 코드 품질 향상의 중요성을 강조합니다.
2. SRP 핵심 이해: 책임 분리의 기술
단일 책임 원칙(SRP)은 클래스나 모듈이 변경되어야 하는 이유가 단 하나여야 한다는 원칙입니다. 즉, 하나의 클래스는 오직 하나의 책임만을 가져야 합니다. 이는 코드의 응집도를 높이고 결합도를 낮춰 유지보수성과 확장성을 향상시키는 데 기여합니다. 책임이 분리되지 않은 코드는 변경이 발생했을 때 예상치 못한 부작용을 초래할 수 있습니다.
SRP를 준수하면 코드 변경 시 영향 범위를 최소화할 수 있습니다. 또한, 각 클래스가 명확한 목적을 가지므로 코드의 가독성이 향상됩니다. SRP는 객체 지향 설계의 핵심 원칙 중 하나이며, 다른 SOLID 원칙들과 함께 적용될 때 더욱 효과적입니다. 책임 분리는 코드의 복잡성을 줄이고, 재사용성을 높이는 데 중요한 역할을 합니다.
→ 2.1 SRP 위반 사례와 리팩토링
다음은 SRP를 위반하는 흔한 사례와 이를 리팩토링하는 방법입니다. 예를 들어, 사용자 정보를 관리하는 User 클래스가 있다고 가정합니다. 이 클래스가 사용자 정보 관리뿐만 아니라 데이터베이스 저장 기능까지 담당하고 있다면 SRP를 위반하는 것입니다.
이 경우, 데이터베이스 저장 기능을 별도의 클래스(예: UserRepository)로 분리해야 합니다. 이를 통해 User 클래스는 사용자 정보 관리에만 집중하고, UserRepository 클래스는 데이터베이스 상호작용에만 집중할 수 있습니다. 이러한 리팩토링은 코드의 유지보수성을 크게 향상시킵니다.
→ 2.2 SRP 적용 시 주의사항
SRP를 지나치게 적용하면 클래스의 수가 늘어나고 코드의 복잡성이 증가할 수 있습니다. 따라서 적절한 수준에서 책임을 분리하는 것이 중요합니다. SRP 적용 시에는 클래스의 응집도와 결합도를 고려하여 균형을 맞추는 것이 좋습니다. 또한, 미래의 변경 가능성을 예측하여 유연하게 대응할 수 있도록 설계해야 합니다.
📌 핵심 요약
- ✓ ✓ 단일 책임 원칙은 클래스가 변경될 이유가 하나여야 함
- ✓ ✓ SRP 준수는 코드 응집도 향상, 결합도 감소에 기여
- ✓ ✓ 책임 분리 시 과도한 클래스 증가는 지양해야 함
- ✓ ✓ 유지보수성 향상을 위해 역할 분담이 중요합니다
3. 상황별 예시 1: 복잡한 보고서 생성 리팩토링
보고서 생성 클래스가 다양한 형식(PDF, Excel, CSV)으로 보고서를 생성하는 책임을 지고 있을 때 단일 책임 원칙(SRP) 위반이 발생합니다. 이 경우 클래스는 보고서 형식 변경, 데이터 처리 방식 변경 등 다양한 이유로 수정될 가능성이 높습니다. 이는 클래스의 응집도를 낮추고 유지보수를 어렵게 만듭니다.
→ 3.1 리팩토링 전 코드 예시
다음은 리팩토링 전의 코드 예시입니다. ReportGenerator 클래스는 보고서 생성과 관련된 모든 책임을 가지고 있습니다.
public class ReportGenerator {
public void generateReport(List<Data> data, String format) {
if ("PDF".equals(format)) {
// PDF 생성 로직
} else if ("Excel".equals(format)) {
// Excel 생성 로직
} else if ("CSV".equals(format)) {
// CSV 생성 로직
}
}
}
→ 3.2 SRP 적용 리팩토링 전략
SRP를 적용하기 위해 보고서 생성 책임을 분리합니다. 각 보고서 형식에 대한 별도의 클래스를 생성하고, ReportGenerator 클래스는 이러한 클래스를 활용하여 보고서를 생성하도록 변경합니다.
- PDFReportGenerator: PDF 형식 보고서 생성 담당
- ExcelReportGenerator: Excel 형식 보고서 생성 담당
- CSVReportGenerator: CSV 형식 보고서 생성 담당
→ 3.3 리팩토링 후 코드 예시
리팩토링 후 코드는 다음과 같습니다. 각 보고서 형식에 대한 클래스가 분리되었습니다.
public interface ReportGenerator {
void generateReport(List<Data> data);
}
public class PDFReportGenerator implements ReportGenerator {
@Override
public void generateReport(List<Data> data) {
// PDF 생성 로직
}
}
public class ExcelReportGenerator implements ReportGenerator {
@Override
public void generateReport(List<Data> data) {
// Excel 생성 로직
}
}
public class CSVReportGenerator implements ReportGenerator {
@Override
public void generateReport(List<Data> data) {
// CSV 생성 로직
}
}
이러한 방식으로 리팩토링하면 각 클래스는 단일 책임만을 가지게 되어 유지보수성이 향상됩니다. 새로운 보고서 형식을 추가해야 할 경우, 기존 코드를 수정하지 않고 새로운 클래스를 추가하면 됩니다. 이는 코드의 확장성을 높이는 데에도 기여합니다.
4. 상황별 예시 2: DB 연결 클래스 개선 전략
데이터베이스 연결을 담당하는 클래스는 여러 곳에서 사용될 가능성이 높습니다. 단일 책임 원칙(SRP)을 위반하면 데이터베이스 연결 설정 변경, 쿼리 방식 변경 등 다양한 이유로 수정될 수 있습니다. 이는 시스템 전체에 영향을 미치는 결과를 초래할 수 있습니다. 따라서 DB 연결 클래스는 SRP를 준수하도록 설계해야 합니다.
→ 4.1 SRP 위반 사례
다음은 SRP를 위반하는 DB 연결 클래스의 예시입니다.
class DatabaseConnection {
public function connect(string $host, string $user, string $password, string $database): void {
// 데이터베이스 연결 로직
}
public function query(string $sql): array {
// 쿼리 실행 로직
}
public function disconnect(): void {
// 데이터베이스 연결 종료 로직
}
public function logQuery(string $sql): void {
// 쿼리 로깅 로직
}
}
위 클래스는 데이터베이스 연결, 쿼리 실행, 연결 종료, 쿼리 로깅이라는 4가지 책임을 가지고 있습니다. 이러한 구조는 SRP 위반으로 이어집니다.
→ 4.2 리팩토링 전략
SRP를 준수하도록 위 클래스를 리팩토링하는 방법은 다음과 같습니다.
- 데이터베이스 연결을 담당하는 Connection 클래스 생성
- 쿼리 실행을 담당하는 QueryExecutor 클래스 생성
- 쿼리 로깅을 담당하는 QueryLogger 클래스 생성
각 클래스는 자신의 책임만을 수행하도록 분리됩니다.
→ 4.3 리팩토링 결과
리팩토링 후 클래스 구조는 다음과 같습니다.
class Connection {
public function connect(string $host, string $user, string $password, string $database): void {
// 데이터베이스 연결 로직
}
public function disconnect(): void {
// 데이터베이스 연결 종료 로직
}
}
class QueryExecutor {
private $connection;
public function __construct(Connection $connection) {
$this->connection = $connection;
}
public function query(string $sql): array {
// 쿼리 실행 로직
}
}
class QueryLogger {
public function logQuery(string $sql): void {
// 쿼리 로깅 로직
}
}
각 클래스가 단일 책임만 가지게 되어 변경 사항이 발생하더라도 다른 클래스에 미치는 영향을 최소화할 수 있습니다. 예를 들어, 로깅 방식을 변경해야 할 경우 QueryLogger 클래스만 수정하면 됩니다. 데이터베이스 연결 방식 변경은 Connection 클래스만 수정하면 됩니다.
5. 상황별 예시 3: 사용자 인증 로직 분리 노하우
사용자 인증 로직은 애플리케이션의 보안과 직접적인 관련이 있습니다. 단일 책임 원칙(SRP)을 적용하지 않으면 인증 로직 변경 시 예상치 못한 문제 발생 가능성이 커집니다. 사용자 인증, 권한 부여, 세션 관리 등의 책임을 하나의 클래스에 모두 포함하는 것은 SRP 위반의 대표적인 예시입니다.
→ 5.1 인증 클래스의 문제점
하나의 클래스에서 사용자 인증과 관련된 모든 것을 처리하면 코드가 복잡해집니다. 예를 들어, UserAuthenticator 클래스가 인증, 암호 검증, 세션 생성, 로깅 등의 기능을 모두 수행한다고 가정해 보겠습니다. 이 경우, 작은 변경 사항이 전체 시스템에 영향을 미칠 수 있으며, 테스트 또한 어려워집니다.
다음은 SRP를 위반하는 코드의 예시입니다.
public class UserAuthenticator {
public boolean authenticateUser(String username, String password) {
// 사용자 인증 로직
if (isValidUser(username, password)) {
// 세션 생성
createSession(username);
// 로깅
logAuthentication(username);
return true;
}
return false;
}
private boolean isValidUser(String username, String password) {
// 데이터베이스에서 사용자 정보 확인
// ...
}
private void createSession(String username) {
// 세션 생성 로직
// ...
}
private void logAuthentication(String username) {
// 인증 로그 기록
// ...
}
}
→ 5.2 SRP 적용을 통한 리팩토링
SRP를 적용하여 사용자 인증 로직을 분리하면 코드의 유지보수성과 확장성이 향상됩니다. 각 책임(인증, 세션 관리, 로깅)을 별도의 클래스로 분리합니다. 이를 통해 각 클래스는 단 하나의 변경 이유만을 가지게 되며, 코드의 응집도가 높아집니다.
리팩토링 후 코드는 다음과 같습니다.
public class UserAuthenticator {
private final AuthenticationService authenticationService;
private final SessionManager sessionManager;
private final Logger logger;
public UserAuthenticator(AuthenticationService authenticationService, SessionManager sessionManager, Logger logger) {
this.authenticationService = authenticationService;
this.sessionManager = sessionManager;
this.logger = logger;
}
public boolean authenticateUser(String username, String password) {
if (authenticationService.isValidUser(username, password)) {
sessionManager.createSession(username);
logger.logAuthentication(username);
return true;
}
return false;
}
}
public interface AuthenticationService {
boolean isValidUser(String username, String password);
}
public interface SessionManager {
void createSession(String username);
}
public interface Logger {
void logAuthentication(String username);
}
이 예시에서 UserAuthenticator 클래스는 인증, 세션 관리, 로깅에 대한 책임을 각각 AuthenticationService, SessionManager, Logger 인터페이스에 위임합니다. 각 인터페이스는 구현체를 통해 실제 동작을 수행하며, UserAuthenticator는 이들의 조정자 역할만 수행합니다.
→ 5.3 결론 및 적용 전략
사용자 인증 로직 분리는 애플리케이션 보안 및 유지보수에 있어 중요한 요소입니다. SRP를 적용하여 각 책임을 분리하고, 인터페이스를 활용하여 유연성을 확보합니다. 이를 통해 코드의 가독성, 테스트 용이성, 유지보수성을 향상시킬 수 있습니다. 인증 방식 변경, 로깅 방식 변경 등 요구사항 변화에 더욱 유연하게 대처할 수 있습니다.
6. SOLID 원칙 위반, 흔한 실수와 해결책
SOLID 원칙은 객체 지향 설계의 기본 원칙이지만, 실제 개발에서 간과되는 경우가 많습니다. 흔한 실수들은 코드의 유지보수성을 떨어뜨리고 예상치 못한 버그를 발생시킬 수 있습니다. 따라서 SOLID 원칙을 위반하는 일반적인 상황과 그 해결책을 이해하는 것은 중요합니다.
→ 6.1 단일 책임 원칙(SRP) 위반 사례
단일 책임 원칙(SRP) 위반은 클래스가 여러 책임을 가질 때 발생합니다. 예를 들어, 사용자 인증과 로깅 기능을 동시에 수행하는 클래스가 있습니다. 이러한 클래스는 인증 로직 변경 시 로깅 기능에도 영향을 미칠 수 있습니다. 따라서 각 책임을 별도의 클래스로 분리해야 합니다.
이러한 문제는 인증 클래스와 로깅 클래스를 분리하여 해결할 수 있습니다. 인증 클래스는 사용자 인증만을 담당하고, 로깅 클래스는 로깅 기능만을 담당합니다. 이를 통해 각 클래스는 변경될 이유가 하나만 존재하게 되며, 코드의 응집도는 높아지고 결합도는 낮아집니다.
→ 6.2 해결책: 책임 분리 전략
책임 분리를 위한 몇 가지 전략이 존재합니다. 첫째, 클래스가 수행하는 기능을 명확히 파악해야 합니다. 둘째, 각 기능을 독립적인 모듈이나 클래스로 분리합니다. 셋째, 인터페이스를 활용하여 모듈 간의 결합도를 낮춥니다. 예를 들어, 데이터베이스 접근 클래스가 있다고 가정합니다. 데이터베이스 연결, 쿼리 실행, 결과 처리 등의 책임을 분리할 수 있습니다.
- 데이터베이스 연결 클래스: 연결 설정 관리
- 쿼리 실행 클래스: 쿼리 생성 및 실행
- 결과 처리 클래스: 결과 데이터 가공
이러한 분리를 통해 각 클래스는 자신의 책임에만 집중할 수 있습니다. 변경 사항이 발생했을 때 다른 모듈에 미치는 영향을 최소화할 수 있습니다. 또한, 각 모듈은 독립적으로 테스트가 가능해져 코드 품질을 향상시킬 수 있습니다.
→ 6.3 리팩토링 시 주의사항
리팩토링은 기존 코드의 기능을 변경하지 않고 구조를 개선하는 작업입니다. 단일 책임 원칙을 적용하기 위해 리팩토링을 수행할 때는 몇 가지 주의사항이 있습니다. 먼저, 리팩토링 전에 충분한 테스트 케이스를 확보해야 합니다. 리팩토링 과정에서 예상치 못한 문제가 발생할 수 있기 때문입니다. 따라서 자동화된 테스트를 통해 변경 사항을 검증하는 것이 중요합니다.
또한, 점진적으로 리팩토링을 진행해야 합니다. 한 번에 너무 많은 코드를 변경하면 디버깅이 어려워질 수 있습니다. 작은 단위로 변경하고 테스트를 수행하는 과정을 반복하는 것이 안전합니다. 마지막으로, 코드 리뷰를 통해 다른 개발자들의 의견을 수렴하는 것이 좋습니다. 코드 리뷰는 잠재적인 문제를 발견하고 더 나은 해결책을 찾는 데 도움이 됩니다.
📌 핵심 요약
- ✓ ✓ SOLID 원칙 위반은 유지보수를 어렵게 함
- ✓ ✓ 단일 책임 원칙 위반 시 클래스를 분리해야 함
- ✓ ✓ 책임 분리 전략으로 응집도 향상, 결합도 감소
- ✓ ✓ 리팩토링 시 충분한 테스트 확보가 중요
7. SRP 실천을 위한 핵심 체크리스트
단일 책임 원칙(SRP)을 효과적으로 적용하기 위해서는 구체적인 점검 항목을 활용하는 것이 좋습니다. 클래스나 모듈을 설계하거나 리팩토링할 때, 다음 체크리스트를 활용하면 SRP 준수 여부를 판단하고 개선 방향을 설정하는 데 도움이 됩니다. 각 항목에 대해 '예' 또는 '아니오'로 답변하며, '아니오'인 경우 책임을 분리해야 할 가능성이 높습니다.
→ 7.1 SRP 체크리스트
- 클래스의 변경 이유가 하나뿐인가?
- 클래스가 여러 개의 독립적인 기능을 수행하는가?
- 클래스의 기능들이 서로 관련성이 적은가?
- 클래스의 크기가 너무 커서 이해하기 어려운가?
- 클래스를 수정할 때 다른 클래스에 미치는 영향이 큰가?
위 체크리스트에서 하나 이상의 항목에 대해 '아니오'라고 답했다면, 해당 클래스는 SRP를 위반하고 있을 가능성이 높습니다. 따라서 클래스의 책임을 분리하여 응집도를 높이고 결합도를 낮추는 리팩토링을 고려해야 합니다. 클래스를 작은 단위로 분리하고, 각 클래스가 특정 책임만을 수행하도록 설계하는 것이 중요합니다.
→ 7.2 실천 가능한 조언
SRP 적용을 위한 첫 번째 단계는 클래스의 현재 책임을 명확하게 정의하는 것입니다. 각 책임이 서로 독립적인지, 또는 연관되어 있는지 분석합니다. 만약 클래스가 여러 책임을 가지고 있다면, 각 책임을 별도의 클래스로 분리하는 것을 고려합니다. 또한, 인터페이스를 활용하여 클래스 간의 결합도를 낮추는 것도 좋은 방법입니다.
오늘부터 SRP 실천, 클린 코드로!
이제 SRP의 중요성과 실제 리팩토링 예시를 통해 클린 코드 작성에 한 걸음 더 다가가셨습니다. 오늘부터 작은 코드 조각부터 SRP를 적용하여 유지보수성과 확장성이 뛰어난 코드를 만들어 보세요. 꾸준한 실천이 당신의 코드를 더욱 빛나게 할 것입니다.
📌 안내사항
- 본 콘텐츠는 정보 제공 목적으로 작성되었습니다.
- 법률, 의료, 금융 등 전문적 조언을 대체하지 않습니다.
- 중요한 결정은 반드시 해당 분야의 전문가와 상담하시기 바랍니다.
'IT' 카테고리의 다른 글
| Jenkinsfile 활용 CI/CD 파이프라인 구축, 빌드, 테스트, 배포 자동화 A to Z (0) | 2026.03.23 |
|---|---|
| SQL 윈도우 함수, 순위/누적 통계 분석 완벽 가이드 (LAG, LEAD, RANK, NTILE) (0) | 2026.03.22 |
| 파이썬 병렬 프로그래밍, GIL 피하는 3가지 방법 (Python 개발자) (0) | 2026.03.22 |
| 로봇 도시 SLAM 완벽 가이드, 5가지 핵심 기술과 로봇 적용 전략 (1) | 2026.03.17 |
| Stable Diffusion 모델 병합, 나만의 AI 이미지 모델 만들기 3가지 전략 (0) | 2026.03.16 |