일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 안드로이드 라이선스 종류
- android retrofit login
- 안드로이드 유닛테스트란
- ar vr 차이
- 멤버변수
- 객체
- 클래스
- 안드로이드 유닛 테스트 예시
- 2022 플러터 설치
- rxjava disposable
- 서비스 vs 쓰레드
- 안드로이드 레트로핏 crud
- 안드로이드 유닛 테스트
- rxjava cold observable
- 스택 자바 코드
- ANR이란
- 2022 플러터 안드로이드 스튜디오
- 큐 자바 코드
- 안드로이드 라이선스
- jvm이란
- Rxjava Observable
- rxjava hot observable
- 서비스 쓰레드 차이
- android ar 개발
- 자바 다형성
- 스택 큐 차이
- 안드로이드 레트로핏 사용법
- 안드로이드 os 구조
- jvm 작동 원리
- 플러터 설치 2022
- Today
- Total
나만을 위한 블로그
[이펙티브 코틀린] 아이템 37. 데이터 집합 표현에 class 한정자를 사용하라 본문
때로는 데이터들을 한꺼번에 전달해야 할 때가 있다. 일반적으로 이런 상황에 아래와 같은 data 한정자가 붙은 클래스를 사용한다.
fun main() {
val player = Player(0, "철수", 100)
}
data class Player(
val id: Int,
val name: String,
val points: Int
)
data 한정자를 붙이면 몇 가지 함수가 자동 생성된다.
- toString
- equals, hashCode
- copy
- componentN(1, 2, ...)
toString()은 클래스명과 기본 생성자 형태로 모든 프로퍼티와 값을 출력해준다. 로그 출력, 디버그 시 유용하게 쓸 수 있다.
equals()는 기본 생성자의 프로퍼티가 같은지 확인한다. hashCode()는 equals()와 같은 효과를 낸다.
fun main() {
val player = Player(0, "철수", 100)
println(player == Player(0, "철수", 100)) // true
println(player == Player(0, "영희", 100)) // false
}
copy()는 불변 데이터 클래스를 만들 때 편리하다. copy()는 기본 생성자 프로퍼티가 같은 새로운 객체를 복제한다. 새로 만들어진 객체의 값은 이름 있는 아규먼트를 활용해서 바꿀 수 있다.
fun main() {
val player = Player(0, "철수", 100)
val newObj = player.copy(name = "영수")
println(newObj) // Player(id=0, name=영수, points=100)
}
이런 copy()는 data 한정자를 붙이기만 하면 자동으로 만들어지므로 구현을 볼 수도 없고 볼 필요도 없다. 또한 copy()는 객체를 얕은 복사하지만 이것은 객체가 불변이라면 아무 상관이 없다. 불변 객체는 깊은 복사한 객체가 필요없기 때문이다.
componentN()는 위치를 기반으로 객체를 해제할 수 있게 해준다.
fun main() {
val player = Player(0, "철수", 100)
val (id, name, point) = player
}
이렇게 객체를 해제하는 코드를 작성하면 코틀린은 내부적으로 componentN()를 쓰는 코드로 변환한다.
fun main() {
val player = Player(0, "철수", 100)
// val (id, name, point) = player
val id: Int = player.component1()
val name: String = player.component2()
val point: Int = player.component3()
}
위치를 기반으로 객체를 해제하는 건 장점도 있고 단점도 있다. 가장 큰 장점은 변수명을 원하는 대로 지정할 수 있다는 거다. 또한 componentN()만 있다면 List, Map.Entry 등의 형태로도 객체를 해제할 수 있다.
fun main() {
val visited = listOf("China", "Russia", "India")
val (first, second, third) = visited
println("$first $second $third")
// China Russia India
val trip = mapOf(
"China" to "Tianjin",
"Russia" to "Petersburg",
"India" to "Rishikesh"
)
for ((country, city) in trip) {
println("We loved $city in $country")
// We loved Tianjin in China
// We loved Petersburg in Russia
// We loved Rishikesh in India
}
}
위치를 잘못 지정하면 다양한 문제가 발생할 수 있어 위험하다. 위치 순서를 혼동해서 객체를 잘못 해제하는 문제는 자주 발생한다.
fun main() {
val elon = FullName("Elon", "Reeve", "Musk")
val (name, surname) = elon
println("It is $name $surname") // It is Elon Reeve
}
data class FullName(
val firstName: String,
val secondName: String,
val thirdName: String
)
객체를 해제할 때는 주의해야 하므로 데이터 클래스의 기본 생성자에 붙어 있는 프로퍼티 이름과 같은 이름을 쓰는 게 좋다. 그렇게 하면 순서 등을 잘못 지정했을 때 인텔리제이, 안드로이드 스튜디오가 관련된 경고를 준다. 이런 경고는 유용하므로 경고 대신 오류로 업그레이드해도 좋다.
값을 하나만 갖는 데이터 클래스는 해제하지 않는 게 좋다. 간단한 코드지만 읽는 사람에게 혼동을 줄 수 있다. 특히 람다 표현식과 같이 활용될 때 문제가 된다.
fun main() {
val user = User("John")
user.let { a -> println(a) } // User(name=John)
// 이렇게 하지 마라
user.let { (a) -> println(a) } // John
}
data class User(val name: String)
일부 프로그래밍 언어에선 람다 표현식의 아규먼트 주변에 감싸는 괄호를 입력해도 되고 않아도 되므로 문제가 된다.
튜플 대신 데이터 클래스 사용하기
데이터 클래스는 튜플보다 많은 걸 제공한다. 구체적으로 코틀린의 튜플은 Serializable을 기반으로 만들어지며 toString을 쓸 수 있는 제네릭 데이터 클래스다.
public data class Pair<out A, out B>(
public val first: A,
public val second: B
) : Serializable {
public override fun toString(): String = "($first, $second)"
}
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
) : Serializable {
public override fun toString(): String = "($first, $second, $third)"
}
Pair, Triple만 예시로 든 이유는 이것이 코틀린에 남아 있는 마지막 튜플이기 때문이다. 과거엔 (Int, String, String, Long)처럼 괄호와 타입 지정을 통해 원하는 형태의 튜플을 정의할 수 있었다. 튜플은 데이터 클래스와 같은 역할을 하지만 훨씬 가독성이 나빴다. 튜플만 보고는 어떤 타입을 나타내는지 예측할 수 없다. 튜플은 좋아 보였지만 언제나 데이터 클래스를 쓰는 게 더 좋았기 때문에 점차 없어진 것이다. Pair, Triple은 몇 가지 지역적인 목적으로 인해 남아있을 뿐이다.
- 값에 간단하게 이름붙일 때
fun main() {
val (description, color) = when {
degress < 5 -> "cold" to Color.BLUE
degress < 23 -> "mild" to Color.YELLOW
else -> "hot" to Color.RED
}
}
- 표준 라이브러리에서 볼 수 없는 것처럼 미리 알 수 없는 aggregate(집합)를 표현할 때
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val (odd, even) = numbers.partition { it % 2 == 1 }
val map = mapOf(1 to "샌프란시스코", 2 to "암스테르담")
}
이 경우들을 제외하면 무조건 데이터 클래스를 쓰는 게 좋다. 아래는 전체 이름을 이름, 성(surname)으로 분할하는 코드다. 이름과 성을 Pair<String, String>으로 나타냈다.
fun main() {
val fullName = "김 철수"
val (firstName, lastName) = fullName.parseName() ?: return
println("그의 이름은${lastName}(이)다") // 그의 이름은 철수(이)다
}
fun String.parseName(): Pair<String, String>? {
val indexOfLastSpace = this.trim().lastIndexOf(' ')
if (indexOfLastSpace < 0) return null
val firstName = this.take(indexOfLastSpace)
val lastName = this.drop(indexOfLastSpace)
return Pair(firstName, lastName)
}
문제는 다른 사람이 이 코드를 읽을 때 Pair<String, String>이 전체 이름을 나타낸다는 걸 인지하기 어렵단 것이다. 뭣보다 성과 이름 중에 뭐가 앞에 있을지 예측하기 어렵다. 성이 앞에 있을 수도 있고 이름이 앞에 있을 수도 있다.
이걸 좀 더 쓰기 쉽고 함수를 읽기 쉽게 만들려고 한다면 데이터 클래스를 쓰면 된다.
fun main() {
val fullName = "김 철수"
val (firstName, lastName) = fullName.parseName() ?: return
println("firstName : $firstName, lastName : $lastName")
}
data class FullName(
val firstName: String,
val lastName: String
)
fun String.parseName(): FullName? {
val indexOfLastSpace = this.trim().lastIndexOf(' ')
if (indexOfLastSpace < 0) return null
val firstName = this.take(indexOfLastSpace)
val lastName = this.drop(indexOfLastSpace)
return FullName(firstName, lastName)
}
이렇게 해도 추가비용은 거의 들지 않는다. 오히려 아래처럼 함수를 더 명확하게 만들어 준다.
- 함수의 리턴 타입이 더 명확해진다
- 리턴 타입이 더 짧아지며 전달하기 쉬워진다
- 사용자가 데이터 클래스에 적혀 있는 것과 다른 이름을 활용해 변수를 해제하면 경고가 출력된다
이 클래스에 좁은 스코프를 갖게 하고 싶다면 일반적인 클래스와 같은 형태로 가시성에 제한을 걸어 두면 된다. 로컬 처리에서만 이를 활용하고 싶다면 private을 붙여주면 된다. 이렇게 데이터 클래스를 활용하면 튜플을 활용할 때보다 더 많은 장점이 있다.
'책 > Effective Kotlin' 카테고리의 다른 글
[이펙티브 코틀린] 아이템 39. 태그 클래스보다는 클래스 계층을 사용하라 (0) | 2023.03.13 |
---|---|
[이펙티브 코틀린] 아이템 38. 연산 또는 액션을 수행할 때 인터페이스 대신 함수 타입을 사용하라 (0) | 2023.03.03 |
[이펙티브 코틀린] 아이템 36. 상속보다는 컴포지션을 사용하라 (0) | 2023.02.02 |
[이펙티브 코틀린] 아이템 35. 복잡한 객체를 생성하기 위한 DSL을 정의하라 (0) | 2023.01.31 |
[이펙티브 코틀린] 아이템 34. 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라 (0) | 2023.01.30 |