본문 바로가기

개발/백엔드

Kotlin Spring의 장점과 코루틴

가장 큰 장점을 얘기해 보면 아래 3개정도로 요약 할 수 있을 것 같다

 

1. null 안전성과 편한 사용

// 컴파일 시점에 null 체크
fun processUser(user: User?) {
    // null이면 메서드 실행 안함
    user?.process() 
    // null이면 기본값 사용
    return user?.name ?: "Unknown"
}

// Java
public String getUserName(User user) {
    return Optional.ofNullable(user)
            .map(User::getName)
            .orElse("Unknown");
}

 

2. Lombok 제거

// Data Class로 보일러플레이트 제거
// java record 도 있지만 불변이기도 하고 data가 상속도 되는 등 확장도 가능함
data class User(
    val id: Long,
    val name: String,
    val email: String? = null,  // 기본값 설정 가능
    var status: String  // 수정 가능
)

// Java
public class User {
    private Long id;
    private String name;
    private String email;
    
    // getter, setter, equals, hashCode, toString 모두 필요
}

 

3. Coroutine

// 비동기 프로그래밍을 위한 코루틴
// CompletableFuture보다 직관적인 비동기 코드 작성
// Kotlin
suspend fun fetchUser(): User {
    return coroutineScope {
        val user = async { userRepository.findById(1) }
        user.await()
    }
}

// Java
CompletableFuture<User> fetchUser() {
    return CompletableFuture.supplyAsync(() ->
        userRepository.findById(1));
}

 

 

 

여기서 코루틴과 기존 Java에서의 Webflux를 비교한다면 장점이 더 잘보인다.

 

1. 단순 비동기 처리

// Java Reactor
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id)
        .flatMap(user -> Mono.just(enrichUser(user)))
        .defaultIfEmpty(User.empty())
        .doOnError(e -> log.error("Error fetching user", e));
}

// Kotlin Coroutine
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: Long): User {
    return try {
        val user = userRepository.findById(id)
        enrichUser(user) ?: User.empty()
    } catch (e: Exception) {
        log.error("Error fetching user", e)
        throw e
    }
}

 

 

2. 여러 API 병렬 호출

// Java Reactor
public Mono<OrderDetails> getOrderDetails(Long orderId) {
    return Mono.zip(
        orderRepository.findById(orderId),
        paymentService.getPaymentInfo(orderId),
        shippingService.getShippingStatus(orderId)
    ).map(tuple -> new OrderDetails(
        tuple.getT1(),
        tuple.getT2(),
        tuple.getT3()
    )).onErrorResume(e -> Mono.empty());
}

// Kotlin Coroutine
suspend fun getOrderDetails(orderId: Long): OrderDetails {
    return coroutineScope {
        val order = async { orderRepository.findById(orderId) }
        val payment = async { paymentService.getPaymentInfo(orderId) }
        val shipping = async { shippingService.getShippingStatus(orderId) }
        
        OrderDetails(
            order = order.await(),
            payment = payment.await(),
            shipping = shipping.await()
        )
    }
}

 

 

3. 스트림 처리

// Java Reactor
@GetMapping("/prices/stream")
public Flux<Price> streamPrices() {
    return priceRepository.findAll()
        .filter(price -> price.getValue() > 1000)
        .map(this::enrichPrice)
        .buffer(10)
        .delayElements(Duration.ofMillis(100))
        .doOnError(e -> log.error("Error in price stream", e))
        .onErrorReturn(Price.empty());
}

// Kotlin Flow
@GetMapping("/prices/stream")
fun streamPrices(): Flow<Price> = flow {
    priceRepository.findAll()
        .filter { it.value > 1000 }
        .map { enrichPrice(it) }
        .chunked(10)
        .collect { chunk ->
            delay(100)
            emit(chunk)
        }
}.catch { e ->
    log.error("Error in price stream", e)
    emit(Price.empty())
}

 

 

생각보다 어렵지 않으니 고민 중이라면 시작하고 고민해보자~