그래서 트랜잭션이 정확히 뭘까?(2편 @Transactional)

kimseungki·2023년 4월 12일
1

개요

트랜잭션에 대해 저번엔 DB관점에서 설명을 했습니다.
이번엔 스프링에 있는 트랜잭션을 이야기 해보고, 디비와 같이
이야기 해보겠습니다.

이전에 개발자들은?

믿을 수 없지만..엄청 오래 된, 레거시 코드(지금도 그럴 수 있어요..)들을 보면 이런걸 많이 볼 수 있습니다. 어디에 이런 코드가 있냐고요?

public void withdraw(UserVO userVO, BankVO bankVO) throws Exception{
    String sql = "String sql = "Update member Set money = ? Where id =?";
    Connection conn = null;
    PreparedStatement pstmt = null;
    // 유저 돈 출금 update
    try {
        conn = dataSource.getConnection();
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, user.getMoney());
        pstmt.setString(2, user.getId());
        pstmt.executeUpdate();
    } catch (SQLException e) {
        throw new Exception(e);
    } finally {
        try {
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (Exception e) {
        	e.printStackTrace();
        }
        try {
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
        	e.printStackTrace();
        }
    }
    String sql = "String sql = "Update member Set money = ? Where id =?";
    Connection conn = null;
    PreparedStatement pstmt = null;
    // 은행 돈 입금 update
    try {
        conn = dataSource.getConnection();
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, user.getMoney());
        pstmt.setString(2, user.getId());
        pstmt.executeUpdate();
    } catch (SQLException e) {
        throw new Exception(e);
    } finally {
        try {
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (Exception e) {
        	e.printStackTrace();
        }
        try {
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
        	e.printStackTrace();
        }
    }    
}

논평

쉽게 말하면 유저가 돈을 지출하고, 그 돈을 은행은 돈을 저금 프로세스입니다.
이런 코드가 자주 있다면.. 음 아무튼 그렇습니다..
이 코드를 보면 약간 아쉬운 부분이 있는데 다음과 같습니다.
바로 try-catch를 반복하는 것과
close를 통해 끊어야 된다는 이슈가 있습니다.
쿼리가 많게 된다면.. 계속 이 방식으로 만들게 되겠죠..
이부분의 경우, 전 그래서 JPA와 같은 프레임워크에 의존하는 편입니다.
저 코드는.. try catch 자체가 checked Exception이라 구현을 강제로 해야되거든요
근데 간과하는게 있습니다. 바로 롤백입니다.

문제점?

저렇게 만들게 된다면 은행과 고객은 별도의 트랜잭션을 가지고 있고, 별개의 업무로 처리하게 됩니다.
결국 만약 은행쪽 쿼리를 수행하다 오류나면, 그냥 고객은 돈을 헌납(?)하는거고
고객쪽에서 수행하다 오류나면, 고객은 일부 돈을(?) 로또 맞게 되는 이슈가 생깁니다.
결국 이 두가지는 서로 때어내면 안되고, 2개다 실행이 되야하거나 두개다 실행이 되지 말아야합니다.

롤백?

그럼 만약 오류가 나면 어떻게 해야될까요? 예를들어 은행과 고객이 있는데 고객 돈은 빠져나갔는데
은행 돈을 입급하는 과정에서 오류가 난다면? 그러면 고객에 업데이트 한 것을 원복해야 됩니다. 즉 쿼리라는 무거운 비용을 또 써야 된다는 문제가 있죠..
또한 결국 롤백하는 것 역시 구체적인 코드이고 저수준 모듈이기 때문에, 변경 가능성이 높아 생산성을 낮추는데 엄청난 기여를 할 확률이 높습니다.
그럼 이를 해결하기 위해 가벼운 방법이 뭘까요? 바로 기존에 DB에서 말한 트랜잭션 입니다.
트랜잭션을 애플리캐이션에 적용을 시키면 됩니다.

@Transactional은 왜 있는거지?

@Transactional을
애플리캐이션에 적용할 떈, DB와 비슷하게 BEGIN과 COMMIT 역할을 수행합니다.
결국 지금과 같은 insert 메소드가 있고 그 앞 뒤에 BEGIN COMMIT 또는 이슈 발생 시 롤백처리하는 기능이 있기 때문에 @Transactional을 쓰게 되는거죠?
근데 이렇게 우린 아무것도(?) 하지 않고 어노테이션만 넣었는데 이게 가능한 이유가 뭘까요?
바로 프록시 패턴을 쓰고 있고, AOP가 적용이 되어있기 때문입니다.

작동원리?

간단합니다.

우선 AUTO COMMIT를 FALSE로 한 후
어노테이션이 붙은 메소드를 호출하는 시점에 프록시 객체가 가로챕니다.
이후 BEGIN을 실행하고, 메소드를 실행하다 이슈가 발생하면 ROLLBACK
발생하지 않을 경우 커밋을 하게 됩니다.
따라서 우리가 생각하는 DB의 트랜잭션과 동일하게 작동된다고 보시면 됩니다.

고려할 점

  • Transactional을 붙이면 private가 안됩니다. 이유는 AOP를 위한 객체 역시 메소드를 호출하기 위해선, public여야 호출할 수 있기 때문이죠
  • Transactional은 ReadOnly 등을 제공하여 Updat나 Insert 등이 없을 경우에 쓰면 좋습니다. 아무래도 별도의 원본 혹은, 변경된 데이터를 위한 저장소 등이 없기 때문에
    처리속도 및 메모리 상에 이점이 있기 때문입니다.

결론

Transactional을 굳이 안써도 물론, 작동은 시킬 수 있지만, 안쓰게 된다면, 저수준의 코드를 아주(?) 많이 작성할 확률이 높아서 가능한 쓰는게 좋을거 같다는 생각이 들었습니다. 하지만, 이거 역시 서비스적인 측면에서 하나의 트랜잭션으로 묶어야 되는가? 혹은 아닌가 등을 명확히 생각하고 접근을 하는 것이 중요하다 봅니다.

profile
seung 기술블로그

0개의 댓글