Programming/Kotlin

[Kotlin] 이펙티브 코틀린

코딩하는 포메라니안 2026. 3. 31. 00:07

Item 6. 사용자 정의 오류보다 표준 오류를 선호하라

- 요청자에게 특정 응답 코드와 메시지로 응답하는 데에는 예외

- 불필요한 커스텀을 지양

 

Item 7. 결과가 없을 가능성이 있는 경우 널 가능 또는 Result 반환

함수가 원하는 결과를 생성하지 못할 때, 처리하는 2가지 주요 방안이 있다.

 

방안1) null 또는 Result.failture를 반환하여 실패를 나타내기

- 예상되는 오류를 표현하는 데 적합

- 실패 시 추가 정보를 전달해야 할 때, Result(성공/실패) 사용, 아니라면 null을 사용

- 널 가능 값은 사용하기 전에 언래핑(nullable -> non-null로 만드는 과정) 해야한다.

 

 

방안2) 예외 던지기

- 비정상적인 상황, 오류가 예상되지 않는 상황에서는 예외를 던져야 한다.

 

빈 컬렉션은 null과는 완전히 다른 의미이다.

- null = 값을 생성할 수 없음으로 결괏값이 없음

- 빈 컬렉션 = 데이터가 없음 ex) getUsers = 사용자가 없음

 

코틀린의 타입 시스템은 널 가능 여부를 하나의 타입으로 취급하여, null을 의도적으로 처리하도록 강제하고 있다.

타입 시스템이란, 타입과 관련해서 지켜야 하는 규칙을 모아둔 체계를 말한다.

//Java
String name = null;
name.length(); //런타임 NPE

//Kotlin
val name: String? = null
name.length //컴파일 에러

 

 

Item 9. 단위 테스트를 작성하라

단위케이스에서 확인하는 사항은 아래와 같다.

- 일반적인 유스 케이스(happy path) : 예상되는 일반적인 방법을 테스트

- 일반적인 에러 케이스 또는 잠재적인 문제들 : 에러로 예상되거나 문제가 있는 것으로 밝혀진 사례

- 엣지 케이스와 잘못된 인수들: Int.MAX_VALUE나 널 가능 타입에는 null을 넣어보기, 음수면 안되는 값에 음수 넣어보는 등

 

단위 테스트는 빠른 피드백을 주어서 개발하는 동안 매우 유용할 수 있다.

테스트 주도 개발이라고, 먼저 단위 테스트를 작성하고 해당 테스트를 통과시키며 구현해나가는 방식도 있다.

 

단위테스트의 장점은 다음과 같다.

1. 신뢰성 있는 코드

2. 리팩터링하는 것이 두렵지 않다.

3. 수동으로 테스트하는 것보다 단위 테스트가 훨씬 빠를 때가 많다.

 

 

Item 10. 가독성을 목표로 설계하라

간결성보다 가독성!

개발자는 코드를 작성하는 것보다 코드를 읽는 데 훨씬 더 시간을 많이 쓴다.

 

+) let 

- 객체를 블록의 인자로 넘겨서 블록의 결과값을 반환

- 인자로 넘긴 객체는 it 키워드를 통해 접근 가능

- null-safety + 여러 줄을 수행해야할 때 유용

- 호출한 객체를 이용해서 어떤 동작을 수행(*run = 호출한 객체에 어떤 걸 적용할래)

var person: Person? = null
var Company: String?

company = person.let{
	println("test01") //person이 null이여서 실행되지 않음
    it.company
}?:"해당 없음" //person이 null임으로 기본값이 할당됨

println("test02")
println(company)

/*결과
test02
해당 없음
*/

 

 

Item 12. 가독성을 높이려면 연산자를 사용하라

plus(),minus(), compareTo() 보다 +, -, >, <, >=, <= 가 보통 쉽게 읽히는 경우가 많다.

단, 시간 비교할 때 isBefore, isAfter 같은 경우엔 비교 연산자가 더 읽기 쉬울 때도 있다.

