• 먼저 생각해 볼 수 있는 방법은 @EntityGraph로 한 번에 다 가져올 수 있습니다.
  • join fetch을 사용하여 한 번에 쿼리 할 수 있습니다.
  • hibernate의 default batch를 사용하는 방법이 있습니다.

3. Hibernate의 default batch를 사용하는 방법

spring boot를 사용하여 application.yml에 다음 항목을 설정하면 됩니다.

spring:

  jpa:

    properties:

      hibernate.default_batch_fetch_size: 1000

===== LAZY
Hibernate: 
    select
        post0_.id as id1_1_,
        post0_.title as title2_1_ 
    from
        post post0_
2020-06-15 15:32:53.655 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_] : [BIGINT]) - [1]
2020-06-15 15:32:53.659 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_1_] : [VARCHAR]) - [첫 포스트]
2020-06-15 15:32:53.660 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_] : [BIGINT]) - [4]
2020-06-15 15:32:53.660 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_1_] : [VARCHAR]) - [내가 2등~]
2020-06-15 15:32:53.667 TRACE 9690 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#1]
2020-06-15 15:32:53.667 TRACE 9690 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#4]
Hibernate: 
    select
        comments0_.post_id as post_id3_0_1_,
        comments0_.id as id1_0_1_,
        comments0_.id as id1_0_0_,
        comments0_.comment as comment2_0_0_,
        comments0_.post_id as post_id3_0_0_ 
    from
        comment comments0_ 
    where
        comments0_.post_id in (
            ?, ?
        )
2020-06-15 15:32:53.672 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [4]
2020-06-15 15:32:53.673 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2020-06-15 15:32:53.676 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_0_] : [BIGINT]) - [2]
2020-06-15 15:32:53.677 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_0_0_] : [VARCHAR]) - [첫 댓글~! ]
2020-06-15 15:32:53.677 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_0_] : [BIGINT]) - [1]
2020-06-15 15:32:53.678 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_1_] : [BIGINT]) - [1]
2020-06-15 15:32:53.678 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_1_] : [BIGINT]) - [2]
2020-06-15 15:32:53.683 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_0_] : [BIGINT]) - [3]
2020-06-15 15:32:53.683 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_0_0_] : [VARCHAR]) - [두번째야~]
2020-06-15 15:32:53.684 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_0_] : [BIGINT]) - [1]
2020-06-15 15:32:53.684 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_1_] : [BIGINT]) - [1]
2020-06-15 15:32:53.685 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_1_] : [BIGINT]) - [3]
2020-06-15 15:32:53.685 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_0_] : [BIGINT]) - [5]
2020-06-15 15:32:53.685 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_0_0_] : [VARCHAR]) - [좋아요~]
2020-06-15 15:32:53.686 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_0_] : [BIGINT]) - [4]
2020-06-15 15:32:53.686 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_1_] : [BIGINT]) - [4]
2020-06-15 15:32:53.686 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_1_] : [BIGINT]) - [5]
2020-06-15 15:32:53.687 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_0_] : [BIGINT]) - [6]
2020-06-15 15:32:53.688 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_0_0_] : [VARCHAR]) - [감사합니다.]
2020-06-15 15:32:53.688 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_0_] : [BIGINT]) - [4]
2020-06-15 15:32:53.689 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_1_] : [BIGINT]) - [4]
2020-06-15 15:32:53.694 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_1_] : [BIGINT]) - [6]
2020-06-15 15:32:53.695 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_0_] : [BIGINT]) - [7]
2020-06-15 15:32:53.696 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_0_0_] : [VARCHAR]) - [다음글 기대할께요.]
2020-06-15 15:32:53.696 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_0_] : [BIGINT]) - [4]
2020-06-15 15:32:53.696 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_0_1_] : [BIGINT]) - [4]
2020-06-15 15:32:53.697 TRACE 9690 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_1_] : [BIGINT]) - [7]
LAZY : [[Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)]]

프록시를 통해서 연관된 entity의 실제 값을 가져오는 DB 쿼리를 in 절로 수정되어서 한 번에 쿼리가 됩니다.

그리고 @BatchSize 어노테이션을 class나 method, member variable에 각각 적용도 가능합니다. (실제 테스트 결과 @OneToMany에 적용했을 때만 동작했습니다. Entity class에 @BatchSize를 적용하는 방법은 좀 더 확인이 필요한 것 같습니다.)

@Entity
@BatchSize(size=100)
class Product {
    @OneToMany
    @BatchSize(size = 10) /
    Set<Product> getProducts() { ... };
}

 

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 2  (0) 2020.06.11
N + 1 문제 해결 1  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04
  • 먼저 생각해 볼 수 있는 방법은 @EntityGraph로 한 번에 다 가져올 수 있습니다.
  • join fetch을 사용하여 한 번에 쿼리할 수 있습니다.
  • hibernate의 default fetch를 사용하는 방법이 있습니다.

앞서 살펴본 EntityGraph에 이어 join fetch을 살펴 보겠습니다.

2. Join Fetch를 사용하는 방법

먼저 살펴본 방법(2020/06/11 - [Programming/JPA] - N + 1 문제 해결 1)은 JpaRepository에서 원하는 쿼리로 바뀌도록 @EntityGraph를 이용하여 가이드 했다면 이번 방법은 JPQL을 사용하여 직접적으로 해결하는 방법입니다.

public interface PostRepository extends JpaRepository<Post, Long> {

    @Query("select p from Post p join fetch p.comments")
    List<Post> findAllWithComments();

}

해결편1에서 사용한 소스에 PostRepository만 바뀌었습니다. "join fetch p.comments"부분이 추가 되었습니다.

@SpringBootTest
class PostRepositoryTest {

    @Autowired
    private PostService postService;
    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    public void setup() {
        Post post = new Post();
        post.setTitle("첫 포스트");

        Comment comment1 = new Comment();
        comment1.setComment("첫 댓글~! ");
        comment1.setPost(post);
        post.getComments().add(comment1);

        Comment comment2 = new Comment();
        comment2.setComment("두번째야~");
        post.getComments().add(comment2);
        comment2.setPost(post);

        Post post2 = new Post();
        post2.setTitle("내가 2등~");

        Comment comment3 = new Comment();
        comment3.setComment("좋아요~");
        comment3.setPost(post2);
        post2.getComments().add(comment3);

        Comment comment4 = new Comment();
        comment4.setComment("감사합니다.");
        comment4.setPost(post2);
        post2.getComments().add(comment4);

        Comment comment5 = new Comment();
        comment5.setComment("다음글 기대할께요.");
        comment5.setPost(post2);
        post2.getComments().add(comment5);

        postRepository.save(post);
        postRepository.save(post2);
    }

