π Spring Boot νμ΄μ§λ€μ΄μ Troubleshooting
π λͺ©μ°¨
π¨ λ¬Έμ μν©
μ¦μ: κ²μκΈμ΄ μ‘΄μ¬ν¨μλ λΆκ΅¬νκ³ νμ΄μ§λ€μ΄μ 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}"
νμΈ - λ‘κ·Έλ₯Ό ν΅ν νμ΄μ§ μ 보 λλ²κΉ
- ν μ€νΈ λ°μ΄ν° μΆκ° λλ νμ΄μ§ ν¬κΈ° μ‘°μ
π μ΄μ κ³ λ €μ¬ν
- κ°λ° νκ²½: νμ΄μ§ ν¬κΈ°λ₯Ό μκ² μ€μ νμ¬ ν μ€νΈ μ©μ΄μ± ν보
- μ΄μ νκ²½: μ μ ν νμ΄μ§ ν¬κΈ°λ‘ μ¬μ©μ κ²½ν μ΅μ ν
- μ±λ₯: νμ΄μ§ ν¬κΈ°κ° λ무 ν¬λ©΄ μ±λ₯ μ ν κ°λ₯μ± μμ