contains와 in 연산자도 마찬가지로 상황에 따라 읽기 쉬운 것을 선택해야 하며, 어떤 요소가 더 중요한 지에 따라 달라진다.

val SUPPORTED_TAGS = setof("ADMIN", "TRAINER", "ATTENDEE")
val tag = "ATANDEE"

println(SUPPORTED_TAGS.contains(tag))//true
println(tag in SUPPORTED_TAGS)//true

//여기서는 tag가 중요함으로 tag를 앞에 배치하면 쉽게 읽힌다.
//ex) There's a soda in the fridge > The fridge contains a soda

참고로, 액수를 표현하는 경우 정밀도를 위해 Double대신 BigDecimal을 사용해야 한다.

 

Item 13. 타입 명시를 고려하라

문맥상 타입이 명확하여 추가적으로 명시할 필요가 없는 경우 타입을 생략할 수 있어 가독성을 높여주지만,

타입이 명확하지 않을 때는 남용해서는 안된다.

 

다른 개발자가 보면 타입을 몰라서, 깊은 depth까지 매번 들어가서 확인해야할 수도 있고,

잘못된 사용으로 안정성을 해할 수 있다.

val num = 10 //O
val name = "Kotlin" //O
val data = getSomeData() //X
val data UserData = getSomeData() //O

 

추론된 타입은 항상 가능한 한 가장 구체적인 타입이다.

open class Animal
class Bear : Animal()
class Camel : Animal()

fun main(){
	var animal = Bear()
    animal = Camel() //에러: 타입 불일치
    
    var animal2 : Animal = Bear()
    animal2 = Camel() //정상 동작
}

 

Item 14. 리시버를 명시적으로 참조하라

리시버는 함수나 람다 내부에서 this등으로 접근할 수 있는 대상 객체를 말한다.

함수나 프로퍼티가 로컬 또는 최상위 변수가 아닌 "리시버"에서 가져온 것을 강조하려고 할 때, this.xxx와 같이 명시적으로 작성한다.

 

여러 개의 리시버

둘 이상의 리시버를 사용하고 스코프가 중첩되어 있을 경우, 명시적 리시버 사용이 특히 유용하다.

특히, apply, with, run처럼 리시버를 암시적(this생략)으로 지정할 수 있는 함수를 사용할 경우에 명시적으로 표기하는 것이 안전하다.

(*also, let은 this가 아닌 it과 같이 정해두고 있어서 명시적으로 할 수 밖에 없도록 강제된다.)

class Node(val name: String){
	fun makeChild(childName: String) = 
    	create("$name.$childName")
        .apply{print("Created ${name}") } 
        
    fun create(name: String): Node? = Node(name) 
    //여기서 Node?가 아닌 Node타입이였다면, Created parent.child가 출력됨
}

fun main(){
	val node = Node("parent")
    node.makeChild("child")
}

//Created parent

 

- name = 아래 둘 중 하나를 선택하게 됨

- this.name = create로 생성된 노드

- this@Node.name = main에서 생성된 노드

레이블 없이 리시버를 사용할 경우, 리시버는 가장 가까운 리시버를 의미한다.

하지만, 여기서는 create된 노드가 Node? 널 가능 타입임으로, 언팩(this?.name)이 안된 채로 사용하면 컴파일 에러남으로,

main에서 생성된 노드의 name값을 선택해서 쓰는 듯하다.

 

되도록이면, 여러 개의 스코프를 중첩시켜서 혼동을 일으키지 말고, 인수 또는 인스턴스를 직접 받는 것이 더 낫다.

class A {
    fun foo() = "A"
}

class B {
    fun foo() = "B"
}

//리시버 방식
fun test() {
    val a = A()
    val b = B()

    a.apply {
        b.apply {
            println(foo())
        }
    }
}

//명시적으로 인수 사용
fun printFoo(a: A, b: B) {
    println(b.foo())
}

 

Item 15. 프로퍼티는 동작이 아닌 상태를 나타내야 한다

코틀린 프로퍼티는 자바의 필드와 비슷해 보이지만, 다른 개념이다.

 

'데이터 저장'이라는 동일한 용도를 가졌지만, 아래와 같이 차이점이 존재한다.

 

