
코프링에서 내부함수호출을 @Transactional 해보자
Spring 내부함수호출을 @Transactional 안먹... (Dont Work)
우선 스프링은 클래스내에서, 클래스내의 함수를 호출할때는 @Transactional
적용되지 않습니다.
@Service
public class BlablaService{
public void trxTest(){
trx(); // 트랜잭션이 적용되지 않는다.... ㅜㅜ
}
@Transactional
public void trx(){
// DB CRUD... Logic
}
}
이유는 Spring Aop가 Proxy 기반으로 동작하기 때문에 가지는 한계점인데요.
<br/>
그러면 이런 상황이 필요할 때 극복하는 방법이 있어야합니다.
흔히 사용되는것이 클래스를 분리하여 함수 내부 호출아닌 다른 클래스(bean) 함수
를 호출하도록 하는것입니다.
@Service
public class BlablaService{
private BlablaServiceTrx blablaServiceTrx;
public void trxTest(){
blablaServiceTrx.trx();// 트랜잭션 적용!!
}
}
@Service
public class BlablaServiceTrx{
@Transaction
public void trx(){
// DB CRUD... Logic
}
}
하지만 이런 상황에서 필요할때마다 클래스를 분리하여, 서비스 계층을 늘려간다면,
레이어 복잡도가 불필요하게 늘어나기도 합니다.
그리고..가장 중요한것은.... 귀찮습니다..
<br/>
코프링으로 극복..!
코프링에서는 이런 상황을 극복할수있는 방법을 하나 소개해보려 합니다.
바로 Kotlin Aspect
를 이용하는 방법입니다
<br/>
빠른 설명을 통해 코드를 보면 다음과 같습니다.
모든 유저를 조회하는 함수를가지는 클래스가 있습니다.
이 함수는 외부 API 호출을 필요로 합니다. 또한 트랜잭션을 필요로 합니다.
그렇다면 다음과 같이 코드가 작성됩니다.
@Service
class UserReadService(
val userRepository : UserRepository,
val apiCallCient : ApiCallClient,
){
@Tranactional
fun readAll() : List<User>}
apiCallCient.reqGet("blabla") // 외부 API 호출 10초 걸림..
return userRepository.findAll()
}
}
문제는 (외부 API호출)의 응답이 느려지게되면, 트랜잭션을 불필요하게 장시간 유지하게 됩니다.
<br/>
로그를 확인하면 트랜잭션은 최소 10초이후 종료된것을 확인 할 수 있습니다.
<img src="https://static.podo-dev.com/blogs/images/2022/10/06/origin/da978f85-94de-425d-9136-9fd721cc9863.png" alt="스크린샷 2022-10-06 오후 8.38.20.png" style="width:1000px;">
따라서 (DB Read) 부분만 트랜잭션을 적용하는것이 필요했습니다.
<br/>
그리고 Kotlin Aspect
를 사용해보려고합니다
<br/>
우선 TxAop
라는 @Service
를 정의했습니다.
@Service
class TxAop {
@Transactional
fun <T> tx(function: () -> T): T {
return function.invoke()
}
}
이 클래스의 tx 함수
는 함수를 인자로 받아, 인자로 받은 함수를 호출하기만 합니다.
그리고 @Transactional
어노테이션이 명시되어 있습니다.
<br/>
이제 우리는 트랜잭션을 필요로 할때, 이 함수를 사용할 것입니다.
@Service
class UserReadService(
val txAop: TxAop,
val userRepository : UserRepository,
val apiCallClient : ApiCallClient,
){
fun readAll(): List<User> {
apiCallClient.reqGet("blabla") // 외부 API 호출 10초 걸림
return txAop.tx { userRepository.findAll() } // Kotlin Aspect!!
}
}
<br/>
그리고 로그를 확인하게 되면, 외부함수 호출후 tx가 실행되고, tx가 종료되는것을 알 수 있습니다. (야호!)
<img src="https://static.podo-dev.com/blogs/images/2022/10/06/origin/8d3455d8-cc31-4cc2-8dc9-0c3b226ff4c7.png" alt="스크린샷 2022-10-06 오후 8.46.49.png" style="width:1000px;">
하나의 함수 내에서, 트랜잭션이 필요한 범위를 정의한 것 입니다.
이제 클래스의 분리 없이, 간단한 방식으로 함수의 호출안에서 트랜잭션이 필요한 로직을 별도로 분리 할 수있습니다.
<br/>
tx함수
에서 반환해야하는 값이 두개 이상인 경우를 생각해봅니다..
위의 구현은 문제가 될수 있는 요소가 있습니다.
tx함수
에서 반환해야하는 값이 두개 이상인 경우입니다.
@Service
class UserReadService(
val txAop: TxAop,
val userRepository: UserRepository,
val boardRepository: BoardRepository,
val apiCallClient: ApiCallClient,
) {
fun readAll(): List<User> {
// 외부 API 호출
apiCallClient.reqGet("blabla")
///
val tx: Map<String, Any> = txAop.tx { // What the Map..??
val users = userRepository.findAll()
val boards = boardRepository.findAll()
return@tx mapOf("users" to users, "boards" to boards) // Map을 사용하자..?
}
}
}
그럼 이상황에서 Map
을 사용할 수도 있습니다..
하지만 Map
을 사용한다면, 실제 값이 Any
로 upcasting
되어야 할것입니다 ㅜㅜ..
그렇다고 반환값이 있을때마다 dto
를 정의할 순 없습니다.
<br/>
이런 문제로 Kotlin의 중위함수(Infix function)
를 사용하는 것을 소개합니다
다음 코드를 보면 and
라는 키워드를 통해서 Pair
로 반환하게됩니다.
그리고 함수의 반환값을 구조분해를 통해서 타입을 유지한체 변수로 가져올 수있습니다.
fun readAll(): List<User> {
// ..
val (users, boards) = txAop.tx { //
val users = userRepository.findAll()
val boards = boardRepository.findAll()
return@tx users and boards
}
}
<br/>
이런 방식은 infix function
을 다음과 같이 정의했기 때문에 가능할 수 있습니다.
// DynamincValue.kt
infix fun <A, B> A.and(value: B) = Pair(this, value)
infix fun <A, B, C> Pair<A, B>.and(value: C) = Triple(this.first, this.second, value)
infix fun <A, B, C, D> Triple<A, B, C>.and(value: D) = Fourth(this.first, this.second, this.third, value)
infix fun <A, B, C, D, E> Fourth<A, B, C, D>.and(value: E) = Fifth(this.first, this.second, this.third, this.fourth, value)
data class Fourth<out A, out B, out C, out D>(
val first: A,
val second: B,
val third: C,
val fourth: D,
)
data class Fifth<out A, out B, out C, out D, out E>(
val first: A,
val second: B,
val third: C,
val fourth: D,
val fifth: E,
)
<br/>
마지막으로 좀만 더 많은 예제를 소개합니다.
위의 구현을 좀 더 많은 예제를 보면 다음과 같습니다
3개의 반환값을 받기
fun readAll(): List<User> {
//..
val (users, boards, comments) = txAop.tx {
val users = userRepository.findAll()
val boards = boardRepository.findAll()
val comments = commentRepository.findAll()
return@tx users and boards and comments
}
}
<br/>
한 함수내에서 트랜잭션 3번 사용하기
fun readAll(): List<User> {
//..
val users = txAop.tx { userRepository.findAll()}
val board = txAop.tx { boardRepository.findAll()}
val comments = txAop.tx { commentRepository.findAll()}
}
<br/>
private 함수에 트랜잭션 사용하기
fun readAll(): List<User> {
return readAllByTx()
}
private fun readAllByTx() = txAop.tx {
return@tx userRepository.findAll()
}