Database/Spring DB 접근 1

4. 스프링과 문제해결 - 스프링 트랜잭션 처리

hongod 2022. 7. 25. 23:50

 1. 비지니스(서비스) 계층에서 트랜잭션 처리하기가 생각보다 쉽지 않다

 앞서 3장에서(https://hongod.tistory.com/25?category=1063676) 트랜잭션을 시작하고 종료하는 계층을 비지니스 계층이라고 했었다. 

 

 

출처 : 스프링 DB 1편 - 데이터 접근 핵심 원리(인프런) / 김영한

 

 

 이 비지니스 계층의 코드를 작성할 때 가장 중요한 원칙이 하나 있는데 바로 최대한 의존성을 버리고 순수한 자바코드로 작성해야한다는 것이다. 프렌젠테이션 계층은 웹, 서블릿 HTTP 와 같은 기술에 의존하고, 데이터 접근 계층은 JDBC, JPA와 같은 구체적인 DB 접근 기술에 의존하기 때문에 차후 기술 발전에 따른 코드 수정은 불가피하지만, 서비스 계층만은 핵심 비지니스 로직이 바뀌기 전에는 수정하지 않도록 순수한 자바 코드로 격리해놔야 유지보수와 테스트가 용이한 애플리케이션을 만들 수 있다. 물론 이를 위해서는 서비스 계층에서 데이터접근 계층에 접근할 때 실제 클래스가 아니라 인터페이스(껍데기)로 접근해야한다.

 

 만약 트랜잭션을 고려하지 않는다면 데이터 접근 계층(일반적으로 repository)이 호출될 때 알아서 매번 새로운 세션으로 처리하면 된다. 즉 매번 commit 해버리면 된다. 따라서 서비스 계층 순수성에 문제가 되지 않는다. 문제가 되는 것은, 트랜잭션을 유지하기 위해서는 데이터소스로 부터 건내받은 세션을 서비스 계층이 종료되기 전까지 유지하기 위한 코드를 서비스 계층으로 빼내야한다는 것이다. 즉 서비스 계층에 특정 기술(아래 에서는 JDBC 커넥션 관련 소스)에 대한 의존성이 생기게 된다. 다른 말로 서비스 계층의 순수성이 여기서 깨진다는 것이다. 

package hello.jdbc.service;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException; 

//...
//...

//SQLException 은 JDBC 기술에 의존
public void serviceMethod(String param1, String param2) throws SQLException {
	Connection con = dataSource.getConnection() // JDBC 기술 의존 코드
    try {
    	con.setAutoCommit(false); // 트랜잭션 시작
        coreLogic(con, param1, param2);
        con.commit();
    } catch (Exception e) {
    	con.rollback();
        throw new IllegalStateException(e);
    } finally {
    	release(con);
    }
}

private coreLogic(Connection con, String param1, String param2) throws SQLException {
    repositoryA.insert(con, param1);
    repositroyA.update(con, param2);
 }

 위와 같이 서비스 계층 코드에서 JDBC에 의존하는 커넥션 관련 코드가 매우 많아지게 된다. 데이터 접근 기술마다 트랜잭션을 유지하는 방법들이 다 다르기 때문에 결과적으로 향후에 데이터 접근 기술을 JDBC에서 JPA로 바꾸게 되면 이 서비스 계층에서도 수정해야할 코드가 많아진다. 사실 그냥 봐도 핵심 기술부분과 데이터 접근 관련 로직이 섞여있어 가독성과 유지보수성이 썩 좋아보이지 않는다.

 

2. 스프링의 문제 해결

 앞서 알아본 서비스 계층이 트랜잭션 유지를 담당함으로써 생기는 문제를 몇가지로 요약하면 다음과 같다.

  • 트랜잭션 문제
    • 특정기술(위 예에서는 JDBC 구현 기술)이 서비스 계층에 누수되어 서비스 계층의 자바 순수성이 깨지는 문제
    • 데이터 접근 계층을 호출할 때 트랜잭션 동기화를 위해 매 번 커넥션(con)을 파라미터로 넘겨줘야하고, 트랜잭션을 사용하지 않는 경우는 또 분리해서 처리해야하는 문제
    • 트랜잭션을 위핸 반복적인 코드들(try, catch, finally)의 반복이 많다
  • 에외 누수 문제
    • SQLException과 같은 특정기술(여기서는 JDBC)에 의존하는 예외가 발생했을 경우, 그 예외가 runtime 예외라면 서비스 계층 코드에서 잡아서 처리를 하던지 throws로 명시적으로 밖으로 던져줘야하기 때문에 서비스 계층에 특정 기술(여기서는 JDBC)에 대한 의존성이 생긴다
  • JDBC 반복 문제
    • repository 코드에 JDBC .....

 이번 장에서는 위와 같은 문제들을 스프링에서 어떻게 해결하는지 알아보자.

 

 

1) 트랜잭션 문제 - 트랜잭션 추상화

  트랜잭션 유지를 위한 코드를 특정 기술에 의존하지 않고 추상화하여 사용하면 사용하는 기술이 변해도( 예를 들어 JDBC -> JPA) 서비스코드는 영향을 받지 않게된다. 쉽게 생각해서 아래와 같은 인터페이스를 만들어서 사용하면 된다. 

public interface TransactionManager {
	begin();
	commit();
	rollback();
}

 그리고 나서 JDBC, JPA 등 각 기술에 맞게 구현체를 만들어서( 예를 들어 JdbcTransactionManager, JpaTrasactionManager) 사용하면 된다.

 

