Backend
home

이펙티브 코틀린: 안전성

Github 링크
날짜
2025/09/25

이펙티브 코틀린 완전 정복기 #1 - 1장 안전성 시작하기

코틀린을 제대로 배우고 싶다면 마르친 모스칼라의 "이펙티브 코틀린"은 필독서다. 단순한 문법서를 넘어 실무에서 정말 중요한 베스트 프랙티스들을 다루고 있기 때문이다. 오늘부터 이 책을 차근차근 정복해보려고 한다.

왜 이펙티브 코틀린인가?

이 책은 조슈아 블로크의 "이펙티브 자바"처럼, 언어의 기능을 단순히 나열하는 게 아니라 어떻게 써야 하는지를 알려준다. 총 50개의 아이템으로 구성되어 있으며, 각각이 하나의 핵심 원칙을 담고 있다.

1장: 안전성 - 버그 없는 코드를 위한 첫걸음

1장은 코틀린의 가장 큰 강점인 안전성에 대해 다룬다. 런타임에서 터질 수 있는 오류들을 컴파일 타임에 미리 잡아내는 것이 핵심이다.

Item 1: 가변성을 제한하라

첫 번째 원칙부터 강력하다. 상태가 변할 수 있는 지점을 최대한 줄여라는 것이다.

왜 가변성을 제한해야 할까?

프로그램을 추론하기 어려워진다
멀티스레드 환경에서 동기화 문제가 발생한다
테스트하기 복잡해진다
상태 변경을 추적하기 어려워진다

실무 예제: 장바구니 구현

// ❌ 나쁜 예 - 외부에서 마음대로 변경 가능 class BadShoppingCart { val items = mutableListOf<Item>() // 위험! } val cart = BadShoppingCart() cart.items.clear() // 외부에서 장바구니를 비워버릴 수 있다
Kotlin
복사
// ✅ 좋은 예 - 변경을 통제 class GoodShoppingCart { private val _items = mutableListOf<Item>() val items: List<Item> get() = _items.toList() // 읽기 전용으로 노출 fun addItem(item: Item) { _items.add(item) } fun removeItem(item: Item) { _items.remove(item) } fun clearAll() { _items.clear() } }
Kotlin
복사

val vs var 올바른 사용법

// ✅ 읽기 전용 프로퍼티 활용 class BankAccount(private val accountNumber: String) { private var _balance = 0L val balance: Long get() = _balance // 외부에서는 읽기만 가능 fun deposit(amount: Long) { require(amount > 0) { "입금액은 0보다 커야 한다" } _balance += amount } fun withdraw(amount: Long) { require(amount > 0) { "출금액은 0보다 커야 한다" } require(_balance >= amount) { "잔액이 부족하다" } _balance -= amount } }
Kotlin
복사

불변 컬렉션 활용

// ✅ 불변 컬렉션 선호 val numbers = listOf(1, 2, 3) // List<Int> - 불변 val mutableNumbers = mutableListOf(1, 2, 3) // MutableList<Int> - 가변 // ✅ data class로 불변 객체 생성 data class Point(val x: Int, val y: Int) { fun moveBy(dx: Int, dy: Int): Point = Point(x + dx, y + dy) // 새 인스턴스 반환 } val point = Point(0, 0) val movedPoint = point.moveBy(10, 5) // 기존 point는 변경되지 않음
Kotlin
복사

Item 2: 변수의 스코프를 최소화하라

변수가 사용되는 범위를 최대한 좁게 만들어야 한다. 이는 다음과 같은 이점을 제공한다:
프로그램을 추적하고 관리하기 쉬워진다
변수의 초기화를 잊을 가능성이 줄어든다
변수가 잘못 사용될 가능성이 줄어든다

실무 예제: 사용자 이메일 검증

// ❌ 나쁜 예 - 변수 스코프가 불필요하게 넓음 fun findUserEmail(userId: Long): String? { var user: User? = null // 너무 일찍 선언 var email: String? = null user = userRepository.findById(userId) if (user != null) { email = user.email if (email.isNotEmpty()) { return email } } return null }
Kotlin
복사
// ✅ 좋은 예 - 스코프 최소화 fun findUserEmail(userId: Long): String? { val user = userRepository.findById(userId) ?: return null val email = user.email return if (email.isNotEmpty()) email else null }
Kotlin
복사
// 🚀 더 좋은 예 - 코틀린다운 방식 fun findUserEmail(userId: Long): String? { return userRepository.findById(userId) ?.email ?.takeIf { it.isNotEmpty() } }
Kotlin
복사

반복문에서의 스코프 최소화

// ❌ 나쁜 예 fun processUsers() { var user: User? = null // 스코프가 너무 넓음 var isValid = false for (id in userIds) { user = findUser(id) if (user != null) { isValid = validateUser(user) if (isValid) { processValidUser(user) } } } }
Kotlin
복사
// ✅ 좋은 예 fun processUsers() { for (id in userIds) { val user = findUser(id) ?: continue if (validateUser(user)) { processValidUser(user) } } }
Kotlin
복사

구조 분해 선언 활용

// ✅ 구조 분해로 스코프 최소화 fun processCoordinates(points: List<Pair<Int, Int>>) { for ((x, y) in points) { // 필요한 곳에서 바로 분해 if (x > 0 && y > 0) { drawPoint(x, y) } } }
Kotlin
복사

핵심 정리

Item 1: 가변성을 제한하라

불변성을 기본으로 - val을 우선적으로 사용하고, 꼭 필요할 때만 var를 쓴다
컬렉션도 불변을 선호 - List<T> vs MutableList<T>
상태 변경을 통제 - private으로 숨기고 함수로만 접근 허용

Item 2: 변수의 스코프를 최소화하라

사용하는 곳 가까이에서 선언 - 변수를 너무 일찍 선언하지 말자
Elvis 연산자 적극 활용 - ?.?: 로 코드를 간결하게
구조 분해 선언 활용 - 필요한 값만 추출해서 사용

다음 예고

다음 글에서는 Item 3부터 계속해서 1장의 나머지 내용들을 다뤄볼 예정이다. 특히 플랫폼 타입 처리와 inferred 타입 활용법이 흥미로울 것 같다.