더 간결하고 안전하게
- Java의 Optional<T>은 null 참조를 안전하게 다루기 위해 도입되었지만, Kotlin은 자체적인 Nullable 타입 시스템( T? )과 다양한 연산자를 통해 이보다 더 간결하고 직관적으로 null 처리를 가능하게 한다. Optional을 Kotlin Style로 다루는 여러 가지 방법을 알아본다
Kotlin의 Null 타입 ( T? )과 엘비스 연산자 ( ?: ) 활용
- Kotlin은 기본적으로 모든 타입이 Non-null이며, null을 허용하려면 타입 뒤에 ?를 붙여 명시해야 한다. (예: String?, User?) 이 Nullable 타입을 이용하면 Optional 없이도 존재하지 않는 값에 대한 처리를 깔끔하게 할 수 있다
Java Optional 스타일
interface UserRepository: JpaRepository<User, Long> {
fun findByName(name: String): Optional<User> // Optional 반환
}
@Service
class UserService(private val userRepository: UserRepository) {
@Transactional
fun deleteUser(name: String) {
val user = userReposiroty.findByName(name).orElseThrow(::IllegalArgumentException) // Optional.orElseThrow 사용
userRepository.delete(user)
}
}
Kotlin-style (Nullable 타입 및 엘비스 연산자)
interface UserRepository: JpaRepository<User, Long> {
fun findByName(name: String): User? // Nullable 타입 반환
}
@Service
class UserService(private val userRepository: UserRepository) {
@Transactional
fun deleteUser(name: String) {
// findByName이 null을 반환하면 IllegalArgumentException 발생
val user = userReposiroty.findByName(name) ?: throw IllegalArgumentException()
userRepository.delete(user)
}
}
- findByName(name) 메서드가 User 객체를 찾지 못해 null을 반환하면, 엘비스 연산자 (?:)에 의해 IllegalArgumentException이 발생하여 user 변수에는 절대로 null이 할당되지 않음을 보장한다
반복되는 예외 처리를 위한 유틸리티 함수 – fail()
- 특정 예외 (IllegalArgumentException 등)가 반복적으로 발생하는 경우, 이를 처리하는 코드를 매번 작성하는 것을 비효율적이다. 이럴 때는 전용 유틸리티 함수를 만들어서 코드를 간결하게 만들 수 있다
ExceptionUtils.kt
package com.exmaple.utils // 적절한 패키지명 사용
fun fail(): Nothing {
throw IllegalArgumentException()
}
// Default Parameter를 사용할 수도 있다
fun fail(message: String = "부적절한 인수 값입니다"): Nothing {
throw IllegalArgumentException(message)
}
- Nothing 타입은 함수가 절대 성공적으로 반환되지 않음을(항상 예외를 던지거나 무한 루프에 빠짐) 컴파일러에게 알려준다
fail() 적용
@Service
class BookService(
private val bookRepository: BookRepository,
private val userRepository: UserRepository,
private val userLoanHistoryRepository: UserLoanHistoryRepository,
) {
@Transactional
fun loanBook(request: BookLoanRequest) {
val book = bookRepository.findByName(request.bookName) ?: fail()
// 그 밖에 로직
val user = userRepository.findByName(request.userName) ?: fail()
user.loanBook(book)
}
@Transactional
fun returnBook(request: BookReturnRequest) {
val user = userRepository.findByName(request.userName) ?: fail()
user.returnBook(request.bookName)
}
}
- fail() 함수를 사용함으로써 반복되는 throw IllegalArgumentException() 코드를 줄이고 가독성을 높일 수 있다
Java 라이브러리의 Optional을 Kotlin스럽게 다루기 (확장 함수)
- JpaRepository의 findById 처럼 개발자가 직접 제어할 수 없는 Java 라이브러리(CrudRepository Interface 등)가 Optional을 반환하는 경우가 있다. 이때 Kotlin의 확장 함수(Extension Function)을 사용하여 Optional을 T?나 T로 변환하여 활용할 수 있다
- 스프링 프레임워크는 Kotlin과 CrudRepository를 함께 사용할 때는 대비하여 CrudRepositoryExtension을 제공한다
import org.springframework.data.repository.CrudRepository
import java.util.Optional
// CrudRepository의 findById가 반환하는 Optional<T>를 T?로 변환
fun <T, ID> CrudRepository<T, ID>.findByIdOrNull(id: ID): T? = findById(id).orElse(null)
- 이 확장 함수를 사용하면 findById가 반환하는 Optional<T>을 마치 CrudRepository의 메서드인 것처럼 호출하여 T? 타입으로 직접 받을 수 있다
findByIdOrNull 적용
@Service
class UserService(private val userRepository: UserRepository) {
@Transactional
fun updateUserName(request: UserUpdateRequest) {
// 기존: userRepository.findById(request.id).orElseThrow(::IllegalArgumentException)
val user = userRepository.findByIdOrNull(request.id) ?: fail()
iser.updateName(request.name)
}
}
확장 함수를 이용한 완전한 Optional 제거 (findByIdOrThrow)
- 여기서 한 단계 더 나아가, findByIdOrNull과 fail()을 조합하여 Nullable이 아닌 T 타입을 직접 반환하며, null일 경우 예외를 던지는 확장 함수를 만들 수 있다
ExceptionUtils.kt
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.findByIdOrNull
fun fail(): Nothing {
throw IllegalArgumentException()
}
// CrudRepository의 findById 결과가 null이면 예외를 던지는 확장 함수
fun <T, ID> CrudRepository<T, ID>.findByIdOrThrow(id: ID): T {
return this.findByIdOrNull(id) ?: fail()
}
- findByIdOrThrow 함수는 반환 타입이 T로 선언되어 있어, 이 함수가 호출된 후에는 절대로 null이 아님을 컴파일러가 보장한다
findByIdOrThrow 적용
import com.group.libraryapp.util.findByIdOrThrow
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class UserService(
private val userRepository: UserRepository,
) {
@Transactional
fun updateUserName(request: UserUpdateRequest) {
val user = userRepository.findByIdOrThrow(request.id)
user.updateName(request.name)
}
}
- Kotlin의 확장 함수를 활용하면 기존 Java 라이브러리의 제약사항을 우회하고, 코틀린 타입 시스템을 적극적으로 활용하여 더욱 간결하고 type-safe한 코드를 작성할 수 있다. 이는 개발 생산성을 높이고 런타임 NPE 발생 가능성을 줄여준다
출처 – 실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기 (Java 프로젝트 리팩토링)