ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [테코톡] Transactional(+Spring)
    DB/DB 2022. 4. 26. 06:24

    ===

    https://youtu.be/ImvYNlF_saE - 에이든 트랜잭션 메커니즘

    https://youtu.be/cc4M-GS9DoY - 후니의 스프링 트랜잭션, 스프링

    https://youtu.be/e9PC0sroCzc - 예지니어스, 자막o, 스프링

    https://youtu.be/taAp_u83MwA - 리차드, 자막o, 스프링

    https://youtu.be/aX9c7z9l_u8 - 샐리, 자막o, 스프링

     

    ===

    https://youtu.be/aX9c7z9l_u8 - 샐리, 자막o, 스프링

     

    스프링의 @Transactional이라는 애너테이션

    - 왜 사용?

    - 어떻게 사용?

    - 트랜잭션이 뭔지

    - 스프링에선 트랜잭션을 어떻게 지원하고 있는지

     

    ===

    우리는 DB에 쿼리를 날릴 때, DB가 올바른 응답을 줄 것이라는 확신을 어떻게 하고 사용할까?(DB에 장애가 생기면?)

    이 결과를 어떻게 신뢰할 수 있을까?

    데이터에 어떠한 장애가 발생해서, 4개의 Row만 출력될 수도 있고, 16으로 출력될 게 20으로 출력될 수도 있는거 아냐?

     

    이때 Transaction이라는 개념이 등장

     

    DB에서는 트랜잭션을 조작함으로써 사용자가 DB에 대한 완전성을 신뢰할 수 있도록 하고 있다.

     

    ===

    Transaction이란 더이상 나눌 수 없는 가장 작은 하나의 단위를 의미한다.

    모든 DB는 자체적으로 트랜잭션을 지원하는데

    하나의 명령을 실행했을 때, DB가 온전히 그 명령을 실행해주는 것을 의미한다.

    그림에서 볼 수 있다시피, 데이터베이스는 기본적으로 트랜잭션을 관리하기 위한 설정을 갖고 있다.

     

    DB에서는 명령을 끝마칠 때까지 수행 내역을 로그에 저장해둔다

    DB에 반영된 내용을 재반영하기 위한 redo log

    수행을 실패해 이전의 상태로 되돌리는 undo log를 이용해 트랜잭션을 지원한다.

     

    자세한 매커니즘은 테코톡 에이든 트랜잭션 메커니즘

     

    ===

    하지만 이렇게 DB에서 보장해주는 하나의 명령이 아닌, 여러 명령을 하나의 트랜잭션으로 보고 싶은 경우에는 어떻게?

     

    가장 흔한 예인 입출금으로 설명

     

    내가 만원을 보내는 버튼을 누르면,

    1.내 계좌에 만원보다 많은 금액이 존재하는지 확인해야 한다

    2.2만원이 존재할 때

    3.내 계좌에서 만원을 차감하고,

    4.저쪽 계좌에 만원을 더해야 할 것.

     

    이 일련의 업무는 절대로 분리돼서는 안되고, 일부만 실행돼도 안된다.

     

    이렇게 절대로 깨져서는 안되는 하나의 작업을 트랜잭션이라고 한다

     

    ===

    트랜잭션은 네가지 성질을 바탕으로 신뢰를 보장하는데,

     

    위의 예에서 3번까지 실행했는데  

    4번할 때 DB에 장애가 발생하면? 내계좌는 차감되고 저계좌는 +가 안된다

     

    이처럼 트랜잭션은 절대 깨지지 않는 원자처럼

     

    전부 실행되든

    전부 실패하든 해야한다.

     

    일부만 실행되는 경우는 없다는 '원자성'을 지님

     

    ===

    4번에서 실패했다면 1번에서 3번까지 성공했던 내용을 모두 다시 없었던 일처럼 되돌려야 한다.

    = Rollback

     

    반대로 하나로 묶여있는 1번부터 4번까지의 동작을 모두 성공적으로 수행했다면, 수정된 내용을 DB에 반영한다.

    = 트랜잭션 Commit

     

    Rollback 이나 Commit이 수행돼야 트랜잭션이 종료된다

     

    ===

    일관성 : 

    DB상태, DB내의 계층관계, 컬럼의 속성

    등이 항상 일관되게 유지돼야 한다는 것

     

    예를 들어 어떤 컬럼의 속성이 수정되면, trigger를 통해 일괄적으로 모든 DB에 적용해야 한다.

     

    ▽값이 안건드렸는데 지 맘대로 바뀌면 안된다 뭐 이런건가? 잘 모르겠다

     

    ===

    '지속성' : 트랜잭션이 성공적으로 수행되어 커밋됐다면 어떠한 문제가 발생하더라도 DB에 그 내용이 영원히 지속돼야 한다는 것

     

    ▽값이 없어지면 안된다 이건가?

     

    이를 위해 모든 트랜잭션은 로그로 남겨져 어떠한 장애에도 대비할 수 있도록 합니다.

     

    ===

    '독립성' : isolation

    트랜잭션 수행 시 다른 트랜잭션이 작업에 끼어들 수 없고, 각 트랜잭션은 독립적으로 수행해야 한다는 독립성

     

    C는 5만원이 있을 때, A와 B가 C에게 10000원을 동시에 송금하고자 하면,

    순차적으로 진행될 경우 C의 잔액은 +70000이 돼야 하지만

     

    둘이 송금했을 때 C의 잔액이 동일하게 5만원으로 조회된다면 최종 잔액이 6만원이 되는 문제가 발생한다

     

    트랜잭션은 격리 수준 설정을 통한 독립성 보장으로 이런 경우를 방지하고 있다.

     

    하지만 

    데이터베이스에 작업이 들어왔을 때, 모든 작업의 독립성을 보장해 하나씩 순차적으로 진행하게 된다면 CPU는 DBMS보다 인풋 아웃풋 작업을 빈번히 수행하기 때문에 트랜잭션을 순차적으로 수행하면 CPU는 점점 응답을 기다리는 시간이 길어져 프로그램이 비효율적으로 동작하는 문제가 발생할 수 있습니다.

     

    이처럼 데이터베이스에 저장된 데이터의 무결성과, 동시성의 성능을 지키기 위해 트랜잭션의 설정이 중요한 것

     

    ===

    데이터베이스에서는 각각의 명령을 하나의 트랜잭션으로 보고 보장해주기 때문에 여러 명령을 하나의 트랜잭션으로 묶고 싶은 경우 개발자가 직접 트랜잭션의 경계설정을 통해 트랜잭션을 명시하는 일이 필요

     

    우리가 사용하는 프레임워크인 스프링을 통해 트랜잭션의 경계설정을 데이터베이스에 전달할 수 있다.

    어떻게 지원하고 있을까?

    스프링은 트랜잭션 추상화 인터페이스인 PlatformTransactionManager를 제공하여 다양한 DataSource에 맞게 트랜잭션을 관리할 수 있게 하고 있다.

     

    ===

    PlatformTransactionManager는

    getTransaction

    rollback

    commit

     

    으로 구성돼 있다.

    getTransaction를 통해 파라미터로 전달되는 TransactionDefinition에 따라 트랜잭션을 시작한다.

     

    트랜잭션을 문제 없이 마치면 commit을

    문제가 발생하면 Rollback을 호출한다.

     

    getTransaction부터 commit이나 rollback을 하는 부분까지가 트랜잭션 경계설정이다.

     

    ===

    스프링이 제공하는 다양한 트랜잭션 매니저 구현체는 무엇이 있을까?

    대표적으로

    DataSourceTransactionManager : Jdbc에

    JpaTransactionManager : JPA에

    JtaTransactionManager

    가 있다.

     

    DataSourceTransactionManager, JpaTransactionManager 는 하나의 DB를 사용하거나, 각각의 데이터를 독립적으로 사용하는 로컬 트랜잭션의 경우에 사용할 수 있다.

     

    하나 이상의 데이터베이스가 참여하는 경우라면 글로벌 트랜잭션에 사용되는 JtaTransactionManager를 사용

    여러개의 데이터베이스에 대한 작업을 하나의 트랜잭션으로 묶을 수 있고, 다른 서버에 분산된 것도 묶을 수 있다.

     

    이외에도 다른 DataSource가 들어올 때도 사용할 수 있는 다양한 구현체들이 있다.

     

    ===

    하지만 이렇게 직접적으로 코드에 구현하는 방식 외에도 스프링은 AOP를 이용한 선언적 트랜잭션을 제공하고 있다.

     

    선언적 트랜잭션 - tx namespace

    선언적 트랜잭션은 tx 네임스페이스를 사용하는 방안과 애너테이션을 기반으로 설정하는 방안이 있다.

     

    tx네임스페이스를 사용하는 방식은 Bean설정 파일에서 트랜잭션 매니저를 등록하고 속성과 대상을 정의해 트랜잭션을 적용하겠다고 명시하는 것. 이렇게 적용하면 코드에는 영향을 주지 않고 일괄적으로 트랜잭션을 적용하고 변경할 수 있다는 장점이 있다.

     

    ===

    선언적 트랜잭션 - @Transactional

    애너테이션을 기반으로 트랜잭션을 설정하는 방안. 가장 많이 사용.

    메서드, 클래스, 인터페이스 등에 적용 가능.

     

    클래스 상단에 : 해당 클래스에 존재하는 모든 메서드에 애너테이션이 적용

    중첩되어 존재하는 경우에는 클래스 메서드, 클래스, 인터페이스 메서드, 인터페이스 순으로 우선순위를 갖는다.

     

    애너테이션이 적용된 메서드는 메서드 시작부터 트랜잭션이 시작되고

    메서드를 성공적으로 끝마치면 트랜잭션 커밋, 

    도중에 문제가 발생하면 롤백하는 과정이 진행된다.

     

    애너테이션은 데이터베이스에 여러번 접근하면서 하나의 작업을 수행하는 서비스 계층 메서드에 붙이는 것이 통상적

     

    코드에 일일이 붙이기 번거롭고 쉽게 놓칠 수 있다는 단점이 있지만 보다 세밀한 설정을 손쉽고 간편히 할 수 있다

     

    ===

    다음은 트랜잭션 애너테이션에 존재하는 실제 코드이다.

    보다시피 트랜잭션 매니저도 속성으로 지정이 가능

    빈으로 등록돼 있는 특수한 트랜잭션 매니저를 지정하고 싶은 경우에 지정해서 사용할 수 있다

     

    하단에 있는 다양한 속성은 트랜잭션이라고 모두 같은 방식으로 진행되지 않기에 

    트랜잭션은 몇가지 속성이 있어서 필요에 따라 속성 지정이 필요하면 추가할 수 있다.

    첫번째는 propagation, 전달받는 트랜잭션 전파

    *트랜잭션 전파 : 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때, 어떻게 동작할지 결정

    디폴트 설정인 REQUIRED가 지정된 메서드1은 

    진행중인 트랜잭션이 없어 새로 트랜잭션 1로 시작하고, 이미 시작된 트랜잭션 1이 있으면 메서드2는 트랜잭션1에 참여하게 된다.

    이게

    두 메서드가 하나의 트랜잭션으로 실행되기 때문에 어느 메서드에서 문제가 발생해도 실행했던 모든 메서드가 롤백된다.

     

    SUPPORTS는 해당 설정이 된 메서드는 진행중인 트랜잭션이 있으면 REQUIRED처럼 참여하고 트랜잭션이 없으면 트랜잭션 없이 메서드를 실행

     

    MANDATORY는 설정이 된 메서드는 진행중인 트랜잭션이 있으면 참여, 없으면 예외 : 혼자서는 트랜잭션을 시작할 수 없고, 메서드를 실행할 수도 없다.

     

    REQUIRES_NEW : 항상 새로운 트랜잭션을 시작한다. 이에 따라 메서드1은 트랜잭션1이 된다. 새로운 메서드2가 시작됐을 때, 진행중인 트랜잭션인 트랜잭션1을 잠시 보류시키고 자신의 메서드인 메서드2를 트랜잭션으로 실행.

     

    NOT_SUPPORTED : 이미 시작된 트랜잭션이 있으면 보류하고 자신의 메서드를 실행하는, 트랜잭션을 사용하지 않는 설정

     

    NEVER : 트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션이 없다면 자신의 메서드 실행. 트랜잭션이 있다면 예외 발생

     

    NESTED : 이미 진행중인 트랜잭션이 있다면 그 안에 새로운 트랜잭션을 만드는 설정.

    트랜잭션 1 내부에 메서드 2 를 트랜잭션 2로 삽입.

    이렇게 중첩된 트랜잭션 2는 부모인 트랜잭션1의 커밋, 롤백에는 영향을 받지만 트랜잭션2의 커밋 롤백에는 트랜잭션1이 영향을 받지 않는다.

     

    ===

    두번째로는

    isolation으로 전달받는 트랜잭션 격리수준

     

    동시에 여러 트랜잭션이 실행될 때, 트랜잭션의 작업 내역을 다른 트랜잭션에게 보여줄지 말지 결정하는 것.

    가능한 많은 트랜잭션을 동시에 진행하면서도 문제가 생기지 않도록 하려는 설정

     

    기본적으로 데이터베이스에 설정돼 있지만 이 속성을 통해 재설정할 수 있다.

    READ_UNCOMMITTED : 가장 낮은 격리수준, 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있는 설정

    이런 경우 트랜잭션 1 진행 중에 A+1한 데이터를 트랜잭션 2가 읽어왔는데

    A+100실행 중 문제가 발생해 트랜잭션 1시 rollback되면 트랜잭션 2는 존재하지 않는 데이터를 읽어온 것이 될 수 있다.

    이 문제를 해결한 것이 READ_COMMITTED

     

    READ_COMMITTED : 가장 많이 사용되는 두번째로 낮은 격리수준으로 커밋되지 않은 정보는 읽을 수 없다.

    트랜잭션1에서 작업이 완료되어 커밋돼야만 트랜잭션 2는 A의 정보를 읽어올 수 있다.

     

    하지만 트랜잭션2가 읽은 A를 또 다른 트랜잭션인 트랜잭션 3이 수정할 수 있어 트랜잭션 2가 다시 읽으면 값이 다르다는 문제가 발생

     

    이를 REPEATABLE_READ가 수정해준다. : 세번째로 낮은 격리수준으로 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없게 했다.

    하지만 기존에 샐리만 있던 db를 트랜잭션 1이 조회해 샐리라는 로우를 얻었는데 트랜잭션 2가 바다라는 새로운 로우를 추가하는 것은 제한되지 않아 다시 조회했을 때 발견하지 못한 새로운 로우가 발견될 수 있다는 문제가 있다.

     

    마지막 가장 강력한 격리수준인 SERIALIZABLE로 이 문제를 해결할 수 있다.

    이 속성을 가진 트랜잭션이 있으면 동시에 같은 테이블의 정보를 접근할 수 없다.

    하지만 트랜잭션을 순차적으로 수행하는 것과 다를 바 없어서 성능이 매우 떨어지니 극단적인 상황에만 사용하도록 해야한다.

     

    isolation의 DEFAULT 설정은 데이터베이스의 기본 설정을 따른다는 것.

    대부분 db는 READ_COMMITTED를 기본 격리수준으로 갖고 있지만 올바른 트랜잭션 설정을 위해서는 확인을 필수적으로 해야한다.

     

    데이터베이스에서는 이러한 격리 수준에 따라 트랜잭션이 실행되는 동안 각기 다른 lock을 걸고 데이터를 보호하고자 한다. 격리 수준이 높아질수록 더욱 강하게 lock을 걸고 트랜잭션을 마치면 lock을 해제한다

     

    ===

    https://husheart.tistory.com/112

     

    Transaction Isolation 확인 및 변경

    http://blog.naver.com/PostView.nhn?blogId=sjpotato&logNo=40210844379&parentCategoryNo=&categoryNo=7&viewDate=&isShowPopularPosts=true&from=search MySQL Transaction Isolation MySQL Transaction 확인 1..

    husheart.tistory.com

    내가 찾은 mysql과 Transaction Isolation

    MySQL의 innoDB의 기본은 REPEATABLE_READ

    ->여러개의 프로세스가 동시에 DB에 접속해 빈번하게 commit이 일어나고 또한 트랜잭션이 수시로 변경된 내용을 참조해야하는 특징이 있어서 트랜잭션이 READ COMMITED로 변경하여 사용하였다. 

     

    ===

    다음 트랜잭션 속성은 timeout으로 지정되는 트랜잭션 제한시간이 있다.

    초 단위로 제한시간을 지정할 수 있는데 예시와 같이 애너테이션이 달린 메서드를 수행하는데 10초가 지나면 예외가 발생해 롤백된다

     

    따로 설정하지 않으면 timeout은 지정되어 있지 않다.

     

    ===

    다음은 readOnly로 지정하는 읽기전용 트랜잭션입니다.

    True로 설정하면 트랜잭션 작업안에서 update, insert, delete 작업이 일어나는 것을 방지한다.

     

    추가적으로 이 옵션을 적용하면 flush모드가 manual로 설정되어 jpa의 더티체킹 기능을 무시할 수 있다. 이는 성능 향상에 도움이 되기도 한다.

     

    기본값은 false로 모든 작업을 허용한다

     

    ===

    다음은 rollbackFor로 전달받는 트랜잭션 롤백 예외입니다.

     

    기본적으로 트랜잭션은 런타임 예외와 Error가 발생했을 때만 롤백한다.

    하지만, 체크예외나 예외가 발생하지 않으면 커밋하도록 합니다.

     

    체크 예외를 롤백대상으로 삼고 싶으면 특정 exception을 클래스로 전달해 사용할 수 있습니다.

    그림과 같이 설정하면 전달받은 NoSuchElementException에 대해 예외가 발생하면 롤백됩니다.

     

    반대로

    noRollbackFor로 지정하는 트랜잭션 커밋 예외도 존재합니다.

    이 설정이 존재하면 런타임 예외인 IOException이나 SqlException이 발생하면 롤백 대상인 두 예외를 롤백하지 않고 커밋하도록 합니다.

     

    ===

    보신것처럼 단순히 @트랜잭션을 붙이는 것 외에도 트랜잭션에는 다양한 설정을 할 수 있다. 

    상황에 맞게 어떤 속성을 사용해야할 지 고민해야한다.

Designed by Tistory.