이번에는 질문 상세 조회 API에서 답변 리스트를 어떻게 정렬할지에 대한 로직을 정리했다.
우리 서비스는 사용자가 독서 전 → 독서 중 → 독서 후로 생각이 변화하는 과정을 보여주는 게 핵심이다.
따라서 답변을 단순히 한 줄 리스트로 보여주는 게 아니라, 사용자별 블록 단위로 묶어서 정렬하는 방식을 택했다.

질문 상세 조회 API를 만들고, 답변 리스트를 내려줄 때 정렬 방식이 문제였다.
처음 구현은 단순히 createdAt DESC로 정렬해서 보여주고 있었다.
하지만 우리 서비스는 독서 전 → 독서 중 → 독서 후로 변화하는 사용자 생각의 흐름을 강조해야 한다.
→ 따라서 사용자별로 답변을 블록 단위로 묶어 보여주는 방식이 필요했다.
또, 프론트에서 정렬 기준을 바꿀 수 있도록 sort 파라미터(latest, popular)를 지원하고 싶었다.
각 정렬기준에 따라 요구사항을 정리해보자면...
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public QuestionDetailResponseDto getQuestionDetail(Long questionId, String sort) {
Question question = questionRepository.findById(Math.toIntExact(questionId))
.orElseThrow(QuestionNotFoundException::new);
// 1) 모든 답변 조회
List<Answer> answers = answerRepository.findByQuestion_QuestionId(questionId);
// 2) 사용자별 그룹핑
Map<Long, List<Answer>> grouped = answers.stream()
.filter(a -> a.getUser() != null)
.collect(Collectors.groupingBy(a -> a.getUser().getUserId()));
// 3) 상태 우선순위
Map<String, Integer> stateOrder = Map.of("BEFORE", 0, "READING", 1, "AFTER", 2);
// 4) 블록 내부 정렬 (상태 순서만 유지)
Comparator<Answer> innerComparator = Comparator.comparing(
(Answer a) -> stateOrder.getOrDefault(
Optional.ofNullable(a.getAnswerState()).orElse("").toUpperCase(), 99)
);
// 5) 유틸 함수
Function<List<Answer>, Integer> maxLikes = list ->
list.stream().map(Answer::getLikeCount).filter(Objects::nonNull).max(Integer::compareTo).orElse(0);
Function<List<Answer>, LocalDateTime> latestCreated = list ->
list.stream().map(Answer::getCreatedAt).filter(Objects::nonNull).max(LocalDateTime::compareTo).orElse(LocalDateTime.MIN);
// 6) 블록 간 정렬
Comparator<Map.Entry<Long, List<Answer>>> blockComparator;
if ("popular".equalsIgnoreCase(sort)) {
blockComparator = Comparator
.comparing((Map.Entry<Long, List<Answer>> e) -> maxLikes.apply(e.getValue()))
.reversed()
.thenComparing(e -> latestCreated.apply(e.getValue()), Comparator.reverseOrder())
.thenComparing(Map.Entry::getKey);
} else {
blockComparator = Comparator
.comparing((Map.Entry<Long, List<Answer>> e) -> latestCreated.apply(e.getValue()), Comparator.reverseOrder())
.thenComparing(e -> maxLikes.apply(e.getValue()), Comparator.reverseOrder())
.thenComparing(Map.Entry::getKey);
}
// 7) 사용자 블록 정렬 + DTO 변환
List<AnswersByUserDto> answersByUser = grouped.entrySet().stream()
.sorted(blockComparator)
.map(entry -> {
User user = userRepository.findById(entry.getKey())
.orElseThrow(UserNotFoundException::new);
List<AnswerResponseDto> answerDtos = entry.getValue().stream()
.sorted(innerComparator)
.map(a -> AnswerResponseDto.from(
a,
user.getUserId().toString(),
Optional.ofNullable(user.getUserNickname()).orElse("알 수 없음"),
Optional.ofNullable(user.getProfileUrl()).orElse("")
))
.collect(Collectors.toList());
return new AnswersByUserDto(user, answerDtos);
})
.collect(Collectors.toList());
QuestionResponseDto questionDto = QuestionResponseDto.from(question, answers.size());
return new QuestionDetailResponseDto(questionDto, answersByUser);
}
간단한 리팩토링이었지만 여러가지 배운 점이 있다.
답변 리스트 조회 리팩토링 끝!