소개
이전 글 다시 보기
이전 글에서는 SOLID 원칙 중 첫 번째인 단일 책임 원칙(SRP)에 대해 다뤘습니다.
SRP를 준수함으로써 어떻게 코드를 더 단순하고 유지보수하기 좋게 만들 수 있는지 알아보았습니다.
이번 글에서는 SOLID 원칙의 두 번째, OCP(개방-폐쇄 원칙)에 대해 살펴보겠습니다.
이 원칙은 소프트웨어 시스템이 변경에 유연하게 대처할 수 있도록 설계하는 데 중요한 역할을 합니다.
OCP(개방-폐쇄 원칙)이란?
OCP는 다음과 같이 정의됩니다:
“소프트웨어 엔티티는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.”
간단히 말해:
- 새로운 기능이나 동작을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있어야 합니다.
- 이를 통해 기존 시스템을 안정적으로 유지하면서도 새로운 요구사항에 쉽게 적응할 수 있습니다.
예제로 이해하는 OCP
로또 판매를 처리하는 간단한 예제를 통해 OCP를 살펴보겠습니다.
초기 구현
먼저, 고객이 LottoSeller
를 통해 로또를 구매하는 간단한 코드를 살펴보겠습니다:
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(),
)
}
처음에는 이 코드가 괜찮아 보일 수 있습니다.
그러나 새로운 요구사항이 추가되면 OCP를 위반할 가능성이 있습니다.
OCP가 위반되는 상황
새로운 요구사항
새로운 유형의 로또 판매자가 추가되었습니다.
- 일반 로또 판매자: 티켓 한 장당 1,000원에 판매
- 할인 로또 판매자: 티켓 한 장당 500원에 판매
이를 처리하기 위한 초기 구현입니다:
class Customer {
fun buyLotto(
money: Int,
lottoSeller: LottoSeller,
lottoSellerType: LottoSellerType,
): List<Lottery> {
when (lottoSellerType) {
LottoSellerType.NORMAL -> {
return lottoSeller.soldLotto(money)
}
LottoSellerType.ILLEGAL -> {
val count = money / ILLEGAL_LOTTO_PRICE
return List(count) { LotteryGenerateStrategy().autoGenerate() }
}
}
}
companion object {
private const val ILLEGAL_LOTTO_PRICE = 500
}
}
enum class LottoSellerType {
NORMAL, ILLEGAL
}
OCP 위반의 문제점
- 기존 코드 수정:
새로운LottoSeller
타입(DiscountedLottoSeller
)을 추가하려면 기존Customer
클래스의 코드를 수정해야 합니다. - 유연성 부족:
판매자 유형이 추가될 때마다Customer
클래스에 새로운 로직을 추가해야 하므로 시스템이 점점 더 복잡해집니다. - 결합도 증가:
Customer
클래스는 로또 판매자들의 세부 구현에 강하게 결합되어 있어 확장성이 떨어집니다.
OCP를 준수하기 위한 리팩토링
인터페이스 도입
OCP를 준수하려면 인터페이스를 도입하여 LottoSeller
의 동작을 캡슐화해야 합니다.
이를 통해 새로운 LottoSeller
타입을 추가할 때 기존 코드를 수정할 필요가 없게 됩니다.
리팩토링된 코드
class Customer {
fun buyLotto(
money: Int,
lottoSeller: LottoSeller,
): List<Lottery> = lottoSeller.soldLotto(money)
}
interface LottoSeller {
fun soldLotto(money: Int): List<Lottery>
}
class DiscountedLottoSeller : LottoSeller {
override fun soldLotto(money: Int): List<Lottery> {
val count = money / LOTTO_PRICE
return List(count) { LotteryGenerateStrategy().autoGenerate() }
}
companion object {
private const val LOTTO_PRICE = 500
}
}
class NormalLottoSeller : LottoSeller {
override fun soldLotto(money: Int): List<Lottery> {
val count = money / LOTTO_PRICE
return List(count) { LotteryGenerateStrategy().autoGenerate() }
}
companion object {
private const val LOTTO_PRICE = 1000
}
}
무엇이 개선되었나요?
- 기존 코드 수정 불필요:
새로운LottoSeller
타입(e.g.,PremiumLottoSeller
)을 추가할 때, 기존Customer
클래스는 수정할 필요가 없습니다. - 행동 캡슐화:
판매자 각각의 로직이 독립적으로 관리되어 코드가 더 깔끔하고 이해하기 쉬워졌습니다. - 확장성 향상:
LottoSeller
의 서브타입이 추가되더라도 기존 시스템에 영향을 주지 않으므로 유연성과 확장성이 크게 향상됩니다. - 테스트 가능성 향상:
각 판매자 클래스는 독립적으로 테스트할 수 있으므로 신뢰성이 높아집니다.
결론
OCP(개방-폐쇄 원칙)을 준수하면, 코드가 변경에 강하고 확장에 유연해집니다.
인터페이스와 같은 추상화를 도입함으로써 기존 코드를 수정하지 않고도 새로운 요구사항을 쉽게 추가할 수 있습니다.
핵심 요약
- 항상 기존 코드를 수정하지 않고 확장 가능하도록 설계하세요.
- 인터페이스 또는 추상 클래스를 활용하여 동작을 분리하고 캡슐화하세요.
OCP를 준수하면 시스템의 안정성과 유지보수성이 크게 향상됩니다.
다음 글에서는 SOLID 원칙 중 세 번째인 리스코프 치환 원칙(LSP)에 대해 다뤄보겠습니다!
이전 글 보기
'Computer Science > 객체지향' 카테고리의 다른 글
SRP(단일 책임 원칙) in SOLID (0) | 2024.12.31 |
---|---|
코드의 네이밍과 코딩 컨벤션은 왜 중요한가? (0) | 2024.02.20 |
서브클래싱과 서브타이핑 - 코드로 이해하는 객체지향 (0) | 2023.09.19 |
다형성 - 코드로 이해하는 객체지향 (0) | 2023.09.19 |
합성과 유연한 설계 - 코드로 이해하는 객체지향 (0) | 2023.09.19 |