본문 바로가기

연재작/Database

Transaction Deep Dive (5)

 

이제 transaction 추상화의 최종장이라고 할 수 있는 부분까지 도달했다.

지난번 까지의 글의 흐름에서는 아래와 같은 순서로 추상화를 진행해왔다.

  1. JDBC API를 사용한 Transaction의 구현 : Connection, Statement, ResultSet
  2. TransactionSynchronizationManager를 통한 Transaction 동기화의 추상화
    + DataSourceUtils를 사용한 connection의 공유
  3. PlatformTransactionManager를 통한 Transaction 관리, connection등 resource의 추상화
    + DataSourceUtils를 사용한 connection의 공유
  4. PlatformTransactionManager와 JdbcTemplate을 통한 최종적인 추상화 + connection 내재화

 

 

1. JPA를 통한 Transaction 관리

 

 

이제부터 설명하는 것은 JPA를 사용할 때 사용하는 Transaction의 관리를 설명한다.

엄밀히 말하자면, JPA는 Transaction이 아닌, Persistence Context를 다루는 것이기 때문에 조금 다르긴 하다.

그렇지만 JPA를 사용할 때의 추상화 방식도 존재하기 때문에 소개하겠다.

 

코드는 매우 단순하다.

EntityManagerFactory로부터 EntityManager를 얻어오고, 이를 통해서 Transaction을 가져온다.

commit or rollback후 EntityManager를 닫는다.로 요약 가능하다.

public void saveMyEntityToTheDatabase(MyEntity entity) {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction tx = entityManager.getTransaction();
    try {
        tx.begin();
        entityManager.persist(entity);
        tx.commit();
    } catch (RuntimeException e) {
        if(tx != null && tx.isActive()) {
            tx.rollback();
        }
        throw e;
    } finally {
        entityManager.close();
    }
}

 

여기서 중요한 것이 몇 가지가 있다.

  • EntityManagerFactory는 전역에서 단 한 번만 선언되어야 하고, Tomcat의 생명 주기와 일치 해야 한다.
    ➡️ Connection Pool에 대한 Resource관리 및 각각의 영속성 컨텍스트의 생명주기를 담당하기 때문이다.
    ➡️ Connection Pool을 하나 이상 선언하지 않는 것과 동일한 이유이다.
    • 따라서 EntityManagerFactory는 Bean으로 등록해 싱글턴으로 관리하는 것이 좋다.
    • 그러나 Spring Data JPA를 사용하는 환경이라면, spring.datasource와 spring.jpa로 시작하는 설정들을 읽어 데이터 소스 및 JPA설정을 기반으로 EntityManagerFactory를 반환하기에, Bean의 등록이 없어도
      싱글턴 객체로 Bean 의존성 주입을 하는 것이 가능하다.
  • @Entity로 등록된 Entity를 persist해준 후, commit을 해주어야만 변경사항이 DB에 적용된다는 점
    Persistence Context등과 관련된 내용은 JPA와 관련된 여러 작성 글들을 참고하기 바람.
  • try - catch 구문이 아직도 적용이 된다는 것.
    • 참고로 EntityManager는 AutoCloseable이기 때문에 try-with-resource 구문을 통해 아래와 같이 적용해도 된다.
public void saveMyEntityToTheDatabase(MyEntity entity) {
    EntityTransaction tx = null;
    try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
        tx = entityManager.getTransaction();
        tx.begin();
        entityManager.persist(entity);
        tx.commit();
    } catch (RuntimeException e) {
        if (tx != null && tx.isActive()) {
            tx.rollback();
        }
        throw e;
    }
}

 

 

가장 중요한 것은 EntityManager와 Transaction의 생명주기가 선언된 것과 별도의 것일 수 있다는 점이다.

EntityManager의 내부에서 Transaction을 get해왔으니

얼핏보면 EntityManager의 생명주기가 Transaction보다는 길 것이라고 생각할 수 밖에 없다.

심지어 commit 혹은 rollback은 transaction의 종료를 의미하므로 close()가 호출되는 EntityManager보다 짧아 보인다.

 

그러나 이는 위에 적용된 내용이 단순한 물리 트랜잭션이라고 가정했을 때의 얘기이다.

지금 여기서 getTransaction은 Transaction이 물리 트랜잭션인지, 논리 트랜잭션인지 알지 못한다.


예를 들어서 아래와 같이 호출되는 부분이 하나의 물리 트랜잭션의 내부라면

여기서의 getTransaction은 논리 트랜잭션을 가져온 것이라고 보아야 하기 때문에 단정할 수 없다.

 

 

 

2. Aspect Oriented Programming

 

마지막으로 해야하는 것은 여태까지 사용해왔던 Transaction과 관련된 코드에 대한

통찰이 조금 필요하다. 큰 그림을 보았을 때, 핵심 로직과 부가 로직으로 나눠본다면