1. 항상 사용자 정의 세터와 게터를 가질 수 있다.

var name: String = "Kotlin"
    get() = field // 백킹 필드 값 반환
    set(value) {
        if (value.isNotEmpty()) {
            field = value // 백킹 필드에 값 저장
        }
    }

백킹 필드는 프로퍼티 값을 저장하는 필드를 말한다.

컴파일러가 자동을 생성하며, 세터와 게터 디폴트 구현이 백킹 필드를 사용한다.

커스텀 getter/setter내에서 field 키워드를 통해 접근 가능하다. 이를 통해 무한 재귀 호출을 방지한다.

 

활용)

래핑(wrap)/언래핑(unwrap)으로 공통화하여, 수정 범위를 최소화할 수 있다.

var date: Date
	get() = Date(millis)
    set(value) {
    	millis = value.time
    }

 

2. 프로퍼티를 오버라이드할 수 있다.

open class Supercomputer{
	open val theAnswer: Long = 42
}

class AppleComputer : Supercomputer(){
	override val theAnswer: Long = 1_800_275_2273
}

 

3. 확장 프로퍼티로 만들 수 있다.

단, 프로그램 로직 등 복잡한 연산을 나타내는 데 프로퍼티를 사용해서는 안된다.

딱 getter, setter 역할에 맞는 일만 해야지 로직을 넣지 말자.

val Context.preferences: SharedPreferences
	get() = PreferenceManager
    	.getDefaultSharedPreferences(this)

 

 

Item 28.  외부 API를 래핑하는 것을 고려하라

Picasso.get()
	.load(url)
    .into(imageView)
    
//wrapper
fun ImageView.loadImage(url: String){
	Picasso.get()
    	.load(url)
        .into(imageView)
 }

외부 API가 변경되더라도 래퍼(wrapper)내에서만 변경할 수 있다. 즉, 외부 API를 여러 곳에서 쓰는데, 이걸 하나의 함수로 래핑해서 수정 범위를 줄이는 것으로 볼 수 있다.

그만큼 내부 함수(API)가 많아지고, 외부 API + 내부 API 를 모두 이해하고 있어야 하긴하지만, 래핑했을 때의 장점이 큰 편으로 알고있으면 좋다.

 

 

Item 32.  보조 생성자 대신 팩토리 함수를 고려하라.

팩토리 함수란, 생성자 대신 "객체 생성"을 담당하는 특수 "함수"를 말한다.

//생성자 사용
val user = User("Kotlin")

//팩토리 함수 사용
fun createUser(name: String): User{
	val normalized = name.trim()
    return User(normalized)
}

//타입 매개변수를 구체화 => 하나의 함수로 여러 타입 생성 가능
//일반 생성자는 타입이 고정됨
inline fun <reified T> create(): T

val user = create<User>()
val order = create<Order>()

 

- 생성자와 달리 함수에는 이름이 있음으로, 해당 함수가 무엇을 의미하는지 정확히 알 수 있다.

- 생성자와 달리 함수는 반환 타입(인터페이스로)을 지정해서, 해당 타입의 모든 하위 타입 객체를 반환할 수 있다.

- 생성자와 달리 함수는 새 객체를 생성할 필요가 없다. (캐싱, 싱글톤 패턴처럼 재사용, null반환도 가능)

- 지연 생성이 가능하다.

- 객체의 가시성(private, public...)을 제어할 수 있다.

- 팩토리 함수는 인라인될 수 있으므로, 타입 매개변수를 구체화할 수 있다.

 

참고) inline + reified

//1. 일반 함수
// 컴파일할 때 타입T가 제거됨 => 런타임에 T가 뭔지 모르고 Object로 처리됨 => 컴파일 에러
fun <T> printType(){
	println(T::class)
}

//2. 인라인 함수
//컴파일할 때, 호출 위치마다 코드 복사 + 타입 매칭되어 호출한 곳에 코드 치환이 됨으로 정상 동작
inline fun <reified T> printType(){
	println(T::class)
}

printType<String>()

=> 컴파일 시: println(String::class)

 

 

팩토리 함수의 사용 방식

1. 동반 객체 팩토리 함수(Companion Object Factory)

= 클래스 내부에 정의된 정적(static) 생성 함수

*인터페이스에서도 동일하게 쓸 수 있음

* 동반 객체란? 클래스에 속한 단 하나의 대표 객체 (*User.of와 같이 동반객체의 메서드를 간단하게 사용 가능)

 

static한 요소를 대신하여 동반 객체를 사용하는 경우가 많지만, 동반 객체에는 더 많은 기능이 있다.

1) 동반 객체는 인터페이스를 구현할 수 있다.

2) 클래스를 상속할 수 있다.

abstract class Activity Factory{
	abstract fun getIntent(context: Context): Intent
    
    fun start(context: Context){
    	val intent = getIntent(context)
        context.startActivity(intent)
    }
    
    fun startForResult(activity: Activity, requestCode: Int){
    	val intent = getIntent(activity)
        activity.startActivityForResult(
        	intent,
            requestCode
        )
    }
}


class MainActivity : AppCompatActivity(){
	//...
    
    companion object : ActivityFactory(){
    	override fun getIntent(context: Context): Intent = 
        	Intent(context, MainActivity::class.java)
        }
    }
}

//사용법
val intent = MainActivity.getIntent(context)
MainActivity.start(context)
MainActivity.startForResult(activity, requestCode)

 

 

[언제 사용]

- 객체 생성 규칙을 클래스 내부에 두고 싶을 때

- 생성자를 직접 못 쓰게 하고 싶을 때

- of(), from() 같은 네이밍 쓰고 싶을 때

class User private constructor(val name: String){
	companion object{
    	fun of(name: String): User{
        	return User(name)
        }
    }
}

val user = User.of("Kotlin")

 

 

2. 최상위 수준 팩토리 함수(Top-level Factory)

= 클래스 밖에 있는 팩토리 함수

ex) listOf, setOf, mapOf

 

[언제 사용]

- 여러 타입을 조합해서 생성할 때

- 특정 클래스에 종속시키기 애매할 때

- 유틸 성격

- 크기가 작고 생성이 빈번한 객체를 만들 때

 

[주의점]

- 어디에서나 사용할 수 있으므로 IDE사용할 때 곳곳에서 해당 함수를 제안해서 복잡해진다. (보통 {Class명}.메서드로 사용하는데 최상위수준 함수는 그냥 메서드 호출만 해도되니까 모든 곳에서 추천 대상이 됨)

- 특히 최상위 수준 함수가 클래스 메서드와 이름이 동일하여 혼동될 때가 심각하다. 

fun createUser(name: String): User{
	return User(name)
}

 

3. 빌더

= 복잡한 객체를 단계적으로 생성하는 방식

- 생성자 파라미터 많을 때 사용

- 코틀린에서는 보통 data class + default parameter로 대체하기도 함

//1. 자바식 빌더
class User private constructor(
	val name: String,
    val age: Int,
    val email: String?
){
	class Builder{
    	private var name: String = ""
        private var age: Int = 0
        private var email: String? = null
        
        fun name(name: String) = apply{this.name = name}
        fun age(age: Int) = apply{this.age = age}
        fun email(email: String?) = apply{this.email = email}
        
        fun build() = User(name, age, email)
    }
}

val user = User.Builder()
	.name("Kotlin")
    .age(20)
    .email("test@abc.com")
    .build()
    
//2. 코틀린 스타일(최상위 함수 + DSL)
class User(
	val name: String,
    val age: Int
)

class UserBuilder{
	var name: String = ""
    var age: Int = 0
    
    fun build() = User(name, age)
}

fun user(block: UserBuidler.() -> Unit): User{
	val builder = UserBuilder()
    builder.block()
    return builder.build()
}

//사용(DSL스타일 - 설정 파일 처럼 읽힘)
val user = user{
	name = "Kotlin"
    age = 20
}

 

 

4.  변환 메서드 (Conversion Method)

