트랜잭션?
https://anythingis.tistory.com/171
주로 DB에서 활용되는 개념이지만, 애플리케이션에서 데이터를 다루고 DB에 저장하기에, DB에서 커밋이 되거나 롤백 된 후 비즈니스 로직까지 연계된 로직을 처리하기 위해 스프링도 트랜잭션 기능을 제공합니다.
스프링 트랜잭션
스프링은 다양한 DB 접근 기술과 같이 활용할 수 있습니다.
JDBC
public void accountTransfer(String fromId, String toId, int money) throwsSQLException {
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);
}
}
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(); //엔티티 매니저 팩토리 종료
}
하지만 각가의 데이터 접근 기술들은 트랜잭션을 처리하는 방식에 차이가 있어 위와 같이 활용하면 DB접근 기술의 변화는 트랜잭션을 활용하는 코드를 모두 수정하는 결과를 초래합니다.
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;
}
트랜잭션 획득, 커밋, 롤백으로 추상화합니다.
실제 트랜잭션을 활용하는 코드에서는 PlatfornTransactionManager를 활용하면 DB 접근 기술이 변화해도 사용하는 DB 접근 기술을 확인하고 그에 맞는 인터페이스 구현체 갈아끼는 방식으로 코드의 변화가 필요 없습니다.
스프링 트랜잭션 관리
프로그래밍 방식 트랜잭션 관리
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
트랜잭션 관리 로직(획득, 커밋, 롤백)과 비즈니스 로직이 같은 코드내에 존재하는 방식이 프로그래밍 방식 트랜잭션 관리입니다.
코드를 보며 의도 파악은 빠르게 할 수 있지만 관점분리가 안되어 복잡성이 증가합니다.
선언형 트랜잭션 관리
@Service
@Transcational
public class UserService {
}
@Transactional을 통해 메소드나 클래스가 트랜잭션을 사용하는 것을 나타내는 방식이 선언형 트랜잭션 관리입니다.
트랜잭션 관리를 AOP 관점분리 추상화를 통해 개발자는 비즈니스 로직에 집중할 수 있습니다. 하지만 추상화된 부분이 어떻게 동작하는지 알아야 예상한대로 동작하지 않는 논리적 오류가 발생했을 때 디버깅을 할 수 있습니다.
스프링 트랜잭션과 프록시
간편하게 비즈니스 로직에 집중할 수 있게 해주는 선언형 트랜잭션 관리를 활용하면서 스프링 AOP를 활용하는 프록시 객체로 한단계의 추상화합니다.
프록시 도입전
프록시 도입후
트랜잭션과 비즈니스 로직의 관점 분리를 위해 트랜잭션을 관리하는 프록시 객체를 생성하고, 비즈니스 로직은 원래 객체를 참조합니다.
프록시 도입 후 트랜잭션 흐름
트랜잭션 적용 확인
선언형 트랜잭션 관리를 활용하는 메소드와 그렇지 않은 메소드 내에서 트랜잭션 동기화 매니저에 접근해 트랜잭션이 활성화중인지 체크할 수 있습니다.
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Slf4j
static class BasicService{
@Transactional
public void tx(){
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active = {}", txActive);
}
public void noneTx(){
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active = {}", txActive);
}
}
@Autowired
BasicService basicService;
@TestConfiguration
static class TxApplyBasicConfig{
@Bean
BasicService basicService(){
return new BasicService();
}
}
@Test
void txTest(){
basicService.tx();
basicService.noneTx();
}
}
2024-08-02T14:45:07.981+09:00 INFO 4728 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : call tx
2024-08-02T14:45:07.981+09:00 INFO 4728 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : tx active = true
2024-08-02T14:45:07.985+09:00 INFO 4728 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : call tx
2024-08-02T14:45:07.985+09:00 INFO 4728 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : tx active = false
트랜잭션을 감지하는 패키지의 로깅 레벨을 TRACE로 낮춰서 로그를 찍어본다면 트랜잭션 획득과 커밋 로그를 볼 수 있습니다.
application.properties 추가
logging.level.org.springframework.transaction.interceptor=TRACE
2024-08-02T14:53:50.355+09:00 TRACE 11908 --- [SrpingDB2] [ Test worker] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.srpingdb2.TxBasicTest$BasicService.tx]
2024-08-02T14:53:50.355+09:00 INFO 11908 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : call tx
2024-08-02T14:53:50.356+09:00 INFO 11908 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : tx active = true
2024-08-02T14:53:50.356+09:00 TRACE 11908 --- [SrpingDB2] [ Test worker] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.srpingdb2.TxBasicTest$BasicService.tx]
2024-08-02T14:53:50.358+09:00 INFO 11908 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : call tx
2024-08-02T14:53:50.359+09:00 INFO 11908 --- [SrpingDB2] [ Test worker] c.e.srpingdb2.TxBasicTest$BasicService : tx active = false
빈을 생성할 때 클래스, 메소드의 트랜잭션 여부를 스캔하고 하나라도 적용이 되어 있다면 트랜잭션 프록시 빈 생성합니다. 만약 트랜잭션 적용이 아예 없는 빈은 프록시 객체가 아닌 순수한 객체가 애플리케이션 컨텍스트에 적용됩니다.
메소드 호출 시마다 메소드의 트랜잭션 사용여부를 확인하고
1. 사용한다면 트랜잭션을 실행하고 비즈니스 로직을 실행합니다.
2. 사용하지 않으면 순수한 비즈니스 로직만 실행합니다.
프록시 객체는 기존 객체를 상속해서 사용하기에 다형성을 통해 의존성 주입을 받아 활용할 수 있습니다.
트랜잭션 AOP 주의사항
내부 호출
1. 클래스에 트랜잭션이 붙는 경우가 아니고
2. 트랜잭션 적용 안된 메소드에서 트랜잭션 메소드를 호출할 때
트랜잭션이 적용되지 않습니다.
그 이유는 프록시 객체를 호출하고 트랜잭션이 적용안된 메소드를 실행하면 트랜잭션 없이 실제 객체의 비즈니스 로직을 호출한 뒤, 내부 로직을 호출할 때 별다른 키워드가 없으면 this.method()를 호출하기 때문입니다.
@Slf4j
@SpringBootTest
public class InternalTest {
@TestConfiguration
static class InternalConfig{
@Bean
public InternalService internalService(){
return new InternalService();
}
}
@Slf4j
static class InternalService{
public void external(){
log.info("call external");
printTxInfo();
internal();
}
@Transactional
public void internal(){
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
@Autowired InternalService internalService;
@Test
public void callExternal(){
internalService.external();
}
@Test
public void callInternal(){
internalService.internal();
}
}
InternalTest$InternalService : call external
InternalTest$InternalService : tx active=false
InternalTest$InternalService : call internal
InternalTest$InternalService : tx active=false
해결방법
가장 쉬운 해결방법은 트랜잭션을 타는 메소드를 클래스를 분리해서 다시 프록시 객체를 호출해 트랜잭션 관리 로직을 호출하게 하는 것입니다.
Only public 접근제한자 트랜잭션 AOP 적용
private, package, protected 등의 접근제한자는 트랜잭션 AOP 적용이 제한됩니다.
이는 스프링이 막아둔 것으로 의도하지 않는 곳까지 트랜잭션이 과도하게 적용되는 것을 막기위해서입니다.
트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 외부에 열어준 곳을 시작점으로 활용하기 때문에 public 메서드에만 트랜잭션을 적용하도록 설정되어 있습니다.
@Transactional
public class Hello {
public method1(); // TX 적용
method2(): // TX X
protected method3(); // TX X
private method4(); // TX X
}
하지만 예외가 발생하는 것이 아닌 트랜잭션 적용만 무시되므로, 외부 호출 시작점이 public 메소드가 아닐 때 주의해야합니다.
초기화 시점에 트랜잭션 활용시
@PostConstruct와 같이 빈 초기화 시에 실행되는 메소드 이후에 트랜잭션 AOP가 적용되기에 초기화 시점에서 트랜잭션을 획득할 수 없습니다.
해결방법
@EvenListener(ApplicatonReadyEvent.class)을 활용해 애플리케이션이 가동될 때로 메소드 호출을 지연시키면 됩니다.
정리
스프링의 트랜잭션 추상화 - 플랫폼 트랜잭션 매니저선언형 트랜잭션 적용 - AOP 활용한 프록시 객체주의점 - 동일 클래스 트랜 x 메소드 -> 트랜 O 메소드 호출 시 트랜잭션 적용 X
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard