최근에 기능 개발하면서 실수했던 이야기에 대해서 공유해보려고 한다 ㅠㅠ
공부하게 된 배경
기능 개발을 하면서 데이터 클래스에 콜렉션을 자체를 데이터 형태로 갖도록 구현한 적이 있다.
이렇게
1class Person(val name:String) {2 private val cars:List<Car> = listOf();34 //bla bla5}67public class Car {8 private val name:String;9 private val price:Int;10}
처음에는 별 문제가 없어 보였다 하지만 여기에 비즈니스 로직을 하나씩 추가하다 보니 문제가 생겼다 …
문제점
1class Person(val name:String) {2 private val cars:List<Car> = listOf();34 fun getCarsSum() {5 //bla bla6 }78 fun getCarsCount():Int {9 //bla bla10 }1112 // etc13}
위처럼 계속 컬렉션에 대한 함수들이 사용하는 클래스들에 생겨나기 시작했다…
이렇게 하니 문제점이
- 해당 컬렉션을 가지고 있는 곳에 중복되는 관리 함수들이 생긴다
- 관리하는 곳이 여기저기 흩어져 있어 관리하기 어렵다
이었다.
그래서 회사 선배의 조언으로 해당 컬렉션을 가지고 있는 하나의 클래스로 만들어 구현하였는데 이게 알고 보니 First Class Collection 였다.
Fist Class Collection 이란?
Collection 을 Wrapping 하면서, Wrapping 한 Collection 외 다른 멤버 변수가 없는 클래스
1// example2class Cars {3 private val cars:List<Car>45 fun getCarsSum() {6 //bla bla7 }89 fun getCarsCount():Int {10 //bla bla11 }12}
장점
- 비즈니스 로직에 맞는 자료구조를 만들 수 있으며
- 하나의 클래스에서 컬렉션을 관리하기 때문에 관리가 편하다
예제 코드
1//Before23class ClipBefore {4 private val adjustmentList: List<Adjustment> = Adjustment.newAdjustment()56 // adjustment 에 대한 비즈니스 로직이 Clip 에 추가 되어야 했다...7 // clipBefore 말고도 다른 곳에서 adjustmentList 를 사용해야 한다면 ?? ==> Hell8 fun applyAdjustmentToEngine() {9 val brightness = getEngineValue(AdjustmentType.BRIGHTNESS)10 val contrast = getEngineValue(AdjustmentType.CONTRAST)11 val saturation = getEngineValue(AdjustmentType.SATURATION)1213 println("apply adjustment brightness: $brightness")14 println("apply adjustment contrast: $contrast")15 println("apply adjustment saturation: $saturation")16 }1718 private fun getFactor(type: AdjustmentType): Float19 = adjustmentList.find { it.type == type }?.factor ?: 0f2021 private fun getEngineValue(type: AdjustmentType): Float22 = getFactor(type) * 255f23}2425//Before Refactoring26class Adjustment(27 //factor range : 0f ~ 1.0f28 var factor: Float,29 val type: AdjustmentType30) {3132 companion object {33 fun newAdjustment(): List<Adjustment> = AdjustmentType.values().map { Adjustment(0.5f, it) }34 }35}3637enum class AdjustmentType {38 BRIGHTNESS,39 CONTRAST,40 SATURATION41}
1// After23class ClipAfter {4 private val adjustments: Adjustments = Adjustments()56 //비즈니스에 종속적인 로직7 fun applyAdjustmentToEngine() {8 val brightness = adjustments.getEngineValue(AdjustmentType.BRIGHTNESS)9 val contrast = adjustments.getEngineValue(AdjustmentType.CONTRAST)10 val saturation = adjustments.getEngineValue(AdjustmentType.SATURATION)1112 //apply Adjustments to Engine13 println("apply adjustment brightness: $brightness")14 println("apply adjustment contrast: $contrast")15 println("apply adjustment saturation: $saturation")16 }17}1819//After Refactoring (First Class Collection)20class Adjustments {21 private val adjustmentList: List<Adjustment> = Adjustment.newAdjustment()2223 private fun getFactor(type: AdjustmentType): Float24 = adjustmentList.find { it.type == type }?.factor ?: 0f2526 fun getEngineValue(type: AdjustmentType): Float27 = getFactor(type) * 255f28}
결론
이 글을 읽으시는 독자분들도 데이터 모델이 컬렉션인 경우에 한 번쯤 고려해보면 좋을 것 같다 ㅎㅎ (분명 클린한 코드에 도움이 될 것이다)
참고
- 코드리뷰 모음 서비스를 소개합니다 (우하한 형제들 기술 블로그)
- 일급 컬렉션의 소개와 써야할 이유 (기억보단 기록을)