= 다른 타입 -> 현재 타입으로 변환하는 팩토리 함수

- to 접두사 = 다른 타입을 가진 새 객체를 생성한다

- as 접두사 = 새로 생성된 객체가 래퍼이거나 원본 객체의 추출된 부분임을 의미

- DTO <-> Domain 변환에 자주 사용(보통 확장 함수로 정의)

fun main(){
	val seq1 = sequence<Int>{
    	repeat(10){
        	print(it)
            yield(10)
        }
    }
    seq1.asSequence()//원본 객체의 추출부로 아무것도 출력안됨
    seq1.toList() //출력 0123456789 => sequence순회하면서 새로운 List를 반환
    
    val l1 = mutableListOf(1,2,3,4)
    val l2 = l1.toList()
    val seq2 = l1.asSequence()
    
    l1.add(5)
    
    print(l2) //[1, 2, 3, 4]
    print(seq2.toList()) //[1, 2, 3, 4, 5]
}

 

5. 복사 메서드

- copy = 그대로 복사

- with = 약간의 변경 사항 추가 (ex. user1 = user.withSurname(newSurname))

 

6. 가짜 생성자

= 외형은 생성자 처럼 보이지만, 실제로는 최상위 수준 함수

- 생성자처럼 보이고 생성자처럼 동작해야한다. 함수 이름과 역할을 주의

아래와 같은 방법으로 List를 생성하고 싶어서,
List(4){"User$it"} //[User0, User1, User2]

//코틀린에는 List와 MutableList를 생성하는 함수들이 추가됨
public inline fun<T> List(
	size: Int,
    init: (index: Int) -> T
): List<T> = MutableList(size, init)

 

7. 팩토리 클래스의 메서드

- 팩토리 클래스에는 상태가 있어서 팩토리 함수에는 없는 기능을 추가할 수 있다.

- 객체를 생성할 때, 여러 서비스나 저장소가 필요하다면 팩토리 클래스로 추출하는 경우가 일반적이다.

//상태 저장 및 활용
data class Student(
	val id: Int,
    val name: String,
    val surname: String
)

class StudentsFactory{
	val nextId = 0
    fun next(name: String, surname: String) = 
    	Student(nextId++, name, surname)
}

val factory = StudentsFactory()
val s1 = factory.next("Marcin", "Moskala")
print(s1) //Student(id=0, ...)

val s2 = factory.next("Igor", "Wodja")
print(s2) //Student(id=1, ...)

//여러 서비스가 필요한 경우
class UserFactory(
	private val uuidProvider: UuidProvider,
    private val timeProvider: TimeProvider,
    private val tokenService: TokenService,
){
	fun create(newUserData: NewUserData): User{
    	val id = uuidProvider.next()
        return User(
        	id = id,
            creationTime = timeProvider.now(),
            token = tokenService.generateToken(id),
            name = newUserData.name,
            surname = newUserData.surname,
            ...
        )
    }
}

 

Item 33.  이름 있는 선택적 인수를 갖는 기본 생성자 사용을 고려하라

점층적 생성자 패턴

- 코틀린에서는 디폴트 인수(default parameter)를 사용하여, 인수의 개수를 조절할 수 있다.

- 인수를 어떤 순서로든 제공할 수 있다.

- 인수의 이름을 명시적으로 지정할 수 있어, 값의 의미를 명확하게 알 수 있다.

//Java식
class User{
	val name: String
    val age: Int
    
    constructor(
    	name: String,
        age: Int
    ){
    	this.name = name
        this.age = age
    }
    
    constructor(
    	age: Int
    ) : this("unknown", age)
}

//Kotlin에서는 간결하게 작성 가능
class User(
    val name: String = "unknown",
    val age: Int
)

fun main() {
    val user = User("kotlin")//필수값 누락 오류 발생(age를 필수로 넣어주도록 강제 가능)
    val user1 = User(age = 20)
    print(user)
}

 

빌더 패턴

자바에서는, 이름이 있는 매개변수와 디폴트 인수를 사용할 수 없다.

