트랜잭션 추상화

JIWOO YUN·2024년 4월 16일
0

SpringDB

목록 보기
9/11
post-custom-banner

트랜잭션 추상화

  • 구현 기술에 따른 트랙잭션 사용법이 다르다.
    • JDBC : con.setAutoCommit(false)
    • JPA : transaction.begin()
  • 트랜잭션을 사용하는 코드는 데이터 접근 기술마다 다르다 -> JDBC 기술을 사용하고, JDBC 트랜잭션에 의존하다가 JPA 기술로 변경하게 될 경우 서비스 계층의 트랜잭션을 처리하는 코드도 모두 함께 변경해줘야함.

JPA 트랜잭션 코드 예시

    public static void main(String[] args) {
        //엔티티 매니저 팩토리 생성
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory("jpabook");
        EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
        EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득
        try {
            tx.begin(); //트랜잭션 시작
            logic(em); //비즈니스 로직
            tx.commit();//트랜잭션 커밋
        } catch (Exception e) {
            tx.rollback(); //트랜잭션 롤백
        } finally {
            em.close(); //엔티티 매니저 종료
        }
        emf.close(); //엔티티 매니저 팩토리 종료
    }

이 문제를 해결하기 위해서 트랜잭션 기능을 추상화하는 것으로 해결할 수 있다.

  • 트랜잭션 매니저 인터페이스를 만들어주고 인터페이스 기반으로 각각의 기술에 맞게 구현체를 만들기.
스프링의 트랜잭션 추상화 기술을 제공하기 때문에 그대로 사용하면 된다.
  • PlatformTransactionManager 인터페이스를 제공

  • public interface PlatformTransactionManager extends TransactionManager {
        
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
    	throws TransactionException;
        
    	void commit(TransactionStatus status) throws TransactionException;
    	void rollback(TransactionStatus status) throws TransactionException;
    }
  • getTransaction() : 트랜잭션 시작

  • commit() : 트랜잭션 커밋

  • rollback() : 트랜잭션 롤백

트랜잭션 동기화

  • 스프링이 제공하는 트랜잭션 매니저의 2가지 역할
    • 트랜잭션 추상화
    • 리소스 동기화

리소스 동기화

  • 트랜잭션을 유지하려면 트랜잭션 시작부터 끝까지 커넥션이 유지되어야한다.
    • 이때 사용한 방법은 파라미터 커넥션을 전달하는 방법을 사용했었는데, 이 경우 코드가 지저분해지면서 커넥션을 넘기는 메서드와 넘기지 않는 메서드를 중복해서 만들어야하는 등의 많은 단점을 가지고있다.
  • 스프링은 트랜잭션 동기화 매니저 제공 -> 쓰레드 로컬을 사용해서 커넥션을 동기화 해줌.
    • 트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용

동작 방식 과정

  1. 트랜잭션을 시작하려면 커넥션이 필요 -> 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고 트랜잭션을 시작.
  2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관
  3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용 -> 파라미터로 커넥션을 전달하지 않아도 된다.
  4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션을 닫음.
트랜잭션 매니저 사용을 위해서 MemberRepositoryV2 를 복사하여 V3만들기
  • 바뀐 부분이면서 제거되지 않은 부분
    private final DataSource dataSource;

    public MemberRepositoryV3(DataSource dataSource) {
        this.dataSource = dataSource;
    }


    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        DataSourceUtils.releaseConnection(con,dataSource);
    }

    private Connection getConnection() throws SQLException {
        Connection con = DataSourceUtils.getConnection(dataSource);
        log.info("get Connection={} , class ={}", con, con.getClass());

        return con;
    }
  • V2 부분에서 사용한 커넥션을 파라미터로 전달하는 부분은 전부 제거되었음.
  • DataSourceUtils 사용하여 커넥션 및 릴리즈

DataSourceUtils.getConnection()

  • 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
  • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환한다.

DataSourceUtils.releaseConnection()

  • close() 에서 DataSourceUtils.releaseConnection() 을 사용하도록 변경된 부분 주의.

    • 커넥션을 con.close() 으로 직접 닫아버리면 커넥션이 유지 되지 않기 때문에 이걸 사용하면안됨.
  • 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지.
  • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫음.

트랜잭션 매니저를 사용하는 MemberServiceV3_1 추가하기
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 memberRepository;


public void accountTransfer(String fromId, String toId, int money) throws SQLException {

    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());


    try {
        bizLogic(fromId, toId, money);
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw new IllegalStateException(e);
    }
}

private void bizLogic(String fromId, String toId, int money) throws SQLException {
    Member fromMember = memberRepository.findById(fromId);
    Member toMember = memberRepository.findById(toId);

    memberRepository.update(fromId, fromMember.getMoney() - money);
    validation(toMember);
    memberRepository.update(toId, toMember.getMoney() + money);
}

private void validation(Member toMember) {
    if (toMember.getMemberId().equals("ex")) {
        throw new IllegalStateException("이체중 예외 발생");
    }
}

트랜잭션 매니저를 사용하는 동작 과정

  1. 서비스 계층에서 transactionManager.getTransaction()을 호출해서 트랜잭션을 시작.
  2. 트랜잭션 매니저는 내부에서 데이터 소스를 사용해서 커넥션을 생성
  3. 커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션을 시작.
  4. 커넥션을 트랜잭션 동기화 매니저에 보관
    1. 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관 -> 멀티 스레드 환경에 안전하게 커넥션을 보관할 수있다.
  5. 서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드를 호출 -> 이때 커넥션을 파라미터로 전달하지않음.
    1. 리포지토리 메서드는 트랜잭션이 시작된 커넥션이 필요하기 때문에 DataSourceUtils.getConnection()을 사용하여 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. -> 같은 커넥션을 사용하여 트랜잭션이 유지된다.
      • 트랜잭션이 아닌경우에는 커넥션이 없기 때문에 새로 만들어 받아서 진행됨.
  6. 획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행.
  7. 비즈니스 로직이 끝나고 트랜잭션을 종료 -> 트랜잭션은 커밋하거나 롤백시 종료된다.
    1. 트랜잭션을 종료하려면 동기화된 커넥션이 필요하기 때문에 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득
    2. 획득한 커넥션을 통해 데이터베이스에 커밋 또는 롤백
  8. 전체 리소스 정리
    1. 트랜잭션 동기화 정리 -> 쓰레드 로컬은 사용 후 꼭 정리해야한다.
    2. con.setAutoCommit(true) 로 돌려주기 -> 커넥션 풀을 고려
    3. con.close()를 호출하여 커넥션 종료. -> 커넥션 풀 사용시 con.close()를 호출하면 커넥션풀에 반환된다.

번외

ThreadLocal?

  • Thread에 대한 로컬 변수 제공

  • 각각의 Thread가 변수에 대해서 독립적으로 접근할 수있음.

  • ThreadLocal은 한 Thread에서 실행되는 코드가 동일한 객체를 사용할 수있도록 지원

참고 블로그 : Thread의 개인 수납장 ThreadLocal (gmarket.com)


profile
열심히하자
post-custom-banner

0개의 댓글