소프트웨어공학

[SOLID] SRP 원칙 (Single Responsibility Principle)

타우루스 2025. 10. 5. 09:56

SRP(단일 책임 원칙)는 객체 지향 설계를 깔끔하게 만드는 첫걸음이랍니다.

 

 

SRP 학습 계획 🚀

  1. 단일 책임 원칙(SRP)이란?: SRP가 무엇인지 핵심 개념을 알아봐요. '책임'이 대체 뭘 의미하는지 확실히 짚고 넘어갈 거예요.
  2. SRP는 왜 중요할까?: SRP를 지키면 어떤 점이 좋은지, 왜 프로그래머들이 강조하는지 그 이유를 파헤쳐 봐요.
  3. 실전 예제로 감 잡기: 간단한 코드를 통해 SRP를 지켰을 때와 안 지켰을 때가 어떻게 다른지 비교하며 확실하게 감을 잡아봐요.


1. 단일 책임 원칙(SRP)이란?

단일 책임 원칙(Single Responsibility Principle, SRP)은 아주 간단한 아이디어에서 출발해요. 바로 "클래스는 단 하나의 책임만 가져야 한다"는 원칙이죠.

 

혹시 '맥가이버 칼' 아시나요? 칼, 드라이버, 톱, 가위가 다 들어있어서 만능처럼 보이죠. 하지만 만약 톱날이 무뎌지면 칼 전체를 수리 맡기거나 바꿔야 할 수도 있어요. SRP는 이런 상황을 피하자는 거예요. 클래스를 '맥가이버 칼'처럼 만들지 말고, 각자 자기 역할만 잘하는 '칼', '드라이버', '톱'으로 나누자는 거죠.

 

여기서 말하는 '책임'은 '변경되어야 하는 이유'라고 생각하면 쉬워요. 즉, "클래스를 수정해야 할 이유는 단 하나뿐이어야 한다"는 뜻이에요.

예를 들어, 직원(Employee)이라는 클래스가 있다고 상상해 보세요. 이 클래스가 직원의 월급을 계산하는 책임(calculatePay())과 근무 시간을 기록하는 책임(reportHours()), 그리고 데이터베이스에 직원 정보를 저장하는 책임(save())을 모두 가지고 있다면 어떨까요?

  • 세금 정책이 바뀌면 월급 계산 로직을 바꿔야 하고,
  • 근무 시간 보고 양식이 바뀌면 기록 로직을 바꿔야 하고,
  • 데이터베이스 시스템이 바뀌면 저장 로직을 바꿔야 해요.

이렇게 세 가지 다른 이유로 직원 클래스를 수정해야 한다면, 이 클래스는 SRP를 위반한 거예요. 하나의 기능 수정이 다른 기능에 의도치 않은 버그를 만들 수도 있고요.

 

퀴즈

여기에 이메일을 보내는 역할을 하는 EmailSender 클래스가 있습니다. 이 클래스는 다음과 같은 세 가지 일을 한답니다.

  1. 사용자 이메일 주소 확인: 보내려는 이메일 주소가 올바른 형식인지(aaa@bbb.com) 확인합니다.
  2. 이메일 내용 조합: 제목과 본문을 합쳐서 최종 이메일 내용을 만듭니다.
  3. 이메일 서버로 전송: 만들어진 이메일을 실제 서버를 통해 발송합니다.

이 EmailSender 클래스는 단일 책임 원칙(SRP)을 잘 지키고 있을까요? 왜 그렇게 생각하시나요?

더보기

정답 : SRP 위반

 

'이메일 주소 확인', '내용 조합', '서버로 전송'은 서로 다른 책임이에요.

  • 이메일 주소 형식을 검증하는 정책이 바뀌면 클래스를 수정해야 하고,
  • 이메일 내용을 만드는 방식(예: HTML 템플릿 추가)이 바뀌어도 수정해야 하고,
  • 이메일 발송 서버의 주소나 프로토콜이 바뀌어도 수정해야 하죠.

이렇게 수정해야 할 이유가 여러 개라는 것이 바로 SRP를 위반했다는 강력한 신호랍니다. 하나의 기능을 고치려다 다른 기능을 망가뜨릴 위험도 커지고요.

 

SRP 위반 클래스

 

SRP를 지키도록 수정한 클래스


2. SRP는 왜 중요할까? (SRP의 장점)

우리가 첫 번째 활동에서 봤던 복잡한 EmailSender 클래스를 떠올려보세요. SRP를 적용해서 '주소 검증', '내용 조합', '실제 발송' 클래스로 나눈다면 다음과 같은 장점들이 생긴답니다.

가독성이 높아지고 이해하기 쉬워져요 👀

클래스 이름만 봐도 "아, 이 클래스는 이메일 주소 검증만 하겠구나!" 하고 역할을 바로 파악할 수 있어요. 코드를 처음 보는 사람도 훨씬 쉽게 이해할 수 있죠. 마치 서랍마다 '양말', '속옷', '수건'이라고 이름표를 붙여놓는 것과 같아요.

유지보수가 편해져요 🔧

이메일 서버 전송 방식만 바꾸고 싶다면 '실제 발송' 클래스만 열어보면 돼요! 다른 코드를 건드릴 필요가 없으니 변경으로 인한 실수를 줄일 수 있고(안정성 UP!), 필요한 부분만 쏙쏙 고칠 수 있어서 수정 속도도 빨라져요(생산성 UP!).

재사용성이 올라가요 ♻️

