-
Transaction이 뭐길래?스터디 노트 2023. 10. 19. 13:25
개발을 하다보면 늘 듣는 말이 있습니다.
'트랜잭션 관리를 잘 해야 해!'
대체 트랜잭션이 무엇이길래 우리는 트랜잭션 관리를 해야만 하는 걸까요?
📌 트랜잭션(Transaction)이 대체 뭐길래?
그럼 트랜잭션이라는게 대체 뭘까요?
트랜잭션의 사전적 의미는 '데이터베이스의 상태를 변화시키기 위한 수행 작업의 단위, 즉 더 이상 쪼갤 수 없는 최소 작업 단위를 말한다'입니다.
그래서 트랜잭션의 최종 단계는 'commit'또는 'rollback' 중 하나가 되어야 합니다.
만약 비즈니스 처리가 모두 성공한다면? 트랜잭션의 결과는 'commit'의 성공이 되어야 합니다.
만약 비즈니스 처리가 중간에 실패한다면? 트랜잭션의 결과는 'rollback'으로 실패되어야 합니다.
즉, 비즈니스 로직이 실행 중 어떠한 문제로 인하여 처리에 실패가 되면 DB는 아무일도 없었던 듯 롤백이 되어야 합니다.
만약 트랜잭션 10개의 비즈니스가 실행되면 어떻게 될까요?
하나의 트랜잭션 라이프 사이클 내에서 성공을 하게 되면 DB에 반영, 그게 아니라면 해당 트랜잭션은 전부 롤백이 되어야 겠지요.
📌 트랜잭션 ACID?
이렇듯 DB에 데이터의 변화를 반영해야 하는 최소단위인 트랜잭션은 다음의 요구조건을 충족시켜야만 합니다.
그 요구조건들의 첫 글자들을 따와서 'ACID'라는 명칭으로 부르고 있습니다.
하나씩 간략하게 살펴보도록 하겠습니다.
💡 원자성(Atomicity)
트랜잭션 자체는 더 이상 쪼갤 수 없는 최소의 단위이므로, DB에는 이 트랜잭션이 전부 반영 되던지 아니면 전부 반영되지 않아야 합니다. 이를 원자성이라고 하며 All or Nothing의 성격을 지닙니다.
💡 일관성(Consistency)
트랜잭션이 완료된 이후 반영된 DB의 상태는 그대로 유지가 되어야 합니다. 즉, 시스템이 언제 접근 하여도 트랜잭션 작업 처리 결과는 항상 일관성이 있어야 합니다.
트랜잭션이 진행되는 동안 DB가 변경되더라도 업데이트된 DB로 트랜잭션 되는 것이 아닌 처음 트랜잭션을 진행하기 위해 참조한 값으로 진행되어야 합니다.
💡 고립성(Isolation)
트랜잭션 수행 시 다른 트랜잭션이 해당 트랜잭션 작업에 끼어들지 못하도록 보장하는 것입니다. 즉, 트랜잭션끼리는 서로를 간섭할 수 없습니다.
즉, 트랜잭션이 실행하는 도중 변경 데이터는 이 트랜잭션이 완료될 때 까지 다른 트랜잭션이 참조하지 못하게 하는 특성을 말합니다.
💡 지속성(Durability)
트랜잭션이 정상적으로 종료된 이후에는 영구적으로 데이터베이스에 해당 작업의 결과가 저장되어야 합니다.
📌 스프링에서의 Transaction
스프링의 경우 광범위한 Transaction을 지원합니다.
Transaction을 시작하기 위해 스프링에선 선언적 방식과 코드 베이스 방식을 제공합니다.
이를 통해 우리는 비즈니스 로직에만 집중을 할 수 있게 됩니다.
💡 TransactionTemplate
Spring에서는 데이터 접근 기술에 상관 없이 트랜잭션에 대한 추상화를 제공해 변경에 유연하게 대응하고 있습니다.
PlatformTransactionManager 인터페이스를 통해 트랜잭션을 관리할 수 있습니다.
AbstractPlatformTransactionManager 추상 클래스를 상속 받은 JtaTransactionManager나 DataSourceTransactionManager, HibernateTransactionManager 등을 활용하여 구성할 수 있습니다.
이 인터페이스를 구현체들을 통해 손쉽게 트랜잭션을 얻어와 경계를 설정하거나 commit 또는 rollback을 할 수 있습니다.
트랜잭션의 시작과 끝, 그리고 상태를 개발자가 직접 컨트롤 할 수 있게되며, 이를 '프로그래밍적 트랜잭션 관리'라고 합니다.
💡 @Transactional
Spring에서 제공하는 어노테이션 중 @Transactional을 사용하여 보다 쉽게 Transaction을 활용할 수 있습니다.
AOP를 통해 구현된 어노테이션으로 클래스와 메서드에 모두 적용이 가능하며, 클래스보다 메서드에 선언된 트랜잭션을 더 우선적으로 적용합니다.
과거에는 xml의 설정을 통해 Transaction을 사용했었으나 지금은 어노테이션으로 아주 쉽게 사용할 수 있게 되었습니다.
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { ... }
이 어노테이션을 사용하면 해당 메서드가 포함하고 있는 작업 중 하나라도 실패할 경우 전체 작업을 취소하게 됩니다.
쉽게 말해서, 우리가 트랜잭션을 직접적으로 컨트롤 해야 한다면 PlatformTransactionManager를 구현해 사용해야겠지만, 그럴 일은 크게 없을 것 같습니다. @Transactional을 선언하면 너무나도 쉽게 트랜잭션을 관리할 수 있게 되니까요.
그렇다고 너무 @Transactional 어노테이션을 남발하게 되면, 트랜잭션의 경계가 모호해지며 격리성으로 인해 발생할 수 있는 문제점들이 생깁니다.
📌 격리성에 따른 문제점
트랜잭션의 경우 독립 고립성(격리성)이 보장되어야 하며, 명확하게 설정되지 않으면 다음의 문제점들이 발생할 수 있습니다.
💩 Dirty Read
다른 트랜잭션에 의해서 이미 값이 수정되었지만 현재의 트랜잭션에서 아직 커밋되지 않은 데이터를 읽는 것을 말합니다.
즉, 재고 관리 시스템에서 A와 B가 제품 '가'의 재고를 조회하고 있다고 해보죠.
'가'의 재고는 최초에는 10개였습니다만, A가 방금 9개를 팔아서 1개가 되었습니다.
그리고 B도 '가'의 재고를 조회하고 있었고, B는 재고가 1개가 있다고 보여지겠죠.
하지만 A가 판 9개가 다시 환불이 되었고, 판매 비즈니스는 모두 롤백되었습니다.
하지만 이미 B는 '가'의 재고는 1개로 보여지고, 부족하다고 생각해 B는 '가' 제품의 추가 발주를 진행하게 되었습니다.
💩 Non-Repeatable Read
한 트랜잭션 내 같은 값을 두 번 읽어올 때 그 사이에 데이터가 변경되며 값이 바뀌는 현상을 말합니다.
위에 예시를 다시 들어보면 B는 '가'의 재고를 1로 조회했으나, 다시 A가 재고를 10으로 수정했습니다.
부족하다고 판단한 B가 추가 발주를 하고 다시 '가' 제품의 재고를 조회해보니, 이런..재고가 다시 10개가 되었네요.
발주를 취소해야 겠습니다.
💩 Phantom Read
한 트랜잭션 내 다건의 데이터를 읽어올 때 없던 값이 갑자기 생기는 경우를 말합니다.
A와 B가 모두 재고관리 프로그램에서 전체 상품 목록 데이터를 조회하고 있습니다.
등록되어 있는 상품이 100개군요.
그러던 중 A가 갑자기 새로운 상품이 입고되었다는 사실을 알게되어 부랴부랴 신규 제품을 등록했습니다.
A는 등록을 하고 완료를 해도 B는 아직 재고가 100개라고 알고 있습니다.
갑자기 손님이 새로 추가된 제품을 들고 와서 계산하려고 하니 B는 처음보는 물건이었습니다.
다시 상품 목록을 조회해보니 상품 수가 101개로 늘어나있군요.
📌 트랜잭션의 격리 수준(Isolation Level)
위에서 발생했던 문제들(Dirty Read, Non-Repeatable Read, Phantom Read)을 해결할 수 있는 방법은 없을까요?
초기 트랜잭션을 선언해줄 때 격리 수준을 함께 지정해주면 됩니다.
트랜잭션의 격리 수준은 다음과 같이 있습니다.
@Transactional(isolation = Isolation.DEFAULT) @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Transactional(isolation = Isolation.READ_COMMITTED) @Transactional(isolation = Isolation.REPEATABLE_READ) @Transactional(isolation = Isolation.SERIALIZABLE)
하나씩 알아보도록 하겠습니다.
💡 Isolation.DEFAULT
데이터베이스에서 제공하는 기본 격리 수준을 말합니다. 일반적으로 JDBC isolation levels와 동일하게 설정되어집니다.
즉, 사용하는 데이터베이스의 기본 격리 수준을 따르게됩니다.
💡 Isolation.READ_UNCOMMITTED(Level 0)
Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생할 수 있습니다.
이 수준은 해당 행의 변경사항이 커밋되지 전에 한 트랜잭션이 변경한 행을 다른 트랜잭션에서도 읽을 수 있도록 열려있습니다.
만약 트랜잭션이 롤백되면 다음에 읽은 트랜잭션에서는 잘못된 행을 읽은 것으로 판단합니다.
가장 취약한 격리 수준입니다.
💡 Isolation.READ_COMMITTED(Level 1)
Dirty Read를 허용하지 않고, Non-Repeatable Read와 Phantom Read는 발생할 수 있습니다.
즉, 트랜잭션에 커밋되지 않은 변경사항이 있는 행을 Read하는 것을 금지합니다.
일반적으로 가장 많이 선택되는 격리 수준입니다.
💡 Isolation.REPEATABLE_READ(Level 2)
Dirty Read와 Non-Repeatable Read는 허용하지 않고, Phantom Read는 발생할 수 있습니다.
이 수준은 커밋되지 않은 변경사항이 포함된 트랜잭션을 읽을 수 없도록 막고, 한 번 읽은 Row를 다른 트랜잭션에서 읽지 못하게 방지합니다.
즉, 여러번의 쿼리에도 트랜잭션이 이미 실행되고 있다면 같은 값을 보여주게 됩니다.
참고로 내용만 보면 READ_COMMITTED와 REPEATABLE_READ가 같아 보일 수 있습니다.
REPEATABLE_READ 에는 Undo 로그라는 개념이 추가되며 Transaction ID가 주어지고, 그 ID보다 작은 값만을 반환하게 됩니다.
💡 Isolation.SERIALIZABLE(Level 3)
가장 높은 격리 수준입니다.
Dirty Read와 Non-Repeatable Read, Phantom Read 모두 허용하지 않습니다.
이 수준은 기본적으로 REPEATABLE_READ의 격리 수준을 포함하며, 추가적으로 where 절에 만족하는 모든 행에 대한 추가적인 읽기와 수정 등을 모두 막는 Shared Lock에 걸리게 됩니다.
즉, 한 트랜잭션에서 읽은 데이터들에는 다른 트랜잭션의 접근이 일체 차단됩니다.
단점으로는 모든 접근을 막기 때문에 성능이 떨어지며, 절대적으로 트랜잭션의 고립성을 강하게 보장해야 할 때만 사용합니다.
📌 마치며
Transaction에 대한 개념과 Spring에서 Transaction은 어떻게 사용할 수 있는지 또 처리되는지를 알아보았습니다.
Transaction의 격리 수준에 대해서도 함께 알아보았으며, 다음에는 Trsnacation의 전파에 대해서도 알아보겠습니다.
감사합니다😁
'스터디 노트' 카테고리의 다른 글
HTTP Status Code (0) 2023.10.20 스프링 빈, 자바 빈, DTO, VO의 차이점과 불변객체(Immutable Object) (0) 2023.10.20 @RequiredArgsConstructor 가 하는 일 (1) 2023.10.18 스프링 빈 스코프에 대하여 (0) 2023.10.18 @RestControllerAdvice에 대하여 (0) 2023.10.18