개발자로 후회없는 삶 살기

[문법] 스프링 트랜잭션 이해와 적용 본문

[백엔드]/[spring | 학습기록]

[문법] 스프링 트랜잭션 이해와 적용

몽이장쥰 2024. 8. 6. 22:32

서론

※ 과거에 기록한 내용에서 중요한 부분만 발췌하여 모두가 이해하기 쉽게 다시 서술한다.

 

본론

- DB 연결 구조와 세션

10개의 커낵션 = 10개의 세션

DB에 접근하면 커낵션이 생기고 세션이 열리며, 하나의 세션에서 트랜잭션을 시작해서 SQL을 수행한다.

 

-> 정리

커낵션을 연결하면 db 내부에 세션이 생기고 세션을 통해 트랜잭션을 시작하고 sql을 실행한다. 트랜잭션을 시작할 때 락을 획득하고 끝나면 락을 반납한다.

 

- 실제 로직에 트랜잭션 적용

계좌 이체가 안 되는 비즈니스 상황에 트랜잭션을 적용해보자 입금을 성공하는데 출금에서 실패하는 경우이다.

 

when에서 예외가 발생해서 출금은 성공하는데 입금에서 실패한다.

 

테스트에서 DB를 사용하면 pk 자동 증가 때문에 막힌다. 테스트에서 트랜잭션을 사용하면 테스트 이후 데이터를 롤백해서 테스트를 편리하게 할 수 있다.

 

- 트랜잭션 적용

지금 상황은 A의 돈만 감소한다. B에 입금이 되지 않는다.

 

트랜잭션을 사용해서 로직을 원자적으로 해결해보자.

 

- 비즈니스 로직과 트랜잭션

트랜잭션은 서비스에 적용해야 한다. 비즈니스 로직 모두가 성공하면 커밋하고 실패하면 모두 롤백하기 위해 서비스에 트랜잭션이 있어야 한다. 따라서 서비스 로직이 원자적으로 커밋되거나 롤백되어야 한다. 트랜잭션을 건다는 것은 로직이 시작할 때 auto commit false를 하고 끝날 때 커밋, 롤백하는 것이다.

 

트랜잭션을 하려면 커넥션과 세션이 있어야한다. auto commit이 sql이라서 db에 직접 명령을 날려야 한다. 또한, 하나의 트랜잭션에서 동작하기 위해선 하나의 동일 커낵션이여야 하는데, 만약 다른 커낵션이라면 다른 사용자, 다른 트랜잭션이다. 기존 코드는 모든 레포지토리 메서드에서 커낵션을 새로 생성하고 닫기에 모두 다른 커낵션을 사용한다.

 

→ 파라미터 방법

현재 레포지토리는 메서드를 호출할 때마다, 새로운 커낵션을 풀에서 꺼내고 메서드 끝나면 close한다. 어플리케이션에서 같은 커낵션을 유지하려면 가장 단순한 방법이 파라미터로 전달해서 같은 커낵션이 사용되도록 하는 것이다.

 

계좌 이체 서비스에서 조회와 수정을 할 때 파라미터로 커낵션을 넣고 getConn 메서드를 지운다.

 

새로 생성하는 코드를 제거한다. 서비스 계층에서 트랜잭션이 끝날 때 이 커낵션을 종료해야 한다.

 

→ 서비스에 트랜잭션 적용

서비스에 트랜잭션을 적용하기 위해 커낵션 연결에 필요한 데이터 소스를 서비스에서 주입한다. 커낵션을 만드는 코드를 서비스에 작성하고

 

서비스에서 auto commit으로 트랜잭션을 시작하고, 레포지토리 파라미터로 커낵션을 공유한다. 성공하면 커밋, 예외가 발생하면 롤백한다. 커낵션 반납도 서비스에서 하는데 이때 커낵션 풀에서 재사용하기 위해 auto commit을 true로 하고 반납한다.

 

마지막으로 비즈니스 로직과 트래잭션 로직을 분리하면, 모두 같은 커낵션으로 동일 트랜잭션을 수행한다.

 

실행해보면 예외 발생으로 롤백되어 1만원을 유지한다.

 

- 문제점들

1. 어플리케이션 구조

어플리케이션 3계층에서 서비스가 가장 중요하며, 서비스에는 순수한 자바 로직만 있어야 한다. 시간이 흘러 UI가 바뀌고 DB가 바뀌어도 비즈니스 로직은 그대로 유지되어야 한다. 그렇게 하기위해 서비스 계층을 특정 기술에 종속적이지 않게 개발해야 한다. 그래서 서비스는 순수한 자바 코드로만 작성한다.

 

→ 지금 작성한 코드의 문제점

