
Spring Boot 프로젝트에서 날짜별 콘텐츠를 조회하는 기능을 구현하던 중, 다음과 같은 오류가 발생했다.
Query did not return a unique result: 2 results were returned
처음에는 동일한 날짜에 여러 개의 레코드가 존재하는 것으로 생각했다. 하지만 MySQL에서 직접 쿼리를 실행해보니 예상과 달랐다.
@Query("SELECT e FROM EmailContent e JOIN FETCH e.theme WHERE e.createdAt BETWEEN ?1 AND ?2")
Optional<EmailContent> findByCreatedDateBetween(LocalDateTime startDate, LocalDateTime endDate);
LocalDate targetDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDateTime startOfDay = targetDate.atStartOfDay(); // 2025-07-05T00:00:00
LocalDateTime endOfDay = targetDate.atTime(23, 59, 59); // 2025-07-05T23:59:59
Optional<EmailContent> content = emailContentRepository.findByCreatedDateBetween(startOfDay, endOfDay);
-- 날짜별 레코드 수 확인
SELECT DATE(created_at) as date, COUNT(*) as count
FROM email_content
GROUP BY DATE(created_at)
HAVING count > 1;
-- 결과: Empty set
-- 해당 날짜의 데이터 확인
SELECT count(1)
FROM email_content e
JOIN content_theme t ON t.id = e.theme_id
WHERE e.created_at BETWEEN '2025-07-05 00:00:00' AND '2025-07-05 23:59:59';
-- 결과: 1 (정상)
MySQL에서는 1개의 결과만 나오는데, 왜 JPA에서는 2개 결과가 반환된다고 하지?
문제는 JPA의 JOIN FETCH와 Optional 조합에서 발생했다.
Hibernate:
select
ec1_0.id,
ec1_0.created_at,
ec1_0.detailed_content,
ec1_0.html_content,
ec1_0.sent,
ec1_0.sent_at,
t1_0.id,
t1_0.jlptlevel,
t1_0.topic,
t1_0.used,
t1_0.used_at
from
email_content ec1_0
join
content_theme t1_0
on t1_0.id=ec1_0.theme_id
where
ec1_0.created_at between ? and ?
SQL 자체는 정상적이지만, JPA가 결과를 Optional로 변환하는 과정에서 문제가 발생한다.
@Query("SELECT DISTINCT e FROM EmailContent e JOIN FETCH e.theme WHERE e.createdAt BETWEEN ?1 AND ?2")
Optional<EmailContent> findByCreatedDateBetween(LocalDateTime startDate, LocalDateTime endDate);
@Query("SELECT e FROM EmailContent e JOIN FETCH e.theme WHERE e.createdAt BETWEEN ?1 AND ?2")
List<EmailContent> findByCreatedDateBetween(LocalDateTime startDate, LocalDateTime endDate);
// Controller에서 사용
List<EmailContent> contents = emailContentRepository.findByCreatedDateBetween(startOfDay, endOfDay);
Optional<EmailContent> content = contents.isEmpty() ? Optional.empty() : Optional.of(contents.get(0));
Optional<EmailContent> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
연관된 엔티티는 지연 로딩으로 처리하고, 필요시 별도 조회
단순해 보이는 조회 기능이지만, JPA의 내부 동작을 이해하지 못하면 예상치 못한 오류에 직면할 수 있다.
이번 경험을 통해 ORM의 추상화 뒤에 숨겨진 복잡성을 다시 한번 깨달았다.