코루틴과 트랜잭션 관리: 비동기 처리와 트랜잭션 안정성

Kotlin Coroutines는 비동기 처리를 효율적으로 할 수 있지만, 트랜잭션 처리와 결합될 때는 여러 가지 고려사항이 필요합니다.

이번 글에서는 코루틴의 주요 이점과 함께 Spring 트랜잭션과 코루틴을 효과적으로 사용하는 방법을 포스팅합니다.

1. 코루틴의 주요 이점

1.1. 리소스 효율성

  • 스레드 비용 절감: 코루틴은 기존 스레드보다 가볍기 때문에 수천 개의 코루틴을 동시에 실행할 수 있습니다.
    이는 시스템 리소스를 효율적으로 사용하게 해줍니다.

  • 메모리 최적화: 스레드 기반 처리가 아닌 코루틴을 사용하여 적은 메모리로 많은 작업을 처리할 수 있습니다.

repeat(1000) { 
    launch {
        // 각 요청을 비동기적으로 처리
        processRequest()
    }
}

위 코드에서는 1000개의 요청을 코루틴을 통해 동시에 처리하며, 별도의 스레드 생성 없이 가볍게 실행할 수 있습니다.

1.2. 동시성 처리의 단순화

  • 구조화된 동시성: 부모-자식 관계로 코루틴을 묶어서 쉽게 관리할 수 있습니다. 트랜잭션이 필요할 때도 부모 코루틴 내에서 적절히 관리가 가능합니다.

  • 에러 처리 용이: try-catch를 이용해 코루틴 내에서도 자연스럽게 에러를 처리할 수 있습니다.

coroutineScope {
    val deferred1 = async { fetchData1() }
    val deferred2 = async { fetchData2() }
    // 두 작업의 결과를 동시에 기다림
    val result = deferred1.await() + deferred2.await()
}

1.3. 성능 향상

  • Non-blocking I/O: I/O 작업이 비동기적으로 처리되기 때문에, 스레드가 차단되지 않고 성능이 향상됩니다.

  • 컨텍스트 스위칭 비용 감소: 스레드 간 전환보다 코루틴 간 전환이 훨씬 빠릅니다.

2. 트랜잭션 처리와 코루틴의 결합

Kotlin Coroutines와 Spring 트랜잭션 관리를 결합하려면, 코루틴의 비동기 처리 이점과 Spring의 ThreadLocal 기반 트랜잭션 관리의 안정성을 모두 고려해야 합니다.

2.1 Non-suspend와 Suspend 함수 래핑 방식

코루틴 내에서 JPA의 트랜잭션 처리를 안전하게 하려면, 트랜잭션 관련 로직은 Non-suspend 함수로 작성하고, 이를 suspend 함수로 래핑하는 방식이 좋습니다.

// 1. Non-suspend 트랜잭션 함수
@Transactional
fun doTransactionalWork(param: String): Result {
    // 트랜잭션 범위 내에서 실행되는 작업
    return repository.save(...)
}

// 2. Suspend 래퍼 함수
suspend fun doTransactionalWorkSuspend(param: String): Result {
    return withContext(Dispatchers.IO) {
        doTransactionalWork(param)
    }
}

이 방식은 트랜잭션 안정성을 유지하면서도, 코루틴의 비동기적 특성을 활용할 수 있게 해줍니다.

트랜잭션 로직과 비동기 로직이 분리되어 유지보수성이 높아지며, 테스트 코드 작성도 훨씬 용이해집니다.

3. 실제 적용 예시

이제 실용적인 예시를 살펴보겠습니다.

메시지를 저장하는 과정에서 트랜잭션과 코루틴을 적절히 결합한 예입니다.

@Service
class MessageService {
    
    @Transactional
    fun saveMessage(message: Message): Message {
        // 트랜잭션이 보장된 동기 처리
        return repository.save(message)
    }

    suspend fun saveMessageAsync(message: Message): Message {
        return withContext(Dispatchers.IO) {
            // 비동기적으로 트랜잭션 처리를 수행
            saveMessage(message)
        }
    }

    suspend fun processBatchMessages(messages: List<Message>) {
        coroutineScope {
            messages.map { message ->
                async {
                    saveMessageAsync(message)
                }
            }.awaitAll()
        }
    }
}

4.주의사항

이 구조는 트랜잭션 안정성을 보장하고 대량 처리 시 성능을 향상시키며, 리소스를 효율적으로 사용할 수 있다는 장점이 있습니다. 그러나 몇 가지 주의할 사항도 있습니다.

  • Dispatchers.IO 사용 시 데이터베이스 커넥션 풀 고려: Dispatchers.IO는 주로 I/O 작업에 적합하지만, 많은 코루틴이 동시에 실행될 때 DB 커넥션 풀 크기를 고려해야 합니다.

과도한 코루틴이 커넥션 풀을 고갈시킬 수 있습니다.

  • 트랜잭션 경계 명확히 설정: 트랜잭션의 경계를 명확하게 설정하여 데이터 정합성을 유지해야 합니다.

코루틴 간에 트랜잭션이 전파되지 않으므로, 트랜잭션이 필요한 작업은 모두 Non-suspend 함수에서 처리해야 합니다.

  • 예외 처리 철저히 관리: 코루틴 내부에서 발생하는 예외가 트랜잭션 범위에 영향을 미치지 않도록 예외 처리에도 신경 써야 합니다.

출처

Kotlin 코루틴으로 트랜잭션 관리의 성능을 극대화하는 방법

Dispatchers.IO와 데이터베이스 커넥션 풀 관리

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글