[Spring DB] 트랜잭션(Transaction)

Loopy·2023년 1월 16일
0

스프링

목록 보기
3/16
post-thumbnail

☁️ 트랜잭션이란?

데이터베이스에 데이터를 저장하는 가장 큰 이유 중 하나로, 트랜잭션은 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위를 의미한다.

🔖 트랜잭션 연산
1. 커밋(Commit) : 작업이 성공해서 데이터베이스에 반영하는 작업
2. 롤백(Rollback) : 작업이 하나라도 실패해서 작업 시작 이전 상태로 되돌리는 작업

트랜잭션의 성질: ACID

트랜잭션은 다음의 네가지 성질을 만족한다.

  1. 원자성(Atomicity): 트랜잭션 내에서 실행한 작업들은 하나의 작업이 것처럼 모두 성공하거나 모두 실패해야 한다.
  2. 일관성(Consistency) : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.(ex) 무결성 제약 조건)
  3. 격리성(Isolatation) : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 못하게 격리한다. 격리성은 동시성과 관련된 이슈로 인해 트랜잭션 격리 수준을 선택할 수 있다.
  4. 지속성(Durability) : 트랜잭션을 성공하면 결과가 항상 기록되어야 한다. 즉, 중간에 시스템 장애가 발생해도 기록해둔 로그를 통해 복구가 가능해야 한다.

🔖 트랜잭션 격리 수준
격리성을 보장하기 위해서는, 트랜잭션을 순차적으로 실행시켜야 하는데 이는 동시성(병렬 처리)가 떨어져 성능이 나빠진다는 단점이 있다.

☁️ DB 연결 구조와 DB 세션

기본적인 데이터베이스 연결 구조는 다음과 같다.


사용자가 WAS와 같은 클라이언트에 데이터베이스 서버에 요청을 하면, 클라이언트는 데이터베이스 서버와 TCP/IP 연결을 맺은 이후 커넥션을 반환해준다. 이때, 데이터베이스 서버는 각 커넥션마다 세션을 하나씩 생성한다. 세션은 사용자가 커넥션을 닫거나 DBA가 강제 종료하면 사라진다.

DB 세션의 역할

  1. 트랜잭션을 시작한다.
  2. 커넥션을 통한 모든 요청(SQL)들을 실행한다.
  3. 커밋 또는 롤백을 통해 트랜잭션을 종료시킨다.

