• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

batch size 와 Limit 질문입니다

21.03.24 14:09 작성 조회수 609

0

안녕하세요 영한님!

강의 재밌고 유익하게 잘 보고 있습니다.

토이 프로젝트를 진행하면서 Batch SIze 와 Limit 에 대한 궁금증이 생겨서 질문 드리게 되었습니다.

# 명세

도메인을 대략적으로 설명 드리면 한 명의 사용자는 여러 게시글을 작성할 수 있고 각 게시글에는 여러 개의 댓글이 달릴 수 있습니다.

사용자 (Member) 1 -> 게시글 (Post) N -> 댓글 (Comment) M

제가 뽑고자 하는 데이터는 여러 사용자가 작성한 게시글과 댓글인데, 여기서 페이징을 적용하기 위해 limit 을 사용하려고 합니다.

여러 Member 가 작성한 Post 를 5 개만 뽑고 각 Post 에 달린 Comment 는 3 개만 뽑는 로직을 짜려고 합니다.

default_batch_fetch_size 는 1000 으로 설정했습니다.

# 질문

1. 먼저 Post 를 5 개 뽑아야 하는데 이런 경우 쿼리 자체에 Limit 을 거는 게 좋을까요?

// (1) 직접 순회
List<Member> members = memberRepository.findAll();  // 편의를 위해 findAll 로 했습니다. 실제로는 조건이 있음!!
List<Post> posts = members.stream()
               .map(Member::getPosts)
               .flatMap(Collection::stream)
               .limit(5L)
               .collect(Collectors.toList());


// (2) Repository 메서드의 IN + Limit 쿼리 사용
List<Member> members = memberRepository.findAll();  // 편의를 위해 findAll 로 했습니다. 실제로는 조건이 있음!!
List<Post> posts = postRepository.findTop5ByMemberIn(members);

(1) 번으로 할 때 배치 사이즈 설정이 적용되어 1 + N 문제는 발생하진 않지만 모든 Post 데이터를 다 끌어오게 됩니다.

Fetch Join 도 Limit 쿼리가 제대로 적용되지 않아서 OutOfMemory 발생 위험이 있기 때문에 안쓰는 걸로 알고 있는데 (2) 번으로 하는 게 맞을까요?

(2) 번으로 했을 시 우려되는 점은 만약 members 의 사이즈가 1000 이 넘어가면 IN 쿼리의 성능이 굉장히 떨어지지 않을까 걱정됩니다.

Limit 를 걸어두었으니 괜찮을지 아니면 개발자가 직접 사이즈를 쪼개서 나누어서 IN 쿼리를 호출해야 할지.. 어느 방법이 맞을까요

2. 1번과 비슷한 질문인데 5 개의 Post 를 순회하면서 3 개의 Comment 씩 뽑으려고 할 때도 @OneToMany 컬렉션을 호출하는 것보다 쿼리를 직접 호출하는게 좋을까요?

1 번 질문의 답에 따라서 조금 다를 것 같은데 만약 쿼리를 직접 호출해야 한다면 Post -> Dto 구하는 과정에 넣지 말고 Service 에서 쿼리를 호출한 후 직접 넣어줘야 할까요?

public class PostDto {
    // ... 자잘한 field 생략
    public static PostDto of(Post post) {
        return PostDto.builder()
                    .comment(post.getComments().stream().limit(3L)...)
                    .build();
    }
}

현재는 위와 같이 Post Entity 만을 넘겨줘서 Dto 로 변환시켜 주고 있는데 만약 쿼리를 직접 호출하는게 좋다면 Service 로 옮겨서 작성하는지 아니면 BatchSize 와 Limit 을 동시에 사용하는 꿀팁이 있는지 궁금합니다 !

3. 마지막으로 위의 Post, Comment 와 같이 데이터가 무한히 많아질 가능성이 있는 테이블을 다룰 때는 Batch Size 가 아닌 다른 방법을 사용하기도 할까요?

