소개
SOLID란?
SOLID 는 객체지향 설계를 더 이해하기 쉽고, 유연하며, 유지보수하기 쉽게 만드는 다섯 가지 원칙을 말합니다.
이 원칙들은 시스템을 확장 가능하고, 견고하며, 변경에 적응할 수 있도록 설계하도록 안내합니다.
하지만 이러한 원칙을 이해하는 것은 실용적인 예제가 없으면 어렵게 느껴질 수 있습니다.
왜 이 시리즈를 작성했나요?
이 시리즈의 목적은 간단합니다:
- SOLID 원칙이 어떻게 위반되는지를 명확히 설명합니다.
- 각 원칙을 효과적으로 준수하는 방법을 리팩토링 과정을 통해 보여줍니다.
SRP(단일 책임 원칙)이란?
SRP(Single Responsibility Principle)는 객체지향 설계 원칙의 기초로, 다음을 의미합니다:
- 모듈은 오직 하나의 책임만 가져야 한다.
- 모듈은 변경해야 하는 이유가 단 하나여야 한다.
더 간단히 말하면,
각 클래스, 함수, 또는 모듈은 하나의 책임에만 집중해야 하며,
이를 통해 유지보수와 확장이 쉬워지고 테스트도 간단해집니다.
그럼 이 원칙이 실제로 어떻게 적용되는지 살펴보겠습니다.
SRP를 위반하면 어떻게 될까요?
예제: SRP 위반
아래의 `LottoSeller` 클래스는 로또를 판매하는 책임을 가지고 있습니다.
처음에는 단일 책임처럼 보이지만, 실제로는 여러 역할을 수행하고 있습니다:
- 돈과 가격을 바탕으로 로또 개수를 계산
- 로또 번호를 무작위로 생성
- 생성된 로또 번호를 검증
아래는 SRP를 위반한 코드입니다:
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` 클래스는 과도한 책임을 가지고 있습니다:
- 검증 로직 변경:
- 로또 번호 검증 방식이 바뀌면 `LottoSeller` 클래스를 수정해야 합니다.
- 번호 생성 전략 변경:
- 로또 번호를 생성하는 방식이 변경되면 `LottoSeller` 클래스를 수정해야 합니다.
- 가격 정책 변경:
- 로또 가격 정책이 바뀌어도 `LottoSeller`를 수정해야 합니다.
이러한 결합(coupling)은 코드를 취약하고 유지보수하기 어렵게 만듭니다.
또한, 각각의 책임(기능)이 독립적으로 테스트되지 않습니다.
SRP에 맞게 리팩토링하기
SRP를 준수하려면 각각의 책임을 별도 클래스로 분리해야 합니다.
리팩토링된 코드
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(),
)
}
무엇이 개선되었나요?
- LottoSeller의 역할 축소:
- 이제 `LottoSeller`는 오직 로또를 판매하는 역할만 담당합니다.
- 번호 생성 전략 캡슐화:
- `LotteryGenerateStrategy` 클래스는 로또 번호 생성 로직을 캡슐화합니다.
- 새로운 생성 전략(e.g., `ManualLottoGenerateStrategy`)을 쉽게 추가할 수 있습니다.
- 검증 로직의 이동:
- 로또 번호 검증은 이제 Lottery 클래스에서 처리되며, 검증 책임이 명확해졌습니다.
- 테스트 가능성 향상:
- 각각의 책임이 독립적으로 분리되어, 기능별 테스트가 용이해졌습니다.
- 예를 들어, 번호 생성 전략이나 검증 로직을 따로 테스트할 수 있습니다.
결론
SRP를 준수함으로써:
- 코드는 더 이해하기 쉽고 유지보수하기 쉬워집니다.
- 하나의 기능이 변경되더라도 다른 기능에 불필요한 영향을 주지 않습니다.
- 개별 구성요소를 독립적으로 테스트할 수 있어 신뢰성이 향상됩니다.
SRP는 객체지향 설계의 가장 기본적이지만 가장 중요한 원칙입니다.
여러분의 프로젝트에서도 SRP를 적용해보세요!
코드 품질, 유지보수성, 그리고 테스트 용이성에서 확실한 차이를 느낄 수 있을 것입니다.
다음 글은?
다음 글에서는 SOLID 원칙 중 두 번째인 **OCP(Open-Closed Principle)**에 대해 다뤄보겠습니다.
'Computer Science > 객체지향' 카테고리의 다른 글
LSP (리스코프 치환 원칙) in SOLID (0) | 2025.02.11 |
---|---|
OCP (개방-폐쇄 원칙) in SOLID (0) | 2025.01.14 |
코드의 네이밍과 코딩 컨벤션은 왜 중요한가? (0) | 2024.02.20 |
서브클래싱과 서브타이핑 - 코드로 이해하는 객체지향 (0) | 2023.09.19 |
다형성 - 코드로 이해하는 객체지향 (0) | 2023.09.19 |