    @Test
    void testNPlusOne() {
        // lazy loading
        System.out.println("===== LAZY");
        System.out.println("LAZY : " + postService.findAllComments());

        // Join fetch
        System.out.println("===== JoinFetch");
        System.out.println("JoinFetch : " + postService.findAllWithComments());
    }
}

결과는 아래와 같이 inner join으로 실행되었죠...

===== JoinFetch
Hibernate: 
    select
        post0_.id as id1_3_0_,
        comments1_.id as id1_2_1_,
        post0_.title as title2_3_0_,
        comments1_.comment as comment2_2_1_,
        comments1_.post_id as post_id3_2_1_,
        comments1_.post_id as post_id3_2_0__,
        comments1_.id as id1_2_0__ 
    from
        post post0_ 
    inner join
        comment comments1_ 
            on post0_.id=comments1_.post_id
2020-06-11 17:58:26.842 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [1]
2020-06-11 17:58:26.843 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [2]
2020-06-11 17:58:26.843 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_0_] : [VARCHAR]) - [첫 포스트]
2020-06-11 17:58:26.844 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [첫 댓글~! ]
2020-06-11 17:58:26.844 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 17:58:26.844 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [1]
2020-06-11 17:58:26.845 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [2]
2020-06-11 17:58:26.845 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [1]
2020-06-11 17:58:26.846 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [3]
2020-06-11 17:58:26.846 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [두번째야~]
2020-06-11 17:58:26.846 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 17:58:26.847 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [1]
2020-06-11 17:58:26.849 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [3]
2020-06-11 17:58:26.850 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 17:58:26.850 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [5]
2020-06-11 17:58:26.854 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_0_] : [VARCHAR]) - [내가 2등~]
2020-06-11 17:58:26.855 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [좋아요~]
2020-06-11 17:58:26.855 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 17:58:26.857 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 17:58:26.857 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [5]
2020-06-11 17:58:26.857 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 17:58:26.857 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [6]
2020-06-11 17:58:26.858 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [감사합니다.]
2020-06-11 17:58:26.858 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 17:58:26.859 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 17:58:26.859 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [6]
2020-06-11 17:58:26.861 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 17:58:26.862 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [7]
2020-06-11 17:58:26.863 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [다음글 기대할께요.]
2020-06-11 17:58:26.863 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 17:58:26.863 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 17:58:26.864 TRACE 3302 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [7]
JoinFetch : [[Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)]]

@EntityGraph를 사용한 1편에서는 left outer join으로 실행 되었던 것을 기억하시나요?

그렇습니다. 이 두 방법은 실행 방식이 다릅니다.

  • @EntityGraph : left outer join
  • join fetch : inner join

그런데 두 방법 모두 comments가 중복되어서 출력될 수 있습니다. 카테시안 곱이 되기 때문입니다.

Cartesian Product 해결

코너속의 코너

먼저 두 가지 방법이 있습니다.

  1. collection을 List가 아닌 Set으로 바꾸는 방법입니다.
    private Set<Comment> comments = new LinkedHashSet<>();
    (참고로 HashSet으로 하면 중복 제거는 되는데 순서가 보장이 안됩니다.)
  2. @Query에서 distinct를 사용하는 방법입니다.
    @Query("select distinct p from Post p join fetch p.comments")
    List<Post> findAllWithComments();

개인적으로는 List를 사용하는 것이 더 좋아 보입니다. 왜냐하면 JpaRepository의 interface들은 다 List이기 때문입니다. 하지만 상황에 따라 Set을 쓰지 않는다는 보장은 없으니 여러모로 알고 있는 것이 이로워 보입니다. 그리고 항상 느끼는 것이지만 발생된 query와 결과는 꼭 확인이 필요합니다.

 

github source : https://github.com/aloftcat/blog-code/tree/master/jpa/NPlusOne

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 3  (0) 2020.06.15
N + 1 문제 해결 1  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04

앞서 살펴본 N + 1 문제 원인은 @oneToMany 관계로 reference 하고 있는 collection 내용을 가져오려고 접근할 때 생깁니다. 변수에 접근할 때마다 그때그때 하나씩 쿼리가 날아가는 문제입니다.

그럼 이 문제를 해결하기 위해 어떻게 하는 것이 좋을까요?

  • 먼저 생각해 볼 수 있는 방법은 @EntityGraph로 한 번에 다 가져올 수 있습니다.
  • join fetch을 사용하여 한 번에 쿼리할 수 있습니다.
  • hibernate의 default fetch를 사용하는 방법이 있습니다.

1. @EntityGraph 사용하는 방법

그냥 @oneToMany를 똭 eager로 바꿔 버리면 다시는 lazy로 가져올 수는 없겠죠?^^; 그래서 필요할 때만 eager로 가져오고 평소에는 lazy로 가져오는 방법이 있습니다. 바로 EntityGraph입니다.

@Getter@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@ToString
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Comment> comments = new ArrayList<>();

}

@Setter@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@ToString(exclude = "post")
public class Comment {

    @Id
    @GeneratedValue
    private Long id;

    private String comment;

    @ManyToOne
    private Post post;
}

public interface PostRepository extends JpaRepository<Post, Long> {

    @EntityGraph(attributePaths = {"comments"})
    List<Post> findAllWithComments();
}

기존 소스에 findAllWithComments()를 추가하고 @EntityGraph와 가져올 대상(collection 변수명)을 지정했습니다.

public interface PostRepository extends JpaRepository<Post, Long> {

    @EntityGraph(attributePaths = {"comments"})
    @Query("select p from Post p")
    List<Post> findAllWithComments();
}

@Service
public class PostService {

    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    @Transactional(readOnly = true)
    public List<String> findAllComments() {
        return getAllComments(postRepository.findAll());
    }

    @Transactional(readOnly = true)
    public List<String> findAllWithComments() {
        return getAllComments(postRepository.findAllWithComments());
    }

    /**
     * Lazy Load를 실행하기 위해 모든 comment 순회
     */
    private List<String> getAllComments(List<Post> posts) {
        return posts.stream()
                .map(a -> a.getComments().toString())
                .collect(Collectors.toList());
    }
}