'이메일 주소 검증' 기능은 이메일 보내는 곳 말고도 회원가입이나 내 정보 수정 페이지에서도 필요하겠죠? 이렇게 클래스를 책임 단위로 잘 나눠두면, 필요한 곳에 부품처럼 가져다 쓸 수 있어요. 범용적인 부품은 여기저기 쓸모가 많은 법이죠!

테스트하기 쉬워져요 ✅

책임이 하나인 클래스는 테스트할 범위도 명확해요. '이메일 주소 검증' 클래스는 "잘못된 이메일 주소를 잘 걸러내는가?"만 집중해서 테스트하면 되니까요. 책임이 여러 개 섞여있을 때보다 훨씬 간단하고 정확한 테스트가 가능해져요.

요약하자면, SRP는 코드를 더 깔끔하고, 유연하고, 튼튼하게 만들어주는 아주 중요한 기본 원칙이랍니다!

 

3. 실전 예제

사용자에 대한 간단한 보고서를 만들고 출력하는 상황을 가정해 볼게요.

❌ Before: SRP를 지키지 않은 코드

하나의 클래스가 데이터 처리, 내용 형식 지정, 출력까지 모든 것을 담당하고 있어요.

class BadReport:
    def __init__(self, user_id):
        self.user_id = user_id
        self.user_name = ""

    def get_user_data(self):
        # 1. 데이터베이스에서 사용자 정보를 가져오는 책임
        print(f"{self.user_id} 사용자의 데이터를 DB에서 가져옵니다...")
        self.user_name = "홍길동" # 예시 데이터

    def format_report_as_html(self):
        # 2. 보고서를 HTML 형식으로 만드는 책임
        print("보고서를 HTML 형식으로 변환합니다...")
        return f"<h1>{self.user_name}님의 보고서</h1>"

    def print_report(self):
        # 3. 보고서를 프린터로 출력하는 책임
        self.get_user_data()
        report_html = self.format_report_as_html()
        print("프린터로 보고서를 출력합니다.")
        print(report_html)

# 실행
report = BadReport("user123")
report.print_report()

 

문제점: 이 BadReport 클래스는 'DB 접근 방식', '보고서 디자인', '출력 방식'이라는 세 가지의 서로 다른 변경 이유를 가지고 있어요. 보고서 디자인만 바꾸고 싶은데 DB 관련 코드까지 신경 써야 하는 상황이죠.


✅ After: SRP를 적용한 코드

각자의 책임만 담당하는 세 개의 클래스로 분리했어요.

# 1. 데이터 처리 책임
class UserRepository:
    def get_user_name(self, user_id):
        print(f"{user_id} 사용자의 데이터를 DB에서 가져옵니다...")
        return "홍길동"

# 2. 보고서 형식 책임
class ReportFormatter:
    def format_as_html(self, user_name):
        print("보고서를 HTML 형식으로 변환합니다...")
        return f"<h1>{user_name}님의 보고서</h1>"

# 3. 보고서 출력 책임
class ReportPrinter:
    def __init__(self, repository, formatter):
        self.repository = repository
        self.formatter = formatter

    def print_report(self, user_id):
        user_name = self.repository.get_user_name(user_id)
        formatted_report = self.formatter.format_as_html(user_name)
        print("프린터로 보고서를 출력합니다.")
        print(formatted_report)

# 실행: 부품(클래스)들을 조립해서 사용!
repo = UserRepository()
formatter = ReportFormatter()
report_printer = ReportPrinter(repo, formatter)
report_printer.print_report("user123")

 

개선된 점:

  • 데이터베이스가 바뀌면 UserRepository만 수정하면 돼요.
  • 보고서 디자인을 JSON이나 PDF로 바꾸고 싶다면 ReportFormatter만 수정하거나 새로 만들면 되죠.
  • 출력 방식을 이메일로 바꾸고 싶다면 ReportPrinter만 보면 되고요.

이제 각 클래스가 한 가지 일에만 집중하니 훨씬 깔끔하고, 부품처럼 갈아 끼우기도 쉬워졌어요!

 

✨ 퀴즈 ✨

온라인 쇼핑몰의 상품(Product) 클래스를 만든다고 상상해 보세요. 이 클래스가 단일 책임 원칙(SRP)을 가장 잘 지키려면, 다음 중 어떤 책임을 가져야 할까요?

  1. 상품의 가격, 재고를 관리하고, 상품을 웹페이지에 HTML로 표시하는 기능
  2. 상품의 가격, 이름, 설명 등 순수한 데이터 속성을 관리하는 기능
  3. 상품의 재고 수량을 확인하고, 재고가 부족하면 공급처에 자동으로 재주문을 넣는 기능
  4. 사용자의 장바구니에 상품을 추가하고, 해당 상품의 결제를 처리하는 기능
더보기

정답 : 2번

왜 2번이 정답일까요?

상품 클래스가 오직 상품의 순수한 데이터(이름, 가격 등)를 관리하는 책임만 가지기 때문이에요. 이 클래스를 수정할 이유는 '상품의 데이터 속성이 변경될 때' 단 하나뿐이죠.

다른 보기들은 왜 틀렸을까요?

  • 1번: '데이터 관리'와 '웹페이지 표시'라는 두 가지 책임이 섞여 있어요.
  • 3번: '재고 확인'과 '공급처 재주문'은 서로 다른 비즈니스 로직이에요.
  • 4번: '장바구니'와 '결제'는 완전히 분리되어야 하는 매우 큰 책임들이죠.

 

요약

  • SRP란?: 클래스는 변경되어야 할 이유를 단 하나만 가져야 한다.
  • SRP의 장점: 코드를 깔끔하고, 유연하며, 튼튼하게 만든다.