Navigate back to the homepage

The First Class Collection (일급 콜렉션)

Reno Kim
June 17th, 2020 · 1 min read

최근에 기능 개발하면서 실수했던 이야기에 대해서 공유해보려고 한다 ㅠㅠ

공부하게 된 배경

기능 개발을 하면서 데이터 클래스에 콜렉션을 자체를 데이터 형태로 갖도록 구현한 적이 있다.

이렇게

1class Person(val name:String) {
2 private val cars:List<Car> = listOf();
3
4 //bla bla
5}
6
7public 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();
3
4 fun getCarsSum() {
5 //bla bla
6 }
7
8 fun getCarsCount():Int {
9 //bla bla
10 }
11
12 // etc
13}

위처럼 계속 컬렉션에 대한 함수들이 사용하는 클래스들에 생겨나기 시작했다…

이렇게 하니 문제점이

  1. 해당 컬렉션을 가지고 있는 곳에 중복되는 관리 함수들이 생긴다
  2. 관리하는 곳이 여기저기 흩어져 있어 관리하기 어렵다

이었다.

그래서 회사 선배의 조언으로 해당 컬렉션을 가지고 있는 하나의 클래스로 만들어 구현하였는데 이게 알고 보니 First Class Collection 였다.

Fist Class Collection 이란?

Collection 을 Wrapping 하면서, Wrapping 한 Collection 외 다른 멤버 변수가 없는 클래스

1// example
2class Cars {
3 private val cars:List<Car>
4
5 fun getCarsSum() {
6 //bla bla
7 }
8
9 fun getCarsCount():Int {
10 //bla bla
11 }
12}

장점

  1. 비즈니스 로직에 맞는 자료구조를 만들 수 있으며
  2. 하나의 클래스에서 컬렉션을 관리하기 때문에 관리가 편하다

예제 코드

1//Before
2
3class ClipBefore {
4 private val adjustmentList: List<Adjustment> = Adjustment.newAdjustment()
5
6 // adjustment 에 대한 비즈니스 로직이 Clip 에 추가 되어야 했다...
7 // clipBefore 말고도 다른 곳에서 adjustmentList 를 사용해야 한다면 ?? ==> Hell
8 fun applyAdjustmentToEngine() {
9 val brightness = getEngineValue(AdjustmentType.BRIGHTNESS)
10 val contrast = getEngineValue(AdjustmentType.CONTRAST)
11 val saturation = getEngineValue(AdjustmentType.SATURATION)
12
13 println("apply adjustment brightness: $brightness")
14 println("apply adjustment contrast: $contrast")
15 println("apply adjustment saturation: $saturation")
16 }
17
18 private fun getFactor(type: AdjustmentType): Float
19 = adjustmentList.find { it.type == type }?.factor ?: 0f
20
21 private fun getEngineValue(type: AdjustmentType): Float
22 = getFactor(type) * 255f
23}
24
25//Before Refactoring
26class Adjustment(
27 //factor range : 0f ~ 1.0f
28 var factor: Float,
29 val type: AdjustmentType
30) {
31
32 companion object {
33 fun newAdjustment(): List<Adjustment> = AdjustmentType.values().map { Adjustment(0.5f, it) }
34 }
35}
36
37enum class AdjustmentType {
38 BRIGHTNESS,
39 CONTRAST,
40 SATURATION
41}
1// After
2
3class ClipAfter {
4 private val adjustments: Adjustments = Adjustments()
5
6 //비즈니스에 종속적인 로직
7 fun applyAdjustmentToEngine() {
8 val brightness = adjustments.getEngineValue(AdjustmentType.BRIGHTNESS)
9 val contrast = adjustments.getEngineValue(AdjustmentType.CONTRAST)
10 val saturation = adjustments.getEngineValue(AdjustmentType.SATURATION)
11
12 //apply Adjustments to Engine
13 println("apply adjustment brightness: $brightness")
14 println("apply adjustment contrast: $contrast")
15 println("apply adjustment saturation: $saturation")
16 }
17}
18
19//After Refactoring (First Class Collection)
20class Adjustments {
21 private val adjustmentList: List<Adjustment> = Adjustment.newAdjustment()
22
23 private fun getFactor(type: AdjustmentType): Float
24 = adjustmentList.find { it.type == type }?.factor ?: 0f
25
26 fun getEngineValue(type: AdjustmentType): Float
27 = getFactor(type) * 255f
28}

결론

이 글을 읽으시는 독자분들도 데이터 모델이 컬렉션인 경우에 한 번쯤 고려해보면 좋을 것 같다 ㅎㅎ (분명 클린한 코드에 도움이 될 것이다)

참고

  • 코드리뷰 모음 서비스를 소개합니다 (우하한 형제들 기술 블로그)
  • 일급 컬렉션의 소개와 써야할 이유 (기억보단 기록을)

More articles from Reno

Kotlin 의 constructor 와 init 중에 누가 먼저 호출될까?

코틀린의 생성자들은 어떤 순서로 호출이 되는지 찾아

June 10th, 2020 · 1 min read

Android Accessibility

우아한 형제들의 Android Accessibility 적용 방식에 대한 글을 간단히 요약

June 5th, 2020 · 1 min read
© 2019–2021 Reno
Link to $https://github.com/renovatio0424Link to $https://www.linkedin.com/in/정원-김-33b30415a