Develop

[Spring Boot] 게시판 - 11. 동적 정렬과 필터링으로 사용자 경험 개선하기 본문

백엔드/게시판 만들기

[Spring Boot] 게시판 - 11. 동적 정렬과 필터링으로 사용자 경험 개선하기

230801 2025. 12. 9. 13:09

안녕하세요 .ᐟ

오늘은 어제 구현한 QueryDSL 검색 기능을 확장해서, 실제 게시판에서 필수적인 정렬과 필터링 기능을 추가했습니다!

사용자가 원하는 방식으로 게시글을 조회할 수 있도록 만드는 과정을 공유해보겠습니다.~

 

오늘 구현한 기능

  1. 동적 정렬: 최신순, 조회수순, 제목순 자유롭게 선택
  2. 카테고리 필터링: 특정 카테고리의 게시글만 보기
  3. 복합 조건 검색: 검색어 + 카테고리 + 정렬 조합
  4. 조회수 자동 증가: 게시글을 볼 때마다 조회수 증가
  5. 성능 최적화: DB 인덱스 추가

 


1. 조회수 기능 추가

Post.java

@Column(nullable = false)
private Long viewCount = 0L;

public void increaseViewCount() {
    this.viewCount++;
}

 

 

 

Service에서 조회수 증가 로직

@Transactional
public Post findByIdWithViewCount(Long id) {
    Post post = postRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException(...));
    
    post.increaseViewCount(); // 조회할 때마다 증가
    
    return post;
}

조회 - 별도 메서드로 분리

  • findById: 단순 조회용 (관리자가 조회수 증가 없이 보고 싶을 때)
  • findByIdWithViewCount: 사용자 조회용 (조회수 증가 포함)

 


 

2. QueryDSL로 동적 정렬 구현

  • Spring Data JPA의 Pageable과 Sort를 활용해서 사용자가 선택한 정렬 기준을 적용
  • 동작 방식
    • Pageable에서 Sort 정보 추출
    • 각 정렬 조건을 OrderSpecifier로 변환
    • QueryDSL 쿼리에 적용
private OrderSpecifier<?>[] getOrderSpecifiers(Sort sort) {
    List<OrderSpecifier<?>> orders = new ArrayList<>();
    
    if (sort.isEmpty()) {
        orders.add(post.createdAt.desc()); // 기본: 최신순
    } else {
        sort.forEach(order -> {
            Order direction = order.isAscending() ? Order.ASC : Order.DESC;
            String property = order.getProperty();
            
            switch (property) {
                case "createdAt":
                    orders.add(new OrderSpecifier<>(direction, post.createdAt));
                    break;
                case "viewCount":
                    orders.add(new OrderSpecifier<>(direction, post.viewCount));
                    break;
                case "title":
                    orders.add(new OrderSpecifier<>(direction, post.title));
                    break;
                default:
                    orders.add(post.createdAt.desc());
            }
        });
    }
    
    return orders.toArray(new OrderSpecifier[0]);
}

 


 

3. 카테고리 필터링

  • 검색 조건에 카테고리 필터를 추가
  • BooleanBuilder를 사용하면 조건을 동적으로 조합할 수 있습니다.
public Page<Post> searchPostsWithFilters(
    String keyword, 
    Long categoryId, 
    Pageable pageable
) {
    BooleanBuilder builder = new BooleanBuilder();
    
    // 검색어 조건
    if (keyword != null && !keyword.isBlank()) {
        builder.and(
            post.title.containsIgnoreCase(keyword)
            .or(post.content.containsIgnoreCase(keyword))
            .or(post.user.username.containsIgnoreCase(keyword))
        );
    }
    
    // 카테고리 필터
    if (categoryId != null) {
        builder.and(post.category.id.eq(categoryId));
    }
    
    // ... 쿼리 실행
}

 

 


 

4. Controller에서 사용하기

@GetMapping
public ResponseEntity<Page<PostResponse>> getPosts(
    @RequestParam(required = false) String keyword,
    @RequestParam(required = false) Long categoryId,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "10") int size,
    @RequestParam(defaultValue = "createdAt,desc") String[] sort
) {
    Sort sortObj = Sort.by(
        sort[1].equalsIgnoreCase("asc") ? Sort.Direction.ASC : Sort.Direction.DESC,
        sort[0]
    );
    
    Pageable pageable = PageRequest.of(page, size, sortObj);
    
    Page<PostResponse> posts = postService.searchPostsWithFilters(
        keyword, categoryId, pageable
    );
    
    return ResponseEntity.ok(posts);
}

 

 

API 호출 예시

# 최신순 정렬
GET /posts?sort=createdAt,desc

# 조회수순 정렬
GET /posts?sort=viewCount,desc

# 카테고리 필터링
GET /posts?categoryId=1

# 복합 조건
GET /posts?keyword=스프링&categoryId=1&sort=viewCount,desc

 


 

5. 성능 최적화 - 인덱스 추가

  • 정렬과 필터링이 빠르게 동작하도록 DB 인덱스를 추가
    • created_at: 최신순 정렬 시 빠른 조회
    • view_count: 조회수 순 정렬 시 빠른 조회
    • category_id: 카테고리 필터링 시 빠른 조회
@Entity
@Table(
    name = "posts",
    indexes = {
        @Index(name = "idx_post_created_at", columnList = "created_at"),
        @Index(name = "idx_post_view_count", columnList = "view_count"),
        @Index(name = "idx_post_category_id", columnList = "category_id")
    }
)
public class Post extends BaseEntity {
    // ...
}

 


 

느낀 점

1. 동적 정렬의 필요성

사용자마다 원하는 정렬 기준이 다르기 때문에, 하나의 API로 여러 정렬 옵션을 제공하는 게 중요한 것 같습니다.

 

2. 메서드 네이밍의 중요성

findById와 findByIdWithViewCount처럼 내용은 비슷해도 관리자용/ 사용자용으로 나누어 생각해볼 수 있었습니다.

 

3. 인덱스의 중요성

정렬과 필터링이 자주 일어나는 컬럼에는 인덱스를 추가해야 성능이 좋아집니다. 

 

4. Pageable + Sort 조합

Spring Data JPA의 Pageable은 페이징뿐만 아니라 정렬까지 한 번에 처리할 수 있어서 편리했습니다.