프로필사진

Go, Vantage point

가까운 곳을 걷지 않고 서는 먼 곳을 갈 수 없다.


Github | https://github.com/overnew/

Blog | https://everenew.tistory.com/





티스토리 뷰

반응형

 

 

 

* 김영한님의 스프링 DB 2편 강좌를 수강하며 정리한 글입니다. *

 

스프링 DB 2편 - 데이터 접근 활용 기술 - 인프런 | 강의

백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의 소개 | 인

www.inflearn.com

 

 

 

이전 글에서는 Spring JPA Data와 Querydsl을 정리하였다.

 

 

 

@Transactional

 

스프링은 PlatformTransactionManager로 여러 트랜잭션 구현체를 추상화해 두었다.

스프링 부트는 사용 기술에 따라 자동으로 TransactionManager를 등록해준다.

 

스프링은 선언적 트랜잭션 관리를 사용해, @Transactional 애노테이션을 이용한다.

이러한 방식은 프록시 AOP가 적용되어 트랜잭션과 비즈니스 로직을 분리할 수 있다.

 

 

이처럼 트랜잭션 동기화 매니저를 이용하여 커낵션은 같은 트랜잭션을 유지한다.

 

 

 

@Transactional이 적용된 클래스를 스프링 빈으로 등록한다고 가정하자.

@TestConfiguration
static class TxApplyBasicConfig{
    @Bean
    BasicService basicService(){
        return new BasicService();
    }
}

@Slf4j
static class BasicService{
    @Transactional
    public void tx(){
        log.info("call tx");    //트랜잭션 적용이 맞는지 확인
        boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("tx active ={}", txActive);
    }

    public void nonTx(){	//프록시는 클래스 대상으로 만들어짐, 트랜잭션 적용은 x
        log.info("call nontx");
        boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("tx active ={}", txActive);
    }
}

 

이런 경우 스프링의 트랜잭션 AOP가 프록시를 만들어 빈으로 생성한다.

프록시는 BasicServive를 상속받아 만들어지기 때문에 BasicServive 타입으로도 사용이 가능하다.

이후 컨테이너에서 가져오는 BasicServive 빈은 모두 프록시가 적용된 객체로 보면 된다.

 

 

스프링은 애노테이션이 많다 보니 중복되는 애노테이션이 클래스와 메서드에 서로 다르게 적용될 수 있다.

스프링은 항상 더 구체적인 것이 높은 우선순위를 가진다.

메서드에 적용한 애노테이션은 클래스보다 더 구체적이므로, 메서드에 적용된 것이 실제로 적용된다.

 

@Slf4j
@Transactional(readOnly = true) //쓰기 작업 방지
static class LevelService{

    @Transactional(readOnly = false)	//더 구체적, false가 default
    public void write(){
        log.info("call write");
        printTxInfo();
    }

    public void read(){		//클래스의 적용을 따름
        log.info("call read");
        printTxInfo();
    }
    
    private void printTxInfo(){
    	...
    }
}

 

만약 인터페이스에 @Transactional을 적용한다면 클래스에 의해 우선순위가 밀리게 된다.

단, 인터페이스의 적용은 오류가 발생할 가능성이 있어 적용을 지양하자.

 

 

 

 

트랜잭션 AOP 문제

 

개발하다 보면 분명히 @Transactional을 적용했는데, 적용이 안 돼있거나 롤백이 안 되는 문제들이 발생한다.

이를 방지하기 위해 트랜잭션 AOP의 프록시 내부 호출을 이해해보자.

 

 

위의 실행 그림에서 봤듯이 트랜잭션 어노테이션 적용 시 항상 프록시를 통해 비즈니스 로직이 호출된다.

 

트랜잭션 적용시 컨테이너에는 프록시 객체가 빈으로 등록되어 다른 곳에 주입된다.

따라서 외부에서 직접 비지니스 로직을 호출해 트랜잭션이 적용되지 않는 일은 없다.

하지만 비즈니스 로직 내부에서 메서드를 호출한다면 문제가 발생한다.

 

 

@Slf4j
static class CallService{
    public void external(){     //external 접근시 트랜잭션 적용 x
        log.info("call external");
        printTxInfo();
        internal();
    }

    @Transactional
    public void internal(){     //트랜잭션 적용이 필요한 영역을 가정
        log.info("call internal");
        printTxInfo();
    }
}

 