@SpringBootTest
class PostRepositoryTest {

    @Autowired
    private PostService postService;
    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    public void setup() {
        Post post = new Post();
        post.setTitle("첫 포스트");

        Comment comment1 = new Comment();
        comment1.setComment("첫 댓글~! ");
        comment1.setPost(post);
        post.getComments().add(comment1);

        Comment comment2 = new Comment();
        comment2.setComment("두번째야~");
        post.getComments().add(comment2);
        comment2.setPost(post);

        Post post2 = new Post();
        post2.setTitle("내가 2등~");

        Comment comment3 = new Comment();
        comment3.setComment("좋아요~");
        comment3.setPost(post2);
        post2.getComments().add(comment3);

        Comment comment4 = new Comment();
        comment4.setComment("감사합니다.");
        comment4.setPost(post2);
        post2.getComments().add(comment4);

        Comment comment5 = new Comment();
        comment5.setComment("다음글 기대할께요.");
        comment5.setPost(post2);
        post2.getComments().add(comment5);

        postRepository.save(post);
        postRepository.save(post2);
    }

    @Test
    void testNPlusOne() {
        // lazy loading
        System.out.println("===== LAZY");
        System.out.println("LAZY : " + postService.findAllComments());

        // EntityGraph
        System.out.println("===== EntityGraph");
        System.out.println("EntityGraph : " + postService.findAllWithComments());
    }
}

결과는 left outer join으로 실행되었습니다. 이 부분은 다음 편에서 비교할 부분이라 체크해 두세요.

===== LAZY
Hibernate: 
    select
        post0_.id as id1_3_,
        post0_.title as title2_3_ 
    from
        post post0_
2020-06-11 02:01:04.011 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_] : [BIGINT]) - [1]
2020-06-11 02:01:04.015 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_] : [VARCHAR]) - [첫 포스트]
2020-06-11 02:01:04.015 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_] : [BIGINT]) - [4]
2020-06-11 02:01:04.016 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_] : [VARCHAR]) - [내가 2등~]
2020-06-11 02:01:04.021 TRACE 31191 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#1]
2020-06-11 02:01:04.022 TRACE 31191 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#4]
Hibernate: 
    select
        comments0_.post_id as post_id3_2_0_,
        comments0_.id as id1_2_0_,
        comments0_.id as id1_2_1_,
        comments0_.comment as comment2_2_1_,
        comments0_.post_id as post_id3_2_1_ 
    from
        comment comments0_ 
    where
        comments0_.post_id=?
2020-06-11 02:01:04.030 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2020-06-11 02:01:04.038 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [2]
2020-06-11 02:01:04.038 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [첫 댓글~! ]
2020-06-11 02:01:04.039 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.040 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.040 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [2]
2020-06-11 02:01:04.046 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [3]
2020-06-11 02:01:04.046 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [두번째야~]
2020-06-11 02:01:04.047 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.047 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.047 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [3]
Hibernate: 
    select
        comments0_.post_id as post_id3_2_0_,
        comments0_.id as id1_2_0_,
        comments0_.id as id1_2_1_,
        comments0_.comment as comment2_2_1_,
        comments0_.post_id as post_id3_2_1_ 
    from
        comment comments0_ 
    where
        comments0_.post_id=?
2020-06-11 02:01:04.074 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [4]
2020-06-11 02:01:04.076 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [5]
2020-06-11 02:01:04.076 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [좋아요~]
2020-06-11 02:01:04.077 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.077 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.078 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [5]
2020-06-11 02:01:04.078 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [6]
2020-06-11 02:01:04.079 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [감사합니다.]
2020-06-11 02:01:04.079 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.080 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.080 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [6]
2020-06-11 02:01:04.081 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [7]
2020-06-11 02:01:04.081 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [다음글 기대할께요.]
2020-06-11 02:01:04.082 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.083 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.083 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [7]
LAZY : [[Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)]]
===== EntityGraph
Hibernate: 
    select
        post0_.id as id1_3_0_,
        comments1_.id as id1_2_1_,
        post0_.title as title2_3_0_,
        comments1_.comment as comment2_2_1_,
        comments1_.post_id as post_id3_2_1_,
        comments1_.post_id as post_id3_2_0__,
        comments1_.id as id1_2_0__ 
    from
        post post0_ 
    left outer join
        comment comments1_ 
            on post0_.id=comments1_.post_id
2020-06-11 02:01:04.130 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.131 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [2]
2020-06-11 02:01:04.132 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_0_] : [VARCHAR]) - [첫 포스트]
2020-06-11 02:01:04.133 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [첫 댓글~! ]
2020-06-11 02:01:04.134 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.134 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [1]
2020-06-11 02:01:04.135 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [2]
2020-06-11 02:01:04.135 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.135 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [3]
2020-06-11 02:01:04.136 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [두번째야~]
2020-06-11 02:01:04.137 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.137 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [1]
2020-06-11 02:01:04.138 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [3]
2020-06-11 02:01:04.138 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.139 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [5]
2020-06-11 02:01:04.140 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_0_] : [VARCHAR]) - [내가 2등~]
2020-06-11 02:01:04.140 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [좋아요~]
2020-06-11 02:01:04.141 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.141 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 02:01:04.142 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [5]
2020-06-11 02:01:04.142 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.143 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [6]
2020-06-11 02:01:04.143 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [감사합니다.]
2020-06-11 02:01:04.143 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.144 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 02:01:04.145 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [6]
2020-06-11 02:01:04.145 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.146 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [7]
2020-06-11 02:01:04.146 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [다음글 기대할께요.]
2020-06-11 02:01:04.147 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.147 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 02:01:04.147 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [7]
EntityGraph : [[Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)]]

그리고 EntityGraph에는 EntityGraphType이 존재하는데요. 아래와 같이 두 가지가 있습니다. 아무런 설정하지 않으면 default는 FETCH로 동작합니다.

  • FETCH: entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 LAZY로 패치
  • LOAD: entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 entity에 명시한 fetch type이나 디폴트 FetchType으로 패치 (e.g. @OneToMany는 LAZY, @ManyToOne은 EAGER 등이 디폴트이다.)

github source : https://github.com/aloftcat/blog-code/tree/master/jpa/NPlusOne

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 3  (0) 2020.06.15
N + 1 문제 해결 2  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04

정말 많이 접하는 문제입니다. 왜 발생하고 처리하는 방법은 뭐가 있는지 정리하려 합니다.

