간단한 로또 프로그램을 만든다고 생각해보자.
이때 로또 번호를 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
를 일급 콜렉션
으로 만드는 것이다.
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){
"로또 번호는 중복된 숫자가 존재하면 안됩니다."
}
}
// 나머지 로직
}
이렇게 보니 어떤가?
검증을 해야할 책임을 각자의 내부에서 검증하도록 위임하였더니 중복이 꽤나 줄어들었다.
클래스 내부의 인스턴스 맴버가 원시값이거나 혹은 문자열인데
다른 클래스의 인스턴스 맴버와 중복되는 경우가 존재한다면
그 값을 포장해서 검증 책임을 포장한 클래스에 위임해보는것이 어떨까?
값을 검증하는 책임도 사소하게 생각하지말고 중요한 책임중의 하나로 인식하게 되었다.
값을 검증하는 책임이 중복되거나 다른 책임이 많은 객체의 경우 검증하는 책임을 위임해서 각 객체의 역할을 균형있게 조율해보는 것이 필요해 보인다.
모든 맴버를 포장하는것은 오버엔지니어링이 될 수도 있다.
다른 인스턴스에서도 종종 사용될 것 같은 맴버는 포장해서 사용해보는 습관을 들여야겠다.