SOLID #1 - SRP (단일 책임 원칙)

윤성현·2025년 3월 3일
1

글쓰기 챌린지

목록 보기
5/5
post-thumbnail

서론

객체지향개론을 들었다면 객체지향적인 설계를 도와주는 SOLID 원칙을 나눠보고자 합니다. SOLID 원칙은 객체지향 설계를 위한 5가지 원칙이며, 오늘은 첫번째 원칙인 SRP (Single Responsibility Principle; 단일책임원칙)에 대해서 나눠보려고 합니다. 이런 원칙이 왜 필요한지 알아보는 시간이 되길 바랍니다.

한 클래스는 하나의 책임만 가져야 한다.

- There should never be more than one reason for a class to change.

왜 책임을 분리해야 하는가?

  • 유지보수를 용이하게 하기 위함

왜 SRP를 지키면 유지보수성이 높아지는가?

  • 변경이 한 클래스 안에서만 다루어질 때, 변경에 대한 두려움이 줄어든다.
  • 하나의 변경이 다른 클래스에 영향을 미치지 않는다면, 기존 동작을 유지한 채로 새로운 기능을 추가할 수 있다.

책임을 어떻게 정의할 것인가?

클래스가 하나의 책임만을 가져야 한다고 하는데, 그렇다면 책임이란 무엇인가?

→ 이전 글 "객체의 역할과 책임"에서 역할에서 기인한 어떠한 행위를 책임이라고 정의했다.

그렇다면, 실제적으로 어떤 것을 단일 책임으로 볼 것인가?

1) 하나의 함수 수행만을 가지고 있는 것을 하나의 책임으로 볼 것인가?

이 주장을 검토하기 위해 두 가지 예시를 살펴보자.

첫 번째로, 로또 번호를 담는 LotteryNumber 클래스를 보자.

data class LotteryNumber(val number: Int) {
    init {
        require(number in 1 .. 45) { "에러메세지" }
    }
}

이 클래스는 단순히 숫자의 유효성을 검증하는 책임을 가진다. 하나의 함수만을 수행하며, SRP를 준수하는 것처럼 보인다.

두 번째로, 중복되지 않은 6개의 번호를 담는 Lottery 클래스를 살펴보자.

class Lottery (val lotteryNumbers: List<LotteryNumber>) {
    init {
        require(lotteryNumbers.size == 6) { "에러메세지" }
        require(lotteryNumbers.toSet().size == 6) { "에러메세지" }
    }
}

이 클래스는 6개의 번호가 중복되지 않음을 검증하는 역할을 한다. 검증 로직이 여러 줄이지만, 결국 하나의 역할을 수행하는 것이므로 단일 책임 원칙을 어긴다고 보기 어렵다.

즉, 하나의 책임을 수행하기 위해 여러 개의 함수나 로직이 필요할 수 있으며, 단순히 "하나의 함수 수행만을 책임으로 본다"는 접근은 적절하지 않다.

2) 클래스가 하나의 public 메소드만을 가지는 것을 하나의 책임으로 볼 것인가?

  • 책임을 하나의 메소드가 수행하는 일로 이해한다면 이렇게 이해할 수 있다.
  • 서로 다른 액터가 해당 클래스에 접근하는 것이 아니라면 두 개 이상의 public메소드를 가졌어도 단일 책임 원칙을 지키고 있다고 할 수 있다.
class UserService {
    // 모두 사용자 관리와 관련된 기능으로, 동일한 액터(관리자)가 사용
    fun createUser(user: User): User { ... }
    fun findUserById(id: Long): User { ... }
    fun updateUserProfile(id: Long, profile: Profile): User { ... }
}

3) 클래스가 다중 상속(혹은 다중 구현)을 한다면, 복수의 책임을 갖는가?

  • 특정 클래스가 여러 클래스에서 상속을 받는다면 여러 책임을 상속받아 단일 책임이 위배된다고 생각할 수 있다.
  • 다중 상속을 받더라도 액터가 그 다중상속한 것들을 모두 사용한다면 단일 책임 원칙을 만족한다.
// 모두 UI 렌더링과 관련된 인터페이스
interface Clickable {
    fun onClick()
}

interface Draggable {
    fun onDrag()
}

// UI 담당 액터가 모든 기능을 사용함
class Button : Clickable, Draggable {
    override fun onClick() { println("Button clicked") }
    override fun onDrag() { println("Button dragged") }
}

4) 해당 클래스를 의존하는 사용자(클라이언트)가 여럿이라면 변경되는 이유가 여러 가지가 되는가?

  • 여러 클라이언트가 사용하더라도 동일한 요구사항으로 사용한다면 단일 책임 원칙을 준수하는 것이다.
class PaymentProcessor {
    fun processPayment(payment: Payment): Receipt {
        // 결제 처리 로직
        return Receipt()
    }
}

// 여러 클라이언트가 사용하지만 모두 동일한 목적(결제 처리)으로 사용
class WebCheckout {
    private val paymentProcessor = PaymentProcessor()
    
    fun checkout(cart: Cart) {
        val payment = createPayment(cart)
        paymentProcessor.processPayment(payment)
    }
}

class MobileApp {
    private val paymentProcessor = PaymentProcessor()
    
    fun purchase(product: Product) {
        val payment = createPayment(product)
        paymentProcessor.processPayment(payment)
    }
}

그래서 답은?

  • 답은 없다. 당신의 소프트웨어 세상을 그려나가는 데에 있어서 남이 주는 어떠한 잣대를 사용한다면, 그것은 진짜 당신의 세상이라고 부를 수 있을까? SRP를 절대적인 규칙으로 받아들이기보다는, 소프트웨어의 변화와 유지보수를 고려하여 적용하는 것이 중요하다.
  • 하지만 위 4가지 의문에 있어서 조금 해결을 찾아줄 수 있는 하나의 도움이 되는 아이디어가 있다.

    하나의 모듈은, 오직 하나의 액터에 대해서만 책임져야 한다. - Robert C. Martin

    액터란, (시스템이 동일한 방식으로 변경되기를 원하는) 사용자 집단을 의미한다. 액터는 한 명일수도, 여러 명일 수도 있다. 해당 클래스에 어떤 액터가 의존하고 있는지 생각할 때, 우리는 진짜 SRP에 조금 더 가까워질 수 있다.

0개의 댓글