그래서 빌더 패턴을 사용해서 다음과 같은 것을 할 수 있다.

  • 매개변수 이름 지정
  • 매개변수를 원하는 순서로 지정
  • 기본값 지정

빌더 대신 이름 있는 매개변수를 사용했을 때의 이점

1) 이름 있는 매개변수는 더 짧고, 깔끔하다.

2) 이름 있는 매개변수는 기본적으로 내장된 기본 생성자를 활용함으로 더 쉽습니다. 빌더 패턴은 언어에서 제공하는 것이 아닌 알려진 패턴을 개발자가 개발해야한다.

3) 이름 있는 매개변수는 동시성 문제가 거의 없다. 함수의 매개변수는 항상 불변인 반면, 대부분의 빌더에서는 프로퍼티 변경이 가능하기 때문이다.

 

빌더 패턴을 사용하는 것이 더 나은 경우

1) 하나의 이름에 대해 필요한 여러 값을 전달할 수 있다.

=> 기본 생성자의 경우, 중간 단계 없이 완성된 객체를 전달해야하다보니 여러 값을 가진 하나의 클래스를 만들어서 전달해야 함

2) 하나의 객체에 빌더 함수를 연속해서 사용하는 것이 가능하다.

//Java식 빌더패턴
val dialog = AlertDialog.Builder(context)
	.setMessage(R.string.fire_missiles)
    .setPositiveButton(R.string.fire){d, id -> 미사일 발사}
    .setNegativeButton(R.string.cancel){d, id -> 취소 버튼 누를 경우}
    .create()
    
val router = Router.Builder()
    .addRoute(path = "/home", ::showHome)
    .addRoute(path = "/users", ::showUsers)
    .build()
    
//Kotlin
val dialog = AlertDialog(
	context,
    message = R.string.fire_missiles,
    positiveButtonDescription = 
    	ButtonDescription(R.string.fire){d, id -> 미사일발사},
        //fire 1개와 함수 1개를 인자로 가짐 = 총 2개의 프로퍼티를 가지는 클래스를 생성해서 전달 필요
    negativeButtonDescription = 
    	ButtonDescription(R.string.cancel){d, id -> 취소 버튼 누를 경우}
    )
    
val router = Router(
	routes = listOf(
    	Route("/home", ::showHome),
        Route("/users", ""showUsers)
    )
)

위와 같은 코드는 코틀린에서 지양하는 패턴이며, DSL 빌더를 사용하는 것이 일반적이다. 

 

Item 34.  복잡한 객체 생성을 위해 DSL 정의를 고려하라

DSL(도메인 특화 언어)는 복잡한 객체나 객체의 계층구조를 정의해야할 때 유용하다.

짧게 정리하면, DSL은 리시버를 가진 함수타입 + this생략의 중첩된 구조이다.

 

함수 타입이란, 함수로 사용할 수 있는 객체를 나타내는 타입이다.

함수타입의 인스턴스를 생성하는 기본 방법은 다음과 같다.

//프로퍼티 타입이 지정되었음으로 함수에서 사용되는 인수 타입을 추론할 수 있다.

//1. 람다 표현식 사용(익명 함수를 더 간결하게 작성하는 방법임)
val plus1: (Int, Int) -> Int = {a, b -> a + b }

//2. 익명 함수 사용(이름 없는 함수)
val plus2: (Int, Int) -> Int = fun(a,b) = a + b

//3. 함수 레퍼런스 사용
val plus3: (Int, Int) -> Int = Int::plus

//반대로, 인수타입을 지정하면 함수 타입 추론이 가능하다.
val plus4 = {a: Int, b: Int -> a + b}
val plus5 = fun(a: Int, b: Int) = a + b

 

이를 확장 함수에 적용하여 '익명 확장 함수'를 정의해보자.

리시버가 있는 함수 타입으로 볼 수 있다.

//확장 함수
fun Int.myPlus(other: Int) = this + other

//익명 확장 함수
val myPlus = fun Int.(other: Int) = this + other
val myPlus = Int.(Int) -> Int = fun Int.(other: Int) = this + other
val myPlus: Int.(Int) -> Int = {this + it} //람다 표현식 + 인자가 1개이면 it을 기본적으로 사용

 