우선 왜 이런 문제가 생기는지 정리합니다. 언제나 문제는 원인부터 알아야 올바르게 대처하니까요.

 

포스트(Post)와 댓글(Comment)와의 관계를 예로 작성해 보았습니다.

@Getter@Setter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Entity
@ToString
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Comment> comments = new ArrayList<>();

}

@Setter@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Entity
@ToString(exclude = "post")
public class Comment {

    @Id
    @GeneratedValue
    private Long id;

    private String comment;

    @ManyToOne
    private Post post;
}
public interface PostRepository extends JpaRepository<Post, Long> {
}

public interface CommentRepository extends JpaRepository<Comment, Long> {
}
@Slf4j
@Service
public class PostService {

    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    @Transactional(readOnly = true)
    public List<String> findAllComments() {
        return getAllComments(postRepository.findAll());
    }

    /**
     * Lazy Load를 실행하기 위해 모든 comment 순회
     */
    private List<String> getAllComments(List<Post> posts) {
        return posts.stream()
                .map(a -> a.getComments().toString())
                .collect(Collectors.toList());
    }
}

 

test code는 junit5을 사용하였습니다.

@SpringBootTest
class PostRepositoryTest {

    @Autowired
    private PostService postService;
    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    public void setup() {
        Post post = new Post();
        post.setTitle("첫 포스트");

        Comment comment1 = new Comment();
        comment1.setComment("첫 댓글~! ");
        comment1.setPost(post);
        post.getComments().add(comment1);

        Comment comment2 = new Comment();
        comment2.setComment("두번째야~");
        post.getComments().add(comment2);
        comment2.setPost(post);

        Post post2 = new Post();
        post2.setTitle("내가 2등~");

        Comment comment3 = new Comment();
        comment3.setComment("좋아요~");
        comment3.setPost(post2);
        post2.getComments().add(comment3);

        Comment comment4 = new Comment();
        comment4.setComment("감사합니다.");
        comment4.setPost(post2);
        post2.getComments().add(comment4);

        Comment comment5 = new Comment();
        comment5.setComment("다음글 기대할께요.");
        comment5.setPost(post2);
        post2.getComments().add(comment5);

        postRepository.save(post);
        postRepository.save(post2);
    }

    @Test
    void testNPlusOne() {
        System.out.println(postService.findAllComments());
    }
Hibernate: 
    select
        post0_.id as id1_3_,
        post0_.title as title2_3_ 
    from
        post post0_
2020-06-05 11:50:15.206 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_] : [BIGINT]) - [1]
2020-06-05 11:50:15.210 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_] : [VARCHAR]) - [첫 포스트]
2020-06-05 11:50:15.211 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_] : [BIGINT]) - [4]
2020-06-05 11:50:15.211 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_] : [VARCHAR]) - [내가 2등~]
2020-06-05 11:50:15.218 TRACE 4256 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#1]
2020-06-05 11:50:15.219 TRACE 4256 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#4]
Hibernate: 
    select
        comments0_.post_id as post_id3_2_0_,
        comments0_.id as id1_2_0_,
        comments0_.id as id1_2_1_,
        comments0_.comment as comment2_2_1_,
        comments0_.post_id as post_id3_2_1_ 
    from
        comment comments0_ 
    where
        comments0_.post_id=?
2020-06-05 11:50:15.228 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2020-06-05 11:50:15.239 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [2]
2020-06-05 11:50:15.240 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [첫 댓글~! ]
2020-06-05 11:50:15.240 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-05 11:50:15.242 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [1]
2020-06-05 11:50:15.242 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [2]
2020-06-05 11:50:15.250 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [3]
2020-06-05 11:50:15.250 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [두번째야~]
2020-06-05 11:50:15.250 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-05 11:50:15.251 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [1]
2020-06-05 11:50:15.251 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [3]
Hibernate: 
    select
        comments0_.post_id as post_id3_2_0_,
        comments0_.id as id1_2_0_,
        comments0_.id as id1_2_1_,
        comments0_.comment as comment2_2_1_,
        comments0_.post_id as post_id3_2_1_ 
    from
        comment comments0_ 
    where
        comments0_.post_id=?
2020-06-05 11:50:15.264 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [4]
2020-06-05 11:50:15.266 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [5]
2020-06-05 11:50:15.267 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [다음글 기대할께요.]
2020-06-05 11:50:15.267 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-05 11:50:15.268 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-05 11:50:15.268 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [5]
2020-06-05 11:50:15.269 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [6]
2020-06-05 11:50:15.270 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [좋아요~]
2020-06-05 11:50:15.270 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-05 11:50:15.271 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-05 11:50:15.271 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [6]
2020-06-05 11:50:15.271 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [7]
2020-06-05 11:50:15.271 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [감사합니다.]
2020-06-05 11:50:15.272 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-05 11:50:15.272 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-05 11:50:15.272 TRACE 4256 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [7]
[[Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=7, comment=감사합니다.), Comment(id=6, comment=좋아요~), Comment(id=5, comment=다음글 기대할께요.)]]

결과를 보면 처음 select 하나만 원했을텐데 이 후로 post의 수 많큼 select * from comment where post_id=1, select * from comment where post_id=4 이렇게 2번더 실행 되었습니다. 그래서 원하는 1번의 query가 아니라 n+1번 실행되어 N+1문제라고 합니다.

 

문제는 하위 entity가 처음 select에서 가져오지 않고 실제 사용될 때  하나씩 하나씩 따로 가져오게되어 N+1 문제가 발생합니다. 실제 변수를 사용하는 시점에 JPA가 해당 변수 하나만을 가져오는 쿼리가 실행이 되는 것이죠.

JPA는 fetchType과 무관하게 그때 그때 JPQL에 맞춰 쿼리를 실행합니다. 그래서 eager든 lazy든 n+1 문제는 동일하게 일어납니다.

 

 

 

 


나는 항상 오늘 하루만 생각을 해... 가 아니라 나는 변수 하나만을 생각해... 지금 그 변수를 당장 가져와야지라고 동작한다고 보시면 됩니다.

다음 포스트는 어떻게 처리할지 알아보겠습니다.

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 2  (0) 2020.06.11
N + 1 문제 해결 1  (0) 2020.06.11
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04
jpa 복합키에서 auto increment  (0) 2019.06.27

JPA를 왜 써야 할까에 앞서 이전에 진행했던 프로젝트들의 형태를 먼저 짚고 넘어가면,

 

