thumbnail

Spring 내부함수호출을 @Transactional 안먹... (Dont Work)

우선 스프링은 클래스내에서, 클래스내의 함수를 호출할때는 @Transactional 적용되지 않습니다.

https://stackoverflow.com/questions/3423972/spring-transaction-method-call-by-the-method-within-the-same-class-does-not-wo

@Service
public class BlablaService{

    public void trxTest(){
        trx(); // 트랜잭션이 적용되지 않는다.... ㅜㅜ
    }

    @Transactional
    public void trx(){
        // DB CRUD... Logic
    }
}

이유는 Spring Aop가 Proxy 기반으로 동작하기 때문에 가지는 한계점인데요.

설명 : https://www.podo-dev.com/blogs/133?search=aop

<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을 사용한다면, 실제 값이 Anyupcasting 되어야 할것입니다 ㅜㅜ..
그렇다고 반환값이 있을때마다 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()
    }
CommentCount 0
이전 댓글 보기
등록
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
TOP