스프링 DB - 트랜잭션 문제 해결

Heeeoh·2024년 2월 26일
0

스프링 DB

목록 보기
4/9
post-thumbnail

🌿 시작하기 앞서


스프링 부트 3.2.2 버전을 기준으로 작성됨
H2 데이터베이스 Version 2.2.224 (2023-09-17)
개념 위주로 정리 실질적인 실습은 강의 자료 참조

애플리케이션 구조

  • 프레젠테이션 계층
    • UI와 관련된 처리 담당
    • 웹 요청과 응답
    • 사용자 요청을 검증
    • 주 사용 기술: 서블릿과 HTTP 같은 웹 기술, 스프링 MVC
  • 서비스 계층
    • 비지니스 로직을 담당
    • 주 사용 기술 : 가급적 특정 기술에 의존하지 않고, 순수 자바 코드로 작성
  • 데이터 접근 계층
    • 실제 데이터베이스에 접근하는 코드
    • 주 사용 기술 : JDBC, JPA, File, Redis, Mongo..

순수한 서비스 계층

비지니스 로직이 들어있는 서비스 계층이 가장 중요하다.
비지니스 로직은 최대한 변경없이 유지되어야한다.

그렇기에 특정 기술에 종속적이지 않게 개발해야하고,
이로 인하여 비지니스 로직의 유지보수테스트가 쉬워진다.

🌱 문제점 및 해결


@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {

    private final DataSource dataSource;
    private final MemberRepositoryV2 memberRepository;


    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Connection con = dataSource.getConnection();
        try {
            con.setAutoCommit(false); //트랜잭션 시작
            // 비지니스 로직
            bizLogic(con, fromId, toId, money);
            con.commit(); // 성공시 커밋

        } catch (Exception e) {

            con.rollback(); //실패시 롤백
            throw new IllegalStateException(e);
        }finally {
            release(con);

        }


    }

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

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

}

트랜잭션은 비지니스 로직이 있는 서비스 계층에서 시작하는 것이 좋다.

문제는 트랜잭션을 사용하기 위해서 DataSource, Connection, SQLException 같은 JDBC 기술에 의존해야한다는 점이다.

JDBC -> JPA 기술로 바꾸면 서비스 코드도 모두 변경해야하고 비지니스 로직과 JDBC 기술이 섞여 있어 유지보수가 어렵다.



✔️ 문제 정리


  • 트랜잭션 문제
    • JDBC 구현 기술이 서비스 계층에 누수
    • 트랙잭션 동기화 (커넥션을 파라미터로 넘겨야함)
    • 반복이 많음
  • 예외 누수 문제
    • 데이터 접근 계층의 JDBC 구현 기술 예외가 서비스 계층으로 전파 (throws)
    • SQLException 체크 예외 (JDBC 기술)
    • 기술 전환시 기존 예외를 전부 변경해야하는 사태 발생
  • JDBC 반복 문제
    • 연결, 커밋, 롤백 등

스프링이 이러한 문제들을 해결해준다.


✔️ 트랜잭션 추상화


구현 기술마다 트랜잭션 사용 방법이 다르므로
트랜잭션을 추상화하여 해당 인터페이스를 의존하게 한다.

스프링은 이미 제공하고 있다.
PlatformTransactionManager 인터페이스다.

package org.springframework.transaction;

	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() : 트랜잭션 시작
    • 이름이 getTransaction인 이유는 기존에 이미 진행중인 트랜잭션이 있는 경우 해당 트랜잭션에 참여 가능하기 때문
  • commit() : 트랜잭션 커밋
  • rollback() : 트랜잭션 롤백

참고
JdbcTransactionManager 도 있다.
둘의 기능 차이는 크지 않다.


✔️ 트랜잭션 동기화


스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할

  • 트랜잭션 추상화 (앞단 참고)
  • 리소스 동기화
    • 트랜잭션 유지 조건은 시작 ~ 끝까지 같은 DB 커넥션 유지다.
    • 그렇기에 파라미터로 커넥션을 전달하는 방법을 사용
    • 이방법은 코드가 지저분해지고, 커넥션을 넘기는 메서드와 넘기지 않는 메서드를 중복해서 만들어야하는 등 단점이 많다.

스프링은 트랜잭션 동기화 매니저를 제공

  • 쓰레드 로컬(ThreadLocal)을 사용해서 커넥션을 동기화해준다. 트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용

  • 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션 동기화 가능하고 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면되기에 파라미터로 커넥션 전달은 필요없다.

동작 방식

  1. 트랜잭션 시작을 위한 데이터 소스를 통한 커넥션 생성 및 시작
  2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 동기화 매니저에 보관 (쓰레드 로컬)
  3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용 (DataSourceUtils.getConnection())
  4. 트랜잭션 종료시, 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고, 커넥션을 닫음

참고
쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여
따라서 해당 쓰레드만 해당 데이터에 접근 가능



✔️ 트랜잭션 매니저


DataSourceUtils.getConnection()

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

DataSourceUtils.releaseConnection()

  • 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지

  • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫음

private final PlatformTransactionManager transactionManager

트랜잭션 매니저를 주입받는다.

  • JDBC 기술이라면 DataSourceTransactionManager 구현체를 주입
  • JPA : JpaTransactionManager
 //트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(
                            new DefaultTransactionDefinition());

