본문 바로가기
  • 개발하는 곰돌이
Programming Language/Common

[OOP] 원시 타입을 Wrapper Class로 포장해보자(Value Object)

by 개발하는 곰돌이 2024. 3. 18.

목차

    들어가기 전에

    주의!
    이 글에서 다루는 Wrapper Class는 일반적으로 자바에서 이야기하는 Integer, Double, Long 등과 같이 원시 타입에 대응하는 클래스가 아니라 개발자가 필요에 의해 작성한 별도의 클래스를 이야기 합니다.

     

    여러 클래스들을 작성하다 보면 숫자 등의 원시 타입 멤버 변수들을 추가하는 경우가 매우 많다.

     

    이러한 멤버 변수들은 클래스의 성질을 나타내기 위해 반드시 필요한 존재들인데 원시 타입 멤버 변수를 Wrapping(포장)해주는 클래스를 작성해서 사용한다면 몇가지 부가 효과를 얻을 수 있다.

     

    이렇게 작성된 Wrapper Class의 객체를 값 객체(VO : Value Object)라고 한다.

     

    Wrapper Class를 사용하면 얻을 수 있는 장점을 정리해본다.

    원시 타입을 바로 사용하는 경우

    class Sword(
        private var _durability: Int,
        private val maxDurability: Int,
        private var _power: Int,
        private val maxPower: Int
    ) {
        val durability get() = _durability
        val power get() = _power
        
        companion object {
            private const val MIN = 0
        }
    
        init {
            if (_durability !in MIN..maxDurability) {
                throw IllegalArgumentException("내구도 값을 확인하세요.")
            }
    
            if (_power !in MIN..maxPower) {
                throw IllegalArgumentException("공격력 값을 확인하세요.")
            }
        }
    
        fun attack() {
            if (_durability <= 0) {
                throw IllegalStateException("내구도가 0입니다.")
            }
            _durability--
            println("공격")
        }
    
        fun reinforce() {
            if (_power >= maxPower) {
                throw IllegalStateException("공격력이 최대치라서 강화할 수 없습니다.")
            }
            _power++
        }
    }

    검을 나타내는 Sword 클래스에는 Int 타입의 내구도, 공격력이 있다.

     

    Int 타입은 원시 타입이기 때문에 스스로 상태를 관리할 수 없다. 이로 인해 Sword에서 별도로 내구도나 공격력의 최대치, 최소치 등의 상태를 관리하고 있고 조건까지 검사하고 있다.

     

    내구도와 공격력을 사용하는 클래스가 Sword 뿐이라면 큰 상관이 없겠지만 다른 클래스에서도 이런 속성들을 사용한다면 어떨까?

     

    비슷한 로직들이 계속 작성될 것이고 수정사항이 발생했을 때 미처 수정하지 못한 부분으로 인해 버그가 발생할 수도 있다. 중복 코드가 많이지니 지저분해지는 코드는 덤이다. 이 얼마나 끔찍한 일인가!

    Wrapper Class로 포장한다면?

    내구도와 공격력을 Wrapper Class로 포장해보자.

    class Durability(
        private val durability: Int,
        private val maxDurability: Int
    ) {
        val value get() = durability
        
        companion object {
            private const val MIN = 0
        }
        
        init {
            if (durability !in MIN..maxDurability) {
                throw IllegalArgumentException("내구도 값을 확인하세요.")
            }
        }
        
        fun decrease(): Durability {
            if (durability <= 0) {
                throw IllegalStateException("내구도가 0입니다.")
            }
            return Durability(durability - 1, maxDurability)
        }
    }
    class Power(
        private val power: Int,
        private val maxPower: Int
    ) {
        val value get() = power
        
        companion object {
            private const val MIN = 0
        }
        
        init {
            if (power !in MIN..maxPower) {
                throw IllegalArgumentException("공격력 값을 확인하세요.")
            }
        }
        
        fun increase(): Power {
            if (power == MAX_POWER) {
                throw IllegalStateException("공격력이 최대치라서 강화할 수 없습니다.")
            }
            return Power(power + 1, maxPower)
        }
    }
    class Sword(
        private var _durability: Durability,
        private var _power: Power
    ) {
        val durability get() = _durability.value
        val power get() = _power.value
        
        fun attack() {
            _durability = _durability.decrease()
            println("공격")
        }
        
        fun reinforce() {
            _power = _power.increase()
        }
    }

    내구도를 Int에서 Durability 타입으로, 공격력을 Int에서 Power 타입으로 변경했다. 이로 인해 Sword 클래스가 훨씬 간결해진 것을 볼 수 있다.

    변수가 의미하는 바를 명확히 나타낼 수 있다

    원시 타입을 사용하면 변수의 이름을 통해서만 어떤 의미를 가진 변수인지 알 수 있다.

     

    하지만 Wrapper Class를 사용하면 변수의 이름과 별개로 변수의 타입으로 어떤 의미를 가졌는지 명확히 나타낼 수 있다.

     

    위의 예시에서 Durability는 내구도를, Power는 공격력을 나타낸다. 공격력을 나타내는 Durability나 내구도를 나타내는 Power는 존재할 수 없는 것이다.

    스스로 상태를 관리할 수 있다

    이제 내구도와 공격력은 각자의 상태를 관리할 수 있게 되었고 최소, 최대값도 Durability와 Power 클래스에서 관리할 수 있게 되어서 Sword 클래스에 두 속성을 관리하기 위한 부수적인 프로퍼티나 메소드도 모두 제거되었다.

     

    그 외에도 내구도나 공격력을 사용하는 다른 클래스들에 관련된 비슷한 로직을 작성할 필요가 전혀 없어졌다.

     

    Wrapper Class로 포장해줬을 뿐인데 기존 클래스는 간결해지고 이후 동일한 속성을 사용하는 새로운 클래스를 작성할 때 중복 코드를 작성할 일도 사라진 것이다!

    유지보수가 쉬워진다

    내구도나 공격력의 수치가 \(2^{31}-1\)보다 커질 수 있는 상황이 생겼다고 가정해보자.

     

    기존의 방식대로라면 Int 타입의 내구도와 공격력을 사용하는 모든 클래스를 추적하여 Long이나 BigInteger 혹은 BigDecimal 타입으로 변경해야 할 것이고, 이 과정에서 변경 누락이 발생하여 버그가 발생할 여지가 있다.

     

    하지만 우리는 Durability와 Power라는 클래스를 작성했기 때문에 실제 수치를 나타내는 변수의 타입만 변경해주면 된다. 다른 클래스들은 Wrapper Class를 가져다 쓰기만 하기 때문에 신경 쓸 필요가 없어서 유지보수가 훨씬 쉽다.

    마치며

    정말 단순한 경우가 아니라면 클래스의 멤버 변수로 원시 타입을 사용하면 여러 문제를 야기할 수 있으니 Wrapper Class를 활용하는 것이 좋다고 생각한다.

     

    Wrapper Class들은 각자의 역할에만 충실하면 되고 이를 사용하는 클래스에서는 단순히 "시키기만 하면 된다"라는 객체 지향 프로그래밍의 장점을 살릴 수 있기 때문에 여러모로 유용하게 사용할 수도 있다.

    참조 링크

     

    더 나은 소프트웨어를 향한 9단계: 객체지향 생활 체조(4)

    규칙 3: 원시값과 문자열의 포장 int 값 하나 자체는 그냥 아무 의미 없는 스칼라 값일 뿐이다. 어떤 메서드가 int 값을 매개변수로 받는다면 그 메서드 이름은 해당 매개변수의 의도를 나타내기

    developerfarm.wordpress.com

     

    원시 타입을 포장해야 하는 이유

    변수를 선언하는 방법에는 두 가지가 있다. 원시 타입의 변수를 선언하는 방법과, 원시 타입의 변수를 객체로 포장한 변수를 선언하는 방법이 있다. (Collection…

    tecoble.techcourse.co.kr

     

    댓글