save(), saveAll(), saveAndFlush()

박영준·2023년 3월 6일
0

JPA

목록 보기
1/8

상황?
객체 A 를 DB 에 insert 하기 위해 JPA 메소드를 사용할 것이다.
그런데 inser 쿼리 메소드 3개 中 하나를 사용해야한다.

1. save(), saveAll(), saveAndFlush()

save()

  • 1개 저장

  • 즉시 DB에 저장되지 않고
    영속성 컨텍스트에 저장되었다가, 추후에 flush() 또는 commit() 해줘야 DB 에 저장된다.

    flush() 와 commit()
    flush
    - 영속성 컨텍스트의 변경 내용을 DB에 동기화 (비우는 것이 아님)
    - Transaction commit 을 하면, flush 가 동작해서 쓰기 지연에 있는 쿼리들을 수행
    - rollback O

    commit
    - DB에 동기화된 내용들이 영구 저장되어, rollback X

  • JpaRepository 를 사용하면, save() 메소드는 저장한 객체를 그대로 반환한다
    Entity

    @Getter
    @Setter
    @Entity
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Sample {
    
        @Id @GeneratedValue
        private Long id;
    
        private String title;
    
        private String content;
    
        @Builder
        public Sample(String title, String content) {
            this.title = title;
            this.content = content;
        }
    }

    ※ setter 는 보통 사용하면 안되지만, 예시에서는 편의를 위해 사용

    Repository

    public interface SampleRepository extends JpaRepository<Sample, Long> {
    }

    Test Code

    @Test
    public void 새로_저장() throws Exception {
        //given
        Sample sample = new Sample("title", "content");
        Sample savedSample = sampleRepository.save(sample);
    
        //when
        em.flush();
        em.clear();
    
        Optional<Sample> foundSample = sampleRepository.findById(sample.getId());
        Optional<Sample> foundSavedSample = sampleRepository.findById(savedSample.getId());
    
        //then
        assertThat(foundSample.isPresent()).isTrue();
        assertThat(foundSavedSample.isPresent()).isTrue();
        assertThat(foundSample.get().getId()).isEqualTo(foundSavedSample.get().getId());
    }
  • 주의!
    SimpleJpaRepository(구현체)의 save() 메소드를 사용할 때는 신중해야한다.

    SimpleJpaRepository

    @Transactional
    @Override
    public <S extends T> S save(S entity) {
    
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }

    save() 메소드 내부에서 일어나는 일

    1. 파라미터로 들어온 entity 가 새로운 엔티티라면, persist 를 호출
      아니라면, merge 한다.
      (merge : 보통 detach 된 엔티티를 다시 영속 상태로 만들기 위해 사용)

      주의!
      식별자에 @GenerateValue 를 설정했다면, DB 에 식별자 생성을 위임하므로 save 호출 시점에 새로운 엔티티로 인식하여 persist 가 호출 되겠지만
      @Id 만 사용해서 식별자를 직접 할당 했다면, save 호출 시점에 새로운 엔티티가 아니므로 merge 를 호출

      참고: 식별자

  1. 파라미터로 들어온 entity가 영속성 컨텍스트 1차 캐시에 있는지 확인 후 없다면,
    DB 에 select 쿼리를 날려서 조회

  2. DB 에서 조회가 되어 1차 캐시에 엔티티가 저장되면
    트랜잭션 커밋 시점에 파라미터 entity의 값과 1차 캐시에 저장되어 있는 entity의 값을 비교하여 다른 점이 있을 경우, updata 쿼리가 발생
    DB 에 해당 값이 없을 경우, insert 쿼리가 발생

    → 즉, save() 메서드로 단순히 저장하려고 해도 update 쿼리가 발생할 수 있다

    주의!
    DB 에 해당 식별자로 하는 데이터가 이미 들어가 있을 경우
    해당 데이터는 파라미터 entity 의 값으로 모두 update 되어버린다.

    SimpleJpaRepository

  • 스프링 데이터 JPA가 제공해주는 가장많은 기능을 가지고 있는 가장 밑단에 있는 클래스

  • JpaRepository를 상속받으면 가져오게 되는 구현체

    entity가 새로운 엔티티인지는 구별하는 법
    entity 식별자가 객체(Long, String ...)일 경우, null
    entity 식별자가 기본 타입(int, long ...)일 경우, 0 → 이 경우가 새로운 엔티티

saveAll()

  • n개 저장

  • List에 entity를 모두 담아서, 한 번에 saveAll 하는 게 성능면에서 더 좋음

saveAndFlush()

  • 즉시 DB 에 변경 사항(데이터)을 적용

결론
데이터의 개수와 목적에 따라 선택하면 된다.

2. save(), saveAll()

save()