기본적으로 JPA를 사용하고 있지만 더불어 JOOQ도 같이 사용하고 있습니다. 

 

public interface AccountInteHisRepositoryCustom {
    List<ActivityMonthlyReportDto> findAccountIntegrationHistoryMonthlyActiveStateByCreateDate(LocalDateTime date);
}

 

custom repository inteface를 정의하고 

 

@Repository
public class AccountInteHisRepositoryImpl implements AccountInteHisRepositoryCustom {

    ....

}

 

위와 같은 구현체인 XXXImpl에서 DSLContext를 주입받아 쿼리를 작성합니다.

이렇게 작성한 custom repository는 아래와 같이 JpaRepository와 같이 interface로 상속받아 적용합니다.

 

public interface AccountInteHisRepository extends JpaRepository<AccountInteHis, Long>, AccountInteHisRepositoryCustom {
....
}

 

그런데 이렇게 만들어진 custom repository는 변경에 대해 JPA보다 취약합니다.

native 쿼리보다 jooq dsl이 type safe하고 DB엔진에 영향을 받지 않습니다. 하지만 결국 쿼리가 바뀌면 custom repository도 바뀌어야 합니다.

예를 들어 필요에 의해 join해야하는 table이 하나 더 추가되면 JPA는 reference를 추가하는 것만으로 해결되지만 Jooq는 구현된 DSL을 다 바꿔야 하는 것입니다.

this.dslContext.select()
      .from(acctInteHis)
      .innerJoin(T_DEVICE)
      .on(acctInteHis.IMEI.eq(T_DEVICE.DEVICE_UID))
      .leftJoin(T_DEVICE_EXT)
      .on(device.DEVICE_UID.eq(T_DEVICE_EXT.DEVICE_UID))
      .where(acctInteHis.SEQ.in(selectSubQuery))
      .fetch()
      .stream()
      .map(m -> {
        return ActivityMonthlyReportDto.builder()
          .imei(m.getValue(acctInteHis.IMEI))
          .userId(m.getValue(acctInteHis.USER_ID))
          .state(AccountStateType.valueOf(m.getValue(acctInteHis.STATE)))
          .reqDate(m.getValue(acctInteHis.REQ_DATE).toInstant())
          .opCode(m.getValue(deviceExtension.OPCO))
          .productCode(m.getValue(deviceExtension.PRODUCE_CD))
          .build();
      })
      .collect(Collectors.toList());

jooq DSL의 예시입니다. 분명 내가 원하는 복잡한 쿼리를 구현할 수 있죠. 하지만 (비즈니스 로직 추가로 인해) query가 변경되면 DSL도 격하게 바뀔게 눈에 보이지 않나요? 실제 프로젝트는 영향받는 쿼리가 한 두 군데가 아닐 거라 생각합니다.

그리고 ActivityMonthlyReportDto라는 return 값도 바뀌지 말라는 보장도 없고 로직에 따라 Dto가 엄청 많아질 수 있습니다. 옛날에는 이게 귀찮아서 superset Dto를 만들어 사용하기도 했는데 이게 상황에 따라 어떤 member변수는 null이 된다는 것이 문제입니다.

하지만 JPA는 entity의 모든 reference는 (DB가 비어있지 않는 한) 채워져 있다는 것은 보장합니다. (이건 JPA가 application이 구동될 때 entity와 DB 스키마를 체크하기 때문에 믿을 수 있죠.)

더보기

이외에 JPA의 persistenceContext에 대한 장점등이 있는데요... dirty check, write behind, lazy loading등의 이점들이죠. 또한 domain 중심으로 데이터를 객체로 다룰 수 있는 기반을 JPA가 제공합니다.

 

결론은 DSL이 복잡하고 어려운 쿼리를 명확하게 구현한다는 것에는 의심의 여지가 없어 보입니다. 위의 dsl 예시처럼 subquery(selectSubQuery)도 마음먹은 대로 구현이 가능하니까요.

하지만 JPA를 잘 알고(!) 적용할 수 있다면 상당한 생산력 향상과 확장성을 확보 한다는 것은 매우 큰 매리트입니다.

JPA를 제대로 공부해서 마음껏 개발해야 겠습니다. (무슨 캠페인 같네요.ㅋㅋ)

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 1  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04
jpa 복합키에서 auto increment  (0) 2019.06.27
JPA 기본 키 전략  (0) 2019.06.27

@Entity

@Entity 어노테이션은 데이타베이스의 테이블과 일대일로 매칭되는 객체 단위이며 Entity 객체의 인스턴스 하나가 테이블에서 하나의 레코드 값을 의미합니다. 그래서 객체의 인스턴스를 구분하기 위한 유일한 키값을 가지는데 이것은 테이블 상의 Primary Key 와 같은 의미를 가지며 @Id 어노테이션으로 표기 됩니다.

먼저 Spring Boot 를 설정할때 spring.jpa.hibernate.ddl-auto 설정이 create 혹은 update 로 되어 있을 경우 Spring 프로젝트가 시작될때 EntityManager 가 자동으로 DDL 을 수행해 테이블을 생성해 줍니다.

이때 명시적으로 @Table  name 속성을 이용해 데이타베이스상의 실제 테이블 명칭을 지정하지 않는다면 Entity 클래스의 이름 그대로 CamelCase 를 유지한채 테이블이 생성이 되기 때문에 테이블 이름을 명시적으로 작성하는 것이 관례입니다. 왜냐하면 데이타베이스상에서 보편적으로 사용 되는 명명법은 UnderScore 가 원칙이기 때문입니다. 

@Entity
@Table(name = "ORGANIZATION")
public class Organization { 
    ...
}

@Entity
@Table(name = "EMPLOYEE")
public class Employee { 
    ...
}

@Entity
@Table(name = "EMPLOYEE_ADDRESS")
public class EmployeeAddress { 
    ...
}

 

@Column

@Column 어노테이션은 데이타베이스의 테이블에 있는 컬럼과 동일하게 1:1로 매칭되기 때문에 Entity 클래스안에 내부변수로 정의 됩니다. 만약 테이블에 a, b, c 컬럼이 있다면 각각 3개의 @Column 어노테이션을 작성 하게 됩니다. 다만 이때 의도적으로 필요없는 컬럼들은 작성하지 않아도 되는데 데이타베이스 테이블에 실제 a, b, c, d 총 4개의 컬럼이 있더라도 a,b,c 컬럼만 Entity 클래스에 작성해도 무방 하다는 이야기 입니다.

