[주간 무심코 10月.3] Transaction 이해하고 쓰자

무심코·2022년 10월 23일
1
post-thumbnail

Transaction이란?

Transaction이란 데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위를 뜻한다.

그렇다면 Transaction을 왜 쓰는가? → 일련의 업무들을 개별단위로 처리하면 문제가 생기는 경우 사용한다. 즉, 일련의 업무처리를 하나의 단위로 묶어서 사용해야할 때 쓰인다. 아래와 같은 상황을 보자

ATM으로 계좌이체를 한다고 생각해보면,

1. A 은행에서 출금하여 B은행으로 송금

  1. 송금 중, 알 수 없는 오류가 발생하여 A은행 계좌에서 돈은 빠져 나갔지만 B은행의 계좌에 입금되지 않았음

→ 이와 같은 상황을 막기위해 거래가 성공적으로 모두 끝나야 이를 완전한 거래로 승인하고, 거래 도중 뭔가 오류가 발생했을 때는 이 거래를 처음부터 없었던 거래로 완전히 되돌리는 것이다.

Transaction의 특징(ACID)

  1. 원자성(Atomicity)

    트랜잭션의 결과는 DB에 반영이 되거나 반영이 안되거나 이 두 가지 결과만을 나타낸다.

  2. 일관성(Consistency)

    트랜잭션이 진행되는 동안 DB가 변경되더라도 이전에 사용한 DB를 참조한다. 즉 트랜잭션은 시작부터 종료 시까지 같은 형태의 DB를 참조한다.

  3. 독립성 (Isolation)

    트랜잭션이 두 개이상 실행될 때 트랜잭션 끼리 영향을 주지 못한다, 즉 하나의 트랜잭션이 또 다른 트랜잭션의 결과를 참조할 수 없다.

  4. 영구성 (Durability)

    트랜잭션이 성공적으로 완료(Commit)이 되었을 때 결과를 영구적으로 DB에 반영됩니다.

선언적 트랜잭션 (@Transactional)

@Transactional을 붙으면 스프링은 해당 타깃을 포인트 컷의 대상으로 자동 등록하며 트랜잭션 관리 대상이 된다. 즉, 이 어노테이션을 통해 포인트 컷에 등록하고 트랜잭션 속성을 부여하는 것이다. 이렇듯 AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 해주는 것을 선언적 트랜잭션(declarative transaction)이라고 한다.

반대로 TransactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 이용해 직접 코드안에서 사용하는 방법을 프로그램에 의한 트랜잭션(programmatic transaction)이라고 한다. 스프링에서는 두 가지 방식을 모두 지원하지만, 특별한 경우가 아니라면 선언적 트랜잭션을 사용하는 것이 좋다.

왜냐하면 @Transactional 어노테이션을 이용하면 트랜잭션 속성을 메소드 단위로 다르게 지정할 수 있어 매우 세밀한 트랜잭션 속성 제어가 가능할 뿐만 아니라 직관적이므로 이해하기도 좋다.

대신 이 어노테이션을 이용하려면 @EnableTransactionManagement을 추가해주어야 한다.

그러면 트랜잭션은 어느 부분에 적용하는 것이 적합할까?

1. 비지니스 로직과의 결합

트랜잭션을 중구난방으로 적용하는 것은 좋지 않다. 대신 특정 계층의 경계를 트랜잭션 경계와 일치시키는 것이 좋은데, 일반적으로 비지니스 로직을 담고 있는 Service 계층의 메소드와 결합시키는 것이 좋다. 왜냐하면 데이터 저장 계층으로부터 읽어온 데이터를 사용하고 변경하는 등의 작업을 하는 곳은 일반적으로 Service 계층이기 때문이다. 위와 같이 클래스 레벨에 트랜잭션 어노테이션을 붙여주면 메소드까지 적용이 된다.

서비스 계층을 트랜잭션의 시작과 종료 경계로 정했다면, 테스트와 같은 특별한 이유가 아니고는 다른 계층이나 모듈에서 DAO에 직접 접근하는 것은 차단해야 한다. 트랜잭션은 보통 Service 계층의 메소드 조합을 통해 만들어지기 때문에 DAO가 제공하는 주요 기능은 서비스 계층에 위임 메소드를 만들어둘 필요가 있다. 그리고 가능하면 다른 모듈의 DAO에 접근할 때는 Service 계층을 거치도록 하는 것이 바람직하다.

2. 읽기 전용 트랜잭션의 공통화

클래스 레벨에는 공통적으로 적용되는 읽기전용 트랜잭션 어노테이션을 선언하고, 추가나 삭제 또는 수정이 있는 작업에는 쓰기가 가능하도록 별도로 @Transacional 어노테이션을 메소드에 선언하는 것이 좋다.

→ 성능적인 이점이 있다고 한다.

3. 테스트의 롤백

트랜잭션 어노테이션을 테스트에 붙이면 테스트의 DB 커밋을 롤백해주는 기능이 있다.

DB와 연동되는 테스트를 할 때에는 DB의 상태와 데이터가 상당히 중요하다. 하지만 문제는 테스트에서 DB에 쓰기 작업을 하면 DB의 데이터가 바뀌는 것인데, 트랜잭션 어노테이션을 테스트에 활용하면 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트를 진행하기 전의 상태로 만들어준다. 어떠한 경우에도 커밋을 하지 않기 때문에 테스트가 성공하거나 실패해도 상관이 없으며 심지어 예외가 발생해도 어떠한 문제가 발생하지 않는다. 강제로 롤백시키도록 설정되어 있기 때문이다.

