스프링 부트(Spring Boot)와 JPA를 사용하여 특정 조건에 따라 사용자를 조회하는 기능을 구현할 때,
경우에 따라 동적으로 쿼리를 생성해야 하는 요구사항이 발생할 수 있습니다.
현재 진행중인 스케쥴 관리 애플리케이션 개발 프로젝트에서 사용자 정보를 효율적으로 조회하기 위해
@Query를 활용하여 아이디, 이름, 이메일을 기반으로 사용자를 조회하는 기능을 구현하고,
개발 중 발생하였던 문제와 해결 방법을 설명하겠습니다.
🔍 요구사항
1️⃣ 검색 조건을 입력하지 않으면 전체 사용자 조회
2️⃣ 아이디(pk)를 입력하면 해당 사용자 조회
3️⃣ 이름을 입력하면 동일한 이름을 가진 사용자 조회
4️⃣ 이메일을 입력하면 동일한 이메일을 가진 사용자 조회
🔷 동적 쿼리 구현하기
이번 스케줄 관리 어플리케이션에서는 다양한 조건을 통해 사용자 정보를 검색해야한다.
따라서 JPA의 `@Query`와 `@Param`을 사용하여 동적으로 쿼리를 실행할 수 있다.
(1) 사용자 검색을 위한 JPA Repository 코드
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query ("SELECT u From User u WHERE " +
"(:id IS NULL OR u.id = :id) AND " +
"(:username IS NULL OR u.username = :username) AND " +
"(:email IS NULL OR u.email = :email)")
List<User> findUsers (
@Param("id") Long id,
@Param("username") String username,
@Param("email") String email
);
}
📌 @Query 어노테이션과 JPQL
`@Query`는 Spring Data JPA에서 제공하는 기능으로, JPQL(JPA Query Language)을 사용하여 직접 쿼리를 작성할 수 있다.
JPQL에서는 엔티티에 대한 별칭(Alias)을 지정하고 사용해야 한다.
예를 들어, u는 User 엔티티의 별칭이며, 다음과 같이 작성하면 User 엔티티를 조회할 수 있다.
@Query("SELECT u FROM User u")
또한, 매개변수와 쿼리 변수를 연결하기 위해 @Param을 사용한다.
예를 들어, 다음 코드에서 @Param("username")은 :username과 메서드 매개변수를 연결하는 역할을 한다.
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
@Param("username")은 메서드의 username 매개변수를 :username 변수와 연결한다.
즉, findByUsername("Alice")를 호출하면 "Alice"가 :username 위치에 바인딩되어 실행된다.
(2) Service : 서비스 계층의 검색 로직
서비스 계층에서 컨트롤러에서 전달 받은 검색 조건을 바탕으로 UserRepository의 동적 쿼리를 호출한다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public List<UserResponseDto> findUsers(Long id, String username, String email) {
return userRepository.findUsers(id,
(username != null && !username.trim().isEmpty()? username : null),
(email != null && !email.trim().isEmpty()? email : null))
.stream()
.map(UserResponseDto::toDto)
.toList();
}
}
}
(3) Controller : 사용자 검색 API 구현
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<UserResponseDto>> findUsers (
@RequestParam(required = false) Long id,
@RequestParam(required = false) String username,
@RequestParam(required = false) String email
){
List<UserResponseDto> userList = userService.findUsers(id, username, email);
return new ResponseEntity<>(userList, HttpStatus.OK);
}
}
🔷 트러블 슈팅
🚨 문제 1
모든 조건을 입력하지 않으면 예외가 발생한다
증상
일부 데이터만 입력하면 예외가 발생하는 경우가 있다.
예를 들어 name을 입력하지 않고, id만 입력하면 오류가 발생하였다.
원인
@Query내 null체크가 제대로 적용되지 않아 일부 조건이 빠진 경우에 예외가 발생하였다.
해결 방법
`@Query`에서 NULL 체크를 활용하여 조건을 동적으로 적용하도록 수정하고,
각 검색 조건이 NULL일 경우 자동으로 해당 필터를 무시하도록 설정하였다.
개선후 적용한 코드
@Query("SELECT u FROM User u WHERE (:id IS NULL OR u.id = :id) " +
"AND (:name IS NULL OR u.name = :name) " +
"AND (:email IS NULL OR u.email = :email)")
List<User> findUsers(
@Param("id") Long id,
@Param("name") String name,
@Param("email") String email
);
🚨 문제2
빈 문자열을 전달하면 쿼리 조건이 적용되지 않는다
증상
매개 변수 중 1개가 null 이 아닌 빈 문자열이 넘어온다면
해당 조건이 무시되지 않고 잘못된 필터링이 수행된다.
원인
null과 빈 문자열을 동일하게 처리하지 않았다.
해결방법
`@Query`내에서 빈 문자열을 null과 동일하게 처리한다.
서비스 계층에서 빈 문자열을 NULL로 변환하는 로직을 추가한다.
개선후 적용한 코드
public List<UserResponseDto> findUsers(Long id, String username, String email) {
return userRepository.findUsers(id,
(username != null && !username.trim().isEmpty()? username : null),
(email != null && !email.trim().isEmpty()? email : null))
.stream()
.map(UserResponseDto::toDto)
.toList();
}