[Spring] @Modifying과 @Transactional 어노테이션

김대현·2024년 10월 31일

📚 공부_Spring

목록 보기
8/8

목차

1. @Modifying과 @Transactional을 공부하게 되었나?
2. @Modifying과 @Transactional 어노테이션 이란?
3. @Modifying과 @Transactional 어노테이션을 적용한 예제
4. 마무리

1. 왜 @Transactional 과 @Modifier을 공부하게 되었나?

오늘 수업 과정에서 JPA를 활용한 미니 게시판 만들기가 있었다. JPA에 대한 블로그 작성을 준비하면서 JPA에대해 깊은 공부를 했고 (아직도 정리할것이 너무 많다 🥲) 덕분에 CRUD를 활용한 미니 게시판을 만드는것은 어렵지 않았다.

그런데 게시판 기능을 구현하던 도중 유독 문제가 있던 기능이 3가지 있었는데 그것이 바로 아래의 3가지 기능이었다.

  1. 검색기능
  2. 검색기능에서 유저가 선택한 내용 유지 기능
  3. UPDATE

1) 검색기능 구현 과정에서 만난 문제

<select name="searchName" >

<option value="author" th:selected="*{searchName == null} OR *{searchName == 'author'}">작성자</option>

<option value="title" th:selected="*{searchName == 'title'}">제목</option>

</select>


<input type="text" name="searchValue" th:value="*{searchValue}">&nbsp;<button>검색</button>&nbsp;<input type="button" id="reset" value="초기화">

우선 검색기능의 경우 select를 사용해서 유저가 검색하고자 하는 category를 선택하고 값을 입력한 뒤 검색버튼을 누르면 해당 키 값이 전송되는 구조로 작성했다. 그리고 해당 값을 Controller에서 Model로 보내줘 해당 값을 기준으로 검색을 할 수 있는 JPQL을 아래와 같이 작성해봤다.

그런데 위와 같이 작성했을 때 오류가 지속적으로 발생하는 것을 발견했고, 관련 글을 찾아보니 JPQL은 Field의 동적할당이 불가능하기에 나는 오류라고 한다. 그래서 해당 문제는 2개의 쿼리로 나눠 searchName이 무엇인지 비교하는 조건문으로 설정해 구현할 수 있었다.

@Query("SELECT b FROM BoardVo AS b WHERE author LIKE CONCAT('%', ?1,'%')")
List<BoardVo> searchByAuthor(String searchValue);

@Query("SELECT b FROM BoardVo AS b WHERE title LIKE CONCAT('%', ?1,'%')")
List<BoardVo> searchByTitle(String searchValue);

2) 검색기능에서 유저가 선택한 내용 유지 기능 구현 과정에서 만난 문제

위의 과정을 통해 검색기능이 잘 작동하는것을 확인했고, 이후 UI를 위해 검색 이후에도 유저가 입력한 값과, select가 유지되는 기능 구현을 도전해 봤다.

해당 기능은 Thymeleaf에서 제공하는 th:checkedth:selected 와 같은 속성을 사용하면 이후 조건문을 통해서 구현할 수 있었다. 그러나 select의 초기값 설정을 위해 th:selected="*{searchName == null} || *{searchName == 'author'}" 를 설정한 순간 오류가 발생하기 시작했다.

처음에는 해당 오류가 어디서 발생하는지 찾지 못해 많은 시간이 소요되었으나 곧 이유를 알게 되었다. 그것은 바로 OR 연산자 기호(||)를 사용했기 때문이다.

Thymeleaf의 기본 문법중에 | 내용 | 이있는데 이때에 |는 Java의 백틱 과 같이 템플릿 문자열 기능을 수행한다. 즉 OR 연산자 기호(||)를 입력했을 때 Thymeleaf는 해당 문자를 템플릿 문자열을 입력하기 위한 문자로 받아들이게 된다. 즉 Thymeleaf에서는 ||이 아닌 OR 을 작성해줘야 한다.