하지만 테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고는 싶지만 강제 롤백을 원하지 않을 수 있다. 테스트의 작업을 그대로 DB에 반영하고 싶다면 @Rollback(false)를 이용해주면 된다. 하지만 @Rollback은 메소드에만 적용가능하므로, 클래스 레벨에 부여하기를 원한다면 @TransactionConfiguration(defaultRollback=false) 를 이용하고, 롤백을 원하는 메소드에 @Rollback(true)를 이용하면 된다.

물론 여기서 auto_increment나 sequence 등에 의해 증가된 값은 롤백이 되지 않는다. 그렇기 때문에 테스트를 위해서는 별도의 데이터베이스로 연결을 하거나 또는 H2와 같은 휘발성(인메모리) 데이터베이스를 사용하는 것이 좋다.

Transaction이 왜 안될까

아래 코드에서 보이는 것과 같이 Shop Section에서의 postShop 메소드는 shop DB에 결재 내역을 저장하는 shopDao.addShop() 메소드(1)와 store DB에 orderNum을 증가시키는 shopDao.increaseStoreOrderNum(postShopReq) 메소드(2)를 순서대로 호출하여 진행한다.

이때 Transaction이 제대로 작동하는지 확인하기 위해 shopDao.increaseStoreOrderNum(postShopReq) 안 SQL문에 에러 코드를 삽입하고 실행시켜 보았다. 보았지만.. 에러는 에러대로 발생하고 shop DB에 결재 내역 또한 저장되는 결과가 나왔다. 즉 Transaction이 안된 것이다.

// In ShopService

@Transactional // <- 안 걸린다...
public PostShopRes postShop(PostShopReq postShopReq) throws BaseException {
    // 1. shop DB에 결재 내역 저장
    int shopIdx;
    try{
        shopIdx = shopDao.addShop(postShopReq);
    } catch (Exception exception){
        throw new BaseException(ADD_FAIL_STOP);
    }

    // 2. store DB에 orderNum 증가
    try {
        int result = shopDao.increaseStoreOrderNum(postShopReq);
        if (result == 0) { // result값이 0이면 과정이 실패한 것이므로 에러 메서지를 보냅니다.
            throw new BaseException(INCREASE_FAIL_STORE_ORDER_NUM);
        }

    } catch (Exception exception) { // DB에 이상이 있는 경우 에러 메시지를 보냅니다.
        throw new BaseException(DATABASE_ERROR);
    }

    return new PostShopRes(shopIdx);
}
// In ShopDao

public int increaseStoreOrderNum(PostShopReq postShopReq) {
    String increaseStoreOrderNumQuery = "update Store S " +
                                        "inner join Menu M " +
                                        "on S.storeIdx = M.storeIdx " +
                                        "set S.orderIdx = S.orderIdx+ 1 " + 
																				// \-> 고의로 오류 발생 부분
                                        "where M.menuIdx = ? ";
    Object[] increaseStoreOrderNumParams = new Object[]{postShopReq.getMenuIdx()};

    return this.jdbcTemplate.update(increaseStoreOrderNumQuery, increaseStoreOrderNumParams);
}

→ In Client Side


→ In Shop DB

그렇다면 이유는?

문제는 예외처리문에 있었다. try에서 예외 발생시 catch처리 했을 경우 rollback이 되지 않는다고 한다. 즉 transaction 처리가 안되는 것이다.

다음과 같이 try-catch문을 제외하고 사용하였을 때 rollback이 정상 작동하였다!!

@Transactional
    public PostShopRes postShop(PostShopReq postShopReq) throws BaseException {
        shopIdx = shopDao.addShop(postShopReq);
        int result = shopDao.increaseStoreOrderNum(postShopReq);
        if (result == 0) { // result값이 0이면 과정이 실패한 것이므로 에러 메서지를 보냅니다.
                throw new BaseException(BaseResponseStatus.INCREASE_FAIL_STORE_ORDER_NUM);
            }
        return new PostShopRes(shopIdx);
    }

그렇다면 try-catch문을 사용시에는 transaction을 사용할 수 없는 것인가?

→ No 아니다. 방법이 있다

다음은 Transactional 선언 시 기본 속성이다.

@Transactional
//@Transactional(rollbackFor = {RuntimeException.class, Error.class}) 기본 속성
public void save(User user) {
	userRepository.save(user);
}

rollbackFor 은 rollback을 지원하는 예외 종류를 뜻한다. 즉 Transactional은 기본적으로 RuntimeException과 Error에 대해서만 rollback을 지원한다는 뜻이다.

그렇다면 모든 예외에 대해 rollback을 지원하도록 설정해줘보자!

@Transactional(rollbackFor = Exception.class)

다음과 같이 어노테이션 옵션을 변경 후 실행시키면 rollback이 정상적으로 작동하는 것을 확인할 수 있다!

++ 수동으로 catch안에서 rollback을 처리하는 방법

 @Transactional(rollbackFor = {RuntimeException.class, Error.class}) // 옵션 생략 가능
public void save(User user) {
    try {
        userRepository.save(user);
    } catch(Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

다음은 Spring Framework에 TransactionAspectSupport 클래스를 이용해 수동으로 rollback 해주는 방법이 이다. 위와 같이 작성하면 Exception 발생 시 해당 메서드를 rollback 시킬 수 있다.

++ @Transactional의 두가지 종류

  • org.springframework.transaction.annotation의 @Transactional
  • javax.transaction의 @Transactional

[ 참고 자료 ]

[Spring] 트랜잭션에 대한 이해와 Spring이 제공하는 Transaction(트랜잭션) 핵심 기술 - (1/3)
아노테이션 드리븐 트랜잭션(@Transactional)에서 Exception을 throw할 경우 롤백(rollback)이 안됩니다.
@Transactional 내에서 try catch와 rollback 연관성

profile
지나치지 않기 위하여

0개의 댓글