답변 4

·

답변을 작성해보세요.

3

안녕하세요. 지운님

큰 기준은 DB에서 애플리케이션으로 데이터를 올릴 때 row수를 최대한 줄여서 조회해야 합니다.

1. 먼저 Post 를 5 개 뽑아야 하는데 이런 경우 쿼리 자체에 Limit 을 거는 게 좋을까요?

-> 네 꼭 쿼리 자체에 limit 5를 걸어주어야 합니다. 그렇지 않으면 DB -> 애플리케이션으로 모든 Post가 조회되겠지요?

자바 스프림의 limit를 사용하게되면 DB -> 애플리케이션으로 수백만건의 데이터를 조회한 이후에 거기에서 데이터를 5건 조회하기 때문에, 바로 OOM 장애로 이어집니다.

POST -> Member는 xToOne 관계이기 때문에 fetch join을 사용해서 한번에 조회해주세요. 그리고 위에 언급한 쿼리 limit를 사용하면 됩니다.

2. 1번과 비슷한 질문인데 5 개의 Post 를 순회하면서 3 개의 Comment 씩 뽑으려고 할 때도 @OneToMany 컬렉션을 호출하는 것보다 쿼리를 직접 호출하는게 좋을까요?

-> 네 이 경우에도 1번에서 설명드린 내용과 같습니다. DB에서 데이터 row 수를 최대한 줄여서 조회해야 합니다. 컬렉션을 호출하게 되면 모든 데이터가 일단 로딩되어 버립니다. 따라서 컬렉션을 호출하는 것은 데이터가 많을 때는 사용하지 않는 것이 좋습니다.

3. 마지막으로 위의 Post, Comment 와 같이 데이터가 무한히 많아질 가능성이 있는 테이블을 다룰 때는 Batch Size 가 아닌 다른 방법을 사용하기도 할까요?

-> 네 앞서 설명드린 것 처럼 데이터가 무한이 많아질 때는 컬렉션을 직접 조회하는 것이 좋은 방법은 아닙니다. 데이터가 너무 많을 때는 필요한 용도에 맞게 별도의 DTO로 최적화 해서 각각 조회하는 것이 좋습니다.

감사합니다.

1

안녕하세요. 지운님

1. 다음 코드를 통해서 어떤 목적을 달성하고 싶으신 것인지 잘 이해가 되지 않습니다. 목적을 알면 거기에 맞추어 설명을 드릴 수 있을 것 같아요.

List<Member> members = memberRepository.findAll();  // 편의를 위해 findAll 로 했습니다. 실제로는 조건이 있음!!

List<Post> posts = postRepository.findTop5ByMemberIn(members);

만약 이것이 단순히 여러 조건을 기반으로 Post를 조회하는 것이 목적이라면 조인 쿼리로 한번에 해결이 가능합니다.

둘을 나누지 말고 JPQL 조인 쿼리로 한번에 해결하도록 고민해보시겠어요?

JPQL 조인 쿼리는 JPA 기본편 강의를 참고해주세요.

2. 이 부분은 어쩔 수 없을 것 같아요. 그런데 각각 조회하면 성능이 안나오니, 성능 최적화 방법은 고민해보셔야 할 것 같아요.

네이티브 SQL을 사용하는 방법을 고민해보거나 또는

다음과 같이 Post마다 자신의 최신 댓글 id 5개를 가지고 있는 컬럼이 있어서 이 값을 조회한 다음에 한번에 in 쿼리에 담아서 보내는 식으로 풀던가 해야할 듯요.

POST(PostA=[lastCommentsIds=1,2,3,4,5], PostB=[lastCommentsIds=6,7,8,9,10])

감사합니다.

0

박지운님의 프로필

박지운

질문자

2021.03.27

안녕하세요! 답변 감사드립니다.

