Home > Troubleshooting > πŸ”[Troubleshooting] πŸš€ Spring Boot νŽ˜μ΄μ§€λ„€μ΄μ…˜

πŸ”[Troubleshooting] πŸš€ Spring Boot νŽ˜μ΄μ§€λ„€μ΄μ…˜
Troubleshooting Backend Development Spring Boot νŽ˜μ΄μ§€λ„€μ΄μ…˜ Pagination

πŸš€ Spring Boot νŽ˜μ΄μ§€λ„€μ΄μ…˜ Troubleshooting

πŸ“‹ λͺ©μ°¨

  1. 문제 상황
  2. 원인 뢄석
  3. ν•΄κ²° λ°©μ•ˆ
  4. μ½”λ“œ μ˜ˆμ‹œ
  5. 베슀트 ν”„λž™ν‹°μŠ€

🚨 문제 상황

증상: κ²Œμ‹œκΈ€μ΄ μ‘΄μž¬ν•¨μ—λ„ λΆˆκ΅¬ν•˜κ³  νŽ˜μ΄μ§€λ„€μ΄μ…˜ UIκ°€ 화면에 ν‘œμ‹œλ˜μ§€ μ•ŠμŒ

ν™˜κ²½:

  • Spring Boot + JPA
  • Thymeleaf ν…œν”Œλ¦Ώ μ—”μ§„
  • @PageableDefault μ–΄λ…Έν…Œμ΄μ…˜ μ‚¬μš©

πŸ” 원인 뢄석

1. Thymeleaf 쑰건문 확인

<ul class="pagination" th:if="${posts.totalPages > 1}">
    <!-- νŽ˜μ΄μ§€λ„€μ΄μ…˜ UI -->
</ul>

뢄석: th:if="${posts.totalPages > 1}" 쑰건문이 핡심

  • 전체 νŽ˜μ΄μ§€ μˆ˜κ°€ 1을 μ΄ˆκ³Όν•  λ•Œλ§Œ νŽ˜μ΄μ§€λ„€μ΄μ…˜ UI ν‘œμ‹œ
  • UX κ΄€μ μ—μ„œ μ˜¬λ°”λ₯Έ 처리 방식 (νŽ˜μ΄μ§€κ°€ 1개뿐이면 λΆˆν•„μš”)

2. Controller μ„€μ • 확인

@GetMapping
public String index(Model model, 
    @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) 
    Pageable pageable) {
    
    Page<PostResponseDto> posts = postService.findAll(pageable);
    model.addAttribute("posts", posts);
    return "index";
}

뢄석: @PageableDefault(size = 5) μ„€μ •

  • ν•œ νŽ˜μ΄μ§€λ‹Ή 5개의 κ²Œμ‹œκΈ€ ν‘œμ‹œ
  • 총 κ²Œμ‹œκΈ€μ΄ 5개 μ΄ν•˜ β†’ totalPages = 1 β†’ νŽ˜μ΄μ§€λ„€μ΄μ…˜ μˆ¨κΉ€

3. 데이터 μƒνƒœ 확인

총 κ²Œμ‹œκΈ€ 수 νŽ˜μ΄μ§€ 크기 총 νŽ˜μ΄μ§€ 수 νŽ˜μ΄μ§€λ„€μ΄μ…˜ ν‘œμ‹œ
1-5개 5 1 ❌ μˆ¨κΉ€
6-10개 5 2 βœ… ν‘œμ‹œ
11-15개 5 3 βœ… ν‘œμ‹œ

βœ… ν•΄κ²° λ°©μ•ˆ

방법 1: 데이터 μΆ”κ°€ (ꢌμž₯)

μ‹€μ œ 운영 ν™˜κ²½μ— μ ν•©ν•œ 방법