save 는
트랜잭션이 있으면, 프록시 로직을 타고 기존 트랜잭션에 참여
트랜잭션이 없으면, 생성됐다가 종료

saveAll()

saveAll 은
save 를 호출하긴하나, 같은 인스턴스에서 내부적으로 계속 호출하므로 다음 순서의 save 가 프록시 로직을 타지 않는다.
트랜잭션이 없으면, 한번 생성하고 기존의 트랜잭션에 참여해 save를 호출하므로 프록시 로직을 또 타지 않는다.

결론
둘의 성능의 차이는 프록시 로직을 타느냐 타지 않느냐의 차이다.

3. save(), saveAndFlush()

1) @Transactional 없는 상태에서

save()

코드 작성

public Member saveMember(MemberDTO memberDTO){
	Member member = Member.create(memberDTO);
    member = memberRepository.save(member);
    member.setName("TaeHyun");
    return member;	// break point 설정
}

결과

Hibernate :
	insert
    into
    	member
        (name, uuid ... )
   	values
    	(?,?, ... )

saveAndFlush()

코드 작성

public Member saveMember(MemberDTO memberDTO){
	Member member = Member.create(memberDTO);
    member = memberRepository.saveAndFlush(member);
    member.setName("TaeHyun");
    return member;	// break point 설정
}

결과

Hibernate :
	insert
    into
    	member
        (name, uuid ... )
   	values
    	(?,?, ... )

결론
save(), saveAndFlush() 모두 console 에 출력되지만, 변경된 name(태현) 은 입력되지 않는다.

2) @Transactional 있는 상태에서

(1) 공통점

@Transactional
public Member saveMember(MemberDTO memberDTO){
	Member member = Member.create(memberDTO);
    member = memberRepository.save(member);
    member.setName("TaeHyun");
    return member;	// break point 설정
}
@Transactional
public Member saveMember(MemberDTO memberDTO){
	Member member = Member.create(memberDTO);
    member = memberRepository.saveAndFlush(member);
    member.setName("TaeHyun");
    return member;	// break point 설정
}

결론
save(), saveAndFlush() 모두 breakpoint 까지는 update 되지 않지만,
함수가 종료된 후 update 된다.

(2) 차이점

@Transactional + save()

@Transactional
public Member saveMember(MemberDTO memberDTO){
	Member member = Member.create(memberDTO);
    
    member = memberRepository.save(member);		// 첫번째 breakpoint : insert query 출력 / db 업데이트 X
    member.setName("TaeHyun");
    
    member = memberRepository.save(member);		// 두번째 breakpoint : query 출력 X / db 업데이트 X
    member.setName("TaeHyun2");
    
    member = memberRepository.save(member);		// 세번째 breakpoint : query 출력 X / db 업데이트 X
    member.setName("TaeHyun3");
    
    return member;		// 메소드 종료 후 breakpoint : update query 출력 / db 업데이트 됨
}

실제 DB 에 TaeHyun3만 update 된 것

@Transactional + saveAndFlush() + set 을 여러번

@Transactional
public Member saveMember(MemberDTO memberDTO){
	Member member = Member.create(memberDTO);
    
    member = memberRepository.saveAndFlush(member);		// 첫번째 breakpoint : insert query 출력 / db 업데이트 X
    member.setName("TaeHyun");
    
    member = memberRepository.saveAndFlush(member);		// 두번째 breakpoint : update query 출력  / db 업데이트 X
    member.setName("TaeHyun2");
    
    member = memberRepository.saveAndFlush(member);		// 세번째 breakpoint : update query 출력  / db 업데이트 X
    member.setName("TaeHyun3");
    
    return member;		// 메소드 종료 후 breakpoint : update query 출력 / db 업데이트 됨
}    

결론
saveAndFlush() 동작 과정

saveAndFlush() 의 Flush 는 DB 로 업데이트를 하는 게 아니라,
Persistence Context 내부에 특정 공간(임시로 Query Space라고 명명) 으로 flush 를 해두고, 트랜잭션 종료 시점이 되서야 DB 에 업데이트한다.

따라서, 효율성 측면에서 saveAndFlush() 보다는 save() 를 권장한다.


참고: [JPA] save(), saveAll(), saveAndFlush() 차이
참고: Spring jpa save(), saveAndFlush() 제대로 알고 쓰기
참고: [JPA] save 와 saveAndFlush의 차이
참고: JpaRepository를 사용할 때, save() 메소드가 객체를 반환하는 이유
참고: [스프링 데이터 JPA] 3-8. 스프링 데이터 Common: 기본 리포지토리 커스터마이징
참고: [Spring Data Jpa] JpaRepository save() 메서드 주의 사항

profile
개발자로 거듭나기!

0개의 댓글