2 번에서 최근에 들어온 데이터들을 미리 캐싱하는 것처럼 사용하는 건 정말 좋은 아이디어 같습니다!

영한님 말씀을 듣고 1 번에 대해서 고민을 해보았는데 Fetch Join 에서는 Limit 가 적용 안되어서 Inner Join 과 Limit 을 함께 JPQL 로 작성했습니다.

혹시 다른 방법을 말씀하셨던 걸까요?

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @Query(value = "SELECT p" +
            " FROM Post p" +
            " INNER JOIN Follow f" +
            " ON p.member.id = f.toMember.id" +
            " WHERE p.id < :lastPostId AND f.fromMember.id = :memberId")
    List<Post> findByJoinFollow(@Param("memberId") Long memberId, @Param("lastPostId") Long lastPostId, Pageable pageable);
}


@RequiredArgsConstructor
@Service
public class PostService {
    private final PostRepository postRepository;

    public List<Post> getFeeds(Long lastPostId) {
        PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id").descending());
        return postRepository.findByJoinFollow(getCurrentMember().getId(), lastPostId, pageRequest);
    }
}

최대한 간단하게 질문하려고 했었는데 오히려 질문의 목적을 파악할 수 없어 혼란만 드렸던 것 같네요 ㅠ

토이프로젝트로 진행 중인 건 인스타 클론 코딩이고 구현중인 API 는 인스타그램 처음 들어가면 나오는 첫 화면입니다.

인스타그램 첫 화면에서는 내가 팔로우 중인 모든 사람들의 게시글을 최신 순서대로 노출하기 때문에 자칫 잘못하면 많은 데이터를 가져오게 되어 최적화 하는 방법을 고민해보던 중이었습니다.

영한님이 말씀해주신 대로 Inner Join 을 사용하니 List<Member> 나 List<Post> 를 DB 에서 많이 끌어오지 않고 처리할 수 있는 것 같아요!

자꾸 질문 드리는데 친절하게 답변해주셔서 감사합니다.

잘 해결하셨군요^^

화이팅!

박지운님의 프로필

박지운

질문자

2021.03.27

감사합니다 !

영한님의 Querydsl 강의 들으면서 이쁘게 바꾸겠습니다~

0

박지운님의 프로필

박지운

질문자

2021.03.26

안녕하세요. 영한님

친절한 답변 감사합니다!

핵심은 DB 에서 너무 많은 양의 데이터를 한번에 끌어오지 않는 거군요.

추가적으로 두 가지만 더 질문드려도 될까요?

1. postRepository.findTop5ByMemberIn(members) 로 조회할 때 members 의 사이즈가 크다면 IN 쿼리 사이즈를 직접 나누어서 호출해야 하나요?

모든 Member 의 Post 를 조회하는 게 아니라 임의의 조건으로 필터링된 Member 리스트를 먼저 가져올 예정이라서 Fetch Join 사용이 불가능할 것 같습니다.

결국 members 를 대상으로 호출을 해야 하는데 MySQL 같은 경우는 IN 조건에 최대 1000 개 밖에 넣지 못한다고 알고 있습니다.

여기서는 Batch Size 적용이 안될 것 같은데 개발자가 임의로 사이즈를 조절해서 IN 쿼리를 나누어 호출해야 하는지 궁금합니다.

2. 말씀해주신 대로라면 Comment 를 뽑을 때도 쿼리를 직접 호출해야 할 것 같은데 BatchSize 적용이 안되어서 Post 갯수만큼 Comment 조회 쿼리가 날아갈 건 어쩔 수 없는 부분인가요?

제가 구현하려고 하는 건 Post 5 개에 대해서 호출하니 5 개의 Comment 조회 쿼리만 나갈테지만 커지면 커질수록 부담이 될 것 같습니다.

Post 의 갯수가 한정되어 있으니 그 갯수만큼 쿼리가 발생하는 건 감수하고 가야하는 건가요?