본문으로 바로가기

Spring DataSource/TransactionManager 정리

category Coding/Java Spring 2023. 7. 19. 09:04
반응형

개요

Spring Batch 5 버전을 통해 배치를 생성하고 관리하고 있는데, 멀티 모듈로 구성된 상황에서 DataSource 설정 관련 동작이 원활하지 않아 Spring DataSource와 Transaction Manager에 대한 내용에 대해 정리한다. 본 포스팅의 내용은 인프런 김영한님의 데이터 접근 핵심 원리 강의에 기반한다.

DataSource

일반적으로 커넥션을 얻는 방법으로 JDBC DriverManager를 직접 사용하거나 HikariCP등의 커넥션 풀을 사용하는 방법이 존재한다. DriverManager를 통해 커넥션을 획득하는 경우 항상 신규 커넥션을 획득하게 되는데 이를 커넥션 풀을 사용하는 방법으로 변경하려면 어떻게 해야할까?

어플리케이션 로직에서 DriverManager를 사용하여 커넥션을 획득하다가 HikariCP등의 커넥션 풀을 사용하도록 변경하면 의존관계가 변경되기 때문에 어플리케이션 코드 또한 변경해줘야 한다. 따라서 스프링에는 커넥션을 획득하는 방법을 추상화 해두었고, 우리는 이를 이용하면 코드상의 변경없이 손쉽게 커넥션 획득 방법을 교체할 수 있다.

이렇게 추상화된 인터페이스가 바로 DataSource이다. DataSource는 커넥션을 획득하는 방법을 추상화해둔 인터페이스이고 내부적으로 커넥션 조회하는 기능을 가지고 있다.

public interface DataSource {
    Connection getConnection() throws SQLException;
}

대부분의 커넥션 풀은 DataSource 인터페이스의 구현체를 이미 만들어둔 상태이기에 우리는 HikariCP 커넥션 풀 등에 직접적으로 의존하는게 아니라 DataSource 인터페이스에만 의존하도록 코드를 작성하면 된다. 그리고 커넥션 풀을 교체하고 싶다면 다른 구현체로 갈아끼기만 하면 된다.

DriverManager

반면 DriverManager를 사용하는 경우에는 DataSource 인터페이스를 사용하지 않는다. 그렇기 때문에 풀 교체 시 관련된 어플리케이션 코드를 모두 교체해야하는데, 스프링은 이를 위해 DriverManagerDataSource라는 구현체를 제공해준다.

일반적으로 DriverManager를 통해 커넥션을 획득하는 경우 다음과 같은 코드를 사용한다.

Connection conn1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection conn2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);

위 conn1, conn2는 각각 별개의 커넥션이다. 이를 DriverManagerDataSource 사용으로 변경하면 다음과 같다.

DataSource datasource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
Connection conn1 = dataSource.getConnection();
Connection conn2 = dataSource.getConnection();

 

Connection Pool

HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("Pool");

HikariCP Connection Pool을 얻으려면 위처럼 먼저 정의해준다. (참고로 Hikari는 JDBC의존성을 설치하면 자동으로 추가된다) HikariDataSource는 내부적으로 DataSource 인터페이스를 구현하고 있다. 커넥션 풀에서 커넥션을 생성하는 작업은 어플리케이션 실행속도에 영향을 주기 때문에 별도의 스레드에서 동작한다.

Transaction Manager

JPA Transaction

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();

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;

}

스프링은 트랜잭션 추상화를 위해 PlatformTransactionManager라는 인터페이스를 제공한다. 해당 인터페이스는 getTransaction(), commit(), rollback() 이라는 3가지 메소드를 가지고 있다. 

  • DataSourceTransactionManager: JDBC 트랜잭션 관리
  • JpaTransactionManager: JPA 트랜잭션 관리
  • HibernateTransactionManager: Hibernate 트랜잭션 관리

또한 스프링은 위처럼 인터페이스를 구현한 구현체또한 제공해주고 있다. 따라서 필요에 따라 구현체만 갈아끼게 되면 원하는 형태로써 트랜잭션을 관리할 수 있다.

트랜잭션 동기화

스프링은 트랜잭션 동기화 매니저를 제공한다. 트랜잭션 매니저는 내부에서 트랜잭션 동기화 매니저를 사용하여 각 트랜잭션을 동기화한다. 간단하게 동작 방식을 정리하자면 다음과 같다.

  • 트랜잭션 매니저는 DataSource를 통해 커넥션을 만들고 트랜잭션을 시작한다.
  • 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관한다.
  • 레포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내어 사용한다.
  • 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션도 닫는다.

org.springframework.transaction.support.TransactionSynchronizationManager 를 확인해보면 트랜잭션 매니저가 ThreadLocal을 사용하고 있음을 확인할 수 있다. 따라서 멀티 스레드 환경에서도 안전하게 사용할 수 있다.

