본문 바로가기
스프링부트

영속성 컨텍스트와 트랜잭션의 관계

by pius712 2024. 2. 18.

1. 영속성 컨텍스트와 트랜잭션의 관계

spring data jpa 만 사용하다보니, typescript의 mikro orm 사용하다가 헷갈리는 개념이 있었다.

그것은 바로 머릿속에 다른 것들은 지워지고, persistence context 는 당연히 transaction 단위로 할당 라는 개념만 남아있었던 것이다.

이것은 일부는 맞고, 일부는 틀린 얘기인데, spring 과 같은 컨테이너 환경에서 jpa 는 트랜잭션당 persistence context 를 가지고 있다.

즉, spring 환경 기준에서는 아래의 식이 맞으나 (OSIV 를 옵션을 끈 상태라면) , 다른 경우에는 아닐 수도 있다.

transaction : persistence context = 1 : 1

만약 transaction 이 commit 혹은 rollback 된 경우(transactional 범위를 벗어난 경우), entity 는 detached(준영속) 상태가 된다.

entity 가 detached 상태가 되었다는 것은 더 이상 트랜잭션과 관련없이 persistence context 에 의해 관리되지 않기 때문에, 아래 두가지가 불가능하게 된다.

  • 변경감지 (dirty checking) 불가능
  • 지연로딩 불가능

2. 스프링의 OSIV 옵션을 사용한다면?

spring 의 OSIV 옵션을 사용한다면, persistence context 가 request 마다 할당이 된다.

이 경우, transaction 범위가 아닌 곳에서는 아래와 같다.

  • 변경감지 → 사용시 주의해야함
  • 지연로딩 → 가능

2.1 왜 변경감지는 불가능한가?

물론, 변경감지가 완전히 불가능한 것은 아니고 트랜잭션이 끝난 후에는 변경감지가 안된다는 뜻.

변경감지가 불가능한 이유를 알기 위해서는 변경감지가 어떻게 일어나는지 이해가 필요하다.

기본적으로 entity manager, persistence context 개념이 있는 ORM 에서는 entity manager 에는 save 라는 것이 없다. JPA 스펙에는 save 메서드가 없다. spring data jpa 의 crud repository 가 이를 구현하고 있을뿐이다.

개념적으로 접근하자면, entity manager 는 데이터베이스에 직접 어떤 행위를 가하는 것이 아니라, persistence context 에 entity 들을 관리하고, 이를 flush 를 통해 db에 쿼리를 날리고, commit 을 통해 반영한다.

변경감지는 이 persistence context 에 최초로 생성된 스냅샷과 비교해서 스냅샷과 다른 상태를 가지는 entity를 flush 할 때, 업데이트 쿼리가 나가는 것이다.

OSIV 에서, entity manager 에 entity가 있음에도 transaction 에서 flush, commit 동작이 없기 때문에 변경감지가 동작하지 않는 것이다.

@RestController
@RequestMapping("/api/v1/foo")
class FooController(
	private val fooService: FooService
) {

	
	// 이 경우, 이미 flush 동작이 없기 때문에 변경감지가 동작하지 않음. 
	fun changeFoo(): FooResponseDto {
		val entity = fooService.findFoo();
		entity.name = "XXX"
		// 생략 
	}
}

class FooService(
	private val fooService:FooService
){

	@Transactional
	fun changeFoo():FooEntity { 
		// 생략 
		return entity
	}
}

2.2 왜 지연로딩은 가능한가?

지연 로딩이 가능한 것은 지연로딩의 메커니즘은 트랜잭션이 아니어도 가능하기 때문이다. non transactional read 를 통해서 지연로딩을 할 수 있다.

2.3 그럼 OSIV 를 사용하는 것이 좋은가?

OSIV 를 사용하는 것이 좋은가? 라고 하면 그럴 수도 있고 아닐 수도 있다 라고 할 수 있는데, 현업에서는 사용하지 않는다 (적어도 필자가 다니는 회사는 사용하지 않음).

