[JPA] JpaRepository 저장 기능들 비교

LDB·2024년 11월 12일
post-thumbnail

Spring Data JPA 저장 메서드

Spring Data JPA에는 save라는 entity를 저장하는 메서드가 존재한다, 그런데 save를 사용하려고하면 다음과 같이 추천메서드가 함께나오는 것을 볼 수 있다.

  • save
  • saveAll
  • saveAllAndFlush
  • saveAndFlush

이렇게 4개의 메서드를 확인할 수 있다.

저장메서드들 간의 비교

이제 저장메서드들에 대해 설명할 건데 설명하기에 앞서 다음의 단어들을 알아두면 도움이 될 것이다.

flush()

  • 영속성 컨텍스트의 변경 내용을 DB에 동기화한다.
  • Transaction commit을 하면, flush가 동작하는데 쓰기 지연 SQL 저장소의 쿼리를 수행한다.
  • rollback 가능

commit

  • DB에 동기화한 내용들을 영구적으로 저장하는 SQL문법이다.
  • rollback 불가능

transaction

  • 데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위이다.
  • 예를 들어 insert를 하고 select를 하는 것처럼 작업의 단위를 트랜잭션이라고 한다.

save(), saveAll()

두 메서드는 저장한다는 관점에서 보면 같지만 save는 단일건을 저장하는방식이고 saveAll은 다수의 건을 한번에 처리한다는 점에서 다르다, 하지만 saveAll메서드 내용을 보면 의아한점을 발견할 수 있는데


// save 메서드
@Transactional
@Override
public <S extends T> S save(S entity) {

	Assert.notNull(entity, "Entity must not be null");

	if (entityInformation.isNew(entity)) {
		entityManager.persist(entity);
		return entity;
	} else {
		return entityManager.merge(entity);
	}
}

// saveAll 메서드
@Transactional
@Override
public <S extends T> List<S> saveAll(Iterable<S> entities) {

	Assert.notNull(entities, "Entities must not be null");

	List<S> result = new ArrayList<>();

	for (S entity : entities) {
		result.add(save(entity));
	}

	return result;
}

saveAll은 save를 엔터티 리스트의 크기만큼 반복시켜 저장하는 것을 볼 수 있다. 그렇다면 save를 반복해서 다량의 데이터를 처리하지 왜 saveAll메서드를 별도로 만들었을까 라고 의문이 들 수 있다, 실제로 테스트 코드를 작성해 테스트를 해보았다.

save(), saveAll() 테스트

@Service
public class memberService {

	private final memberRepository memberrepository;
	
    memberService(memberRepository memberrepository){
        this.memberrepository = memberrepository;
    }
    
	@Transactional
    public void save(){
        long time = System.currentTimeMillis();

        for(int i=1; i<=10; i++){
            Member member = Member.builder()
                                .name("tester")
                                .age(10 + i)
                                .build();
            memberrepository.save(member);
        }

        System.out.println("clear time : "  + (System.currentTimeMillis() - time) + "ms.");
    }

    @Transactional
    public void saveAll(){
        long time = System.currentTimeMillis();

        List<Member> members = new ArrayList<>();
        for(int i=1; i<=10; i++){
            Member member = Member.builder()
                                .name("tester")
                                .age(10 + i)
                                .build();
            members.add(member);
        }
        
        memberrepository.saveAll(members);

        System.out.println("clear time : "  + (System.currentTimeMillis() - time) + "ms.");
    }
}
save() 메서드 결과 : 73ms saveAll() 메서드 결과 : 18ms

성능차이가 나는 이유

코드를 실행시키고 실행시간이 얼마나 걸렸는지를 보면 이렇게 확연히 차이가 나는 것을 확인할 수 있다 고작 10건의 데이터를 넣었을 뿐인데 이렇게 차이가 나는걸 보면 100건이 아니라 10000건 데이터를 넣을때를 생각하면 어마어마한 차이이다, 대체 왜 이런결과가 나오는걸까 확인을 해보면 transaction(트랜잭션)과 관련이 있다.

save는 한번 실행 할 때마다 트랜잭션이 생성되어있는지 확인하고 없으면 생성을 해주는 작업을 매번확인을 해주기 때문에 리소스소모를 한다.

saveAll은 내부에서 save를 호출해 주기 때문에 하나의 트랜잭션으로 동작하기 때문에 리소스 소모가 적다. 그래서 여러건의 데이터를 저장 및 수정을 해야한다면 save를 여러번 반복하기 보다는 saveAll을 하는게 성능이 좋다고 할 수있다.

