본문 바로가기

코딩/미완

[JPA] @Transcational의 유무에 따른 동작 차이

https://velog.io/@roro/JPA-JPQL-update-%EC%BF%BC%EB%A6%AC%EB%B2%8C%ED%81%AC%EC%99%80-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8

 

[JPA] JPQL update 쿼리(벌크)와 영속성 컨텍스트

JPQL의 update 쿼리 호출 후 발생하는 상황에 대해 알아보자. Post Entity 먼저 테스트를 위해 id와 title만 있는 간단한 Post 엔티티를 생성한다. PostRepository PostRepository를 인터페이스 만들고 JpaReposi

velog.io

 

제목을 잘 지은것 같다. ㅎㅎ

먼저 위의 블로그에 정리된 2가지 이슈를 반드시 보고 오자. 저걸 모르면 디버깅할 때 찾기 매우 힘들 것 같다..

 

@

이제 본격적으로 내가 어떤 상황이었는지 알아보자. 아래는 주문(Demand) 엔티티의 상태를 waiting->accepted로 바꾸는 코드가 잘 동작하는지 확인하는 과정이다. 사실상 아래 코드의 //when에 해당되는 demandService.changeStatus()를 확인하기 위한 테스트임. 

JPA에 익숙하지 않아 주석을 구체적으로 달아놓았다.

@Test
@Transactional//이 붙어있다.
//	@Commit//테스트에서는 commit하지 않기 때문에 변경감지가 안된다. tranactional에서 commit없이 다 돌려버림. commit을 붙이니까 update문이 실행되었다. commit이 없어도 sql만 실행 안되지 영속성 컨텍스트에서는 그대로 남아있다. 그래서 테스트는 통과함.
	public void Demand_changeStatusTest() {
		//given
		testRepository.insertAll();//테스트용 데이터 삽입.
		List<Store> storeList = storeService.findAll(i -> i);
		PageRequest pageRequest = PageRequest.of(0, 6, Sort.by("created"));
		
		//select가 실행됨. 영속성 컨텍스트에 불러와짐. 영속성 컨텍스트에서 찾지 못해서 SQL실행. status==WAITING인 List<Demand>를 가져옴.
		List<Demand> demandList = demandService.findByStoreIdAndDemandStatusPageable(i -> i, storeList.get(0).getUuid(), DemandStatus.WAITING, pageRequest);
		Demand demand = demandList.get(0);
		ChangeStatusRequestDTO dto = ChangeStatusRequestDTO.builder()
				.changeStatusTo(DemandStatus.ACCEPTED) //ACCEPTED로 상태를 바꿔라!
				.demandId(demand.getUuid())
				.build();
		
		//when
		demandService.changeStatus(dto);//demandUUID를 참조해서 status를 ACCEPTED로 바꾼다. update SQL실행 안됨. 영속성 컨텍스트에서만 바꾼다. 이미 영속성 컨텍스트에 있기 때문에 그렇다.
		Demand byId = demandService.findById(i -> i, demand.getUuid());//select문이 실행안됨. 영속성 컨텍스트에서 바로 가져오기 때문임.(1차 캐시)
		System.out.println(demand);
		
		assertEquals(demand, byId);//이건 통과한다...
		assertEquals("a", "a");//주소가 아니라 실제 값을 비교함. 위의 assertEquals()도 주소가 아니라 값을 비교함.
        
		assertEquals(demand == byId, true);//영속성 컨텍스트에서 가져오기 때문에 둘은 주소도 같다.
		assertEquals(byId.getStatus(), DemandStatus.ACCEPTED);//accepted로 변경된 것을 확인할 수 있다.
		
	}

아래에는 위의 코드에서 사용된 함수를 적어놓았다.

//demandService.changeStatus()
//Service 레이어라서 클래스에 @Transactional이 붙어있다.
public void changeStatus(ChangeStatusRequestDTO dto) {
		UUID demandId = dto.getDemandId();
		Optional<Demand> demandOpt = demandRepository.findById(demandId);//select 호출안됨. 영속성 컨텍스트에 있기 때문이다.
		if (!demandOpt.isPresent()) {
			throw new NullPointerException("demandRepository.findById() not found!");
		}
		
		Demand demand = demandOpt.get();
		DemandStatus changeStatusTo = dto.getChangeStatusTo();
		if (changeStatusTo == DemandStatus.ACCEPTED) {
			demand.changeToAccepted();
		} else if (changeStatusTo == DemandStatus.WAITING) {//waiting으로 기다리는 것은 말이 안된다.
			throw new IllegalArgumentException("change to WAITING doesn't make sense!");
		} else if (changeStatusTo == DemandStatus.COMPLETED) {
			demand.changeToCompleted();
		} else if (changeStatusTo == DemandStatus.REJECTED) {
			demand.changeToRejected();
		} else {
			throw new IllegalArgumentException("ENUM에 정의되어 있지 않음.");
		}
	}