함수 타입 객체 호출 방법

//1. invoke 매서드 사용
myPlus.invoke(1, 2)

//2. 비확장 함수처럼 호출
myPlus(1, 2)

//3. 확장 함수로 호출
1.myPlus(2)

 

html예시 분석

fun createTable(): TableBuidler = table{
	tr{
    	for(i in 1..2){
        	td{
            	+"This is column $i"
            }
        }
    }
}


//위의 코드는 아래와 같은 구조의 중첩된 구조라고 볼 수 있을듯
fun createTable(): TableBuilder = table{
	this.tr{
    	//...
    }.init//실행
}

inline fun<T> T.apply(block: T.() -> Unit): T{
	this.block()//block.invoke(this)
    return this
}//자주 사용할 듯하여 apply함수로 선언

fun table(init: TableBuilder.() -> Unit): TableBuidler{
	TableBuilder().apply(init)
}

class TableBuilder{
	fun tr(init: TrBuilder.() -> Unit){
    	val tr = TrBuilder()
        tr.init()
    }
}

 

DSL을 대신할 더 간단한 기능들이 있다면, 굳이 쓰지 말자, DSL내부 동작을 모르고 사용하여 혼란을 줄 수도있다.

복잡한 구조를 처리할 때 이를 해결할 수 있는 코틀린 기능이 없다면 DSL을 사용하자.

 

Item 37. 데이터 묶음을 표현할 때 data한정자를 사용하라

최근 프로젝트에서는 두 종류의 객체로 분류하여 개발하는 것을 선호한다.

 

1) 서비스, 컨트롤러, 리포지토리 같은 활성 객체

2) 데이터 모델 클래스 객체

 

여기서 2번의 경우에 data 한정자를 사용한다. 활성 객체의 경우 Any(모든 non-null타입의 최상위 타입, Java의 Object와 유사 개념)의 기본 동작으로 충분하고 equals, toString등의 매서드를 사용할 필요가 없거나 혼란을 줄 수 있음으로 data한정자를 사용하지 않는다.

 

data 한정자가 오버라이드하는 매서드

- toString

- equals와 hashCode

- copy = 얕은 복사, 복사하면서 일부 프로퍼티 값을 변경하여 생성 가능

- componentN = 위치 기반 프로퍼티 구조 분해

 

구조 분해의 장단점

- 장점 = 원하는 방식으로 변수의 이름을 지정할 수 있다.

- 단점 = 데이터 클래스에서 요소의 순서나 수가 변경되면 모든 구조 분해를 조정해야 함

따라서, 데이터 클래스의 기본 생성자 프로퍼티와 같은 이름을 할당하는 것이 좋다. 구조 분해 순서가 틀렸을 경우 IDE에서 경고 메시지를 띄워줄 것이다.

val visited = listOf("China", "Russia", "India")
val (first, second, third) = visited //구조분해
println("$first $second $third") // China Russia India

 

헷갈리기 쉬움으로 첫 번째 값만 얻기 위해 구조 분해를 사용하는 것은 지양하기.

data class User(val name: String)

fun main(){
	val user = User("John")
    
    val (name) = user //=> 이렇게 쓰지 말기
    print(name) //John
    
    user.let{ a -> print(a)} //User(name=John)
    user.let{(a) -> print(a)} //John //=> 이렇게 쓰지 말기
}

 

튜플 대신 데이터 클래스를 사용하는 것을 권장한다. 

코틀린에서 제공하는 튜플은 Pair, Triple밖에 없고 데이터 클래스는 일반적으로 튜플보다 많은 것을 제공하기 때문이다.

단, 튜플이 유용한 경우도 있어서 튜플이라는 개념이 남아있는 것이다.

//1. 값 이름을 즉시 지정할 때
val (description, color) = when{
	degrees < 5 -> "cold" to Color.BLUE
    degrees < 23 -> "mild" to Color.YELLOW
    else -> "hot" to Color.RED //=Pair("hot", Color.RED) 
}