참고로 각 세션에는 SID(Sesion ID)시리얼번호(Serial#) 가 부여된다. 시리얼번호가 따로 존재하는 이유는, 세션이 종료되었으나 다른 세션이 동일한 SID 를 갖고 시작되었을 때 세션 명령들이 정확한 세션에 적용될 수 있도록 하기 위해서이다.

☁️ 트랜잭션 개념 예제

커밋 전까지는 임시로 저장된 상태이다. 따라서 SQL 문을 수행한 본인 세션에게만 데이터의 변경이 보이며, 커밋을 하면 그때서야 데이터베이스에 반영이 되어 다른 세션들에게도 변경 사항이 보이게 된다.

위의 그림처럼 세션 1 은 본인이 추가한 데이터를 모두 조회 가능하지만, 세션 2 는 아직 커밋 전이므로 DB에 반영되지 않아 추가한 데이터를 조회할 수 없다.

🔖 커밋하지 않은 데이터를 조회한다면?
데이터 정합성에 문제가 생긴다. 즉, 세션 2가 커밋전 데이터를 조회해서 로직을 수행하는 도중 세션 1이 롤백을 해버리는 경우가 발생하는 것이다.

이후 커밋을 하면 상태가 임시 -> 완료 로 바뀌면서 DB에 반영된다. 만약 커밋이 아닌 롤백을 한다면, 아래 그림과 같이 모든 데이터의 변경이 사라지면서 트랜잭션을 시작한 상태로 돌아가게 된다.

🔖 자동 커밋과 수동 커밋
DB 트랜잭션의 시작은 곧 자동 커밋에서 수동 커밋으로의 변환을 의미한다. 자동 커밋으로 한다면 매 SQL 문마다 자동으로 커밋이 되기 때문에 중간에 문제가 발생하더라도 롤백이 불가능하기 때문이다.

☁️ DB 락(Lock)

만약 세션 1 이 트랜잭션을 시작하고 데이터를 수정하는 동안 커밋을 수행하지 않았는데, 세션 2 에서 동시에 같은 데이터를 수정하게 된다면? 트랜잭션의 원자성이 깨지게 된다.

따라서 이러한 문제들을 방지하려면 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는, 해당 세션의 커밋이나 롤백 이전까지 다른 세션에서 데이터를 변경할 수 없도록 막는 장치가 필요한데 이것을 Lock(잠금) 이라 한다.

🔗 데이터 변경

데이터 변경은 락 획득과 반납이 필수로 필요하다.

세션 1 이 트랜잭션을 종료, 즉 커밋하기 전까지 세션 2는 락을 얻기 위해 무한정 대기하는 데드락 현상이 발생한다. 이를 막기 위해 DB에서는 LockTimeOut 이라는 타이머를 둬서 대기 시간의 제한을 주고 시간이 초과되면 예외를 터트린다.

🔗 데이터 조회

일반적인 데이터 조회는 락을 획득하지 않고 바로 수행 가능하다. (임시 데이터가 아닌 다른 세션에서 커밋되기 전의 원래 데이터)

만약 조회할때 락을 걸고 싶다면, select for update 를 사용하면 된다. 이렇게 하면 세션 1 이 조회를 할때 락을 가져가므로 다른 세션에서 데이터를 변경할 수 없다.

🔖 조회 시점에 락이 필요한 경우
트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용

☁️ 트랜잭션의 적용 위치

계좌이체 상황을 예시로 들어보자.

사용자 AB 에게 2000원을 보내려 하는데, A의 돈에서 2000원을 빼는 로직의 DB 반영 작업 이후 예외가 터져서 B 의 돈에서 2000원을 더해주는 로직이 실행이 되지 않을 수 있는 것이다.

잘못된 수행이 있다면 초기 상태로 되돌리는 롤백을 수행하도록, 어플리케이션에 트랜잭션을 도입해야 하는데 트랜잭션은 어플리케이션의 여러 계층중에 어디에 적용하는 것이 좋을까?

바로 비즈니스 로직이 위치한 서비스 계층에 트랜잭션을 적용해야 한다.
비즈니스 로직 수행 도중에 잘못 되거나 예외가 발생하면 전체 수행들을 롤백 해야 하기 때문이다.

하지만 무엇보다 중요한 것은, 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 한다는 것이다. 같은 커넥션이 유지되어야 하나의 세션에서

같은 커넥션 사용 방안 1: 파라미터 전달

커넥션 객체 자체를 파라미터로 전달하는 방법이 있다.

기존 레파지토리에서 매 함수마다 드라이버에서 커넥션을 얻어오지 않고, 서비스 계층에서 커넥션 하나만 생성해서 파라미터로 넘겨준다.

MemberService

public void accountTransfer(String fromId, String toId, int money) throws SQLException {   // 계좌 이체
        // 1. 커넥션 생성
        Connection con = dataSource.getConnection();
        try {
            con.setAutoCommit(false);  // 트랜잭션 시작
            // 2. 비즈니스 로직 수행
            bizLogic(fromId, toId, money, con);
            // 3. 로직 성공 시 커밋
            con.commit();
        } catch (Exception ex) {
            // 4. 예외가 터져서 로직 실패 시 롤백
            con.rollback();
            throw new IllegalStateException(ex);
        } finally {
            // 5. 사용한 커넥션 릴리즈
            releaseConnection(con);
        }
    }
  1. setAutoCo
private void releaseConnection(Connection con) {
        if (con != null) {
            try {
                con.setAutoCommit(true);  // 주의 : 기본이 자동 커밋 모드이므로 커넥션 풀에 반납할때는 true로 변경
                con.close();
            } catch (Exception ex) {
                log.info("error", ex);
            }
        }
 }

남은 문제점

  1. 서비스 계층의 코드가 지저분해지고 복잡해진다.
  2. 커넥션을 인자로 넘기지 않는 함수들도 존재한다.

해당 문제점들은, 스프링을 활용하면 해결할 수 있다.(다음 챕터에)

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글