프로젝트/사이드 프로젝트(2024.09.01-2024.10.04)

[사이드 프로젝트 : DevToolKit] 왜 트랜젝션 Requires_new가 안되는가?

dal_been 2024. 9. 20. 18:55
728x90

유저관련된 로직을 하다가 맞다은 에러??

 


비밀번호 5회틀릴시 로그인 막기

 

현재 유저가 로그인할때 5회 틀릴시 로그인을 막고 있다.

    fun login(request: UserLoginRequest): UserLoginResponse {
        val user = userRepository.findByEmail(request.email) ?: throw RestException.notFound(ErrorMessage.NOT_FOUND_USER.message)
        if (!user.isEnabled || !user.isVerified) {
            throw RestException
                .badRequest(ErrorMessage.IMPOSSIBLE_LOGIN.message)
        } else if (!passwordEncoder.matches(request.password, user.password)) {
            user.failCount = ++user.failCount
            throw RestException
                .badRequest(ErrorMessage.NOT_MATCH_PASSWORD.message)
        }

        return user
            .apply { failCount = 0 }
            .toUserLoginResponse(authService.create(user.email))
    }

 

근데 문제가 비밀번호 틀릴시 예외가 반환되면서 롤백이 되는것이다...
엥?? 이 아니라 트랜젝션이 당연히 예외를 반환하니 롤백이 될 수 밖에 없다.

 

그래서 트랜젝션을 분리해보자 라고 생각했다.

    @Transactional
    fun login(request: UserLoginRequest): UserLoginResponse {
        val user = userRepository.findByEmail(request.email) ?: throw RestException.notFound(ErrorMessage.NOT_FOUND_USER.message)
        if (!user.isEnabled || !user.isVerified) {
            throw RestException
                .badRequest(ErrorMessage.IMPOSSIBLE_LOGIN.message)
        } else if (!passwordEncoder.matches(request.password, user.password)) {
            user.failCount = ++user.failCount
            throw RestException
                .badRequest(ErrorMessage.NOT_MATCH_PASSWORD.message)
        }

        return user
            .apply { failCount = 0 }
            .toUserLoginResponse(authService.create(user.email))
    }

	@Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateFailCount(user: User) {
    	user.failCount = ++user.failCount
    }

 

login과 updateFailCount의 트랜젝션을 별개로 취급하여 각각 커밋과 롤백이 이루어지게 하자라고 생각했다.

 

그러나... 예상과 다르게.. 트랜젝션은 분리되지 않았다. 

 

 

왜 트랜젝션이 분리되지 않았을까?

 

예상과 다르게 나오자 마자 갑자기 떠오른 단어... 프록시...

이 내용과 비슷한 에러를 맞주친 적이 있었다.

원인은 클래스가 같아서 첫번째 트랜젝션에서는 프록시를 통해 메서드가 호출되었지만 두번째 메서드는 this를 사용하게 되면서(같은 클래스 안이라) 내부 메서드를 호출하게 되어, 실제 대상 객체의 인스턴스를 가리키게 된다.

 

다시말해, 하나의 클래스 내에서 트랜젝션 전파옵션으로 새로운 트랜젝션을 생성하려 해도 메서드 내부 호출은 프록시를 거치지 않는다.

(트랜젝션의 경우 프록시를 통해 대상 객체를 호출해야한다)

 

 

그래서 해결방법..?

 

첫번째 해결 방법 클래스 분리

 

클래스를 분리하면 두번째 트랜젝션 호출시 다른 클래스가 프록시를 통해 호출되기 때문에 트랜젝션 분리가 가능하다.

그러나.. 비밀번호 카운트 up을 위해서 클래스를 생성해야할까..? 너무 코드 낭비다.

 

두번째 해결방법 트랜젝션  noRollbackFor 설정

    @Transactional(noRollbackFor = [RestException::class])
    fun login(request: UserLoginRequest): UserLoginResponse {
        val user = userRepository.findByEmail(request.email) ?: throw RestException.notFound(ErrorMessage.NOT_FOUND_USER.message)
        if (!user.isEnabled || !user.isVerified) {
            throw RestException
                .badRequest(ErrorMessage.IMPOSSIBLE_LOGIN.message)
        } else if (!passwordEncoder.matches(request.password, user.password)) {
            user.failCount = ++user.failCount
            throw RestException
                .badRequest(ErrorMessage.NOT_MATCH_PASSWORD.message)
        }

        return user
            .apply { failCount = 0 }
            .toUserLoginResponse(authService.create(user.email))
    }

 

트랜젝션에 비밀번호 틀릴시 반환하는 예외에 대해 롤백이 되지 않도록 설정하였다.

그랬더니 바로 failcount의 변경이 반영되었다.

 

 


오랜만에 만난 트랜젝션과 프록시 문제.. 다행히 이전에 조금 트랜젝션과 프록시에 대해 공부한 기억이 있어서... 빠르게 해결가능했다.