// ν…ŒμŠ€νŠΈ 데이터 μΆ”κ°€ (μ˜ˆμ‹œ)
@PostConstruct
public void initTestData() {
    for (int i = 1; i <= 10; i++) {
        Post post = Post.builder()
            .title("ν…ŒμŠ€νŠΈ κ²Œμ‹œκΈ€ " + i)
            .content("ν…ŒμŠ€νŠΈ λ‚΄μš© " + i)
            .author("μž‘μ„±μž" + i)
            .build();
        postRepository.save(post);
    }
}

μž₯점:

  • μ‹€μ œ μ‚¬μš© ν™˜κ²½κ³Ό 동일
  • λ‹€μ–‘ν•œ νŽ˜μ΄μ§€λ„€μ΄μ…˜ μ‹œλ‚˜λ¦¬μ˜€ ν…ŒμŠ€νŠΈ κ°€λŠ₯

방법 2: νŽ˜μ΄μ§€ 크기 쑰절 (개발/ν…ŒμŠ€νŠΈμš©)

개발 λ‹¨κ³„μ—μ„œ λΉ λ₯Έ ν™•μΈμš©

Before

@PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC)

After

@PageableDefault(size = 2, sort = "id", direction = Sort.Direction.DESC)

μž₯점:

  • μ½”λ“œ μˆ˜μ •λ§ŒμœΌλ‘œ λΉ λ₯Έ 확인 κ°€λŠ₯
  • 적은 λ°μ΄ν„°λ‘œλ„ νŽ˜μ΄μ§€λ„€μ΄μ…˜ ν…ŒμŠ€νŠΈ

μ£Όμ˜μ‚¬ν•­: 운영 배포 μ‹œ μ›λž˜ κ°’μœΌλ‘œ 볡ꡬ ν•„μš”


πŸ’» μ½”λ“œ μ˜ˆμ‹œ

1. Controller μ™„μ „ν•œ μ˜ˆμ‹œ

@Controller
@RequestMapping("/posts")
@RequiredArgsConstructor
public class PostController {
    
    private final PostService postService;
    
    @GetMapping
    public String index(
        Model model,
        @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) 
        Pageable pageable) {
        
        Page<PostResponseDto> posts = postService.findAll(pageable);
        
        // λ””λ²„κΉ…μš© 둜그
        log.info("총 κ²Œμ‹œκΈ€ 수: {}", posts.getTotalElements());
        log.info("총 νŽ˜μ΄μ§€ 수: {}", posts.getTotalPages());
        log.info("ν˜„μž¬ νŽ˜μ΄μ§€: {}", posts.getNumber() + 1);
        
        model.addAttribute("posts", posts);
        return "index";
    }
}

2. Thymeleaf ν…œν”Œλ¦Ώ κ°œμ„ 

<!-- 디버깅 정보 ν‘œμ‹œ (개발 μ‹œμ—λ§Œ) -->
<div th:if="${#profiles.active == 'dev'}" class="debug-info">
    <p>총 κ²Œμ‹œκΈ€: <span th:text="${posts.totalElements}"></span></p>
    <p>총 νŽ˜μ΄μ§€: <span th:text="${posts.totalPages}"></span></p>
    <p>ν˜„μž¬ νŽ˜μ΄μ§€: <span th:text="${posts.number + 1}"></span></p>
</div>

<!-- κ²Œμ‹œκΈ€ λͺ©λ‘ -->
<div class="post-list">
    <div th:each="post : ${posts.content}" class="post-item">
        <h3 th:text="${post.title}"></h3>
        <p th:text="${post.content}"></p>
    </div>
</div>