//2. 집계를 표현할 때
val (odd, even) = numbers.partition{ it % 2} == 1
val map = mapOf(1 to "San Francisco", 2 to "Amsterdam")

 

Item 51. 함수형 타입 매개변수를 갖는 함수에 inline한정자를 사용하라

inline한정자를 사용하면, 컴파일 할 때, 함수 호출부가 함수의 본문으로 대체, 즉 사용부에 복사되어 컴파일된다.

inline한정자를 사용하면 다음과 같은 장점들이 있다.

 

1. 타입 인수가 구체화될 수 있다.

- 제네릭은 자바 초기에는 없던 개념으로 나중에 추가되었으며, JVM 바이트 코드에는 아직 반영되지 않아서, 컴파일 시 제네릭 타입은 지워짐 => 제네릭으로 함수를 생성할 때는, 타입은 끝까지 모른채로 잘 동작하는 함수를 작성해야함으로 제약이 있다.

- reified한정자 + inline으로 공통 로직은 한번 작성하되 각 타입별로 커스텀하게 적용할 수 있어 간단하다.

 

2. 함수형 매개변수가 있는 함수는 인라인화되었을 때 더 빠르다.

- 함수가 호출된 곳으로 점프 + 함수 객체 생성 시간 + 백스택(back-stack)추적 시간이 필요 없기 때문

- 함수형 매개변수가 없는 함수도 inline으로 처리했을 때가 빠르긴한데 거의 차이 없다. => 객체(함수 클래스) 생성 시간에서 차이가 큼

- 추가적으로, 함수 내에서 지역 변수를 사용할 경우 일반 함수는 이를 캡처하고, 캡처된 값은 참조 객체로 래핑되기 때문에 느리다.

val l = 1L
noinlineRepeat(100_000_000){
	l+=it
}

//컴파일 된 후
// l 값을 바깥변수와 함수 내부에서 사용할 때 얕은 복사가 아닌 완전히 같은 객체를 바라보기 위함
val a = Ref.LongRef()
a.element = 1L
noinlineRepeat(){
	a.element = a.element + it
}

 

3. 비지역 반환이 허용된다.

- 일반 함수에서 람다함수를 인자로 넣을 때, 해당 람다의 return은 람다만 종료인지 외부 함수의 종료인지 모호하여 허용하지 않음

- 인라인 함수는 그대로 복사됨으로 외부 함수의 종료로 처리됨

fun hasZero(numbers: List<int>): Boolean{
	numbers.forEach{ //inline
    	if(it == 0) return true
    }
    
    return false
}

 

 

인라인 함수의 비용

1. 인라인 함수는 제한된 가시성을 가진 요소를 사용할 수 없다.

- private 또는 internal 제어자가 있는 함수나 프로퍼티를 public inline 함수에서 사용할 수 없다. public inline은 어디든 코드가 복사되어 대체되니까 private을 외부에서 보도록 하면 안됨.

2. 인라인 함수는 재귀적으로 사용할 수 없다. 

- 코드 무한 복사됨

3. 인라인 함수는 코드 규모를 증가시킨다.

- 함수가 호출되는 위치마다 복사가 되니까

 

crossinline = 인라인이어야 하지만, 비지역 반환은 허용하고 싶지 않을 때 사용

noinline = 인수로 들어온 함수에 인라인 함수를 허용하고 싶지 않을 때, 즉 함수 객체를 받도록 하고 싶을 때 사용

 

Item 59. 가변 컬렉션 사용을 고려하라

요소를 추가하는 연산은 가변 컬렉션이 더 빠르지만,

불변 컬렉션은 가변성을 제한하여 안정성을 높일 수 있는 장점이 있다.

지역 스코프에서는 가변성을 제한할 필요가 없기 땜누에 가변 컬렉션을 사용하는 것이 좋다.

 

참고 도서) 코틀린 아카데미 이펙티브 코틀린

'Programming > Kotlin' 카테고리의 다른 글

[Kotlin] 코루틴 (+Virtual Thread)  (0) 2026.03.28
Kotlin의 기초  (0) 2026.03.28