본문 바로가기

DEVELOPER/Spring & JPA

[JPA] @Transactional과 JPA의 플러시와 변경 감지(Dirty Checking)

안녕하세요😎 백엔드 개발자 제임스입니다 :)

오늘 다룰 주제는 스프링과 JPA를 다루는 개발자 혹은 백엔드 개발자에게 중요한 내용이 될 수 있겠다고 예상합니다. 그 이유는 실제로 개발을 진행하면서 많이 접하게 되는 개념이기 때문입니다. 저는 이번 내용에 대해서 분석하다 보니, JPA와 트랜잭션에 대해서 보다 더 알게 된 것 같아 굉장히 즐거웠습니다. 오늘 내용을 이해하고, 함께 성장하는 개발자가 되면 좋겠습니다.

설명 전에 가볍게 돌발 퀴즈로 시작하도록 하겠습니다. 내용이 다소 길더라도 천천히 끝까지 읽어주세요!

 


 
현재 철수는 [모임 관리 서비스] 개발을 진행하던 중, 모임 생성 기능을 구현하려고 합니다. 해당 기능의 요구사항은 아래와 같습니다.

* 모임 생성 기능 요구사항
1) 서비스의 회원이 모임에 필요한 정보를 모두 입력하고, 생성 버튼을 누르면 모임이 생성됩니다.
2) 모임을 생성한 회원은 해당 모임의 관리자가 됩니다.

철수는 요구사항에 따라 아래와 같이 엔티티와 비즈니스 로직을 작성했습니다.
1. 엔티티

왼) 그룹, 오) 회원 엔티티

  • 그룹과 회원은 다대다 관계입니다. (위 코드에서는 단방향으로 매핑된 상황입니다.)
  • 그룹을 만든 회원이 관리자가 되기 때문에 Managers 라는 연결 테이블을 갖게 됩니다.
  • managers 는 컬렉션 Set으로 이루어졌습니다.

2. 비즈니스 로직

철수는 요구사항과 일치하는 논리로 기능을 구현했습니다.

모임 생성 로직

  • 회원이 입력한 정보(그룹 이름, 그룹 내용 등)를 기반으로 생성된 Group 타입의 인스턴스입니다.
  • Account는 현재 로그인 되었으며, 그룹을 생성하는 회원의 인스턴스입니다.
* 여기서 퀴즈!!!
추후 개발자 철수는 모임과 모임 관리자 이름을 조회하는 기능도 만들어서 서비스를 배포했습니다.
이후 '제임스'라는 이름의 회원이 해당 서비스에서 모임을 개설했습니다.
곧바로 제임스는 본인이 개설한 모임의 정보를 조회했습니다. 하지만 문제가 생겼습니다.
모임을 조회한 페이지의 관리자란에는 '제임스' 뿐만 아니라 어떠한 이름도 적혀있지 않았습니다.
무슨 문제가 발생한 것일까요?


문제는 무엇이었을까요?


철수가 작성한 코드의 가장 큰 문제는 트랜잭션으로 인해서 발생했습니다. JPA에 대해서 깊게 알지 못해도, save()가 발생하는 시점에서 하나의 트랜잭션이 끝났다는 것을 알 수 있습니다. 따라서 모임의 관리자 정보는 저장되지 않아서 Null이 되었습니다. 사실 문제의 원인에는 복잡한 내용이 있지만, 지금 당장은 원인을 가볍게 파악해 보겠습니다.
아마 눈치가 빠른 분들은 "코드 작성 순서가 틀린 게 아닌가?"라고 생각할 수 있습니다. 그 또한 트랜잭션과 같은 맥락이기 때문에 문제의 정답이 될 수 있습니다.
하지만 가령 복잡한 로직을 작성하는 상황이면 어땠을까요? 다시 말해서 로직에 순서를 변경할 수 없는 경우가 있을 수 있습니다. 따라서 이 문제를 해결하기 위해선 로직의 과정을 하나의 트랜잭션으로 묶어줘야 합니다. 오늘은 그 방법에 대해서 알아보고, 원리를 자세하게 분석하도록 하겠습니다.

저번 포스팅에서 트랜잭션에 대해서 알아본 적이 있습니다. 따라서 자세한 내용은 아래 링크를 참고해 주세요!

2023.01.23 - [DEVELOPER/Database] - [DB] 트랜잭션(Transaction)에 대해서 알아보기

 

[DB] 트랜잭션(Transaction)에 대해서 알아보기

안녕하세요😎 백엔드 개발자 제임스입니다 :) 모두 새해 복 많이 받고, 하는 일 모두 잘되세요! 오늘은 트랜잭션에 대해서 정리하겠습니다. 트랜잭션은 데이터베이스를 다루기 위해 필수로 알

kang-james.tistory.com

 

@Transactional


핵심부터 말하자면, 우리는 스프링에서 제공하는 @Transactional 어노테이션을 통해서 위에 문제를 해결할 수 있습니다.

@Transactional을 적용하여 문제 해결

정말 간단하죠? 그렇다면 @Transactional은 무엇이며, 생기게된 이유가 무엇일까요?

스프링에서 제공하는 3가지 트랜잭션 기술

  1. 트랜잭션(Transaction) 동기화
  2. 트랜잭션(Transaction) 추상화
  3. AOP를 이용한 트랜잭션(Transaction) 분리