<!-- νŽ˜μ΄μ§€λ„€μ΄μ…˜ -->
<nav th:if="${posts.totalPages > 1}" aria-label="νŽ˜μ΄μ§€ λ„€λΉ„κ²Œμ΄μ…˜">
    <ul class="pagination justify-content-center">
        <!-- 이전 νŽ˜μ΄μ§€ -->
        <li class="page-item" th:classappend="${posts.first} ? 'disabled'">
            <a class="page-link" 
               th:href="@{/posts(page=${posts.number - 1})}"
               th:unless="${posts.first}">이전</a>
            <span class="page-link" th:if="${posts.first}">이전</span>
        </li>
        
        <!-- νŽ˜μ΄μ§€ 번호 -->
        <li class="page-item" 
            th:each="pageNum : ${#numbers.sequence(0, posts.totalPages - 1)}"
            th:classappend="${pageNum == posts.number} ? 'active'">
            <a class="page-link" 
               th:href="@{/posts(page=${pageNum})}"
               th:text="${pageNum + 1}"
               th:unless="${pageNum == posts.number}"></a>
            <span class="page-link" 
                  th:if="${pageNum == posts.number}"
                  th:text="${pageNum + 1}"></span>
        </li>
        
        <!-- λ‹€μŒ νŽ˜μ΄μ§€ -->
        <li class="page-item" th:classappend="${posts.last} ? 'disabled'">
            <a class="page-link" 
               th:href="@{/posts(page=${posts.number + 1})}"
               th:unless="${posts.last}">λ‹€μŒ</a>
            <span class="page-link" th:if="${posts.last}">λ‹€μŒ</span>
        </li>
    </ul>
</nav>

<!-- νŽ˜μ΄μ§€λ„€μ΄μ…˜μ΄ 없을 λ•Œ μ•ˆλ‚΄ λ©”μ‹œμ§€ -->
<div th:if="${posts.totalPages <= 1}" class="text-center text-muted">
    <p>전체 <span th:text="${posts.totalElements}"></span>개의 κ²Œμ‹œκΈ€</p>
</div>

3. Service Layer μ˜ˆμ‹œ

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {
    
    private final PostRepository postRepository;
    
    public Page<PostResponseDto> findAll(Pageable pageable) {
        Page<Post> posts = postRepository.findAll(pageable);
        
        // Entity β†’ DTO λ³€ν™˜
        return posts.map(post -> PostResponseDto.builder()
            .id(post.getId())
            .title(post.getTitle())
            .content(post.getContent())
            .author(post.getAuthor())
            .createdAt(post.getCreatedAt())
            .build());
    }
}

πŸ’‘ 베슀트 ν”„λž™ν‹°μŠ€

1. ν™˜κ²½λ³„ μ„€μ • 뢄리

# application-dev.yml (κ°œλ°œν™˜κ²½)
spring:
  data:
    web:
      pageable:
        default-page-size: 2  # 개발 μ‹œ μž‘μ€ κ°’μœΌλ‘œ ν…ŒμŠ€νŠΈ

# application-prod.yml (μš΄μ˜ν™˜κ²½)
spring:
  data:
    web:
      pageable:
        default-page-size: 10  # 운영 μ‹œ μ μ ˆν•œ κ°’

2. μ»€μŠ€ν…€ Pageable Configuration

@Configuration
public class PaginationConfig {
    
    @Bean
    @Primary
    public PageableHandlerMethodArgumentResolver pageableResolver() {
        PageableHandlerMethodArgumentResolver resolver = 
            new PageableHandlerMethodArgumentResolver();
        resolver.setMaxPageSize(100);  // μ΅œλŒ€ νŽ˜μ΄μ§€ 크기 μ œν•œ
        resolver.setOneIndexedParameters(true);  // 1λΆ€ν„° μ‹œμž‘ν•˜λŠ” νŽ˜μ΄μ§€ 번호
        return resolver;
    }
}

3. 디버깅을 μœ„ν•œ λ‘œκΉ…

@Slf4j
@Service
public class PostService {
    
    public Page<PostResponseDto> findAll(Pageable pageable) {
        log.debug("νŽ˜μ΄μ§€ μš”μ²­ - νŽ˜μ΄μ§€: {}, 크기: {}, μ •λ ¬: {}", 
            pageable.getPageNumber(), 
            pageable.getPageSize(), 
            pageable.getSort());
            
        Page<Post> posts = postRepository.findAll(pageable);
        
        log.debug("νŽ˜μ΄μ§€ κ²°κ³Ό - 총 μš”μ†Œ: {}, 총 νŽ˜μ΄μ§€: {}, ν˜„μž¬ νŽ˜μ΄μ§€ μš”μ†Œ 수: {}", 
            posts.getTotalElements(), 
            posts.getTotalPages(), 
            posts.getNumberOfElements());
            
        return posts.map(this::convertToDto);
    }
}

4. ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„±

@SpringBootTest
@Transactional
class PostControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private PostRepository postRepository;
    
    @Test
    @DisplayName("κ²Œμ‹œκΈ€μ΄ 5개 μ΄ν•˜μΌ λ•Œ νŽ˜μ΄μ§€λ„€μ΄μ…˜μ΄ ν‘œμ‹œλ˜μ§€ μ•ŠμŒ")
    void pagination_not_shown_when_posts_less_than_page_size() throws Exception {
        // Given: 3개의 κ²Œμ‹œκΈ€ 생성
        createTestPosts(3);
        
        // When & Then
        mockMvc.perform(get("/posts"))
            .andExpect(status().isOk())
            .andExpect(model().attributeExists("posts"))
            .andExpect(xpath("//ul[@class='pagination']").doesNotExist());
    }
    
    @Test
    @DisplayName("κ²Œμ‹œκΈ€μ΄ νŽ˜μ΄μ§€ 크기λ₯Ό μ΄ˆκ³Όν•  λ•Œ νŽ˜μ΄μ§€λ„€μ΄μ…˜ ν‘œμ‹œλ¨")
    void pagination_shown_when_posts_exceed_page_size() throws Exception {
        // Given: 7개의 κ²Œμ‹œκΈ€ 생성 (νŽ˜μ΄μ§€ 크기 5 초과)
        createTestPosts(7);
        
        // When & Then
        mockMvc.perform(get("/posts"))
            .andExpect(status().isOk())
            .andExpect(model().attributeExists("posts"))
            .andExpect(xpath("//ul[@class='pagination']").exists());
    }
    
    private void createTestPosts(int count) {
        for (int i = 1; i <= count; i++) {
            Post post = Post.builder()
                .title("ν…ŒμŠ€νŠΈ κ²Œμ‹œκΈ€ " + i)
                .content("ν…ŒμŠ€νŠΈ λ‚΄μš© " + i)
                .build();
            postRepository.save(post);
        }
    }
}

πŸ“ μš”μ•½

βœ… 정상 λ™μž‘ 확인

ν˜„μž¬ μ½”λ“œλŠ” μ •μƒμ μœΌλ‘œ λ™μž‘ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. νŽ˜μ΄μ§€λ„€μ΄μ…˜μ΄ 보이지 μ•ŠλŠ” 것은 μ„€κ³„λœ λ™μž‘μž…λ‹ˆλ‹€.

🎯 ν•΄κ²° 체크리슀트

  • 총 κ²Œμ‹œκΈ€ 수 확인 (νŽ˜μ΄μ§€ 크기보닀 λ§Žμ€κ°€?)
  • @PageableDefault μ„€μ • 확인
  • Thymeleaf 쑰건문 th:if="${posts.totalPages > 1}" 확인
  • 둜그λ₯Ό ν†΅ν•œ νŽ˜μ΄μ§€ 정보 디버깅
  • ν…ŒμŠ€νŠΈ 데이터 μΆ”κ°€ λ˜λŠ” νŽ˜μ΄μ§€ 크기 쑰절

πŸš€ 운영 고렀사항

  • 개발 ν™˜κ²½: νŽ˜μ΄μ§€ 크기λ₯Ό μž‘κ²Œ μ„€μ •ν•˜μ—¬ ν…ŒμŠ€νŠΈ μš©μ΄μ„± 확보
  • 운영 ν™˜κ²½: μ μ ˆν•œ νŽ˜μ΄μ§€ 크기둜 μ‚¬μš©μž κ²½ν—˜ μ΅œμ ν™”
  • μ„±λŠ₯: νŽ˜μ΄μ§€ 크기가 λ„ˆλ¬΄ 크면 μ„±λŠ₯ μ €ν•˜ κ°€λŠ₯μ„± 있음