3) UPDATE 구현 과정에서 만난 문제

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface DataMapperInter extends JpaRepository<BoardVo, Integer>{
	@Query("UPDATE BoardVo b SET readcnt = readcnt + 1 WHERE num = ?1 ")
	void viewIncrement(int num);
}

해당 문제가 처음 발생했을 때 '얘가 왜이러지? 🤔' 라는 생각밖에 들지 않았다. 왜냐하면 쿼리는 UPDATE로 작성했는데 자꾸 SELECT를 기대했는데 UPDATE가 들어왔다고 하는것이다. 그래서 해당 에러를 검색해 봤고 곧 JPQL의 특성으로 인함임을 깨닫게 되었다.

💡 다시 공부해보는 JPQL 특성

JPQL은 Java Persistence Query Language의 약어로, 엔티티 객체를 대상으로 하는 쿼리 언어이다. JPQL은 SQL과 유사한 문법을 사용하지만, 데이터베이스 테이블 대신 엔티티와 속성을 대상으로 하며, 객체 지향 프로그래밍과 잘 통합된다.

JPQL이 실행될 때 1차 캐시가 아닌 데이터베이스에 직접 접근하여 실행되고, 조회된 결과는 영속성 컨텍스트에 저장된다. 이때 영속성 컨텍스트에 동일한 식별자가 있을 경우, 데이터베이스에서 조회한 데이터를 버리고 영속성 컨텍스트의 데이터를 사용한다.

즉 JPQL을 통해 DB에 직접 접근하여 실행된 Update 결과를 가지고 영속성 컨텍스트에 돌아왔을 때 이미 1차 캐시안에 동일한 식별자가 있기에 발생하는 문제였다. 또한 JPQL은 일반적으로 객체 지향적인 쿼리 언어로 조회를 위해 설계된 언어이다. 즉 별도의 명시가 없다면 JPQL은 기본적으로 SELECT라고 인식하게 된다.

2. @Modifying과 @Transactional 어노테이션 이란?

1) @Modifying 어노테이션이란?

위에서 언급한 것과 같이 기본적으로 JPQL은 조회를 위해 설계되었다. 그렇기 때문에 우리는 JPQL에게 조회가 아닌 다른 작업을 하는 쿼리임을 명시해줘야 한다. 그때에 붙여주는것이 @Modifying 어노테이션 이다.

"Modifying"이라는 단어의 뜻이 '수정'이라는 의미를 가지고 있듯이, @Modifying 어노테이션은 JPA에게 해당 JPQL이 수정을 위한 쿼리임을 명시해 줄 수 있다. 이 어노테이션을 붙여주게 되면 JPA는 해당 쿼리가 수정을 위한 쿼리임을 인지하게 된다. 그러면 우리는 UPDATE를 입력했을 때 SELECT를 기대했다고 하는 첫 번째 문제를 해결할 수 있다.

하지만 아직 모든 문제가 해결된 것은 아니다. 위에서 언급했듯이 JPQL은 JPA의 생명주기를 무시하고 바로 DB에서 실행된 뒤, 해당 결과를 다시 영속성 컨텍스트 내부의 1차 캐시와 비교하게 된다. 이때 이미 동일한 식별자가 있을 경우 가져온 값을 버리게 된다. 그렇기 때문에 해당 문제를 해결하기 위해 JPQL이 실행되기 전, 혹은 실행된 직후 1차 캐시에 동일한 식별자의 값을 비우는 작업을 진행해야한다. 그 작업은 @Modifying 어노테이션의 clearAutomaticallyflushAutomatically 속성을 통해 값을 비우는 작업을 진행할 수 있다.

❗'clearAutomatically' 속성과 'flushAutomatically' 속성

우선 clearAutomatically 속성은 JPQL 실행 후 영속성 컨텍스트에서 EntityManager.clear() 명령을 수행하여 영속성 컨텍스트를 비우는 속성이다. 이로 인해 관리되던 모든 엔티티가 detached 상태가 된다.

반면, flushAutomatically 속성은 JPQL 실행 전에 EntityManager.flush() 명령을 수행하여 쓰기 지연 저장소의 쿼리들이 flush되도록 하는 속성이다.

