
JPA의 @Transactional, 성능을 저하시킬 수도 있다?!🧐 오늘은 JPA에서 트랜잭션을 관리하는 @Transactional 어노테이션 사용 시, 성능 최적화를 위해 주의해야 할 점들을 살펴보겠습니다!
JPA의 @Transactional은 데이터베이스 트랜잭션을 관리하는 강력한 도구이지만, 불필요하게 사용하면 성능에 부정적인 영향을 줄 수 있습니다.
테스트 결과에 따르면, @Transactional을 사용하지 않을 때 select 쿼리 성능이 약 2~3배 향상되었습니다.
효율적인 트랜잭션 관리를 위해 세 가지의 규칙을 적용했습니다.
서비스가 성장하면서 DB 업그레이드나 샤딩을 고려하기 전에, 기존 설정을 점검하고 최적화하는 것이 중요한데요.
JPA의 기본 설정을 맹신하기보다 성능에 미치는 영향을 테스트하고 최적화하는 것이 핵심입니다.
이 아티클은 JPA의 @Transactional 사용이 성능에 미치는 영향을 이해하고, 적절한 사용 방식을 적용하는 방법을 포스팅 하였습니다.
트랜잭션의 성능을 정확하게 추적하기 위해서 @Transactional이 적용된 메서드의 실행 시간을 추적하고, 이를 바탕으로 성능 지표를 수집하는 도구를 작성했습니다.
여기에서는 Aspect를 사용하여 트랜잭션이 시작될 때부터 종료될 때까지의 시간을 측정합니다.
위의 코드 예시에서 성능 모니터링 기능을 추가한 것은 중요한 포인트입니다. 이를 통해 성능 저하를 일찍 발견하고 대응할 수 있습니다.
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.util.concurrent.ConcurrentHashMap
@Aspect
@Component
class TransactionalPerformanceMonitor {
private val logger = LoggerFactory.getLogger(TransactionalPerformanceMonitor::class.java)
private val performanceMetrics = ConcurrentHashMap<String, MethodPerformanceMetric>()
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
fun measureTransactionalMethodPerformance(joinPoint: ProceedingJoinPoint): Any? {
val methodName = joinPoint.signature.name
val startTime = System.nanoTime()
return try {
val result = joinPoint.proceed()
val endTime = System.nanoTime()
updatePerformanceMetrics(methodName, endTime - startTime)
result
} catch (e: Exception) {
logger.error("Transaction method execution error", e)
throw e
}
}
private fun updatePerformanceMetrics(methodName: String, executionTime: Long) {
performanceMetrics.compute(methodName) { _, existingMetric ->
existingMetric?.let {
MethodPerformanceMetric(
totalExecutions = it.totalExecutions + 1,
totalExecutionTime = it.totalExecutionTime + executionTime,
minExecutionTime = minOf(it.minExecutionTime, executionTime),
maxExecutionTime = maxOf(it.maxExecutionTime, executionTime)
)
} ?: MethodPerformanceMetric(
totalExecutions = 1,
totalExecutionTime = executionTime,
minExecutionTime = executionTime,
maxExecutionTime = executionTime
)
}
}
fun getPerformanceReport(): Map<String, MethodPerformanceMetric> = performanceMetrics.toMap()
}
data class MethodPerformanceMetric(
val totalExecutions: Long,
val totalExecutionTime: Long,
val minExecutionTime: Long,
val maxExecutionTime: Long
) {
val averageExecutionTime: Double get() = totalExecutionTime.toDouble() / totalExecutions
}
위 코드에서 각 트랜잭션 메서드의 최소/최대 실행 시간, 총 실행 횟수, 평균 실행 시간을 추적합니다.
성능 저하를 실시간으로 확인할 수 있으며, 이를 바탕으로 트랜잭션을 최적화할 수 있습니다.
최적화 전략은 크게 세 가지로 나누어 볼 수 있습니다.
불필요한 트랜잭션 제거: 단건 조회나 수정에서는 트랜잭션을 사용하지 않도록 합니다.
예를 들어, 읽기 전용 트랜잭션에서는 @Transactional(readOnly = true)를 적용하여 DB 락을 피하고 성능을 개선할 수 있습니다.
예를 들어, 트랜잭션 내에서 또 다른 트랜잭션을 시작할 때는 REQUIRES_NEW 전파를 사용하여 새 트랜잭션을 분리할 수 있습니다.
필요한 경우, 읽기 전용 트랜잭션은 @Transactional(readOnly = true)로 지정하여 최적화할 수 있습니다.
고급 트랜잭션 관리 패턴에서는 트랜잭션을 조건부로 실행하거나, 재시도 메커니즘을 적용하여 성능을 최적화할 수 있습니다. 다음은 이를 구현한 코드입니다.
@Service
class EnhancedTransactionManager(
private val transactionTemplate: TransactionTemplate,
private val performanceMonitor: TransactionalPerformanceMonitor
) {
// 조건부 트랜잭션 실행
fun <T> executeConditionalTransaction(condition: () -> Boolean, transactionLogic: () -> T): T? {
return if (condition()) {
transactionTemplate.execute {
transactionLogic()
}
} else {
transactionLogic()
}
}
// 재시도 가능한 트랜잭션
fun <T> executeWithRetry(maxRetries: Int = 3, retryDelay: Long = 100, transactionLogic: () -> T): T {
var lastException: Exception? = null
repeat(maxRetries) { attempt ->
try {
return transactionTemplate.execute {
transactionLogic()
}!!
} catch (e: Exception) {
lastException = e
Thread.sleep(retryDelay * (attempt + 1))
}
}
throw lastException ?: RuntimeException("Transaction failed after $maxRetries attempts")
}
// 성능 임계값 초과 시 경고
fun warnIfPerformanceDegraded(thresholdMillis: Long) {
performanceMonitor.getPerformanceReport().forEach { (method, metric) ->
val averageExecutionTimeMs = metric.averageExecutionTime / 1_000_000
if (averageExecutionTimeMs > thresholdMillis) {
logger.warn(
"Performance degradation detected in method: $method. " +
"Average execution time: $averageExecutionTimeMs ms"
)
}
}
}
}
이 코드는 조건부 트랜잭션 실행과 재시도 기능을 구현하여 성능을 개선하고, 성능 저하를 감지하여 경고를 발생시킵니다.
외부 API 호출은 트랜잭션 내에서 실행하지 않는 것이 좋습니다. 이로 인해 DB 연결을 지연시키거나 과도하게 차단할 수 있기 때문입니다.
외부 API 호출은 별도의 비동기 방식이나 배치 방식으로 처리하는 것이 성능에 유리합니다.
클래스 레벨에서 트랜잭션을 설정하는 것보다, 각 메서드마다 트랜잭션을 세밀하게 설정하는 것이 좋습니다.
클래스 레벨에서 적용된 @Transactional은 모든 메서드에 일괄 적용되어 예기치 않은 성능 저하를 일으킬 수 있습니다.
읽기 전용 트랜잭션의 경우 PROPAGATION_REQUIRES_NEW와 같은 세부 설정을 통해 불필요한 DB 업데이트를 막고 성능을 개선할 수 있습니다.
트랜잭션에서 성능 저하를 일으킬 수 있는 원인은 주로 불필요한 set_option 쿼리, 과도한 autocommit 설정, 과도한 트랜잭션 전파 등입니다.
이 문제를 해결하기 위한 최적화 전략을 고려해야 합니다.
최적화 전 코드
@Transactional(readOnly = true)
fun findOrderDetails(orderId: Long): OrderDetails {
return orderRepository.findById(orderId)
.orElseThrow { EntityNotFoundException("Order not found") }
}
최적화 후 코드
fun findOrderDetailsOptimized(orderId: Long): OrderDetails {
return enhancedTransactionManager.executeConditionalTransaction(
condition = { orderId > 0 },
transactionLogic = {
orderRepository.findById(orderId)
.orElseThrow { EntityNotFoundException("Order not found") }
}
)
}
위에서 설명한 최적화 전략과 실제 코드 사례를 바탕으로 불필요한 트랜잭션을 제거하고, 트랜잭션 전파 전략을 세밀하게 조정함으로써 성능을 크게 개선하는것을 배운 계기가 되었습니다.
또한, 성능 모니터링 시스템을 통해 실시간으로 성능을 추적하고, 성능 저하를 미리 감지하여 경고를 받을 수 있도록 구현할 수 있었습니다.
이러한 최적화 작업을 통해 DB 쿼리 수 감소, 네트워크 오버헤드 최소화, 동적 트랜잭션 관리 등의 이점을 얻을 수 있었습니다.