//demand.changeToAccepted()
public boolean changeToAccepted() { if (status == DemandStatus.WAITING) { status = DemandStatus.ACCEPTED; return true; } return false; }

 

<의도>

  • 테스트에서는 @Transactional을 붙인 상태에서는 @Commit을 붙이지 않으면 DB에 들어가지 않는다.
  • 위의 demand변수의 status를 수정한 것이기 때문에 assertEquals(WAITING, demand.getstatus()) 하면 안된다. demand == byId이기 때문에 demand의 status도 바뀜. (영속성 컨텍스트는 같은 객체를 반환한다.)
  • 테스트에서 @Transactional을 붙이지 않았다면 @Commit이 없어도 DB에 들어간다. 하지만 대부분 @Test 함수의 독립적인 실행을 위해 클래스 단위에서 @Transactional을 붙인다. 그래서 반영하고 싶다면 @Commit을 붙여야함.
  • insert구문은 @Transactional이 없어도 commit되지만 update 동작은 @Transactional, @Commit 없이는 반영되지 않는다.(트랜잭션이 닫히면서 영속성 컨텍스트도 닫히기 때문임. 그래서 테스트 코드에 @Transactional이 붙어있다면 컨텍스트가 닫히지 않으면서 변경감지가 동작하는 것이다.) 엔티티의 메소드를 DB에 반영하려면(변경감지를 사용하려면) 테스트 환경이 아니라 실제 소스코드에는 @Transcational만 있으면 되고 테스트코드에서는 @Transactional, @Commit이 필요하다. 테스트 환경에서도 실제로 update문이 실행되는 것을 확인할 필요까지는 없다면 @Commit은 없어도 된다.
  • 마지막으로 실행된 SQL이 아래에 있다. 영속성 컨텍스트에 객체가 없기 때문에 실행됨. 이 이후로는 SQL이 아무것도 실행되지 않았다. //위의 코드에서 demandService.findByStoreIdAndDemandStatusPageable(i -> i, storeList.get(0).getUuid(), DemandStatus.WAITING, pageRequest); 의 함수를 호출할 때 사용된 SQL임.
  • Demand byId = demandService.findById(i -> i, demand.getUuid());//select문이 실행안됨. 영속성 컨텍스트에서 바로 가져오기 때문임.(1차 캐시)그리고 여기서 알 수 있는 것이 @Transactional은 계층구조에 따라 중복해서 덮어 씌여질 수록 가장 큰 commit()만 따른다. 무슨 말이냐면 위의 코드에서 demandService.findById()에도 @Transactional이 붙어있지만 테스트코드에 @Transactional이 붙었다는 이유로 commit의 시점이 미뤄졌다는 것을 말하는 것임. 즉 뒤에 tx.commit()이 있다면 앞의 commit()은 효과가 없는 것 같다. (애매하다. 이렇다고 확정지을 수는 없다.)

Hibernate: 
    select
        demand0_.uuid as uuid1_2_,
        demand0_.created as created2_2_,
        demand0_.modified as modified3_2_,
        demand0_.cake_uuid as cake_uui8_2_,
        demand0_.customer_uuid as customer9_2_,
        demand0_.file_path as file_pat4_2_,
        demand0_.option as option5_2_,
        demand0_.price as price6_2_,
        demand0_.status as status7_2_,
        demand0_.store_uuid as store_u10_2_ 
    from
        demand demand0_ 
    where
        demand0_.store_uuid=? 
        and demand0_.status=? 
    order by
        demand0_.created asc limit ?

 

 

