base

단일 책임 원칙 (Single Responsibility Principle, SRP)이란

개발을 하다 보면 한 번쯤은 코드가 점점 복잡해지고 한 클래스나 함수가 너무 많은 일을 하고 있는 것 같다는 느낌을 받을 때가 있습니다. 이런 상황이 지속되면 결국 유지보수가 어려워지고, 코드가 엉망이 되기 십상입니다. 만약 여러분의 클래스가 이메일 전송, 데이터 처리, 사용자 인증, 로그 기록까지 모두 처리하고 있다면 어떻게 될까요? 하나의 클래스에서 여러 가지 기능을 다루다 보면 나중에 코드의 어느 한 부분을 수정할 때 다른 부분까지 영향을 미치게 되어 버그가 발생할 가능성이 커집니다.


이때 유용한 개념이 바로 단일 책임 원칙(Single Responsibility Principle, SRP)입니다. 이번 글에서는 소프트웨어 설계의 핵심 원칙 중 하나인 SRP의 개념을 명확하게 이해하고, 실제 개발에 어떻게 적용할 수 있는지에 대해 살펴보겠습니다.


단일 책임 원칙(SRP)의 의미와 필요성

단일 책임 원칙(SRP)이란, 하나의 클래스나 함수가 "단 하나의 책임만 가져야 한다"는 원칙입니다. 이 원칙은 로버트 C. 마틴(Robert C. Martin, Uncle Bob)의 SOLID 원칙 중 첫 번째 원칙으로 잘 알려져 있습니다. 여기서 말하는 "책임"이란 단순히 클래스나 함수가 하는 일 이상의 의미를 갖습니다. 실제로 SRP는 다음과 같은 개념으로 정의됩니다. "한 클래스는 변경되어야 하는 오직 하나의 이유만을 가져야 한다." 즉, 클래스가 수정되어야 하는 이유가 한 가지여야 한다는 것이 핵심입니다. 이메일 전송과 사용자 인증을 함께 처리하는 클래스가 있다면, 이메일 전송 방식이 바뀔 때도, 사용자 인증 로직이 바뀔 때도 동일한 클래스를 수정하게 됩니다. 이런 경우가 SRP를 위반한 사례가 됩니다.


SRP를 준수해야 하는 이유

SRP를 잘 지키게 되면, 다음과 같은 장점들이 생깁니다.

  • 유지보수 용이성: 클래스가 단 하나의 책임만을 가지므로, 기능 변경이나 수정이 필요할 때 다른 기능에 영향을 주지 않고 독립적으로 변경할 수 있습니다.
  • 가독성 향상: 클래스의 역할이 명확하므로 코드를 이해하고 읽기 쉬워집니다.
  • 테스트 용이성: 각각의 클래스나 함수가 하나의 책임만 가지므로 테스트 케이스 작성이 쉬워지고, 더 정밀한 단위 테스트가 가능합니다.
  • 재사용성 증가: 책임이 명확히 분리된 클래스는 다른 프로젝트나 모듈에서 재사용이 쉬워집니다.

SRP 적용 전후 비교 예시

이제 간단한 예시를 통해 SRP를 적용하기 전과 후를 비교해 보겠습니다.

SRP를 위반한 예시

srp
class UserService {
  registerUser(user) {
    this.saveToDatabase(user)
    this.sendWelcomeEmail(user)
  }
 
  saveToDatabase(user) {
    // 데이터베이스 저장 로직
  }
 
  sendWelcomeEmail(user) {
    // 이메일 전송 로직
  }
}

위 코드에서 UserService 클래스는 "사용자를 등록하는 책임"뿐 아니라, "데이터베이스 저장"과 "이메일 전송"이라는 추가 책임도 함께 가지고 있습니다. 따라서 이메일 전송 방식이 바뀌거나 데이터베이스가 바뀌는 경우에도 클래스 전체가 영향을 받게 되어 유지보수가 어려워집니다.

SRP를 적용한 개선된 예시

srp
class UserService {
  constructor(
    private userRepository: UserRepository,
    private emailService: EmailService,
  ) {}
 
  registerUser(user) {
    this.userRepository.save(user)
    this.emailService.sendWelcomeEmail(user)
  }
}
 
class UserRepository {
  save(user) {
    // 데이터베이스 저장 로직
  }
}
 
class EmailService {
  sendWelcomeEmail(user) {
    // 이메일 전송 로직
  }
}

이제 각 클래스는 명확한 책임을 가지고 있습니다.

  • UserService: 사용자 등록 프로세스만 책임
  • UserRepository: 데이터베이스 저장만 책임
  • EmailService: 이메일 전송만 책임

이렇게 SRP를 적용하면 기능 변경 시 영향받는 부분이 명확해지고, 유지보수가 용이해집니다.


SRP를 적용할 때의 주의사항

단일 책임 원칙을 적용할 때 지나치게 책임을 세분화하면 오히려 복잡성이 증가할 수도 있습니다. 항상 적절한 추상화와 분리 수준을 유지하는 것이 중요합니다.

  • 책임을 지나치게 세분화하면 관리해야 할 클래스가 너무 많아질 수 있습니다.
  • 책임의 경계를 명확히 하는 것은 좋지만, 너무 작게 나누면 코드가 더 이해하기 어렵고 유지보수가 까다로워질 수 있습니다.

적절한 균형점을 찾는 것이 중요합니다. 이는 회사 내부의 convention과 상황에 따라 고려하면 좋을 것 같습니다.


정리

이번 글에서 다룬 내용을 다시 한 번 정리하면 다음과 같습니다.

  • SRP는 클래스나 함수가 변경되어야 하는 이유를 한 가지로 제한하여, 명확한 책임과 역할을 부여하는 원칙입니다.
  • SRP를 준수하면 코드의 유지보수가 쉬워지고, 테스트와 재사용성이 향상됩니다.
  • 책임을 분리할 때는 적절한 수준의 추상화와 균형을 유지하는 것이 중요합니다.

단일 책임 원칙을 잘 지키면 더 깔끔하고 효율적인 코드를 작성할 수 있으며, 장기적으로 개발 생산성을 높일 수 있습니다.


참고