티스토리 뷰
개요
- 비지니스 로직을 아무리 명확하게 짜고 유닛 테스트를 촘촘하게 작성해도 실제 프로덕션 레벨에서는 유입량에 따라 예상하지 못한 수많은 데드락을 경험할 수 있다. 이번 글에서는 데드락을 최소화하는데 도움이 되는
READ-COMMITTED
트랜잭션 격리 레벨과@Retryable
사용법을 설명하고자 한다.
앞서 읽어보면 좋은 글
데드락 최소화 전략
- 트랜잭션 범위를 최대한 작게 조정한다. (공유 잠금 시간 최소화) 특히, @Controller 레벨에 명시된 @Transactional은 데드락을 유발할 가능성이 매우 크므로 제거한다.
- 모든 쿼리의 트랜잭션 격리 레벨을 READ-COMMITTED로 실행한다. (공유 잠금 시간 최소화)
- 데드락이 발생할 경우 @Retryable을 이용하여 지정된 시간 범위 내에서 랜덤한 시간 후에 재시도하여 실행한다. (불가피하게 일시적으로 발생하는 데드락에 대응)
build.gradle.kts
- 프로젝트의 루트의 build.gradle.kts에 아래 내용을 추가한다.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-aop:2.6.3")
implementation("org.springframework.retry:spring-retry:1.3.1")
}
@EnableRetry
- @Configuration 빈을 작성하고 클래스 레벨에
@EnableRetry
을 명시해야@Retryable
작동이 활성화된다. - 아래 예제는 @EnableRetry을 명시하지 않고 직접 커스텀 빈을 작성했는데 이유는 @Transactional과 @Retryable이 동시에 명시된 메써드에서의 서로 간의 작동 순서에 대한 고려 때문이다. 어떤 경우에도 @Retryable이 먼저 작동할 수 있도록 아래와 같이 우선순위를 최우선으로 명시했다.
import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered
import org.springframework.retry.annotation.RetryConfiguration
@Configuration
class RetryConfig : RetryConfiguration() {
override fun getOrder(): Int {
return Ordered.HIGHEST_PRECEDENCE
}
}
@Retryable
@Retryable
은 스프링 빈의 모든 클래스, 메써드 레벨에 명시될 수 있다. @Retryable의 쓰임이 가능 많은 부분이 바로 데드락에 대한 자동 재시도 처리일 것이다. 데드락에 대비하여 @Repository에서는 아래와 같이 작성할 수 있다. (꼭 @Repository에만 적용할 필요는 없다. @Repository을 호출하는 상위 클래스에서 필요에 따라 작성하면 된다.)
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.retry.annotation.Backoff
import org.springframework.retry.annotation.Retryable
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Transactional
@Repository
// 트랜잭션 격리 레벨을 READ-COMMITTED로 완화하여 공유 잠금을 최소화
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED)
// 예외 발생시 1~5초 사이의 랜덤한 간격으로 최대 5회까지 자동 재시도 활성화
@Retryable(
maxAttempts = 5,
backoff = Backoff(random = true, delay = 1000, maxDelay = 5000),
// 데이터 무결성 위반으로 인한 예외는 재시도 대상에서 제외
exclude = [DataIntegrityViolationException::class]
)
interface FooRepository : JpaRepository<Foo, Long>, QuerydslPredicateExecutor {
// write에는 기본값인 readOnly = false를 설정
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
fun deleteById(id: Long) { ... }
}
@Repository와 @Transactional
- Spring Data JPA의 모든 @Repository는 기본적으로 클래스 레벨에
@Transactional(readOnly = true)
가 명시되어 있고, 모든 delete, save, flush 메써드에는@Transactional(readOnly = false)
가 명시되어 있다. [관련 링크] 즉, 호출하는 상위 레벨에서 어떠한 @Transactional도 명시하지 않았더라도 각 쿼리의 최소 단위로서의 트랜잭션이 실행되는 것이다. - 앞서 소개한 코드에서는 이런 특징을 이해하고, 클래스 레벨에는
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED)
,@Transactional(isolation = Isolation.READ_COMMITTED)
을 명시하여 어떠한 상황에서도 READ_COMMITTED 트랜잭션 격리 레벨이 작동하도록 명시했다. - 각 메써드에 @Transactional이 선언되면 상위 레벨에 선언된 트랜잭션을 무시하고 개별 트랜잭션이 생성되는 것 아닌가하는 우려를 할 수 있는데 어노테이션 내부를 살펴보면 기본값으로 Propagation.REQUIRED 전파 레벨을 가지므로, 자연스럽게 메써드를 호출한 측의 상위 트랜잭션을 이어 받아 하나의 트랜잭션으로 작동하게 된다.
참고 글
댓글
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- chrome
- Tomcat
- kotlin
- spring
- bootstrap
- DynamoDB
- 자전거
- graylog
- JHipster
- 로드 바이크
- Kendo UI Web Grid
- jstl
- 구동계
- Docker
- MySQL
- Kendo UI
- 평속
- Eclipse
- Spring Boot
- java
- maven
- Spring MVC 3
- CentOS
- 태그를 입력해 주세요.
- 알뜰폰
- jpa
- JavaScript
- node.js
- 로드바이크
- jsp
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
글 보관함