Computer Science/객체지향

SRP(단일 책임 원칙) in SOLID

sh1mj1 2024. 12. 31. 02:24

소개

SOLID란?

SOLID 는 객체지향 설계를 더 이해하기 쉽고, 유연하며, 유지보수하기 쉽게 만드는 다섯 가지 원칙을 말합니다.
이 원칙들은 시스템을 확장 가능하고, 견고하며, 변경에 적응할 수 있도록 설계하도록 안내합니다.

하지만 이러한 원칙을 이해하는 것은 실용적인 예제가 없으면 어렵게 느껴질 수 있습니다.

왜 이 시리즈를 작성했나요?

이 시리즈의 목적은 간단합니다:

  1. SOLID 원칙이 어떻게 위반되는지를 명확히 설명합니다.
  2. 각 원칙을 효과적으로 준수하는 방법을 리팩토링 과정을 통해 보여줍니다.

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

SRP(Single Responsibility Principle)는 객체지향 설계 원칙의 기초로, 다음을 의미합니다:

  • 모듈은 오직 하나의 책임만 가져야 한다.
  • 모듈은 변경해야 하는 이유가 단 하나여야 한다.

더 간단히 말하면,
각 클래스, 함수, 또는 모듈은 하나의 책임에만 집중해야 하며,
이를 통해 유지보수와 확장이 쉬워지고 테스트도 간단해집니다.

그럼 이 원칙이 실제로 어떻게 적용되는지 살펴보겠습니다.

SRP를 위반하면 어떻게 될까요?

예제: SRP 위반

아래의 LottoSeller 클래스는 로또를 판매하는 책임을 가지고 있습니다.
처음에는 단일 책임처럼 보이지만, 실제로는 여러 역할을 수행하고 있습니다:

  • 돈과 가격을 바탕으로 로또 개수를 계산
  • 로또 번호를 무작위로 생성
  • 생성된 로또 번호를 검증

아래는 SRP를 위반한 코드입니다:

kotlin
닫기
class LottoSeller {
    fun soldLotto(money: Int): List<Lottery> {
        val count = money / LOTTO_PRICE

        val lotteries = (1..count)
            .map { Lottery((lottoRange).shuffled().take(6).sorted()) }

        lotteries.forEach { validate(it) }

        return lotteries
    }

    private fun validate(lottery: Lottery) {
        require(lottery.numbers.size == LOTTO_NUMBER_COUNT) { "Invalid lotto number count" }
        lottery.numbers.forEach {
            require(it in MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER) { "Invalid lotto number" }
        }
    }

    companion object {
        private const val LOTTO_PRICE = 1000
        private const val MIN_LOTTO_NUMBER = 1
        private const val MAX_LOTTO_NUMBER = 45
        private const val LOTTO_NUMBER_COUNT = 6

        private val lottoRange = (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER)
    }
}

data class Lottery(val numbers: List<Int>)

문제점은 무엇인가요?

LottoSeller 클래스는 과도한 책임을 가지고 있습니다:

  1. 검증 로직 변경:
    • 로또 번호 검증 방식이 바뀌면 LottoSeller 클래스를 수정해야 합니다.
  2. 번호 생성 전략 변경:
    • 로또 번호를 생성하는 방식이 변경되면 LottoSeller 클래스를 수정해야 합니다.
  3. 가격 정책 변경:
    • 로또 가격 정책이 바뀌어도 LottoSeller를 수정해야 합니다.

이러한 결합(coupling)은 코드를 취약하고 유지보수하기 어렵게 만듭니다.
또한, 각각의 책임(기능)이 독립적으로 테스트되지 않습니다.

SRP에 맞게 리팩토링하기

SRP를 준수하려면 각각의 책임을 별도 클래스로 분리해야 합니다.

리팩토링된 코드

kotlin
닫기
class LottoSeller {
    fun soldLotto(money: Int): List<Lottery> {
        val count = money / LOTTO_PRICE
        return List(count) { LotteryGenerateStrategy().autoGenerate() }
    }

    companion object {
        private const val LOTTO_PRICE = 1000
    }
}

data class Lottery(val numbers: List<Int>) {
    init {
        numbers.forEach {
            require(numbers.size == NUMBER_COUNT) { "Invalid lotto number count" }
            require(it in MIN_NUMBER..MAX_NUMBER) { "Invalid lotto number" }
        }
    }

    companion object {
        private const val MIN_NUMBER = 1
        private const val MAX_NUMBER = 45
        val numberRange = (MIN_NUMBER..MAX_NUMBER)
        const val NUMBER_COUNT = 6
    }
}

class LotteryGenerateStrategy {
    fun autoGenerate(): Lottery =
        Lottery(
            (Lottery.numberRange).shuffled().take(Lottery.NUMBER_COUNT).sorted(),
        )
}

무엇이 개선되었나요?

  1. LottoSeller의 역할 축소:
    • 이제 LottoSeller는 오직 로또를 판매하는 역할만 담당합니다.
  2. 번호 생성 전략 캡슐화:
    • LotteryGenerateStrategy 클래스는 로또 번호 생성 로직을 캡슐화합니다.
    • 새로운 생성 전략(e.g., ManualLottoGenerateStrategy)을 쉽게 추가할 수 있습니다.
  3. 검증 로직의 이동:
    • 로또 번호 검증은 이제 Lottery 클래스에서 처리되며, 검증 책임이 명확해졌습니다.
  4. 테스트 가능성 향상:
    • 각각의 책임이 독립적으로 분리되어, 기능별 테스트가 용이해졌습니다.
    • 예를 들어, 번호 생성 전략이나 검증 로직을 따로 테스트할 수 있습니다.

결론

SRP를 준수함으로써:

  • 코드는 더 이해하기 쉽고 유지보수하기 쉬워집니다.
  • 하나의 기능이 변경되더라도 다른 기능에 불필요한 영향을 주지 않습니다.
  • 개별 구성요소를 독립적으로 테스트할 수 있어 신뢰성이 향상됩니다.

SRP는 객체지향 설계의 가장 기본적이지만 가장 중요한 원칙입니다.
여러분의 프로젝트에서도 SRP를 적용해보세요!
코드 품질, 유지보수성, 그리고 테스트 용이성에서 확실한 차이를 느낄 수 있을 것입니다.

 

다음 글은?

다음 글에서는 SOLID 원칙 중 두 번째인 **OCP(Open-Closed Principle)**에 대해 다뤄보겠습니다.