외부에서 internal()을 호출한다면 정상적으로 트랜잭션 프록시가 적용된다.

 

 

하지만 external()을 외부에서 호출하면 @Transactional이 적용되지 않은 메서드이기 때문에 트랜잭션이 없이 시작된다.

external()의 내부에서 internal()이 호출되어도 여전히 트랜잭션이 진행되지 않는다.

사실 자바에서 메서드 앞에 참조가 없다면, this.이 생략되어 있는 것이다.

this.internal()이 호출되면, 프록시 객체가 아닌 실제 객체의 internal()로 호출된다.

 

이것이 @Transactional로 적용한 트랜잭션 AOP의 한계이다.

 

 

 

트랜잭션 AOP 문제 해결

 

가장 간단한 해결 방법은 내부 호출을 끄집어내 클래스로 만들어, 외부 호출로 바꾸는 것이다.

 

@Slf4j
@RequiredArgsConstructor
static class CallService{

    private final InternalService internalService;	// 프록시 객체를 주입받음

    public void external(){     //external 접근시 트랜잭션 적용 x
        log.info("call external");
        internalService.internal();	//프록시 객체로 부터 호출
    }
}

static class InternalService{

    @Transactional
    public void internal(){     //트랜잭션 적용이 필요한 영역을 가정
        log.info("call internal");
    }
}

 

 

 

@Transactional는 public 에만

 

@Transactional는 public 메서드에만 적용이 된다.

private와 같은 내부 로직은 트랜잭션을 걸 필요가 없는 경우가 많다. 이런 경우 비용 낭비가 발생하기 쉽자.

public과 같은 외부 호출로 시작하는 곳에만 트랜잭션이 걸리는 게 옳다.

 

 

 

 

초기화 주의 사항

 

@PostConstruct는 의존성 주입이 끝나고 초기화 작업이 실행하기 위해 적용한다.

만약 @PostConstruct와 @Transactional을 같은 메서드에 적용하면, 해당 메서드는 트랜잭션에서 동작하지 않는다.

그 이유는, 초기화가 끝나고 트랜잭션  AOP가 적용되기 때문이다.

 

만약 트랜잭션으로 초기 설정을 원한다면 @EventListener를 사용하자.

 

@EventListener(ApplicationReadyEvent.class) 	//모든 스프링 초기 작업이 끝난 시점에 호출됨
@Transactional
public void initV2(){
		//...
}

 

 

 

스프링 트랜잭션 옵션

 

@Transactional("매니저 이름")으로 사용한 트랜잭션 매니저를 설정할 수 있다. 매니저가 하나라면 생략 가능하다.

 

트랜잭션의 timeout도 직접 설정 가능한다.

 

readOnly 옵션으로 읽기 전용 트랜잭션으로 만들 수 있다.

기본으로는 읽기 쓰기 모두 사용이 가능하나, DB에 따라 지원하지 않는 경우가 있다.

읽기 전용은 성능 최적화에 중요하다.

읽기 전용 트랜잭션이 끝나면 변경된 부분을 찾아 commit을 해야 할 필요가 없어지고, 다른 트랜잭션이 쓰기 완료를 대기할 필요가 없어진다.

JDBC는 읽기 전용 커낵션을 획득해 사용하기도 한다.

 

 

 

스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션의 커밋과 롤백 여부가 결정된다.

기본적으로 런타임 예외는 롤백, 체크 예외는 커밋되는데,

@Transactional(rollbackFor = 체크예외.class) 로 지정한 체크 예외에서도 롤백할 수 있다.

 

체크 예외가 커밋되는 이유는 스프링의 기본적인 예외 가정 때문이다.

스프링은 체크 예외를 비즈니스에서 의미가 있는 예외, 런타임 예외는 복구가 불가능한 시스템 예외로 판단한다.

 

여기서 비즈니스의 의미가 있다는 것을 예시를 들어보자.

결제 시스템에서 계좌에 잔액이 부족하다고 생각하자.

주문 데이터까지 롤백을 한다면 고객 입장에서는 처음부터 다시 진행해야 할 것이다.

이는 시스템은 정상적으로 동작하지만 잔액이 부족한 비즈니스 상황의 문제이다.

이런 문제는 반드시 예외로 잡아 처리해야 하므로 체크 예외를 사용한다.

 

 

 

반응형
댓글
반응형
인기글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함