이때 @Column 어노테이션은 별다른 옵션을 설정하지 않는다면 생략이 가능합니다. 즉 Entity 클래스에 정의된 모든 내부변수는기본적으로 @Column 어노테이션을 포함한다고 볼 수 있습니다.

Spring Boot 의 spring.jpa.hibernate.ddl-auto 설정이 create 혹은 update 로 되어 있을 경우 create 일때는 최초에 한번 컬럼이 생성이 되고, update 일때는 Entity 클래스에 있지만 해당 테이블에 존재하지 않는 컬럼을 추가로 생성해 줍니다. 하지만 컬럼의 데이타 타입이 변경 되었거나 길이가 변경 되었을때 자동으로 데이타베이스에 반영을 해주지는 않기 때문에 속성이 변경되면 기존 테이블을 drop 후 새롭게 create 하던지 개별로 alter table 을 통해 직접 ddl 구문을 적용하는 것이 좋습니다.

spring.jpa.hibernate.ddl-auto 설정이 create-drop 로 되어 있으면 프로젝트가 시작될때 자동으로 기존 테이블을 drop 한 후 create 를 해줍니다. 하지만 기존 스키마가 전부 삭제 되기 때문에 시스템 설계와 개발 시점에만 사용해야 하며 운영 시점에 create, update, create-drop 을 사용 하지 않아야 합니다.

@Column  @Entity 어노테이션과 동일하게 name 속성을 명시하지 않으면 Entity 클래스에 정의한 컬럼 변수의 이름으로 생성이 됩니다. 그렇기 때문에 CamelCase 로 작성된 컬럼 변수가 있다면 UnderScore 형식으로 name 을 명시적으로 작성 합니다.

데이타베이스상에서 컬럼은 실제 데이타가 가질 수 있는 최대 길이를 가지게 되는데 이것은 데이타베이스에 데이타를 효율적으로 관리하기 위해서 입니다. @Column 에도 이처럼 length 속성으로 길이를 명시 할 수 있습니다. 만약 length 속성이 없다면 기본 길이인 255가 지정 됩니다. 이것은 문자열 형태인 데이타 속성에만 해당 되며 큰 숫자를 표현하는 BigDecimal 일 경우 precision, scale 로 최대 길이를 지정 할 수 있습니다.

@Column 
private String code;

@Column(length = 100)
private String name;

//@Column 은 생략이 가능합니다.
private String description; 

@Column(precision = 11, scale = 2)
private BigDecimal amount;

@Column
private Integer size;

@Column(name ="register_date")
private LocalDateTime registerDate;

 

@Id

데이타베이스의 테이블은 기본적으로 유일한 값을 가집니다. 그것을 PK (Primary Key) 라고 하는데 데이타베이스는 이 유일한 키값을 기준으로 질의한 데이타를 추출해 결과셋으로 반환해 줍니다. 테이블 상에 PK 가 없는 테이블도 있지만 대부분의 경우 반드시 PK 가 존재합니다.

JPA 에서도 Entity 클래스 상에 해당 PK 를 명시적으로 표시를 해야 되는데 그것을 @Id 어노테이션을 이용해 이것이 PK 임을 지정 합니다.

만약 Spring Boot 의 spring.jpa.hibernate.ddl-auto 속성이 create 로 되어 있고 아직 해당 테이블이 데이타베이스상에 존재하지 않는다면 EntityManager 가 DDL 을 통해 테이블을 생성하면서 PK 를 같이 생성해 줍니다.

@Id
@Column
private String code;

 

@GeneratedValue

PK 컬럼의 데이타 형식은 정해져 있지는 않으나 구분이 가능한 유일한 값을 가지고 있어야 하고 데이타 경합으로 인해 발생되는 데드락 같은 현상을 방지 하기 위해 대부분 BigInterger 즉 Java 의 Long 을 주로 사용합니다.

물론 String 형태의 고정된 키값을 직접 생성해서 관리하기도 합니다. 중요한 것은 대량의 요청이 유입 되더라도 중복과 deadlock 데드락이 발생 되지 않을 만큼 키값이 빨리 생성이 되고 안전하게 관리 되어야 한다는 점입니다.

deadlock

동일한 시점에 요청이 유입 되었을때 데이타베이스는 테이블 혹은 레코드를 lock 을 걸어 데이타가 변경되지 않도록 막아 놓고 다른 작업을 진행합니다.

이때 1번째 요청이 A 테이블의 값을 변경하고 lock 을 걸어둔 상태에서 B 테이블을 사용하려고 하고, 2번째 요청이 B 테이블의 값을 변경하고 lock 을 걸어둔 상태에서 A 테이블을 사용하려고 할때 데이타베이스는 우선순위를 판단 할 수 없어 그대로 교착상태에 빠져 버립니다.

이때는 어쩔 수 없이 강제로 시스템을 재시작하여 데이타베이스 커낵션을 초기화 시켜줘야 합니다.

 

가장 보편적으로 사용이 되는 데이타베이스인 MySQL, ORACLE 에는 Long 타입의 키값을 생성하는 방식이 서로 조금 다른데요. MySQL 은 auto increment 방식을 ORACLE 은 sequence 방식을 사용합니다.

- auto increment

먼저 MySQL 의 auto increment 방식은 숫자형의 PK 컬럼 속성을 auto increment 로 지정하면 자동으로 새로운 레코드가 생성이 될때마다 마지막 PK 값에서 자동으로 +1 을 해주는 방식입니다. 이를 위해 @GeneratedValue 어노테이션의 strategy 속성을 GenerationType.IDENTITY 로 지정해 auto increment 컬럼인 것을 EntityManager 에 알려 줍니다.

이때 자동으로 생성되는 값을 가지는 PK 컬럼의 이름은 명시적으로 id 로 지정하는 것이 관례 입니다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

- sequence

ORACLE 에서 사용되는 sequence 방식은 sequence ORACLE 객체를 생성해 두고 해당 sequence 를 호출할때마다 기존 값의 +1 이 된 값을 반환해 주는 방식입니다. 이를 위해 @GeneratedValue 어노테이션의 strategy 속성을 GenerationType.SEQUENCE 로 지정해 sequence 를 사용해 PK 값을 사용하겠다고 지정합니다.

@Id
@SequenceGenerator(name="seq", sequenceName="jpa_sequence")
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq")
private Long id;

 