개발자가 하고 싶은 것은 핵심 로직만을 생각하고 싶고, 부가 로직은 안하고 싶은 게 당연하다.

AOP는 그러한 관점에서 우리를 도와준다.

 

비즈니스 로직을 제외한 Transaction의 생성, commit 혹은 rollback과

EntityManager의 관리, try-catch구문과 같은 부가 로직은 핵심 로직을 제외한 "횡단 관심사"라고 볼 수 있다.

따라서 AOP를 통해 부가 로직을 annotation과 같은 방식으로 관리할 수 있고,

Spring 에서는 Spring AOP를 통해 이를 적용한다.

https://namucy.tistory.com/81

 

Aspect Oriented Programming

요즘 DB와 Transaction에 대한 공부를 하면서 AOP라는 흥미로운 개념에 대해 배우게 됐다.방식 자체도 특이하지만 기존에 배웠던 Lombok과 유사한 것처럼 보여서 해당 개념을 설명하고 어떤 부분이 다

namucy.tistory.com

 

이에 대한 원리와 설명은 위의 글에서 충분히 했기 때문에 바로 첫 번째 방식으로 넘어가겠다.

 

 

 

3. @Transactional : 선언형 트랜잭션

 

 

간단하다. Spring AOP를 통해 만들어진 선언형 트랜잭션을 사용하면 되는 것이다.

@Transactional이라는 메서드를 해당 메서드에 붙이기만 한다면 다음과 같은 부가로직, aspect를

자동적으로 추가해준다. 

  1. 트랜잭션의 생성 혹은 참여
  2. EntityManager의 관리
  3. try-catch 구문을 통한 commit / rollback의 동기화

이에 따라서 개발자는 핵심 로직에만 관심을 가지면 된다.

위의 코드를 @Transactional을 통해서 줄여보자면 아래와 같이 바뀐다.

private final EntityManager entityManager; // EntityManager 의존성 주입

@Transactional
public void saveMyEntityToTheDatabase(MyEntity entity) {
    entityManager.persist(entity);
}

 

말도 안되게 짧게 바뀐다. persist로 새로운 entity를 영속성 컨텍스트에 등록하기만 한다면

트랜잭션의 commit, rollback을 자동으로 이뤄지도록 한다.

 

 

 

4. @Transactional의 옵션

 

  • propagation : 물리 트랜잭션 / 논리 트랜잭션 및 트랜잭션 동기화를 위한 옵션
    • Propagation.REQUIRED : 물리 트랜잭션이 없을 경우 이를 생성
      있을 경우, 해당 물리 트랜잭션에 논리 트랜잭션으로 참여
    • Propagation.REQUIRES_NEW : 기존에 물리 트랜잭션의 유무와 상관 없이
      새로운 물리 트랜잭션을 생성, 즉 새로운 connection을 생성한다. (HikariCP 사용의 경우)
    • Propagation.NESTED : 물리 트랜잭션이 없다면 새로 생성,
      있다면 물리 트랜잭션에 참여하긴 하나,
      논리 트랜잭션이 물리 트랜잭션의 rollback여부에 영향을 주지 않음
      다만, 물리 트랜잭션이 rollback되는 경우에는 논리 트랜잭션에 영향을 준다.
    • 그 외에도 SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER가 있다.
  • isolation : 트랜잭션의 고립 수준
    • Isolation.DEFAULT : 데이터베이스의 기본 격리수준을 사용
      • Oracle, MSSQL, PostgresQL : READ COMMITTED
      • MySQL(Inno DB) : REPEATABLE READ
    • Isolation.READ_UNCOMMITTED
    • Isolation.READ_COMMITTED
    • Isolation.REPEATABLE_READ
    • Isolation.SERIALIZABLE
  • readOnly : 해당 트랜잭션이 읽기 전용인지 유무
    • true : R만 허용, CUD(INSERT, UPDATE, DELETE) 수행 시 런타임 오류 발생
    • false : CRUD 전부 다 허용. DB 조작 및 동기화 허용
  • timeout : 트랜잭션 타임아웃 (트랜잭션이 이 시간 이상 걸릴 때 트랜잭션 rollback.)
    • 참고로, connection의 타임아웃도 존재하기에 이 두 개를 조합해서 타임 아웃 설정이 가능하다.
    • 기본 값은 -1(없음), 단위는 초 단위이다.
  • rollbackFor : 해당하는 값의 class가 생성될 경우, 트랜잭션을 롤백한다.
    • Checked Exception의 경우, 예외가 발생하더라도 트랜잭션이 중지되지 않는다.
      그러나, rollbackFor = CustomChekcedException.class 와 같이 설정할 경우,
      CustomCheckedException이 발생하게 된다면 이를 롤백하는 것이 가능하다.
  • noRollbackFor : 해당하는 값의 class가 생성되더라도 롤백하지 않는다.
    • Runtime Exception이 발생한다면 무조건 트랜잭션은 롤백된다.
      그러나, noRollbackFor = CustomRuntimeException.class로 설정한다면
      CustomRuntimeException이 발생하더라도 롤백하지 않는다.

 

 