출처 : 스프링 DB 1편 - 데이터 접근 핵심 원리(인프런) / 김영한

 

 스프링은 이미 위 그림의 각 부분에 해당하는 기능들을 다 만들어 두었는데, PaltformTransactionManager라는 트랜잭션 인터페이스 뿐만 아니라 각 데이터 접근 기술에 맞는 구현체까지 만들어놓았기 때문에 가져다 사용하면 된다. 

 

 스프링의 트랜잭션 매니저의 기능에는 트랜잭션 관련 코드 추상화 뿐만 아니라 한가지 중요한 것이 더 있는데 바로 리소스 동기화이다. 리소스 동기화란 dataSource로 부터 얻은 커넥션을 한 트랜잭션이 끝날 때 까지 유지시켜주는 것인데, 이전에는 서비스 계층에서 얻은 커넥션을(con 변수) 데이터 접근 계층 함수 호출마다 파라미터로 넘겨서 이를 구현했다. (이런 방법의 단점은 앞서 설명을 했다.) 트랜잭션 매니저에서는 트랜잭션 동기화 매니저라는 것을 통해서 리소스 동기화 기능을 제공한다.

 

 

출처 : 스프링 DB 1편 - 데이터 접근 핵심 원리(인프런) / 김영한

 

 

 트랜잭션 동기화 매니저는 트랜잭션 매니저가 내부에서 사용하는데, 트랜잭션이 시작될 때 dataSource로 부터 커넥션(세션)을 만들고 이를 트랜잭션이 끝날 때까지 보관하고 있다. 데이터 접근 계층(Repository)에서는 만약 이 커넥션 필요하다면 이전처럼 함수 파라미터로 받아서 사용하는 것이 아니고 이 트랜잭션 동기화 매니저로부터 커넥션을 획득해서 쓰면 된다. 이는 쓰레드 로컬이라는 기술을 사용해 구현돼있는데 쓰레드 로컬에 대한 내용은 이 강의에서 다루지 않는다. 이름에서 추측하기에는 같은 쓰레드 내에서 안전하게 공유하는 로컬 변수같은 것을 사용할 수 있게 하는 기술? 같은 것으로 보인다. 

 

 트랜잭션 매니저를 사용하면 데이터 접근 계층 의 코드가 아래처럼 바뀐다. 

public class MemberRepository {

	//...
    //...

	private final DataSource dataSource;
    
	public MemberRepository (DataSource dataSource) {
    	this.dataSource = dataSource;
    }
    	
	//...
    //...
    
    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values (?, ?)";
        
        Connection con = null;
        PreparedStatement pstmt = null;
        
        try {
            con = getConnection();
            pstmt = con.preparedStatement(sql);
            pstmt.setString(1, member.getMemberId());
            /...
            /...
        } catch (SQLException e) {
        	log.error("db error", e);
            throw e;
        } finally {
        	close(con, pstmt, null);
        }
    }
    
    /...
    /...
    
    private Connection getConnection() throws SQLException {
    	//트랜잭션 동기화 매니저에서 관리중인 커넥션 가져오기(없으면 새로 만들어서)
    	Connection con = DataSourceUtils.getConnection(dataSource);   
        return con;
    }
    
  	private void close(Connection con, Statement stmt, ResultSet rs) {
    	JdbcUtils.closeResultSet(rs);    //jdbc 의존 
        JdbcUtils.closeStatement(stmt);
        // 트랜잭션을 동기화하려면 DataSourceUtils 사용
        DataSoureUtils.releaseConnection(con, dataSource);
    }
   
	//..
	//..
}

 위 코드에서 DataSourceUtils를 사용한 부분들이 트랜잭션 동기화 매니저를 사용하는 부분이다. DataSourceUtils 의 getConnection 함수를 사용하면 현재 트랜잭션 동기화 매니저가 관리하는 커넥션이 존재하면 해당 커넥션을 반환하고, 없으면 새로운 커넥션을 열어서 반환한다. 기존에 무조건 서비스 계층에서 커넥션을 만들어서 파라미터로 전달하던 것과 완전히 다른 모습이다. 또한 releaseConnection 함수는 커넥션을 바로 종료 시키는 con.close()와 달리 커넥션을 바로 종료하는 것이 아니라 트랜잭션 동기화가 필요한 커넥션은 그대로 유지하고(트랜잭션 동기화 매니저에서 관리) 그렇지 않은 경우에 (트랜잭션 동기화 매니저에서 관리하는 커넥션이 아닌 경우) 그대로 해당 커넥션을 닫아버린다. 

 

 서비스 계층 코드는 어떻게 될까? 서비스 계층 코드에서는 아래와 같이 JDBC 기술에 의존하던 코드들이 사라진다.

public class MemberService {
	
    private final PlatformTrasactionManager transactionManager;
    private final MemberRepository memberRepository;

	//...
    //...
    
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    	
        // 커넥션 만들어서 트랜잭션 동기화 매니져에 저장, 트랜잭션 동기화 매니져는 이것을 쓰레드 로컬에 저장(쓰레드 안전)
    	TransactionStatus status = transactionManager.getTransaction(new DefaultTrasactionDefinition());
        	
        try {
            bizLogic(fromId, toId, money);
            trasactionManager.commit(status); // 트랜잭션 종료 - 커밋
        } catch (Exception e) {
        	transactionManager.rollback(status);  // 트랜잭션 종료 - 롤백
            throw new IllegalStateException(e);
        }
    }
    
    //...
    //...
    
}

 

'Database > Spring DB 접근 1' 카테고리의 다른 글

3. 트랜잭션 이해  (0) 2022.07.03
2. Connection Pool 과 DataSource 의 이해  (0) 2022.06.22
1. JDBC 이해  (0) 2022.06.12