@EmbeddedId

앞서 데이타베이스의 테이블은 기본적으로 유일한 값을 가지는데 그것을 PK 라고 이야기 한다고 설명 드렸습니다. 일반적인 경우에는 PK 를 단일 @Id 로 구성하지만 경우에 따라선 복합키로서 테이블의 PK 를 정의 하기도 합니다. 복합키는 두개 이상의 @Id 로 구성이 되는데 이를 직접 Entity 에 정의하는 것이 아니라 별도의 Value 를 사용해 복합키를 정의합니다.

먼저 Value 를 생성한 다음 @Embeddable 어노테이션을 이용해 이 Value 가 Entity 에 삽입이 가능함을 명시 하고 Entity 에서는 @EmbeddedId 어노테이션을 이용해 이 Entity 에 해당 Value  PK 로 사용한다고 지정합니다.

@Embeddable
public class CompanyOrganizationKey implements Serializable {
	@Column(name = "company_code")
    private String companyCode;
    
    @Column(name = "organization_code")
    private String organizationCode;
}

@Entity(name = "company_organization")
public class CompanyOrganization {
    @EmbeddedId
    protected CompanyOrganizationKey companyOrganizationKey;
}

 

@Enumerated

@Enumerated 어노테이션은 java 의 enum 형태로 되어 있는 미리 정의되어 있는 코드 값이나 구분값을 데이타 타입으로 사용하고자 할때 사용됩니다. 속성으로 EnumType.ORDINAL, EnumType.STRING 이 있는데 ORDINAL 은 enum 객체에 정의된 순서가 컬럼의 값으로 사용되고 STRING 은 enum 의 문자열 자체가 컬럼의 값으로 사용이 됩니다.

enum FlagYN {
    Y, N
}

@Enumerated(EnumType.ORDINAL)
@Column(name = "access_yn")
private FlagYN accessYn; //0, 1 이 값으로 저장

@Enumerated(EnumType.STRING)
@Column(name = "use_yn", length = 1)
private FlagYN useYn; //'Y', 'N' 이 값으로 저장

 

@Transient

만약 Entity 객체에 속성으로서 지정되어 있지만 데이타베이스상에 필요없는 속성이라면 @Transient 어노테이션을 이용해서 해당 속성을 데이타베이스에서 이용하지 않겠다 라고 정의합니다. 이렇게 하면 해당 속성을 Entity 객체에 임시로 값을 담는 용도로 사용이 가능해 집니다.

@Transient
private String tempValue;

출처 : https://jogeum.net/6


사실 마지막 2개는 잘 쓰지 않았던거 같다. Entity에 Enum을 사용하는 것은 application과 DB간의 변환과정이 필요해서 많이 꺼려한 내용이었다. 그러나 @Enumerated을 몰랐던 것이였다... 보통 아래와 같이 Entity에 Enum을 쓰지 않고 String으로 사용했다.

@ColumnDefault("SEND")
@Column(name = "STATE", nullable = false, length = 10)
private String state;

NotificationMessage.builder()
  .messageId(messageId)
  .notification(notification)
  .sendDate(Instant.now())
  .state(STATE.SEND.name()).build());

이럴땐 @Enumerated(EnumType.STRING)를 사용하면 깔끔하게 해결될 것 같다.

그런데 @Transient는 조금 회의적이다. Entity에 과연 임시로 값을 담아야 하나?라는 생각은 의문이 들기 때문이다. 나는 Entity와 DTO의 구분을 지어줘야 한다고 생각하기 때문이다. 어쨌든 enum은 더 나이스하게 사용 가능 할 것 같다.

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 1  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
jpa 복합키에서 auto increment  (0) 2019.06.27
JPA 기본 키 전략  (0) 2019.06.27

먼저 중요 패키지 버전을 보면 아래와 같다.

  • springboot 2.1.5.RELEASE
    • spring-data 2.1.5.RELEASE
  • spring-core 5.1.7.RELEASE
  • hibernate-core 5.3.10.Final
  • mysql 5.7.21-log

entity는 아래와 같습니다.

@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "T_NOTIFICATION")
public class Notification {

    @EmbeddedId
    private NotificationId id;

    @CreatedDate
    @Column(name = "CREATE_DT", nullable = false, updatable = false, columnDefinition = "datetime(3)")
    private Instant createdDate;

    @LastModifiedDate
    @Column(name = "RECEIVER_RESPONSE_DT", nullable = true, updatable = false, columnDefinition = "datetime(3)")
    private Instant receiverResponseDate;

    @Column(name = "NOTI_REASON", nullable = false, length = 64)
    private String notiReason;

    @ColumnDefault("1")
    @Column(name = "SEND_COUNT", nullable = false, columnDefinition = "tinyint")
    private int sendCount;

    @Column(name = "REQUEST_USER", nullable = false, length = 36)
    private String requestUser;

    @Column(name = "SEND_MESSAGE", nullable = false, columnDefinition = "text")
    private String message;

    @Builder.Default
    @OneToMany(mappedBy = "notification", fetch = FetchType.EAGER, cascade = {CascadeType.ALL}, orphanRemoval = true)
    private Collection<NotificationMessage> notificationMessages = new ArrayList<NotificationMessage>();
}
@Embeddable
@AllArgsConstructor
@NoArgsConstructor
@Data
public class NotificationId implements Serializable {

    private static final long serialVersionUID = 1L;

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "NOTI_SEQ", unique = true, updatable = false, nullable = false)
    private BigInteger NotiSeq;

    @Column(name = "PROJECT_ID", nullable = false, length = 36)
    private String projectId;

    @Column(name = "RECEIVER_ID", nullable = false, length = 128)
    private String receiverId;

    @Column(name = "NOTI_GID", nullable = false, length = 128)
    private String notiId;

}
@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "T_NOTIFICATION_MESSAGE")
public class NotificationMessage {

    @Id
    @Column(name = "MESSAGE_ID", nullable = false, length = 100)
    private String messageId;

    @ColumnDefault("SEND")
    @Column(name = "STATE", nullable = false, length = 10)
    private String state;

    @CreatedDate
    @Column(name = "SEND_DT", nullable = false, columnDefinition = "datetime(3)")
    private Instant sendDate;

    @Column(name = "END_DT", nullable = true, columnDefinition = "datetime(3)")
    private Instant endDate;

    @Column(name = "SEND_RESULT_CODE", nullable = true, columnDefinition = "smallint(5)")
    private Integer resultCode;

