원시값 포장과 일급컬렉션의 사용은 소트웍스 앤솔로지의 객체지향 생활체조로부터 시작되었습니다.
1. 모든 원시값과 문자열을 포장한다.
2. 일급 컬렉션을 쓴다.
해당 포스팅에서는 원시값 포장과 일급 컬렉션이 무엇인지, 그리고 무조건 원시값과 컬렉션을 무조건 감싸주는 것이 옳은가에 대해 알아보겠습니다.
원시값 포장
원시값 포장은 Primitive 타입을 그대로 사용하지 않고, 객체로 사용하기 위해 하나의 클래스를 선언해주는 것을 의미합니다.
Lotto 미션에서 로또 번호를 단순히 Int로 사용할 수도 있지만, 우리는 원시값을 포장하라는 요구사항을 받았기에 대부분 아래와 같이 코드를 작성했을 것입니다.
class LottoNumber(val value: Int) {
init {
validateLottoNumberRange()
}
private fun validateLottoNumberRange() {
require(value in MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER) { ERROR_MESSAGE_LOTTO_NUMBER_OUT_OF_RANGE }
}
}
(LottoNumber에 대한 구현에는 value class 사용하거나, 1부터 45까지의 Caching을 하는 등 여러 방법이 있지만, 원시값 포장에 대한 이야기이므로 따로 다루지는 않겠습니다.)
LottoNumber라는 래퍼 클래스를 별도로 구현해줌으로써, 로또 번호에 대한 값을 사용하는 곳에서 매번 숫자의 범위를 검증해줄 필요가 없어졌습니다.
또한, 로또 번호와 관련된 추가적인 비즈니스 로직이 추가된다면, LottoNumber에 해당 로직을 선언해주고 외부에서 호출해주기만 하면 되기 때문에 중복되는 코드를 줄일 수 있을 것으로 예상됩니다.
일급 컬렉션
일급 컬렉션 또한 비슷한 맥락입니다.
Collection을 그대로 사용하지 않고, 하나의 클래스로 감싸는 형태를 띕니다.
class Lotto(numbers: Set<LottoNumber>) {
private val numbers: Set<LottoNumber> = numbers.map { it }.toSet()
init {
validateLottoNumbers()
}
private fun validateLottoNumbers() {
require(numbers.size == LOTTO_SIZE) { ERROR_MESSAGE_INVALID_LOTTO_NUMBERS }
}
// 각종 비즈니스 로직..
companion object {
const val LOTTO_SIZE = 6
private const val ERROR_MESSAGE_INVALID_LOTTO_NUMBERS =
"$ERROR_PREFIX 로또 번호가 ${LOTTO_SIZE}개가 아니거나, 중복되는 번호가 있습니다."
}
}
일급 컬렉션은 멤버 변수로 하나의 컬렉션 외에는 존재하면 안 된다는 특징을 가지고 있습니다.
일급 컬렉션을 사용했을 때의 장점을 크게 4가지로 아래와 같습니다.
1. 비즈니스에 종속적인 자료구조
Lotto 클래스는 컬렉션 요소의 개수가 반드시 6개여야 생성할 수 있는 규칙이 있습니다.
따라서 비즈니스에 종속적인 자료구조라고 할 수 있습니다.
2. 불변성 보장
처음 주생성자로 numbers가 주어지면, 멤버 변수인 numbers에 새로운 LottoNumber 객체가 담깁니다.
또한, numbers 자체가 Set이기 때문에 요소를 임의 추가, 삭제, 변경하는 것이 불가능하기 때문에 불변성을 보장합니다.
불변성을 보장한다는 것은 외부에서 값이 임의로 바뀌지 않음을 보장하기 때문에 보다 높은 신뢰를 가질 수 있다는 장점이 있습니다. 이는 데이터 무결성과도 높은 관련이 있습니다.
3. 상태와 행위를 한 곳에서 관리
위 코드에는 비즈니스 로직에 대한 세부 구현이 생략되어 있지만, numbers라는 상태와 그에 따른 행위를 비즈니스 로직으로 정의하고 있습니다.
즉, Lotto 클래스는 상태와 행위를 한 곳에서 관리하기 때문에 같은 로직에 대해 외부에서 여러번 구현할 필요가 없어 보일러 플레이트 코드와 중복은 피할 수 있습니다.
만약, 로직이 변경되거나 오류가 발견되어 수정해야 한다면, Lotto 클래스의 일부 메서드만 수정해주면 되기 때문에 유지보수에도 큰 도움이 됩니다.
4. 이름있는 자료구조
Lotto라는 이름을 들었을 때, 우리는 바로 현실 세계의 로또를 떠올릴 수 있습니다.
그만큼 코드를 파악하기에 용이하다는 장점이 있습니다.
또한, 단순히 val mySet = mutableSetOf<Int>() 라고 작성되어 있다면 어떤 용도인지 파악할 수 있을까요?
실제로 프로젝트에서 이렇게까지 힘빠지는 네이밍을 하지는 않겠지만, val mySet = Lotto() 라고 작성되어 있다면 적어도 mySet이 로또를 의미하는구나 라고 파악할 수 있습니다.
이처럼 가독성과 용도를 파악하기 비교적 쉽게 만들어준다는 이점이 있습니다.
이번 미션을 수행하면서 가장 의문이 들었던 부분은 원시값 포장과 일급 컬렉션을 대체 어디까지 적용해야 할까 였습니다.
객체지향 생활체조에 따르면 모든 원시값을 포장하라고 하는데, 과연 이런 방식이 무조건 좋은 방법일까? 라는 생각이 들었습니다.
우선, 구현에는 정답이 없기에 무엇이 옳고 틀리다 라고 주장하기는 어렵습니다.
다만 필자는 무조건 포장하는 것은 지양하자 라는 주의입니다.
두괄식으로 표현해보자면 비즈니스 로직이 포함되어 있거나, 검증이 필요한 경우에 해당 값을 감싸줄 필요가 있다고 생각합니다.
만약 위 조건에 해당되지 않는다면 무조건 원시값과 컬렉션을 포장해주는 것은 큰 의미가 없을 수도 있기 때문입니다.
원시값 포장과 일급 컬렉션은 모두 객체이기 때문에 그만큼 메모리를 차지합니다.
결국 GC(가비지 컬렉터)의 청소 대상이 되기에 GC의 부담이 커지게 되며, 이는 곧 성능 저하로도 이어질 수 있습니다.
또한 무의미한 클래스 생성은 오히려 유지보수를 어렵게 하는 요인이 될 수도 있다고 생각합니다.
따라서, 비즈니스 로직이나 검증에 대한 구현이 여러 곳에서 재사용되는 경우 감싸주는 것이 좋을 수도 있다고 생각합니다.
글을 마무리하며 포스팅 내용 중 오류가 있거나, 다르게 생각하시는 분이 계시다면 댓글을 남겨주세요!
평가, 비판 모두 환영입니다 : )
'대외활동 > 우아한테크코스' 카테고리의 다른 글
[우아한테크코스] 함수형 프로그래밍(Functional Programming)이란? (4) | 2023.03.08 |
---|---|
[우아한테크코스] 우아한테크코스 한 달 생활기 (2) | 2023.03.05 |
[우아한테크코스] 2주차 - 점진적 리팩터링(Incremental Refactoring) (5) | 2023.02.24 |
[우아한테크코스] 2주차 - 로또(자동) 미션 회고 및 피드백 (4) | 2023.02.19 |
[우아한테크코스] 2주차 - TDD(Test Driven Development) (1) | 2023.02.15 |