save() 트랜잭션 존재여부에 따른 차이

트랜잭션 존재트랜잭션 미존재
- 기존 트랜잭션에 참여
- @Transactional 이 걸려있기에 spring의 프록시 로직을 탄다.
- 트랜잭션을 생성하고 종료한다.

saveAll() 트랜잭션 존재여부에 따른 차이

트랜잭션 존재트랜잭션 미존재
- 기존 트랜잭션에 참여
- save()를 호출할 때 같은 인스턴스에서 내부 호출하기에
프록시 로직을 타지 않는다.
- saveAll()을 호출했을 때 트랜잭션을 생성한다.

saveAndFlush(), saveAllAndFlush()

이제 saveAndFlush메서드와 saveAllAndFlush메서드에 대해 알아볼건데 결론부터 말하면 영속성 컨텍스트에 저장을 안하고 바로 DB에 저장한다는 차이점이다.

// saveAndFlush 메서드
@Transactional
@Override
public <S extends T> S saveAndFlush(S entity) {

	S result = save(entity);
	flush();

	return result;
}

// saveAllAndFlush 메서드
@Transactional
@Override
public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {

	List<S> result = saveAll(entities);
	flush();

	return result;
}

여기서 flush는 위에서 설명한 대로 쓰기 지연 SQL저장소에 저장하고 commit을 하면 쓰기 지연 SQL저장소의 내용을 실행한다, 결국 기존의 save / saveAll 기능에 flush를 추가 했다고 보면 된다.

saveAndFlush(), saveAllAndFlush() 테스트

이제 실제로 테스트를 해봤는데 save와 saveAll를 실행했을 때와 같이 실행시간적 면에서는 다르지 않았지만 과정이 달랐다고 볼 수 있었다, 추가적으로 @Transaction 어노테이션을 제외하고 실행 했을 때의 결과도 달랐다.

(이번 테스트에는 saveAll과 saveAllAndFlush는 콘솔도 결과도 같아서 비교하지 않겠다.)

@Service
public class memberService {

	private final memberRepository memberrepository;
	
    memberService(memberRepository memberrepository){
        this.memberrepository = memberrepository;
    }
    
	@Transactional
    public void save(){
        Member member = new Member("tester00", 10);
        System.out.println("1========================1");
        memberrepository.save(member);
        System.out.println("1========================1");

        member.setName("tester11");

        System.out.println("2========================2");
        memberrepository.save(member);
        System.out.println("2========================2");

        member.setName("tester22");
    }

    @Transactional
    public void saveAndFlush(){
        Member member = new Member("tester00", 10);
        System.out.println("1========================1");
        memberrepository.saveAndFlush(member);
        System.out.println("1========================1");

        member.setName("tester11");

        System.out.println("2========================2");
        memberrepository.saveAndFlush(member);
        System.out.println("2========================2");

        member.setName("tester22");
    }
}
@트랜잭션 + saveAndFlush() 메서드 실행과정 @트랜잭션 + save() 메서드 실행과정 save(), saveAndFlush() 메서드 실행과정

과정에서 차이가 나는 이유

먼저 트랜잭션 어노테이션을 붙인 것을 설명하자면 첫 번째 save는 처음에 insert 쿼리가 생성이 되고 두 번째 save에서는 쿼리가 생성되지 않지만 최종적으로 tester22로 업데이트를 할 때에는 update쿼리가 생성되고 tester22가 입력이 된 것을 확인 할 수 있다, 첫 번째 saveAndFlush에서는 insert에서 쿼리가 생성이되고 save와 다르게 두 번째 saveAndFlush는 update쿼리를 볼 수 있고 최종에도 save와 마찬가지로 update쿼리가 생성되고 tester22가 입력이 된 것을 확인 할 수 있다.

트랜잭션을 제외하고 실행한 경우에는 트랜잭션으로 묶여있지 않아서 마지막에 업데이트한 tester22가 DB상에 없는 것을 확인 할 수 있다.

최종정리

결과적으로 용도에 맞게 사용하면 된다.


참고 사이트

https://velog.io/@baekgom/save-saveAll-saveAndFlush

https://velog.io/@sudhdkso/JPA-save와-saveAll의-성능-차이

https://happyer16.tistory.com/entry/Spring-jpa-save-saveAndFlush-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%93%B0%EA%B8%B0

profile
가끔은 정신줄 놓고 멍 때리는 것도 필요하다.

0개의 댓글