    @Column(name = "SEND_RESULT_REASON", nullable = true, length = 1024)
    private String resultReason;

    @Column(name = "SEND_RESULT_MESSAGE", nullable = true, columnDefinition = "text")
    private String resultMessage;

    @ManyToOne
    @JoinColumns(value = {
            @JoinColumn(name = "NOTI_SEQ", referencedColumnName = "NOTI_SEQ"),
            @JoinColumn(name = "PROJECT_ID", referencedColumnName = "PROJECT_ID"),
            @JoinColumn(name = "RECEIVER_ID", referencedColumnName = "RECEIVER_ID"),
            @JoinColumn(name = "NOTI_GID", referencedColumnName = "NOTI_GID"),
    }, foreignKey = @ForeignKey(name = "FK_T_NOTIFICATION_TO_T_NOTIFICATION_MESSAGE"))
    private Notification notification;
}

Notification class에 EmbeddedId를 설정하여 복합키를 사용하고 있고 NotificationId class에 정의된 복합키중에 notiSeq는 @GeneratedValue로 설정하여 auto increment를 사용하려고 하였습니다. 실제 코드에서는 

@Override
public Notification insertOrUpdatePushMessage(Message message) {
    Notification notification = convertMessageToNotification(message);
    return this.repository.saveAndFlush(notification);
}

Message를 받아서 Notification으로 변경해서 save()를 하죠. 그런데 문제는 save()하고 return된 Notification 객체에는 notiSeq가 Null이 들어오는 것이었습니다.

사실 hibernate의 batch를 사용하고 있어서 auto increment를 가져오는 타이밍이 persistant와 안 맞나 보다 하고 batch도 사용하지 않아 봤는데 실패!!! 그럼 flush를 안 해서 그런가??? 하고 위 코드처럼 saveAndFlush()로 바꿔도 실패!!! 멘붕 중에 아래 블로그를 찾았다.

https://kihoonkim.github.io/2017/01/27/JPA(Java%20ORM)/3.%20JPA-%EC%97%94%ED%8B%B0%ED%8B%B0%20%EB%A7%A4%ED%95%91/

 

(JPA - 3) 엔티티 매핑

앞에서 영속성 컨텍스트에 객체(엔티티)를 저장하는 것을 보았다.객체와 관계형 데이터베이스의 다른 개념을 어떻게 매핑하고 데이터가 반영되는지 알아보자. 객체(엔티티)를 영속성 컨테스트에 저장(persist) 후 트랜젝션이 커밋되는 시점에각 데이터베이스에 맞는 SQL을 생성하여 데이터베이스로 보내진다.객체를 어떻게 엔티티로 만들고 테이블과 매핑하는지 알아보

kihoonkim.github.io

두 방식 중 무엇을 사용하든 복합키에는 @GeneratedValue를 통해 기본키를 자동생성할 수 없다.
반드시 직접 할당 전략을 사용해야 된다.

오!!! 복합키는 안 되는 거구나.... 허무함과 안도감이 교차했다. 나중에 왜 그런지 hibernate문서를 좀 더 찾아봐야겠지만 일단 믿고 가기로 했다. 

결론은 복합키에 unique한 auto increment를 사용하는 게 모순이라고 판단해서 auto increment만 PK로 변경해서 진행 중이다.

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 1  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04
JPA 기본 키 전략  (0) 2019.06.27

1. 직접 할당

@id 애노테이션으로 필드와 매핑한다. 자바 기본형, 래퍼형, String, Date, BigDecimal, BigInteger가 타입이 가능하다

 

2. IDENTITY

기본 키 생성을 DB에 위임한다. DB의 auto_increment와 같은 기능을 사용할 때 쓴다. 키 필드에 @GeneratedValue(strategy = GenerationType.IDENTITY)를 사용한다.

이 전략을 사용하면 JPA는 기본 키 값을 얻어오기 위해 DB를 추가로 조회한다. 따라서 이 전략을 사용하는 엔티티를 새로 생성하여 식별자 값을 할당하려면 1차 캐시를 넘어서 DB에서 Insert한 후에 기본 키 값을 조회한다. 즉, persist()를 호출하는 즉시 Insert SQL이 DB에 전달되므로 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.

 

3. SEQUENCE

데이타베이스 시퀀스 : 유일한 값을 순서대로 생성하는 데이타베이스 오브젝트

시퀀스를 사용해서 기본 키를 생성하는 전략이다. 오라클, PostgreSQL, DB2, H2 등이 시퀀스를 지원한다.

@Entity
@SequenceGenerator(name = "MY_SEQ_GENERATOR",
		sequnceName = "MY_SEQ",
		initialValue = 1,
		allocationSize = 1)
public class Board {
	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MY_SEQ_GENERATOR")
	private Long id;
}

@SequenceGenerator를 의 속성에서 name에 식별자 생성기 이름을 정해준다. 그리고 sequenceName에 DB에 등록되어 있는 시퀀스 이름을 지정해주고, 초기값(initialValue)과 한번 호출에 증가하는 수(allocationSize)를 입력하여 시퀀스 생성기를 등록한다.

 

이 전략에서 persist()를 호출할 때 DB의 시퀀스를 사용하여 식별자를 조회한다. 그리고 그 식별자를 엔티티에 할당한 후 엔티티를 영속성 컨텍스트에 저장한다.

 

4. TABLE

키 생성 전용 테이블을 하나 만들고, 여기에 이름과 값으로 사용할 컬럼을 만들어 DB의 시퀀스처럼 동작하게 하는 전략.

@Entity
@TableGenerator(name = "MY_SEQ_GENERATOR",
		table = "MY_SEQUNCES",
		pkColumnValue = "MY_SEQ",
		allocationSize = 1)
public class Board {
	@Id
	@GeneratedValue(strategy = GenerationType.TABLE, generator = "MY_SEQ_GENERATOR")
	private Long id;
}

 

MY_SEQUNCES 테이블은 아래와 같을 것이다

sequnceName next_val

MY_SEQ 2

@TableGenerator.pkColumnValue에서 지정한 "MY_SEQ"가 컬럼명으로 추가되었고, 키 생성기를 사용하여 기본 키를 할당할 때마다 next_val 컬럼 값이 증가한다.

출처: https://feco.tistory.com/96 [wmJun]

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 1  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04
jpa 복합키에서 auto increment  (0) 2019.06.27

+ Recent posts