<만약 @Transactional 이 없다면?>

  • @Transactional을 붙이지 않으면 지연쓰기를 할 수 없다. 예를 들어 위의 코드에는 @Transactional을 사용하지 않으면 @Transactional이 붙은 단위대로 바로 실행된다. 하나의 함수 내에서 commit을 여러번 한다는 말임. 즉, SQL을 여러번 쓰고 DB와 여러번 통신한다는 말임. @Transactional이 있었을 때는 영속성 컨텍스트에서 가져와서 따로 commit하지 않았던 함수들도 전부 SQL을 실행한다. @Transactional이 없다면 영속성 컨텍스트도 무용지물이다. 1차 캐시의 역할도 못함. 영속성 컨텍스트에 분명히 객체가 있는데도 SQL이 계속 실행되는 것을 밑의 SQL에서 확인할 수 있다.
  • @Transactional이 없으면 있을 때와는 다르게 영속성 컨텍스트가 있는 객체에 대한 findById()에서도 select문을 실행한다. 
  • Entity클래스의 멤버함수. 여기서는 changeToAccepted()는 @Transactional이 없으면 반영되지 않는다. @Transactional이 있어야 DB에 반영됨. 테스트 코드에서는 @Commit까지 있어야함. 이게 없다면 단순 영속성컨텍스트에서만 바꿀 뿐임. 영속성 컨텍스트 = 메모리 라서 메모리 안에서만 바꿀 꼴이다. 결론적으로 멤버함수는 Service layer의 함수 안에서만 쓰자. 어쩔 수 없는 상황이라면 Entity의  클래스에 Transactional을 붙이던가.
  • 위에서는 실행되지 않았던 findById()도 @Transactional을 빼니까 바로 SQL이 실행된다.

 

Hibernate: //@Transactional이 붙었을 때 실행되었던 마지막 SQL.
    select
        demand0_.uuid as uuid1_2_,
        demand0_.created as created2_2_,
        demand0_.modified as modified3_2_,
        demand0_.cake_uuid as cake_uui8_2_,
        demand0_.customer_uuid as customer9_2_,
        demand0_.file_path as file_pat4_2_,
        demand0_.option as option5_2_,
        demand0_.price as price6_2_,
        demand0_.status as status7_2_,
        demand0_.store_uuid as store_u10_2_ 
    from
        demand demand0_ 
    where
        demand0_.store_uuid=? 
        and demand0_.status=? 
    order by
        demand0_.created asc limit ?

 

Hibernate: //demand.changeStatus()안에서 findById()에서 호출된 SQL
    select
        demand0_.uuid as uuid1_2_0_,
        demand0_.created as created2_2_0_,
        demand0_.modified as modified3_2_0_,
        demand0_.cake_uuid as cake_uui8_2_0_,
        demand0_.customer_uuid as customer9_2_0_,
        demand0_.file_path as file_pat4_2_0_,
        demand0_.option as option5_2_0_,
        demand0_.price as price6_2_0_,
        demand0_.status as status7_2_0_,
        demand0_.store_uuid as store_u10_2_0_ 
    from
        demand demand0_ 
    where
        demand0_.uuid=?

Hibernate: //demand.changeToAccepted()에서 사용된 update문. 엔티티의 필드 하나만 바꿨는데 set으로 모든 컬럼이 다 들어간다.
    update
        demand 
    set
        modified=?,
        cake_uuid=?,
        customer_uuid=?,
        file_path=?,
        option=?,
        price=?,
        status=?,
        store_uuid=? 
    where
        uuid=?

Hibernate: //findById()에서 호출된 SQL
    select
        demand0_.uuid as uuid1_2_0_,
        demand0_.created as created2_2_0_,
        demand0_.modified as modified3_2_0_,
        demand0_.cake_uuid as cake_uui8_2_0_,
        demand0_.customer_uuid as customer9_2_0_,
        demand0_.file_path as file_pat4_2_0_,
        demand0_.option as option5_2_0_,
        demand0_.price as price6_2_0_,
        demand0_.status as status7_2_0_,
        demand0_.store_uuid as store_u10_2_0_ 
    from
        demand demand0_ 
    where
        demand0_.uuid=?

 

//sout에서 출력된 Demand클래스
Demand(super=BaseTimeEntitiy(created=2022-08-04T15:23:22.522383, modified=2022-08-04T15:23:22.522383), uuid=f250a08b-b64c-4b3a-be68-821157619338, status=WAITING, option=null, price=0, filePath=src\main\resources\static\1.jpg)