환경: springboot3.3
mybatis에서 프로시져는 아래와 같이 사용한다. 디비에 대고 쓰듯 편하게 사용 가능했다..
<parameterMap id="basicMoneyModel" class="com.money.model.BasicMoneyModel">
<parameter property="code" jdbcType="VARCHAR" javaType="java.lang.String" mode="IN"/>
<parameter property="memberId" jdbcType="VARCHAR" javaType="java.lang.String" mode="IN"/>
<parameter property="cip" jdbcType="VARCHAR" javaType="java.lang.String" mode="IN"/>
<parameter property="eventMoney" jdbcType="BIGINT" javaType="java.lang.Long" mode="IN"/>
<parameter property="orderNo" jdbcType="VARCHAR" javaType="java.lang.String" mode="IN"/>
<parameter property="nlevel" jdbcType="INTEGER" javaType="java.lang.Integer" mode="IN"/>
<parameter property="defaultMoney" jdbcType="BIGINT" javaType="java.lang.Long" mode="IN"/>
<parameter property="gameratTable" jdbcType="VARCHAR" javaType="java.lang.String" mode="IN"/>
<parameter property="gamebaseTable" jdbcType="VARCHAR" javaType="java.lang.String" mode="IN"/>
<parameter property="outval" jdbcType="INTEGER" javaType="java.lang.Integer" mode="OUT"/>
</parameterMap>
<procedure id="updateMoneyBasic" parameterMap="basicMoneyModel">
{call UPDATE_MONEY_BASIC(?,?,?,?,?,?,?,?,?,?)}
</procedure>
이걸 JPA로 옮긴다면?
1. entity manager + query
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class GameMoneyRepository {
@Autowired
private EntityManager entityManager;
public void updateGameMoneyBasic(BasicMoneyModel basicMoneyModel) {
// 네이티브 쿼리로 저장 프로시저 호출
String sql = "CALL UPDATE_MONEY_BASIC(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
// 네이티브 쿼리 설정
Query query = entityManager.createNativeQuery(sql);
// 파라미터 설정
query.setParameter(1, basicMoneyModel.getCode());
query.setParameter(2, basicMoneyModel.getMemberId());
query.setParameter(3, basicMoneyModel.getCip());
query.setParameter(4, basicMoneyModel.getEventMoney());
query.setParameter(5, basicMoneyModel.getOrderNo());
query.setParameter(6, basicMoneyModel.getNlevel());
query.setParameter(7, basicMoneyModel.getDefaultMoney());
query.setParameter(8, basicMoneyModel.getGamerateTable());
query.setParameter(9, basicMoneyModel.getGamebaseTable());
// OUT 파라미터 처리
query.executeUpdate();
// 프로시저 실행 후 OUT 파라미터 처리 (OUT 파라미터를 직접 처리하려면 ResultSet을 통해 가져와야 합니다)
Integer outval = (Integer) query.getResultList().get(0);
basicMoneyModel.setOutval(outval);
}
}
2. entity manager + spquery
import javax.persistence.StoredProcedureQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
@Repository
public class GameMoneyRepository {
@Autowired
private EntityManager entityManager;
public void updateGameMoneyBasic(BasicMoneyModel basicMoneyModel) {
StoredProcedureQuery query = entityManager.createStoredProcedureQuery("UPDATE_MONEY_BASIC");
// 프로시저 파라미터 설정
query.registerStoredProcedureParameter(1, String.class, ParameterMode.IN); // code
query.registerStoredProcedureParameter(2, String.class, ParameterMode.IN); // memberId
query.registerStoredProcedureParameter(3, String.class, ParameterMode.IN); // cip
query.registerStoredProcedureParameter(4, Long.class, ParameterMode.IN); // eventMoney
query.registerStoredProcedureParameter(5, String.class, ParameterMode.IN); // orderNo
query.registerStoredProcedureParameter(6, Integer.class, ParameterMode.IN); // nlevel
query.registerStoredProcedureParameter(7, Long.class, ParameterMode.IN); // defaultMoney
query.registerStoredProcedureParameter(8, String.class, ParameterMode.IN); // gameratTable
query.registerStoredProcedureParameter(9, String.class, ParameterMode.IN); // gamebaseTable
query.registerStoredProcedureParameter(10, Integer.class, ParameterMode.OUT); // outval
// 파라미터 값 설정
query.setParameter(1, basicMoneyModel.getCode());
query.setParameter(2, basicMoneyModel.getMemberId());
query.setParameter(3, basicMoneyModel.getCip());
query.setParameter(4, basicMoneyModel.getEventMoney());
query.setParameter(5, basicMoneyModel.getOrderNo());
query.setParameter(6, basicMoneyModel.getNlevel());
query.setParameter(7, basicMoneyModel.getDefaultMoney());
query.setParameter(8, basicMoneyModel.getGamerateTable());
query.setParameter(9, basicMoneyModel.getGamebaseTable());
// 프로시저 호출
query.execute();
// OUT 파라미터 결과 가져오기
Integer outval = (Integer) query.getOutputParameterValue(10);
basicMoneyModel.setOutval(outval);
}
}
3. @Query, nativequery=true
@Query(value = "CALL UPDATE_MONEY_BASIC(:code, :memberId, :cip, :eventMoney, :orderNo, :nlevel, :defaultMoney, :gamerateTable, :gamebaseTable, :outval)", nativeQuery = true)
Integer updateGameMoneyBasic(@Param("code") String code,
@Param("memberId") String memberId,
@Param("cip") String cip,
@Param("eventMoney") Long eventMoney,
@Param("orderNo") String orderNo,
@Param("nlevel") Integer nlevel,
@Param("defaultMoney") Long defaultMoney,
@Param("gamerateTable") String gameratTable,
@Param("gamebaseTable") String gamebaseTable,
@Param("outval") Integer outval);
4. @Procedure 사용; outVal을 함수 리턴값으로 받기
@Procedure(name = "PC_UPDATE_GAMEMONEY_BASIC")
Integer updateGameMoneyBasic(String code,
String memberId,
String cip,
Long eventMoney,
String orderNo,
Integer nlevel,
Long defaultMoney,
String gameratTable,
String gamebaseTable);
5. @Procedure 사용; outVal을 파라미터 값 안에 세팅해서 받기
@Procedure(name = "PC_UPDATE_GAMEMONEY_BASIC")
void updateGameMoneyBasic(String code,
String memberId,
String cip,
Long eventMoney,
String orderNo,
Integer nlevel,
Long defaultMoney,
String gameratTable,
String gamebaseTable,
@Param("outval") Integer outval);
성공 여부를 반환값으로 검증하고자 4번 방식으로 작업하였다.
Persistence Context와 프로시저 호출 사이에서 발생할 수 있는 순서 문제로 인한 오류 주의
@Transactional이 적용된 서비스에서 JPA는 영속성 컨텍스트를 관리하고, 이를 기반으로 엔티티의 상태 변화를 추적한다. 하지만 저장 프로시저나 커스텀 SQL 실행에 대한 관리는 하지 않는다. 저장 프로시저는 데이터베이스에서 실행되는 독립적인 SQL 작업으로, JPA 트랜잭션과는 별개로 실행된다. 예를 들어, EntityManager를 사용해서 저장 프로시저를 호출할 수 있지만, 저장 프로시저 자체는 JPA의 트랜잭션과는 독립적으로 DB에서 실행된다.
따라서 저장 프로시저를 호출하면, 트랜잭션이 시작되지만, 프로시저의 실행은 JPA 트랜잭션의 외부에서 실행된다. 예를 들어, JDBC로 직접 프로시저를 호출하거나 @Procedure 어노테이션을 사용하여 저장 프로시저를 호출할 수 있지만, 이 호출 자체는 JPA의 트랜잭션 관리 범위에 포함되지 않아 별도의 트랜잭션으로 처리된다.
따라서 아래와 같은 현상이 발생할 수 있다.
- A 테이블 select: 데이터가 존재하는지 확인하려는 첫 번째 쿼리 실행.
- B 테이블 select: 두 번째 쿼리로 데이터를 확인.
- C Procedure 실행: 내부적으로 데이터가 없을 경우, A insert
- A 테이블 insert: A 테이블에 데이터 삽입 시 PK 충돌 오류 발생.
- B 테이블 insert: B 테이블에 데이터 삽입.
이 문제는 Persistence Context(즉, Hibernate의 세션 캐시)가 아직 업데이트되지 않은 상태에서 저장 프로시저가 호출되기 때문에 발생한다. Persistence Context에서 수정된 데이터는 DB에 즉시 반영되지 않으며, flush를 호출하지 않으면 데이터베이스에 반영되지 않은 상태에서 프로시저가 실행된다. 그 결과, 프로시저 내에서 insert 쿼리가 실행되기 전에, PK 충돌이나 예상치 못한 동작이 발생할 수 있다.
flush()를 호출하면, Persistence Context에 있는 변경 사항을 DB에 반영하여 쿼리 실행 전에 데이터가 반영되도록 할 수 있다. 이렇게 하면 데이터베이스에서 처리되는 순서를 제어할 수 있고, PK 충돌 같은 오류를 예방할 수 있다.
- A 테이블 select: 데이터가 존재하는지 확인하려는 첫 번째 쿼리 실행.
- B 테이블 select: 두 번째 쿼리로 데이터를 확인.
- EntityManager.flush() 호출 → Persistence Context의 변경 사항을 DB에 반영
- C Procedure 실행: 이제 DB에 반영된 데이터를 기반으로 처리
- A 테이블 insert: A 테이블에 데이터 삽입 시 (이제 PK 충돌이 발생하지 않음)
- B 테이블 insert: B 테이블에 데이터 삽입.
결국 아래와 같은 코드가 되는데..
@Transactional
public void createGrade(CreateGradeCommand command) {
//A
gameRateRepository.save(gameRate);
gameRateRepository.flush();
//B
webRateRepository.save(webRate);
webRateRepository.flush();
//procedure
changeMoneyService.change(changeMoneyCommand);
}
참고로 flush를 해도 디비에 커밋을 한 것은 아니다! 즉, 롤백 가능
- flush()는 Persistence Context에 있는 변경 사항을 DB에 즉시 동기화하는 메서드이다. flush()를 호출하면 Hibernate가 세션 캐시에 저장된 데이터를 DB에 동기화하지만, 트랜잭션은 여전히 커밋되지 않는다.
- 이 경우 flush()는 save() 메서드가 엔티티를 저장한 뒤, 해당 변경 사항을 DB와 동기화하기 위해 호출된다. 다만, flush() 호출 이후에는 트랜잭션 커밋 전까지 DB 상태가 변경될 수 있으며, 만약 트랜잭션 내에서 에러가 발생하면 모든 변경 사항은 롤백된다.
- 즉 changeMoneyService.change()에서 예외가 발생하면, gameRateRepository.save()와 webRateRepository.save()에서의 변경 사항도 롤백된다.
'개발 > spring' 카테고리의 다른 글
[jpa] 커스텀 Transactional 만들기 (0) | 2024.11.22 |
---|---|
[jpa] EntityManager를 타지 않는 작업 (1) | 2024.11.20 |
[test] @WebMvcTest 에서 @PostConstruct 처리... (2) | 2024.11.18 |
[test] @RestClientTest vs mockMvc (0) | 2024.11.16 |
스프링 빈 주입 시 우선 순위 (0) | 2024.11.12 |