현재 트랜잭션, jdbc 관련 코드가 있는데, 만약 jdbc를 jpa로 변경하면 서비스 코드를 전부 변경해야 한다. 서비스가 수십개면 수십개를 전부 변경해야 한다.

 

2. 문제점 정리

위 코드의 문제점을 알아보자

 

1) 트랜잭션 문제

jdbc 기술이 서비스 계층에 너무 많은 문제가 있다. 또한 커낵션을 파라미터로 계속 넣어 유지해야하고, try-catch 문도 너무 많다.

 

2) 예외 누수

레포지토리의 jdbc 예외가 서비스까지 날라와서 서비스에서도 처리해야한다.

 

- 트랜잭션 추상화

jdbc에서는 수동 커밋을 해야하고, jpa는 begin으로 하는데, 그럼 jpa로 변경했을 때 너무 많은 코드 변경이 일어난다.

 

이를 해결하기 위해 트랜잭션을 추상화하면 된다. 인터페이스에 트랜잭션 시작, 커밋, 롤백 관리 코드를 넣고 구현체에 구현해서 DI로 끼워 넣으면 된다. 서비스 코드는 매니저에만 의존하게 짜면 특정 기술에 의존하지 않게 된다.

 

- 스프링에 적용

스프링에서는 이미 추상화된 트랜잭션을 제공한다. 플랫폼 트랜잭션 매니저 인터페이스가 있고 여기에 시작, 커밋, 롤백을 구현하고 서비스는 인터페이스에만 의존하게 코드를 짜면 된다.

 

- 트랜잭션 동기화

트랜잭션을 추상화해서 코드 변경은 해결했지만, 커낵션을 파라미터로 넣어주는 건 해결하지 못 했다. 트랜잭션을 유지하기 위해 같은 커낵션을 유지하는 것을 동기화라고 한다.

 

→ 동기화 매니저

트랜잭션 매니저가 동기화 매니저를 내부에서 사용한다. 동기화 매니저는 멀티 쓰레드 상황에서 안전하게 커낵션 동기화를 할 수 있고, 파라미터로 커낵션을 전달하지 않아도 트랜잭션을 유지할 수 있다.

 

1) 트랜잭션 매니저가 데이터 소스로 커낵션을 획득하고 트랜잭션 시작
2) 생성한 커낵션을 동기화 매니저에 보관
3) 레포지토리가 DB에 접근할 때 보관된 커낵션 사용 (파라미터 전달 X)
4) 커낵션 종료 및 트랜잭션 종료

 

동기화 매니저는 커낵션을 쓰레드 로컬에 저장하여, 멀티 쓰레드 환경에서 안전하게 사용할 수 있다.

 

1) 열기 : 동기화된 커낵션이 없으면 커낵션 새로 생성, 있으면 기존 커낵션 사용
2) 닫기 : 트랜잭션 동기화가 된 커낵션이라면 동기화 매니저에 보관하고 아니라면 풀에 반납

 

→ 적용 후 변경 코드

서비스에서는 매니저에 의존해서 코드를 작성하고, 커낵션 관련 코드는 전부 사라진다. 릴리즈도 매니저가 알아서 처리해줘서 사라진다.

 

→ 정리

추상화를 해서 서비스는 매니저 인터페이스에 의존한다. 이제 DI만 바꾸고 서비스는 변경하지 않아도 된다. 동기화 매니저 덕분에 파라미터로 커낵션을 넘기지 않아도 된다.

 

- 트랜잭션 AOP

하지만, 아직 매니저 관련 코드가 서비스에 남아있다. 서비스에는 비즈니스 순수 로직만 있어야 한다. AOP를 통해 프록시를 도입하면 문제를 해결할 수 있다.

 

이를 트랜잭션 AOP로 해결한다. 트랜잭션을 프록시 객체 코드에 작성하고 프록시에서 타겟 로직을 호출한다. 타겟 로직이 끝나면 프록시에서 트랜잭션을 종료한다.

 

트랜잭션 AOP 적용 후 실제 서비스 로직은 순수 자바 코드만 남는다.

 

프록시 객체에 트랜잭션 관리 코드가 들어있고, 실제 서비스 메서드를 호출한다. 프록시 객체를 빈으로 등록해 사용한다.

 

스프링 AOP가 어노테이션을 감지해서 프록시 객체를 만들어준다.

 

서비스는 CGLIB 객체가 등록된다.

 

- 스프링 부트의 자동 리소스 등록

스프링이 데이터 소스와 트랜잭션 매니저를 자동으로 등록해준다. 프로퍼티스에 데이터 소스 연결 정보를 명시하고, jpa를 사용하면 jpa 매니저가 등록된다. jpa 매니저가 jdbc 매니저 기능도 수행한다.

Comments