문제 상황
고객 CRUD 기능을 구현하던 중 고객 전체 조회 API를 만들게 되었다.
처음에는 단순하게 모든 고객을 조회하는 기능만 구현했다.
List<Customer> customers = customerRepository.findAll();
이를 DTO로 변환하여 반환하는 구조였다.
return customers.stream()
.map(customer -> ...)
.toList();
하지만 단순 조회만 필요한게 아니라 고객 목록 조회 시
- 페이지네이션
- 상태(status) 검색
- 이름 또는 이메일 검색
기능까지 함께 지원해야 했다.
처음 고민했던 방법
처음에는 Repository 메서드를 각각 만들어 해결하려고 했다.
findByStatus(...)
findByNameContainingOrEmailContaining(...)
그리고
findByNameOrEmailAndStatus(...)
서비스에서는
if (status만 있음)
else if (keyword만 있음)
else if (둘 다 있음)
else
처럼 조건을 나누어 Repository를 호출했다.
처음에는 동작은 했지만 코드가 점점 길어지는 문제가 있었다.
Pageable 적용
전체 데이터를 조회하는 것은 비효율적이기 때문에 Spring Data JPA의 Pageable을 적용하였다.
Controller에서는
@GetMapping
public ResponseEntity<Page<GetCustomerResponse>> getAll(
@PageableDefault(page = 0, size = 10)
Pageable pageable,
CustomerSearchCondition condition
)
처럼 Pageable을 전달하였다.
서비스에서는
Page<Customer> customers = customerRepository.findAll(pageable);
을 사용하였다.
처음에는
List<GetCustomerResponse>
를 반환했지만 Page 객체를 사용하면
- 현재 페이지
- 전체 페이지
- 전체 데이터 수
등의 정보를 함께 제공할 수 있기 때문에
최종적으로
Page<GetCustomerResponse>
를 반환하도록 변경하였다.
DTO 변환도
기존
stream()
대신
customers.map(...)
을 사용하여 Page 정보를 유지한 채 DTO로 변환하였다.
조건 검색 구현
상태 검색과 키워드 검색을 함께 처리하기 위해
검색 조건을 하나의 DTO로 분리하였다.
public class CustomerSearchCondition {
private String keyword;
private CustomerStatus status;
}
처음에는 Controller에서 각각 RequestParam으로 받을까 고민했지만,
검색 조건이 늘어날 것을 고려하여 DTO 하나로 관리하는 것이 유지보수에 더 유리하다고 판단하였다.
JPQL 적용
상태와 키워드를 동시에 검색하는 경우
메서드 이름만으로 해결하려고 했다.
하지만
findByNameContainingOrEmailContainingAndStatus(...)
처럼 메서드 이름이 지나치게 길어졌다.
또한
(name LIKE ? OR email LIKE ?)
AND status = ?
와 같은 조건을 메서드 이름만으로 표현하는 것도 가독성이 떨어졌다.
그래서 JPQL을 사용하였다.
@Query("""
SELECT c
FROM Customer c
WHERE
(:keyword IS NULL
OR c.name LIKE %:keyword%
OR c.email LIKE %:keyword%)
AND
(:status IS NULL
OR c.status = :status)
""")
JPQL을 사용하니
검색 조건을 SQL과 비슷한 형태로 작성할 수 있었고,
가독성도 훨씬 좋아졌다.
키워드가 비어있는 경우 처리
검색창에서
keyword=
처럼 빈 문자열이 전달되는 경우가 있었다.
이를 그대로 검색하면 불필요한 LIKE 검색이 수행될 수 있다.
그래서 CustomerSearchCondition에서
public String getKeyword() {
return keyword == null || keyword.isBlank()
? null
: keyword.trim();
}
처럼
공백과 빈 문자열을 null로 변환하도록 처리하였다.
덕분에 서비스에서는
condition.getKeyword() == null
만 검사하면 되었다.
배운 점
이번 기능을 구현하면서 가장 크게 느낀 점은
Spring Data JPA 메서드 이름만으로는 해결할 수 있는 범위가 생각보다 제한적이라는 것이다.
간단한 조회는
findByStatus()
처럼 메서드 이름만으로도 충분하지만,
검색 조건이 여러 개 조합되기 시작하면
JPQL 또는 QueryDSL이 훨씬 적합하다는 것을 알게 되었다.
또한 Pageable을 적용하면서
단순히 데이터를 나누어 조회하는 것이 아니라
Page 객체를 활용하면
페이지 정보까지 함께 제공할 수 있다는 점도 배울 수 있었다.
마무리
이번 기능을 구현하면서
- Pageable
- Page
- JPQL
- 검색 조건 DTO
- Spring Data JPA 메서드 규칙
을 함께 익힐 수 있었다.
처음에는 메서드 이름만으로 해결하려 했지만,
조건이 복잡해질수록 JPQL을 사용하는 이유를 직접 경험할 수 있었던 구현이었다.
'트러블슈팅' 카테고리의 다른 글
| error: cannot find symbol @Value("${jwt.secret.key}") 해결 (0) | 2026.07.22 |
|---|---|
| Session 로그인 적용하면서 API 구조 리팩토링하기 (0) | 2026.07.09 |
| [IntelliJ IDEA 기능] Compact Middle Packages / 패키지 설정 (0) | 2026.07.07 |
| [Spring] 전체조회시, 조건 전체조회 (feat.@RequestParam) (0) | 2026.06.30 |
| ArrayList 기반 장바구니 구현 (0) | 2026.06.16 |