Sealed Class
when에서 모든 경우를 처리했는지 컴파일 타임에 검증해준다. 예외 처리나 상태 관리에서 유용하다.
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val message: String, val code: Int) : ApiResult<Nothing>()
data object Loading : ApiResult<Nothing>()
}
// when에서 else가 필요 없다 — 모든 케이스를 다뤘으니까
fun handleResult(result: ApiResult<Post>) = when (result) {
is ApiResult.Success -> result.data
is ApiResult.Error -> throw BusinessException(result.message)
is ApiResult.Loading -> null
}
Java의 enum은 값만 나열할 수 있지만, sealed class는 각 케이스가 서로 다른 데이터를 가질 수 있다. API 응답이나 에러 처리에서 매우 깔끔하다.
Inline 함수와 Reified 타입
Java에서는 제네릭의 타입 정보가 런타임에 지워진다(type erasure). Kotlin의 inline + reified를 쓰면 런타임에도 타입 정보를 유지할 수 있다.
// Java에서는 불가능한 코드
// if (obj instanceof T) { } // 컴파일 에러
// Kotlin에서는 가능
inline fun <reified T> Any.isType(): Boolean = this is T
42.isType<Int>() // true
42.isType<String>() // false
Spring에서 실용적으로 쓰이는 예:
// RestTemplate에서 타입 안전하게 호출
inline fun <reified T> RestTemplate.getForEntity(url: String): ResponseEntity<T> =
getForEntity(url, T::class.java)
Coroutine (코루틴) — 참고용
코루틴은 Kotlin의 비동기 프로그래밍 솔루션이다. Java의 CompletableFuture나 Thread보다 훨씬 가볍고 직관적이다.
suspend fun fetchPost(id: Long): Post {
val post = postRepository.findById(id) // 비동기인데 동기처럼 읽힌다
val comments = commentRepository.findByPostId(id)
return post.copy(comments = comments)
}
Spring MVC(동기 방식)를 쓰니까 코루틴은 필요 없다. Spring WebFlux(리액티브)를 쓸 때 빛을 발한다. 지금은 "이런 게 있구나" 정도만 알아두면 된다.
Delegation (위임)
클래스 위임
// 인터페이스 구현을 다른 객체에 위임
class PostServiceLogging(
private val delegate: PostService
) : PostService by delegate {
// 특정 메서드만 오버라이드
override fun createPost(request: PostCreateRequest): Long {
log.info("글 작성 요청: ${request.title}")
return delegate.createPost(request)
}
}
데코레이터 패턴을 한 줄로 구현할 수 있다.
프로퍼티 위임
// lazy — 처음 접근할 때 한 번만 초기화
val heavyObject: HeavyThing by lazy {
HeavyThing() // 이 블록은 최초 접근 시에만 실행
}
// observable — 값이 바뀔 때마다 콜백
var name: String by Delegates.observable("초기값") { _, old, new ->
println("$old → $new")
}
by lazy는 Spring에서도 자주 쓴다. 비용이 큰 초기화를 지연시킬 때 유용하다.
연산자 오버로딩
data class Money(val amount: Int, val currency: String) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "통화가 다릅니다" }
return Money(amount + other.amount, currency)
}
}
val total = Money(1000, "KRW") + Money(2000, "KRW")
// Money(amount=3000, currency=KRW)
남용하면 코드가 읽기 어려워지니까 직관적인 경우에만 쓴다.
Destructuring (구조 분해)
// data class는 자동으로 구조 분해 가능
data class Post(val id: Long, val title: String)
val (id, title) = Post(1, "제목")
// Map 순회에서 유용
for ((key, value) in map) {
println("$key: $value")
}
// 람다에서도
posts.forEachIndexed { index, post ->
println("$index: ${post.title}")
}
Type Alias
긴 타입 이름에 별칭을 붙일 수 있다.
typealias PostId = Long
typealias TagNames = List<String>
typealias Predicate<T> = (T) -> Boolean
fun filterPosts(predicate: Predicate<Post>): List<Post> {
return posts.filter(predicate)
}
제네릭이 복잡해질 때 가독성을 위해 쓴다.
DSL (Domain Specific Language)
Kotlin의 람다 + 확장 함수를 조합하면 DSL을 만들 수 있다. Spring에서 이걸 적극 활용한다.
// Spring Security DSL 예시
@Bean
fun securityFilterChain(http: HttpSecurity) = http {
csrf { disable() }
authorizeHttpRequests {
authorize("/api/posts/**", permitAll)
authorize(anyRequest, authenticated)
}
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
}
Java로 쓰면 체이닝에 .and() 붙여가며 복잡한데, Kotlin DSL로 쓰면 구조가 한눈에 보인다. 이건 다음 문서에서 자세히 다룬다.
자주 하는 실수
1. ==과 ===
// == : 값 비교 (Java의 equals)
// === : 참조 비교 (Java의 ==)
val a = "hello"
val b = "hello"
a == b // true (값이 같다)
a === b // true (같은 String Pool 참조 — 이건 JVM 최적화)
Java에서 ==으로 문자열 비교하는 실수를 Kotlin에서는 할 수 없다. Kotlin의 ==이 이미 equals()를 호출하니까.
2. Unit vs Nothing
// Unit — Java의 void와 같다
fun printHello(): Unit {
println("hello")
}
// Nothing — 함수가 절대 정상 종료하지 않는다
fun fail(message: String): Nothing {
throw BusinessException(message)
}
3. lateinit vs lazy
// lateinit — 나중에 초기화할 var
// null이 아닌 타입에만 사용 가능, primitive 불가
lateinit var service: PostService
// lazy — 처음 접근 시 초기화되는 val
val service: PostService by lazy { PostService() }
Spring에서 @Autowired로 주입받을 때 lateinit을 쓸 수도 있지만, 생성자 주입이 더 좋다. 이것도 다음 문서에서 다룬다.