Computer Science/객체지향

OCP (개방-폐쇄 원칙) in SOLID

sh1mj1 2025. 1. 14. 16:01

소개

이전 글 다시 보기

이전 글에서는 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. 일반 로또 판매자: 티켓 한 장당 1,000원에 판매
  2. 할인 로또 판매자: 티켓 한 장당 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 위반의 문제점

  1. 기존 코드 수정:
    새로운 LottoSeller 타입(DiscountedLottoSeller)을 추가하려면 기존 Customer 클래스의 코드를 수정해야 합니다.
  2. 유연성 부족:
    판매자 유형이 추가될 때마다 Customer 클래스에 새로운 로직을 추가해야 하므로 시스템이 점점 더 복잡해집니다.
  3. 결합도 증가:
    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
    }
}

무엇이 개선되었나요?

  1. 기존 코드 수정 불필요:
    새로운 LottoSeller 타입(e.g., PremiumLottoSeller)을 추가할 때, 기존 Customer 클래스는 수정할 필요가 없습니다.
  2. 행동 캡슐화:
    판매자 각각의 로직이 독립적으로 관리되어 코드가 더 깔끔하고 이해하기 쉬워졌습니다.
  3. 확장성 향상:
    LottoSeller 의 서브타입이 추가되더라도 기존 시스템에 영향을 주지 않으므로 유연성과 확장성이 크게 향상됩니다.
  4. 테스트 가능성 향상:
    각 판매자 클래스는 독립적으로 테스트할 수 있으므로 신뢰성이 높아집니다.

결론

OCP(개방-폐쇄 원칙)을 준수하면, 코드가 변경에 강하고 확장에 유연해집니다.
인터페이스와 같은 추상화를 도입함으로써 기존 코드를 수정하지 않고도 새로운 요구사항을 쉽게 추가할 수 있습니다.

핵심 요약

  • 항상 기존 코드를 수정하지 않고 확장 가능하도록 설계하세요.
  • 인터페이스 또는 추상 클래스를 활용하여 동작을 분리하고 캡슐화하세요.

OCP를 준수하면 시스템의 안정성과 유지보수성이 크게 향상됩니다.
다음 글에서는 SOLID 원칙 중 세 번째인 리스코프 치환 원칙(LSP)에 대해 다뤄보겠습니다!


이전 글 보기