SRP(Single Responsibility Principle)란?

단일 책임 원칙은 클래스는 단 하나의 이유로 변경되어야 한다는 원칙입니다. 즉 하나의 클래스는 오직 하나의 기능(책임)만 가져야 하며 여러 가지 역할을 혼합하면 안된다는 원칙입니다

이 원칙을 지키면 코드를 유지보수하기 쉽고, 테스트하기 용이하며 재사용성이 증가합니다

첫 번째로 SRP를 위반한 코드를 살펴보겠습니다.

@Service
class UserService(
  private val userRepository: UserRepository,
  private val mailSender: MailSender
) {

  fun register(user: User): User {
    
    // 사용자 정보 저장
    val user = userRepository.save(user)
    
    // 이메일 전송
    sendWelcomeEmail(user)
    
    return save
    
  }
  
  private fun sendWelcomeEmail(user: User) {
      val message = SimpleMailMessage()
      message.setTo(user.email)
      message.setSubject("Welcome!")
      message.setText("Hello ${user.name}, welcome to our service!")
      
      mailSender.send(message)
  }
}

위와 같이 작성을 한다면 SRP를 위반하게 됩니다.
저도 SOLID 원칙을 배우기 전에는 일반적으로 위와 같이 동작하게 코드를 작성하였던 것 같습니다.

어떤 것이 문제인지 일단 살펴보겠습니다

  • UserService가 사용자 관리(데이터 저장)뿐만 아니라 이메일 전송까지 담당하고 있습니다.
  • sendWelcomeEmail() 메소드가 변경된다면 UserService가 영향을 받게 되면서 UserService 코드도 변경해야 합니다.

그럼 SRP를 준수한 코드로 변경해보겠습니다.

@Service
class UserService(
    private val userRepository: UserRepository,
    private val emailService: EmailService
) {
    
    fun register(user: User): User {
        // 1. 사용자 저장
        val savedUser = userRepository.save(user)

        // 2. 이메일 전송은 EmailService에 위임
        emailService.sendWelcomeEmail(savedUser)

        return savedUser
    }
}

이렇게 UserService 클래스는 유저에 관련된 함수만 동작하게 별도로 생성하고

@Service
class EmailService(
    private val mailSender: MailSender
) {

    fun sendWelcomeEmail(user: User) {
        val message = SimpleMailMessage()
        message.setTo(user.email)
        message.setSubject("Welcome!")
        message.setText("Hello ${user.name}, welcome to our service!")

        mailSender.send(message)
    }
}

메일을 보내는 서비스는 따로 생성하는 것이 SRP를 준수하는 방법입니다.

그렇다면 위의 예시만으로는 단순하게 EmailService를 생성하면 준수하는 것인가? 라는 의문이 생길 수 있는데요

단순하게 본다면 “네” 맞습니다.

정확하게는 Email에 관련된 서비스는 EmailService에 별도로 생성해서 사용하는 것이 SRP를 준수하는 것입니다.

여기에 추가로 이메일 인증을 통한 비밀번호 재설정 같은 요구 사항이 추가된다면

EmailService에 해당 기능을 작성하여 확장성을 증가할 수 있겠죠

또한 TDD를 이용한 개발이 주를 이루는 만큼 UserService의 단위 테스트 시, 이메일 전송 기능을 Mocking하여 분리된 테스트를 진행할 수 있으며, EmailService 또한 독립적으로 테스트가 가능해집니다.

결론

  • SRP를 이용하면 유지보수가 향상되며 테스트를 하기에도 용이해진다.
  • Spring Boot에서는 서비스 레이어를 분리하여 SRP를 자연스럽게 적용할 수 있다.
  • 핵심 비즈니스 로직인 (UserService)와 부가 기능인 (EmailService)를 분리하는 것이 중요하다