스프링은 트랜잭션을 위해 3가지 기술을 제공합니다. 지금 당장 트랜잭션 동기화와 추상화 기술까지 설명하려면 너무 길어지기 때문에, 바로 3번째 기술인 AOP를 이용한 트랜잭션 분리로 넘어가겠습니다. 3번은 말 그대로 '트랜잭션 관리 코드'와 '비즈니스 로직 코드'가 복잡하게 얽혀있지 않도록 AOP 기술을 활용한 것입니다. 트랜잭션 관리 코드를 완전히 분리시키는 것은 불가능했기 때문에 어노테이션으로 기능을 지원했습니다. 이 어노테이션이 @Transactional입니다.

@Transactional document 내용

@Transactional을 클래스에 선언하면, 선언 클래스 및 해당 하위 클래스의 모든 메서드에 기본값으로 적용합니다. (메서드에 선언할 수 있으며, 하위 로직까지 하나의 트랜잭션으로 묶음)
어노테이션을 통해서 트랜잭션의 마무리 작업인 Commit과 Rollback을 상황에 맞게 조작할 수 있습니다. 뿐만 아니라 세부 설정을 통해서 트랜잭션 적용 범위를 관리할 수 있습니다.
결론은 일련의 작업들을 묶어서 하나의 단위로 처리하고 싶다면, @Transactional을 활용하면 됩니다.

 

JPA의 더티 체킹(Dirty Checking)과 플러시(Flush)


내용을 자세하게 분석하다 보니, JPA의 깊은 내용까지 작성하게 되었습니다.
JPA의 영속성 컨텍스트와 상태에 대해서는 추후에 자세하게 다룰 예정입니다.
혹시나 개념이 어렵다면 분석 과정에 대해서만 가볍게 읽어주세요.

@Transactional 어노테이션을 사용함으로써 문제를 해결한다는 것은 알았습니다. 그럼에도 저는 JPA의 save()가 이미 수행되었음에도 불구하고, 데이터가 저장된 이유에 대해서 궁금증이 생겼습니다. 

Step 1. @Transactional 어노테이션의 유무에 따른 로그 분석

1) @Transactional을 적용하지 않았을 때

 

2) @Transactional을 적용했을 때

로그를 통해 커밋 시점을 파악하고 싶었지만, 생각보다 차이가 없어서 쉽지 않았습니다.
확실한 것은 트랜잭션 어노테이션을 적용했을 때, 관리자 정보를 성공적으로 저장하는 것을 발견했습니다.
추가로 특별하게 발견한 것은 Dirty Checking이었습니다.

 

Step 2. 더티 체킹(Dirty Checking)이란?

더티 체킹(Dirty Checking)은 다른 말로 변경 감지입니다. 조사하다 보니 JPA의 아주 큰 장점이자 특징이라는 것을 알게 되었습니다.
즉, 더티 체킹은 트랜잭션 안에서 엔티티의 변경이 일어나면, 이를 감지하는 JPA 특징입니다. 더티 체킹과 관련된 기능이 JPA의 플러시(flush)입니다. 조금 더 자세하게 어떻게 변경 감지가 일어나고, 변경 내용을 데이터베이스 반영하는지 알아보겠습니다.
 

Step 3. JPA의 플러시(Flush)란?

플러시는 영속성 컨텍스트의 감지된 변경내용을 데이터베이스에 반영하는 기능입니다. JPA의 또 다른 특징 중 하나는 Query 쓰기 지연을 지원합니다. 이는 쉽게 말하면, JPA를 통해 전송할 쿼리들을 코드 동작 후 즉시 사용된다기보다, 트랜잭션이 성공적으로 끝나는 시점에 한 번에 모아서 사용하는 것을 의미합니다. 따라서 플러시의 역할은 SQL 저장소에 쌓여있는 쿼리(등록, 수정, 삭제)를 데이터베이스에 전송하는 것입니다. 이에 대한 결과로 감지된 변경 내역들이 데이터베이스에 동기화됩니다.
플러시(Flush)는 entityManager.flush()를 통해 직접 호출할 수도 있지만, 대부분 트랜잭션이 성공적으로 Commit 하는 시점에 자동으로 호출됩니다. 
 

Step 4. JPA의 기본 동작 원리 (feat. persist()와 save())

JPA persist 사용 코드 (영속화)
save() 구현 코드

마지막으로 체크할 부분은 JPA의 동작 원리입니다. JPA에서 발생하는 데이터 변경(추가, 삭제 등) 작업은 항상 트랜잭션 안에서 발생합니다. 위에 2장의 코드 사진을 봤을 때 공통점이 있습니다. 두 코드 모두 하나의 트랜잭션으로 묶여있습니다. 오늘 문제를 해결하기 위한 가장 중요한 단서가 되겠습니다.


다시 문제로 돌아와서 정리하자면


JPA는 트랜잭션 내에서 동작하기 때문에, save() 메서드에는 이미 트랜잭션이 적용된 상태입니다. 따라서 문제에 코드에선 관리자가 추가되기 이전에 Commit이 돼버렸고, 이로 인해 변경 내용이 데이터베이스에 적용되지 않은 채 상태가 저장된 것입니다. 이를 해결하기 위해선 비즈니스 로직 전체를 하나의 트랜잭션으로 묶어줘야 하고, 이때 @Transactional 어노테이션을 활용합니다. 이에 대한 결과로 트랜잭션이 Commit 되기 전에 JPA가 변경된 내용을 감지하고, 데이터베이스에 적용할 수 있습니다.

1) @Transactional이 적용되지 않았던 코드의 더티 체킹 결과

JPA 데이터 반영 원리

2) @Transactional이 적용된 코드의 더티 체킹 결과

비즈니스 로직에 @Transactional 적용 이후 데이터 반영 원리

 

반응형