두 속성 모두 기본값은 false이다.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface DataMapperInter extends JpaRepository<BoardVo, Integer>{
	@Modifying(clearAutomatically = true)
	@Query("UPDATE BoardVo b SET readcnt = readcnt + 1 WHERE num = ?1 ")
	void viewIncrement(int num);
}

2) @Transactional 어노테이션이란?

@Transactional 어노테이션을 설명하기 전 우리는 트랜잭션(Transaction)이라는 개념을 이해해야한다. 트랜잭션이란 '데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위'를 말한다.

❗트랜잭션의 4가지 특징 (ACID)

특징설명
원자성 (Atomicity) 트랜잭션이 데이터베이스에 모두 반영되던가, 아니면 전혀 반영되지 않아야 한다
일관성 (Consistency) 트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다
독립성 (Isolation) 어떤 하나의 트랜잭션이라도, 다른 트랜잭션의 연산에 끼어들 수 없다
지속성 (Durability) 트랜잭션이 성공적으로 완료됬을 경우, 결과는 영구적으로 반영되어야 한다
트랜잭션 상태 (출처 : 창고)

위에서 언급한 트랜잭션의 4가지 특징중 원자성의 성격으로 인해 트랜잭션은 '일부 성공' 이라는 결과가 없으며 성공한경우 Commit 함으로 해당 결과가 적용되며, 실패한 경우 Rollback함으로 다시 원래의 상태로 돌아간다. 이 과정은 JPA에서도 동일하게 이루어 진다.

❗JPA에서 트랜잭션이 적용되는 코드 예제

@Autowired
private EntityManagerFactory emf; //EntityManager를 생성하는 EntityManagerFactory

public void initDatas() {

	List<String> queries = new ArrayList<String>(); //쿼리들이 들어있는 List Collection
	EntityManager em = emf.createEntityManager(); // Entity를 관리하는 EntityManager 생성
	EntityTransaction tx =em.getTransaction(); //트랜잭션 호출
	tx.begin(); // 트랜잭션 시작
	try {
		for(String query : queries) {
			em.createNativeQuery(query).executeUpdate();
		}
		tx.commit(); //성공시 커밋 = 트랜잭션 종료
	} catch (Exception e) {
		e.printStackTrace(); //오류 출력
		tx.rollback(); //실패시 롤백 = 트랜잭션 종료
	}
	finally {
		em.close(); //영속성 컨텍스트 종료
	}
}

위의 예제코드와 같이 데이터를 입력하거나 수정할 때 트랜잭션이 발생하게 되고, 실제로는 이렇게 메소드를 통해 트랜잭션을 관리해줘야한다. 그러나 그 과정을 Spring Boot가 자동으로 수행하게 하는 어노테이션이 @Transactional 이다.

@Transactional어노테이션이 붙은 메소드를 호출하게 되면 Spring은 메소드 시작 시 트랜잭션을 시작하고, 메소드 실행이 완료되면 자동으로 커밋하거나 롤백하며 unchecked 예외가 발생할 경우 트랜잭션을 자동으로 롤백하며, checked 예외는 기본적으로 커밋한다.

3. @Modifying과 @Transactional 어노테이션을 적용한 예제

1) Repository

package pack.model;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface BoardRepository extends JpaRepository<Board, Integer>{
	@Modifying(clearAutomatically = true)
	@Query(value = "UPDATE Board b SET b.readcnt = b.readcnt + 1 WHERE b.num = ?1")
	void updateReadCnt(int num);
}

2) Service

package pack.model;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import pack.controller.BoardBean;

@Repository
public class BoardDaoProcess {
	@Transactional
	public void updateReadcnt(int num) {
		boardRepository.updateReadCnt(num);
	
	}
}

4. 마무리

  • Spring부트에서 어노테이션은 정말 많은 편의성을 제공한다.
  • 그러나 무작정 어노테이션을 사용하기 보다, 해당 어노테이션이 제공하는 기능과 그 원리를 파악하고 사용하는것이 중요하다고 생각한다.
profile
안녕하세요. 날마다 성장하는 김대현입니다 :)

0개의 댓글