5. 프로그래밍형 트랜잭션  : TransactionalTemplate

 

 

위에 링크에 얘기한 이전 글을 참고할 경우, @Transactional을 통한 spring AOP는 

Cglib이나 Jdk Dynamic Proxy를 통해서 Proxy객체를 생성하기 때문에

private함수의 내부 호출이나 final을 통한 재정의 불가 메서드가 로직에 들어가 있을 경우,

이에 대한 프록시 객체를 만들지는 못한다.

그러한 메서드가 존재할 경우, TransactionTemplate을 통해서 프로그래밍을 해주어야 한다.

사용 방법은 @Transactional을 약간 풀어쓴 것이라고 보면 된다.

 

  1. PlatformTransactionManager를 통한 transactionManager 정의
  2. TransactionTemplate에 transactionManager를 파라미터로 해서 생성
  3. TransactionTemplate에 위에 설명한 transaction의 옵션(definition)을 설정
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.setTimeout(-1);
transactionTemplate.setReadOnly(false);

이렇게 할 경우, Bean으로 등록해서 아래와 같이 사용할 수 있다.

@Service
@RequiredArgsConstructor
public class SimpleService {
	  private final TransactionTemplate transactionTemplate;
	
	  public void method() {
        String result = transactionTemplate.execute((status) -> {
            jdbcTemplate.update(...);
            jdbcTemplate.update(...);
						return "";
        });
	  }
}

 

 

 

 

6. Spring Data JPA를 통한 트랜잭션 메서드의 추상화

 

이제부터는 트랜잭션의 추상화라기 보다는,  트랜잭션을 사용하는 메서드의 추상화를 소개하겠다.

Spring Data JPA를 사용할 경우

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

가 build.gradle 의존성에 추가되어야 하고, 이를 사용한다면 JPARepository라는 인터페이스를 통해

메서드를 구체화하지 않아도 사용이 가능하다.

 

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    @Transactional
    Optional<User> findById(Integer id);

    @Transactional
    List<User> findAll();

    @Transactional
    User save(User entity);

    @Transactional
    void deleteById(Integer id);
}

트랜잭션의 설정은 위와 동일하고,

해당하는 Entity와 이에 대해 JpaRepository를 extends하는 위와 같은 쿼리 메서드의 설정을 하면 된다.

추가적으로 findById와 같은 단일 entity 객체를 찾는 경우에는 Optional이 붙는다는점을 주의해야 할 것이다.

또한, 웬만한 메서드는 다 있지만 이를 잘 확인해서 만들어야 한다는 것도 주의해야 한다.

(가령, 단순 save는 있지만 단순 update는 없다.)

 

추가적으로 세세한 쿼리를 요하는 메서드의 경우

@Query를 추가해서 다음과 같이 사용할 수 있다.

 

public interface MemberRepository extends JpaRepository<Member, Long> {
		@Query("select m from Member m where m.username = ?1")
		Member findByUserName(Stirng username);
} // 위치 기반 파라미터 바인딩

public interface MemberRepository extends JpaRepository<Member, Long> {
		@Query("select m from Member m where m.username = :username")
		Member findByUserName(@Param("username") Stirng username);
} // 이름 기반 파라미터 바인딩

public interface MemberRepository extends JpaRepository<Member, Long> {
		@Query(value = "SELECT * FROM member WHERE username = :username", nativeQuery = true)
		Member findByUserName(@Param("username") Stirng username);
} // Native Query

 

이를 잘 다루기 위해서는 JPQL 혹은 이를 사용한 외부 라이브러리인 QueryDSL을 사용한다면

좋을 것이지만 이것은 다른 글에서 설명하겠다.

 

여기까지가 Transaction과 Transaction Synchronization이 고수준으로 추상화된 과정을 다루는 과정이며,

현업에서 주로 사용되는것은 JPA, Spring Data JPA, JPQL, QueryDSL등이라고 한다.

사용하는 것 자체는 매우 쉬워보이지만

 

고수준화에 오기까지 수많은 원리의 이해가 필요했다는 것을 염두에 두고,

문제가 생긴다면 해당하는 원리를 잘 생각해봐야 할 것이다.

'연재작 > Database' 카테고리의 다른 글

Persistence Context의 Entity 상태 관리, Life-Cycle  (1) 2024.11.28
Transaction Deep Dive (4)  (0) 2024.11.26
Transaction Deep Dive (3)  (0) 2024.11.23
Transaction Deep Dive (2)  (0) 2024.11.22
Transaction Deep Dive (1)  (0) 2024.11.20