문제점을 찾아보자!
위 코드는 '한끼족보'에서 로그인을 담당하는 코드이다. 무엇이 문제일까?
트랜잭션 범위
RealMySQL에서는 트랜잭션의 범위를 최소화 하라는 조언이 있다. 특히, 외부 서버와 통신을 하는 과정은 트랜잭션 내에서 제거하는 것이 좋다고 한다. 프로그램이 실행되는 동안, 외부 서버와 통신할 수 없는 상황이 발생한다면 웹 서버 뿐만 아니라 DBMS 서버까지 위험해지는 상황이 발생할 수 있기 때문이다.
다시 돌아와서, 코드를 살펴보자. 무엇이 문제일까?
위 로직은, DB에 유저를 조회하거나 저장하는 로직과 함께 애플과 카카오 서버를 통해 사용자의 정보를 가져오는 getSocialInfo
메서드 즉, 외부 서버와 통신하는 로직이 하나의 트랜잭션 단위로 묶여져있다. 만약, 카카오 서버에 문제가 생긴다면 어떻게 될까? 외부 네트워크 통신은 시간이 오래 걸릴 수 있기 때문에 트랜잭션 타임아웃이 발생할 위험이 존재한다. 또한 트랜잭션이 활성화된 동안 DB 연결과 같은 리소스가 잠금 상태로 유지되기 때문에, 외부 통신이 오래 걸리면 리소스를 오랫동안 점유하게 되어 성능 저하가 발생할 수 있다. 때문에 이처럼 네트워크 작업이 있는 경우에는 반드시 트랜잭션에서 배제해야 한다. 어떻게 배제할 수 있을지 살펴보자.
스프링 트랜잭션의 이해
해결 방법을 찾기 전에 먼저 스프링에서 트랜잭션이 어떻게 동작하고 있는지를 알아야 한다.
선언적 트랜잭션과 AOP
위의 코드처럼 @Transactional
이라는 애노테이션을 사용하여 매우 편리하게 트랜잭션을 적용하는 것을 선언적 트랜잭션 관리라고 한다. 이 방식은 기본적으로 프록시 방식의 AOP가 적용된다.
AOP의 핵심은, 실제 객체 대신 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다는 것이다. 또한 주입을 받을 때도 실제 객체 대신에 프록시 객체가 주입된다. 즉, 선언적 트랜잭션을 사용하면 항상 프록시를 통해서 대상 객체를 호출한다는 것이다.
프록시 내부 호출
만약, 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 어떻게 될까? 2가지 상황에 대해 알아보자.
트랜잭션이 적용된 Internal()
메서드가 내부 호출로 External()
메서드를 호출하는 상황이다. Internal()
메서드에는 트랜잭션이 적용되어 있으므로 프록시 객체를 호출하게 되고, 때문에 External()
메서드에는 트랜잭션이 없더라도 Internal()
이 트랜잭션이 적용된 상태에서 실행되고 있으므로 같은 스레드에서 External()
은 실행되는 내부호출을 통해 트랜잭션에 참여하는 것이다.
그럼 이 경우는 어떨까? 트랙잭션이 적용되지 않는 External()
메서드를 호출하고 External()
메서드는 내부에서 트랜잭션이 적용된 Internal()
메서드를 호출하는 상황이다.
이 경우는 AOP가 적용되지도 않고 트랜잭션도 적용되지 않는다. 즉, Internal()
메서드에 트랜잭션이 적용되어 있어도, 트랜잭션이 적용되지 않는 External()
메서드가 Internal()
메서드를 내부 호출한 것이기 때문에 트랜잭션이 적용되지 않는 것이다.
정리하면 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
해결 방법
위의 내부 호출 문제를 어떻게 해결할 수 있을까? 가장 단순한 방법은 별도의 클래스로 분리하는 것이다.
이 흐름의 핵심은 Internal()
메서드와 External()
메서드를 다른 클래스로 분리함으로써 내부 호출을 외부 호출로 바꾼 것이다. 트랜잭션이 적용되어 있지 않은 External()
을 호출하더라도, 트랜잭션이 선언된 Internal()
메서드를 호출하기 위해 Internal()
메서드가 속한 프록시 객체를 만들게 되고, 이를 통해 호출하면서 Inernal()
메서드에 트랜잭션을 적용할 수 있는 것이다.
생각한 해결방법 🤔
다시 처음으로 돌아와서, 우리는 외부 네트워크 통신하는 로직을 트랜잭션 단위에서 제거할 필요성을 느꼈다.
첫번째 해결방법
현재 흐름
현재는 트랜잭션이 적용되어 있는 Login()
메서드가 같은 클래스에 위치하는 getSocialInfo()
메서드를 내부 호출하는 형태이다. 때문에 외부 네트워크와 통신하는 getSocialInfo()
메서드는 Login()
의 트랜잭션에 참여하는 흐름인 것이다. 이를 해결하기 위해 객체를 분리해보자.
수정된 흐름
이렇게 수정한다면, 외부 네트워크와 통신하는 getSocialInfo()
메서드에 트랜잭션을 걸지 않으면서도 동시에 외부 호출의 대상인 Login()
메서드에는 트랜잭션을 적용할 수 있게 된다.
두번째 해결방법
두번째 해결방법은 트랜잭션 전파 옵션을 활용하는 것이다. 트랜잭션 전파 옵션 중에는 NOT_SUPPORT
라는 옵션이 존재한다. 말 그대로, 트랜잭션을 지원하지 않는다는 의미이며, 기존 트랜잭션이 존재해도 트랜잭션 없이 로직을 수행한다. 위의 코드에서는 외부 네트워크와 통신하는 getSocialInfo
메서드에 @Transactional(propagation = Propagation.NOT_SUPPORTED)
와 같이 전파 옵션을 설정해주면, 기존 트랜잭션에 참여하지 않고 통신하게 된다.
결론
고민 결과 첫번째 방법으로 해결하기로 결심했다. 두번째 해결방법 역시 트랜잭션 전파 옵션을 사용하기 위해서는 내부 호출이 발생하면 안되기 때문에 클래스를 분리해야 한다. 또한 '트랜잭션이 적용되지 않는다' 와 '트랜잭션 없이 동작한다'는 다른 의미라고 한다. 즉, 이미 login()
메서드에서 트랜잭션이 시작된 상태에서 (실제 DB와의 물리 트랜잭션(Connection)은 걸려 있는 상태) getSocialInfo()
에 선언된 전파 옵션(Propagation 등)은 단지 새로운 경계를 만들어낼 수 없다는 의미이며, 때문에 NOT_SUPPORT
옵션을 사용해도 물리 DB 레벨에서는 이미 login()
이 시작한 같은 트랜잭션으로 묶여서 동작하게 되는 셈이 되기 때문이다.
관련 질문 링크 : 인프런 트랜잭션 관련 질문
'대외 활동 > SOPT' 카테고리의 다른 글
로그인 코드 리팩토링 대장정 (0) | 2025.03.19 |
---|---|
중복 추가 이슈를 해결하기 위한 고민 (0) | 2025.03.19 |
동시성 제어 (0) | 2025.03.19 |
애플 로그인 (0) | 2025.03.18 |
[아티클] SSH config를 통해 간편하게 SSH 연결하기 (0) | 2025.03.17 |