제목을 잘 지은것 같다. ㅎㅎ
먼저 위의 블로그에 정리된 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)