상품 공급 시스템 - 회원 관련 기능

회원정보 저장 기능

회원가입의 마지막 단계에서 호출하는 API이다. 회원의 정보와 회원의 권한 정보를 데이터베이스에 저장한다.

트랜잭션 정의

member 테이블에 회원 정보를 저장하는 쿼리와 role 테이블에 회원의 권한을 저장하는 쿼리를 하나의 트랜잭션으로 처리하기 위해 아래와 같이 코드를 작성했다.

public ResponseStatus signup(SignupRequestBody requestBody) {
    NewMemberDto newMemberDto = new NewMemberDto(requestBody);
    try {
        registerMemberInfo(newMemberDto);
    } catch (RecordInsertionFailedException insertFailure) {
        System.out.println("\033[31m " + insertFailure.getCause());
        System.out.println(insertFailure.getClass());
        System.out.println(insertFailure.getStackTrace() + "\033[0m");
        return DATABASE_EXCEPTION;
    }
    return SUCCESS;
}

@Transactional(rollbackFor = RecordInsertionFailedException.class)
void registerMemberInfo(NewMemberDto newMemberDto) {
    Long newMemberId = (long) memberDao.insertNewMember(newMemberDto);
    Role newMemberRole = new Role(newMemberId, newMemberDto);
    int lastRoleId = memberDao.insertNewMembersRole(newMemberRole);
}

RecordInsertionFailedException이 발생했음에도 rollback처리가 되지 않아 회원정보만 저장되었다.

rollback 안됨

solution

@Transactional 어노테이션은 JDBC와는 관련이 없다. 이 어노테이션은 스프링에게 해당 메서드가 하나의 트랜잭션임을 알려주는 것이고 스프링과 JDBC는 서로 알지 못한다.
아래와 같이 하나의 트랜잭션에서 수행할 작업을 JDBC에게 알려줘야 한다.

7일차에 계속…


JDBC에서의 트랜잭션

아래의 트랜잭션 사용방법은 JDK 8 기준이다.

Disabling Auto-Commit Mode

DB와의 Connection이 생성되면, 해당 커넥션은 자동적으로 auto-commit mode이다. 즉, 모든 SQL statement는 하나의 트랜잭션으로 간주되고 실행되면 자동적으로 commit된다. 좀 더 정확히는 SQL statement가 실행(executed)되었을 때가 아니라 완료(completed)되었을때 commit되는 것이 기본(default)이다. 하나의 statement의 완료(completed)는 것은 모든 result set과 update count가 회수된 시점이다. 대부분의 경우에 statement가 실행되자마자 완료되고 그에 따라 commit되게 된다.

connection.setAutoCommit(false);
// connection은 Connection 객체

Committing Transactions

Auto-commit Mode가 해제되면 commit 메서드를 명시적으로 호출하기 전까지 어떤 SQL statement도 commit되지 않는다.
connection.setAutoCommit(false)connection.commit() 사이에서 실행되는 모든 statement들은 하나의 트랜잭션에 포함되며 같이 commit된다.
connection.setAutoCommit(true) 를 통해 auto-commit mode를 다시 활성화 할 수 있다. 트랜잭션이 수행중일 때에만 auto commit mode를 비활성화 하는 것이 권장된다. 여러 statement에 대한 lock을 가지고 있으면 다른 사용자와 충돌할 가능성이 높아지기 때문이다.

// https://docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html

 public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException {
    String updateString =
      "update COFFEES set SALES = ? where COF_NAME = ?";
    String updateStatement =
      "update COFFEES set TOTAL = TOTAL + ? where COF_NAME = ?";

    try (PreparedStatement updateSales = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement))
    
    {
      con.setAutoCommit(false);
      for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
        updateSales.setInt(1, e.getValue().intValue());
        updateSales.setString(2, e.getKey());
        updateSales.executeUpdate();

        updateTotal.setInt(1, e.getValue().intValue());
        updateTotal.setString(2, e.getKey());
        updateTotal.executeUpdate();
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
      if (con != null) {
        try {
          System.err.print("Transaction is being rolled back");
          con.rollback();
        } catch (SQLException excep) {
          JDBCTutorialUtilities.printSQLException(excep);
        }
      }
    }
  }