transactionManager 메서드

  • getTransaction()
    • 트랜잭션 시작
    • TransactionStatus status을 반환 (현재 트랜잭션의 상태 정보가 포함되어 있고 커밋, 롤백할 때 필요)
  • new DefaultTransactionDefinition()
    • 트랜잭션과 관련도니 옵션 지정
  • commit(status)
    • 트랜잭션 선공시 이 로직을 호출하여 커밋
  • rollback(status)
    • 문제 발생시 해당 로직을 호출해서 트랜잭션을 롤백

정리

  • 트랜잭션 추상화로 서비스 계층은 JDBC 기술에 의존하지 않는다.
  • 기술 변경시 의존관계주입만 다르게 변경해주면 된다.
  • java.sql.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);
}

해당 부분이 반복된다. 달라지는 부분은 비지니스 로직 뿐
템플릿 콜백 패턴을 활용하면 해결된다.

템플릿 콜백 패턴에 대한 공부는 따로 해야한다.

트랜잭션 템플릿

스프링은 TransactionTemplate 라는 템플릿 클래스 제공

public class TransactionTemplate {
	private PlatformTransactionManager transactionManager;
    
    // 응답 값이 있을 때 사용
	public <T> T execute(TransactionCallback<T> action){..}
    // 응답 값이 없을 때 사용
	void executeWithoutResult(Consumer<TransactionStatus> action){..}
}

code

private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepository;

public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
    this.txTemplate = new TransactionTemplate(transactionManager);
    this.memberRepository = memberRepository;
}


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

    txTemplate.executeWithoutResult((status) -> {
        // 비즈니스 로직
        try {
            bizLogic(fromId, toId, money);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    });
}
  • 트랜잭션 템플릿이 트랜잭션 시작과 커밋 롤백을 대신 처리
  • 람다에서 체크 예외를 밖으로 던질 수 없기에 언체크 예외로 바꾸어 던지도록 예외 전환

트랜잭션 템플릿의 기본 동작

비지니스 로직이 정상 수행되면 커밋
언체크 예외가 발생하면 롤백 , 그 외에는 커밋

정리

  • 트랜잭션 템플릿으로 인한 반복 코드 제거
  • 하지만 아직 트랜잭션 처리 기술 로직이 서비스 계층에 있다.
  • 핵심 기능은 비지니스 로직, 트랜잭션은 부가 기능에 해당
  • 두 로직이 한 곳에 있으면 코드 유지보수가 어려워짐
  • 서비스 로직은 가급적 핵심 비지니스 로직만 있어야 한다.



✔️ 트랜잭션 AOP 이해


스프링 AOP를 통해 프록시 도입하면 위의 문제를 해결 가능

프록시로 인해 트랜잭션 처리 객체비지니스 로직 처리 서비스 객체를 명확하게 분리 가능

스프링에서 트랜잭션 AOP @Transactional 애노테이션 제공

스프링 AOP에 대해서는 공부가 필요하다. 추후에 공부할 것



✔️ 트랜잭션 AOP 적용


@Transactional
public void accountTransfer(String fromId, String toId, int money){
    bizLogic(fromId, toId, money);
}

이로써 순수한 비지니스 로직만 남게 되었다.
@Transactional 애노테이션은 메서드나 클래스에 붙여도 된다. 클래스에 붙이면 외부에서 호출 가능한 public 메서드가 AOP 적용 대상이 된다.

// test에서 AOP 프록시 적용 확인
Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();

// 결과 
memberService class=class hello.jdbc.service.MemberServiceV3_3$
$EnhancerBySpringCGLIB$$.. //(프록시(CGLIB))



✔️ 트랜잭션 AOP 정리


선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리

  • 선언적 트랜잭션 관리 (Declarative Transaction Management)
    • @Transactional 애노테이션 하나만 선언하여 편리하게 트랜잭션을 적용하는 것
  • 프로그래밍 방식의 트랜잭션 관리 (programmatic transaction management)
    • 트랜잭션 매니저, 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것



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


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

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

기존에는 데이터소스와 트랜잭션 매니저를 직접 스프링 빈으로 등록했어야 했다. -> 현재) 스프링 부트의 자동화

데이터 소스 - 자동 등록

스프링 부트는 데이터소스를 스프링 빈에 자동으로 등록
자동으로 등록되는 스프링 빈 이름 : dataSource

당연하지만 개발자가 직접 등록하면 스프링부트는 자동 등록하지 않는다.

application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
  • 스프링 부트가 기본으로 생성하는 데이터소스는 커넥션 풀을 제공하는 HikariDataSource이다. 커넥션 풀과 관련된 설정도 application.properties 를 통해서 지정 가능

  • spring.datasource.url 속성이 없으면 내장 데이터베이스(메모리 DB)를 생성하려고 시도


트랜잭션 매니저 - 자동 등록

스프링 부트는 적절한 트랜잭션 매니저 (PlatformTransactionManager) 를 자동으로 스프링 빈에 등록
자동으로 등록되는 스프링 빈 이름 : transactionManager

트랜잭션 매니저 선택 기준은 현재 등록된 라이브러리

  • JDBC : DataSourceTransactionManager
  • JPA : JpaTransactionManager
  • 둘다 사용시 : JpaTransactionManager

🔖 학습내용 출처

스프링 DB 1편 - 데이터 접근 핵심 원리

profile
열심히 살자

0개의 댓글