장미정원
✨ 스프링에서 트랜잭션을 처리하는 방법 본문
트랜잭션
데이터베이스에서의 트랜잭션이란 데이터베이스에서 데이터의 상태를 변경하기 위해 수행하는 작업들의 논리적인 최소한의 단위입니다. 쉽게 말해 하나의 작업에 대해 한꺼번에 실행되어야 하는 일련의 작업들의 모음이라고 생각할 수 있습니다.
트랜잭션을 쉽게 설명하기 위한 좋은 예가 하나 있는데요, 바로 계좌이체입니다. 계좌이체라는 작업은 돈을 송금한 측의 계좌에서 돈을 차감, 송금 받을 측의 계좌에 돈을 추가해야합니다. 만약 계좌이체라는 작업 중에 송금 하는 측의 돈을 차감 후 송금 받는 측의 돈을 추가하려 할 때 오류가 발생해서 작업이 중지되었다면... 송금 하는 측의 돈만 차감되는 심각한 장애로 이어지게 됩니다.
이렇듯 계좌이체라는 작업은 돈의 차감, 추가라는 일련의 작업들이 마치 하나의 작업처럼 이루어져야 합니다. 계좌이체가 성공하여서 차감, 추가가 모두 이루어지든, 계좌이체에 실페하여 양 측 계좌에 아무런 변경사항이 일어나지 않아야지 돈이 차감만 되거나 돈이 추가만 되는 현상이 일어나면 안됩니다!
이렇게 데이터베이스에서 어떤 작업을 위해 수행해야하는 일련의 작업들의 최소한의 단위를 트랜잭션으로 묶어서 작업에 대한 원자성, 데이터베이스의 일관성을 보장해줍니다.
스프링에서 트랜잭션 적용
스프링에서 트랜잭션을 적용하려면 비즈니스 로직이 있는 서비스 계층에서 이루어져야 합니다. 그리고 하나의 작업에 대해 트랜잭션을 적용하려면 트랜잭션을 진행하는 동안 하나의 커넥션을 유지해야 합니다.
트랜잭션 관련 로직을 작성하기 위해서는 비즈니스 로직에서 예외가 발생한다면 롤백을 진행해야하고 작업이 완료되었다면 커밋하여 데이터베이스에 변경사항을 반영해야합니다.
위 코드는 비즈니스 로직에 트랜잭션을 적용한 예제입니다. 먼저 커넥션을 생성한 후 오토커밋모드를 끕니다. (이를 트랜잭션 시작이라고 말합니다.) 그 후 비즈니스 로직을 수행합니다. 비즈니스 로직이 끝났다면 커밋을 수행해 변경사항을 데이터베이스에 반영합니다. 만약 try 문에서 예외가 잡혔을 때 catch 문으로 롤백을 진행합니다. finally 문으로 커넥션을 원래 상태로 돌려 놓은 후 커넥션을 커넥션 풀에 반환합니다.
이렇게 비즈니스 로직에 트랜잭션을 적용할 수 있습니다. 하지만 이렇게 직접 트랜잭션 기능을 적용한다면.. 모든 서비스 로직에 트랜잭션 관련 코드를 추가해야하고 하나의 트랜잭션 작업에 대해 커넥션을 유지하도록 기존 코드를 변경해야할 수 있습니다.
스프링의 트랜잭션 추상화 & 동기화
현재 위의 예시에서 살펴볼 수 있는 문제점이 몇가지 있었습니다. 크게 서비스 로직에 JDBC 구현 기술이 의존되는 현상, 트랜잭션의 동기화 문제, 트랜잭션 적용시 반복되는 코드 문제 등이 있습니다. 하지만 스프링에서는 트랜잭션 추상화 기술을 사용해 위 문제들을 모두 해결해줍니다.
트랜잭션 추상화
위의 예시 코드는 JDBC 기술을 사용해서 트랜잭션을 적용하고 있습니다. 하지만 JDBC가 아닌 다른 데이터베이스 접근 기술을 사용한다면 트랜잭션 관련 코드도 모두 변경해야 하기 때문에 레포지토리 계층 뿐만 아니라 서비스 계층의 코드도 모두 변경해야합니다.
그렇기 때문에 스프링에서는 트랜잭션 기능을 트랜잭션 매니저로 추상화해서 제공합니다. 트랜잭션의 필수 기능인 트랜잭션 시작, 커밋, 롤백의 메서드를 가진 인터페이스를 제공하고 그에 따른 구현체도 대부분 제공합니다.
트랜잭션 동기화
트랜잭션을 유지하려면 같은 커넥션을 유지해야합니다. 위의 예제에서는 동일한 커넥션을 사용하기 위해 비즈니스 로직 시작시 커넥션을 생성한 후 파라미터에 커넥션을 넘겨서 유지했습니다. 하지만 이렇게 사용한다면 기존 코드를 변경해야하고 코드가 복잡해진다는 단점이 있습니다.
스프링에서는 트랜잭션 동기화 메니저를 제공하여 동일한 트랜잭션에 대한 커넥션 유지를 도와줍니다. 이는 스레드 로컬을 사용해서 구현합니다. 트랜잭션 동기화 메너저는 커넥션을 생성하여 트랜잭션을 시작하고 커넥션을 보관합니다. 그 후 커넥션이 필요할 때 커넥션을 꺼내 사용합니다.
DataSourceUtils 클래스의 getConnection 메서드로 트랜잭션 동기화 매니저가 보관하고 있는 커넥션을 가져올 수도 있고 (보관 중인 커넥션이 없다면 생성합니다.) releaseConnection 메서드로 사용한 커넥션을 트랜잭션 동가화 매니저에 보관합니다.
스프링에서 제공하는 트랜잭션의 추상화, 동기화 기능들을 적용하면 아래와 같습니다.
비즈니스 로직에서는 특정 데이터접근 기술에 종속되지 않은 트랜잭션 매니저를 주입받아 커넥션을 생성해서 트랜잭션을 시작합니다. 생성된 커넥션은 트랜잭션을 진행하는 동안 유지되어야 하기 때문에 트랜잭션 동기화 매니저에 보관합니다. 그 후 커넥션이 필요할 때마다 트랜잭션 동기화 매니저에서 보관 중인 커넥션을 꺼내서 사용합니다. 트랜잭션이 종료될 때 커넥션을 종료하고 전체 리소스를 정리합니다.
이쯤 되면 스프링에서 지원해주는 트랜잭션 관련 기능들로 즐겁고 편하게 개발할 수 있을것 같지만, 하나의 문제점이 더 남아있습니다.
실제 비즈니스 로직은 한 줄인데 트랜잭션을 적용하기 위한 코드가 너무 많습니다. 그리고 트랜잭션을 적용하려면 모든 비즈니스 로직에 이런 코드를 계속 작성해야한다는 문제가 있습니다.
선언적 트랜잭션 관리 - AOP
스프링에서는 이런 문제를 해결하기 위해 템플릿 메서드 패턴을 제공하지만 그래도 순수한 비즈니스 로직을 작성할 수는 없습니다. 하지만 스프링은 AOP 기술을 활용한 프록시 패턴으로 트랜잭션이 적용된 순수한 비즈니스 로직을 작성할 수 있도록 도와줍니다. AOP 기술을 활용하여 반복적인 트랜잭션 관련 코드들을 부과기능으로 빼서 핵심기능인 비즈니스 로직을 순수한 상태로 유지해줍니다.
위 그림처럼 트랜잭션 처리 로직을 프록시 객체가 가져가고 트랜잭션을 시작한 후 실제 비즈니스 로직을 호출하는 방식으로 동작합니다.
이렇게 @Transactional 어노테이션만 달아주면 해당 메서드에 트랜잭션이 적용됩니다.
이렇게 어노테이션 선언만으로 트랜잭션을 적용시켜주는 방식을 선언적 트랜잭션 관리라고 합니다. 이 방법은 매우 편리하게 트랜잭션을 관리할 수 있기 때문에 보편적으로 사용하는 방식입니다.
ref
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1
'Back-end' 카테고리의 다른 글
[DB] 파티셔닝, 샤딩, 레플리케이션 (0) | 2024.12.16 |
---|---|
✨ MSA 환경에서 분산 트랜잭션 (2PC, SAGA) (1) | 2024.12.02 |
✨ JPA 이해하기 (0) | 2024.09.02 |
✨ MSA와 클라우드 인프라 (0) | 2024.08.26 |
[DB] 인덱스 진짜 이해하기 (1) | 2024.08.19 |