DataSourceUtils.getConnection();

커넥션을 얻어오려면 DataSourceUtils를 사용해야 한다. 해당 코드는 트랜잭션 동기화 매니저가 관리하는 커넥션이 있다면 해당 커넥션을 반환하고 없는 경우 새로운 커넥션을 생성하여 반환한다.

DataSourceUtils.releaseConnection(conn, dataSource);

위 코드는 트랜잭션을 사용하기 위해 동기화된 커넥션은 닫지 않고 그대로 유지해준다. 만약 트랜잭션 동기화 매니저가 관리하는 커넥션이 아닌 경우 해당 커넥션을 닫는다.

private final PlatformTransactionManager transactionManager;
...

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

try {
	logic();
    transactionManager.commit(status);
} catch(Exception e) {
	transactionManager.rollback(status);
} finally {
	// commit 또는 rollback시 트랜잭션 매니저가 자동으로 커넥션을 release해준다
}

간단하게 사용하는 코드를 살펴보자면 위와 같다.

동작 흐름

  1. TransactionManager.getTransaction()을 호출하여 트랜잭션을 시작한다.
  2. 트랜잭션 매니저는 내부에서 DataSource를 사용하여 커넥션을 생성한다.
  3. 커넥션을 수동 커밋 모드로 변경하여 실제 데이터베이스 커넥션을 시작한다.
  4. 커넥션을 트랜잭션 동기화 매니저에 보관한다.
  5. 트랜잭션 동기화 매니저는 ThreadLocal에 커넥션을 보관한다.
  6. 서비스는 비즈니스 로직을 실행하면서 레포지토리의 메소드들을 호출한다. 
  7. 레포지토리 메소드들은 트랜잭션이 시작된 커넥션이 필요하다. 레포지토리는 DataSourceUtils.getConnection()을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내 사용한다. 이 과정에서 ThreadLocal로 인해 동일한 커넥션을 사용하고 트랜잭션도 유지된다.
  8. 획득한 커넥션을 사용하여 SQL을 데이터베이스에 전달하여 실행한다.
  9. 비즈니스 로직이 끝나고 트랜잭션을 종료한다. (커밋 또는 롤백) 이 때 트랜잭션을 종료하려면 동기화된 커넥션이 필요하기 때문에 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
  10. 전체 리소스를 정리한다.
    1. 트랜잭션 동기화 매니저를 정리한다.
    2. conn.setAutoCommit(true)로 되돌린다.
    3. conn.close()를 호출하여 커넥션을 종료한다. 커넥션 풀인 경우 커넥션 풀에 반환된다.

스프링 부트의 자동 리소스 등록

DataSource

@Bean
DataSource dataSource() {
    return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}

@Bean
PlatformTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dataSource());
}

스프링 부트 이전에는 위처럼 DataSource와 Transaction Manager를 개발자가 직접 스프링 빈으로 등록하여 사용했다. 하지만 스프링 부트가 나오면서 많은 부분이 자동화되었고 이제는 더이상 개발자가 수동으로 위 클래스들을 등록하지 않아도 된다. 

스프링 부트는 DataSource를 자동으로 dataSource라는 이름으로 빈에 등록한다. (물론 개발자가 직접 빈으로 등록하는 경우 스프링 부트는 빈으로 등록하는 작업을 수행하지 않는다.) 이 때 스프링 부트는 아래와 같이 application 파일에 있는 속성을 사용하여 DataSource를 생성하고 빈에 등록한다.

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=username
spring.datasource.password=password

참고로 스프링 부트가 기본으로 생성하는 DataSource는 커넥션풀을 제공하는 HikariDataSource이다. 만약 spring.datasource.url속성이 없다면 내장 데이터베이스를 생성하려고 시도한다. 

TransactionManager

트랜잭션 매니저 또한 스프링 부트에 의해 자동으로 transactionManager라는 이름으로 등록된다. 이또한 개발자가 수동으로 등록한다면 스프링 부트는 자동으로 등록하지 않는다. 어떠한 구현체를 선택할지는 현재 등록된 라이브러리를 보고 판단하는데 그 종류는 아래와 같다.

  • JDBC: DataSourceTransactionManager
  • JPA: JpaTransactionManager
  • 둘 다 사용: JpaTransactionManager(JpaTransactionManager는 DataSourceTransactionManager가 제공하는 기능을 대부분 지원하기 때문)

 

출처: 인프런 스프링 핵심원리 - 고급편

반응형

'Coding > Java Spring' 카테고리의 다른 글

Spring AOP 개념 정리  (0) 2023.07.20
Spring 빈 후처리기 정리  (0) 2023.07.19
스프링 시큐리티 MVC 통합  (0) 2023.06.19
스프링 시큐리티 Authentication 아키텍처  (0) 2023.06.19
스프링 시큐리티 아키텍처  (0) 2023.06.19