OSIV 의 문제점.

  1. 여러 트랜잭션에서 persistence context가 공유된다.

아래 코드에서, fooChanger 가 entity 의 속성을 변경하고 커밋하다가 실패하는 경우 어떻게 될까?

rollback이 일어나도, context 자체를 공유하기 때문에, 변경된 데이터를 그대로 가지고 있을까? 

-> 그렇지는 않다. 롤백이 일어나는 경우에는 entity manager 를 통해 persistence context 를 비워준다. 

 

fun doSomething(id:Long, name:String) {
	try { 
		fooChanger.change(id, name)
	}catch { 
		// 생략 
	}

	// fallback 로직
	fallBackService.fallback(id, name);
}

class FooChanger {

	@Transactional
	fun change(id:Long, name:String) {
	  val entity = repository.findOrNull(id) ?: throw FooException();
		entity.name = "XXX"
	} 
}

class FallbackService {

	fun fallback(id: Long, name:String) { 
        // 영속성 컨텍스트가 초기화 되어 새로 조회를 한다. 
		val entity = repository.findOrNull(id)
		
		// 생략 
	}

}
  1. transaction 이 없는 곳에서 entity 변경 후 transaction 시작시 → 데이터 변경됨

아래 코드의 경우, barEntity는 트랜잭션이 종료된 후에도 context 에 남아있다. 따라서, 이후 bazService 에서 트랜잭션이 시작되고 끝날 때, flush 를 하면 변경감지가 작동하여 데이터가 변경된다.

트랜잭션 외부에서 변경감지가 동작하기 때문에, 문제가 발생할 수 있다.

class FooService(
	private val barService: BarService,
	private val bazService: BazService,
) { 

	// 
	fun foo() { 
		val barEntity = barService.findBar()
		barEntity.name = "XXX";
		bazService.baz();
	}

}

class BarService(
	private val barRepository: BarRepository
){
	
	@Transactional
	fun bar():BarEntity { 
		// 생략 
	}

}

class BazService(
	private val bazRepository: BazRepository
) {

	@Transactional
	fun baz() { 

	}
}

3. 그렇다면 어떻게 하는 것이 좋을까?

3.1 OSIV 옵션을 쓰지 않는 것이 좋다.

OSIV 를 사용하는 경우, 코드를 작성하는 내내 persistence context에 대해 고려해야한다. 그렇지 않은 경우, 여러가지 버그가 생길 수 있다.

예를들어 아래와 같은 상황이 발생할 수 있다.

→ case1. 왜 entity 를 새로 조회하지? OSIV 옵션을 켰는데? 

→ case2. 왜 db에 데이터가 변경되지? update 쿼리가 왜 나가지?

3.2 JPA 연관관계를 적절하게 끊어야한다.

연관관계라는 것은 일종의 탐색 가능성을 가진다는 개념이다.

연관관계를 구현하는 방식은 여러가지다.

  • 객체참조
  • id field 참조 등

JPA 의 연관관계는 데이터베이스의 테이블을 실제 entity 객체의 참조하는 방식의 구현 방식이다. 모든 객체가 객체 참조로 연관관계를 가질 필요가 있을까?

아래와 같은 것들을 잘 고려해봐야한다.

  • 결합도 예를들어, order 가 user 정보를 객체참조로 알아야할까? → 다른 서버, 다른 db로 분리되는 경우는? 정말 같이 변경되는 것들인가? 등을 따져봐야함.
  • transaction시 lock
  • jpa 연관관계가 걸려있는 객체들은 fk 를 통해 테이블 매핑이 되어있다. 이 경우, transaction 으로 인해 불필요한 테이블에 lock이 발생하지 않을까? long 트랜잭션으로 인해 다른 서비스에 영향은 없을까 등을 고려해봐야함.