Using Transactions to Preserve Data Integrity

DBMS는 트랜잭션이 진행되는 동안의 충돌(conflict)을 피하기 위해 lock을 사용한다. 한 번 설정된(set) lock은 트랜잭션이 commit되던가 roll back될때까지 유효하다(remain).

  • lock - 특정 트랜잭션에 의해 접근되고 있는 데이터에 다른 트랜잭션이 접근하는 것을 차단하는(blocking) 매커니즘.
  • dirty read - commit되지 않은 업데이트된 값에 다른 사용자가 접근하는 것. rollback 여부에 따라 읽게 되는 값이 달라진다.

lock은 transacion isolation level에 의해 설정된다.

고립성 수준이 높아지면 동시성이 감소하고 일관성이 보장된다. 반대로 고립성 수준이 낮아지면 동시성이 높아져 성능이 개선되지만 일관성을 보장할 수 없다. 애플리케이션의 요구사항에 맞게 적절한 고립성 수준을 선택해야 한다.

Isolation level(고립성 수준) Dirty read Non-repeatable Read Phantom read
TRANSACTION_READ_UNCOMMITTED 허용 허용 허용
TRANSACTIONAL_READ_COMMITTED 허용안함 허용 허용
TRANSACTIONAL_REPEATABLE_READ 허용안함 허용안함 허용
TRANSACTIONAL_SERIALIZABLE 허용안함 허용안함 허용안함
  • Phantom read - 하나의 트랜잭에서 같은 (조건의) 데이터를 두 번 이상 읽었을 때, 첫 번째 쿼리와 두 번째 쿼리 사이에 다른 트랜잭션의 명령어에 의해 데이터가 삽입되거나 삭제 되어 두 쿼리의 결과가 달라지는 현상
  • Repeatable read - 하나의 트랜잭션에서 같은 레코드를 두 번 이상 읽을 때 일관된 결과를 보장한다. 하나의 레코드를 두 번 읽는 사이에 값이 수정 또는 삭제되는 것을 막는다.

Phantom read는 쿼리 결과의 일관성에 대한 문제이고 repeatable read는 레코드 값의 일관성에 대한 문제이다.

고립성 수준의 기본값은 DBMS마다 다르며 JDBC driver에 따라 지원하지 않는 고립성 수준이 있을 수 있다. 드라이버가 지원하지 않는 고립성 수준을 설정하려고 시도할 경우 SQLException이 발생한다. DatabaseMetaData.supportsTransactionIsolationLevel메서드를 호출하면 드라이버가 지원하는 고립성 수준을 알 수 있다.

Setting and Rolling Back to Savepoints

Savepoint 객체는 트랜잭션 내의 특정 지점(상태)를 표현하는 객체이다. Savepoint 객체를 사용하면 원하는 지점으로 롤백할 수 있다.


try (/* 자원 할당 */) {
    connection.autoCommit(false);
    Savepoint savepoint = connection.setSavepoint(); // Savepoint 설정

    /**
     * 트랜잭션 진행
     * /
    connection.commit();
} catch (SQLException e) {
    connection.rollback(); // 인자를 전달하지 않는 경우 트랜잭션의 모든 변경사항이 취소된다.
} finally {
    connection.autoCommit(true);
}

Releasing Savepoints

트랜잭션이 커밋되거나 롤백되면 Savepoint 객체는 자동으로 해제된다. 하지만 많은 수의 Savepoint 객체를 사용하거나 오래 진행되는 트랜잭션에서는 Savepoint의 사용이 끝났다면 명시적으로 해제하는 것이 좋다.

connection.releaseSavepoint(savepoint);

When to Call Method rollback

reference

Comments