객체지향에서 원시 값은 객체로 포장해서 사용하자

jonghyun.log·2023년 7월 12일
0
post-thumbnail
post-custom-banner

원시값이 필요한 예시 상황

간단한 로또 프로그램을 만든다고 생각해보자.
이때 로또 번호를 List<Int> 로 사용하게 된다.

이때 로또 번호의 조건인

  • 로또 번호는 중복되면 안된다.
  • 1~45까지의 숫자만 들어와야 한다.

라는 두가지의 검증 로직이 있는데 이를 위해

LottoShop 객체를 만들고

class LottoShop(val lottoNumbers : List<Int>){
	init{
		require(lottoNumbers.size == lottoNumbers.toMap().size){
			"로또 번호는 중복된 숫자가 존재하면 안됩니다."
		}
		lottoNumbers.forEach{
			require(it in LOTTO_NUMBER_MINUMUM .. LOTTO_NUMBER_MAXIMUN){
				"로또 번호는 1부터 45까지의 숫자만 가능합니다."
			}
		}
		
	}

	// 나머지 로직

	companion object{
		const val LOTTO_NUMBER_MINUMUM = 1
		const val LOTTO_NUMBER_MAXIMUN = 45
	}
}

객체가 생성될때 생성자에서 검증로직이 필요하다.

하지만, lottoNumbers가 다른 객체에서도 사용된다면

다른 객체에서도 lottoNumbers의 검증의 책임을 가지게 된다.

즉, 가령 LottoMachine 이라는 클래스가 존재한다면 LottoMachine에서도

class LottoMachine(val lottoNumbers : List<Int>){
	init{
		require(lottoNumbers.size == lottoNumbers.toMap().size){
			"로또 번호는 중복된 숫자가 존재하면 안됩니다."
		}
		lottoNumbers.forEach{
			require(it in LOTTO_NUMBER_MINUMUM .. LOTTO_NUMBER_MAXIMUN){
				"로또 번호는 1부터 45까지의 숫자만 가능합니다."
			}
		}
	}

	// 나머지 로직

	companion object{
		const val LOTTO_NUMBER_MINUMUM = 1
		const val LOTTO_NUMBER_MAXIMUN = 45
	}
}

중복된 코드가 존재하게 되는것이다. 이럴경우 어떻게 처리하는게 좋을까?

바로 lottoNumbers일급 콜렉션으로 만드는 것이다.

일급 콜렉션이란?

https://jojoldu.tistory.com/412 참고

lottoNumbers를 일급 콜렉션으로 만들기

lottoNumbers를 객체로 감싸고 검증 로직을 그 안에 넣어서 일급 콜렉션으로 만들자.

class LottoNumbers(val lottoNumbers : List<Int>){
	init{
		require(lottoNumbers.size == lottoNumbers.toMap().size){
			"로또 번호는 중복된 숫자가 존재하면 안됩니다."
		}
		lottoNumbers.forEach{
			require(it in LOTTO_NUMBER_MINUMUM .. LOTTO_NUMBER_MAXIMUN){
				"로또 번호는 1부터 45까지의 숫자만 가능합니다."
			}
		}
	}
		// 나머지 로직
		
	companion object{
		const val LOTTO_NUMBER_MINUMUM = 1
		const val LOTTO_NUMBER_MAXIMUN = 45
	}
}

class LottoShop(val lottoNumbers : LottoNumbers){
			// 나머지 로직
}

class LottoMachine(val lottoNumbers : LottoNumbers){
			// 나머지 로직
}

책임을 LottoNumbers로 위임하는 순간 중복된 코드가 사라졌다.

원시값을 포장하자

로또에는 “보너스 넘버”라는 특수한 로또 넘버가 존재한다.

class BonusBall(val bonusNumber : Int){
	init{
		require(it in LOTTO_NUMBER_MINUMUM .. LOTTO_NUMBER_MAXIMUN){
				"로또 번호는 1부터 45까지의 숫자만 가능합니다."
		}
	}
}

보너스 넘버 역시 로또 넘버의 일종이므로 로또 번호를 검증하는 책임을 가진다.
하지만, 이 때문에 검증하는 코드가 중복되어 각 객체에 존재하는 경우가 생기게 된다.

이럴때, Int형인 lottoNumber를 객체로 감싸서 사용하고 값을 검증하는 책임을 위임해보자.

class LottoNumber(val lottoNumber : Int){
	init{
		require(it in LOTTO_NUMBER_MINUMUM .. LOTTO_NUMBER_MAXIMUN){
				"로또 번호는 1부터 45까지의 숫자만 가능합니다."
		}
	}

	companion object{
		const val LOTTO_NUMBER_MINUMUM = 1
		const val LOTTO_NUMBER_MAXIMUN = 45
	}
}

class BonusBall(val bonusNumber : LottoNumber){}

class LottoNumbers(val lottoNumbers : List<LottoNumber>){
	init{
		require(lottoNumbers.size == lottoNumbers.toMap().size){
			"로또 번호는 중복된 숫자가 존재하면 안됩니다."
		}
	}
		// 나머지 로직

}

이렇게 보니 어떤가?
검증을 해야할 책임을 각자의 내부에서 검증하도록 위임하였더니 중복이 꽤나 줄어들었다.

클래스 내부의 인스턴스 맴버가 원시값이거나 혹은 문자열인데
다른 클래스의 인스턴스 맴버와 중복되는 경우가 존재한다면
그 값을 포장해서 검증 책임을 포장한 클래스에 위임해보는것이 어떨까?

레슨런

값을 검증하는 책임도 사소하게 생각하지말고 중요한 책임중의 하나로 인식하게 되었다.

값을 검증하는 책임이 중복되거나 다른 책임이 많은 객체의 경우 검증하는 책임을 위임해서 각 객체의 역할을 균형있게 조율해보는 것이 필요해 보인다.

모든 맴버를 포장하는것은 오버엔지니어링이 될 수도 있다.
다른 인스턴스에서도 종종 사용될 것 같은 맴버는 포장해서 사용해보는 습관을 들여야겠다.

post-custom-banner

0개의 댓글