"오늘의 문제를, 내일의 기록으로 남깁니다."

막연한 이론보다, 구체적인 코드가 필요할 때. 직접 겪고 해결한 문제들을 기록합니다. 실무에서 부딪히는 진짜 이슈와, 내가 이해한 방식 그대로 정리한 가이드입니다.

웹개발/Java

Java Stream API 정렬, 실무에서는 이렇게 씁니다 (Comparable, Comparator 완벽 정리)

자바를잡아 2025. 7. 20. 09:00
반응형

Stream으로 정렬한다고 다 같은 정렬이 아니다

실제 업무 중, 사용자 목록을 JSON으로 내려주는 API 작업을 하다가 담당자 한 명이 이런 이슈를 제기했다. “왜 정렬이 안 돼서 내려오죠?”

확인해 보니 Java 8의 Stream API로 데이터를 가공하고 있었는데, sort()를 누락했거나, Comparator를 잘못 작성해서 정렬 결과가 예상과 달랐다. 특히 정렬 조건이 복잡해질수록 코드가 지저분해지고, 실수도 잦아졌다.

이번 글에서는 Java Stream API로 컬렉션 정렬을 구현할 때 자주 쓰는 실전 예제를 중심으로, Comparator, Comparable, 역순 정렬, 다중 조건 정렬, null 처리까지 정리해보겠다.

기초: Stream API에서 정렬하는 기본 구조

List<String> names = Arrays.asList("Lee", "Kim", "Park", "Choi");

List<String> sorted = names.stream()
    .sorted() // 오름차순 정렬
    .collect(Collectors.toList());

System.out.println(sorted); // [Choi, Kim, Lee, Park]

기본 정렬은 Comparable을 구현한 클래스(String, Integer 등)에만 적용된다.

객체 리스트 정렬: Comparator 사용

실무에선 대부분 객체 리스트를 정렬한다. 예를 들어 아래와 같은 User 클래스가 있다고 해보자.

public class User {
    private String name;
    private int age;

    // 생성자, getter 생략
}

1. 나이(age) 오름차순 정렬

List<User> users = getUserList();

List<User> sorted = users.stream()
    .sorted(Comparator.comparing(User::getAge))
    .collect(Collectors.toList());

2. 나이 내림차순 정렬

List<User> sorted = users.stream()
    .sorted(Comparator.comparing(User::getAge).reversed())
    .collect(Collectors.toList());

3. 이름 기준 오름차순 정렬

List<User> sorted = users.stream()
    .sorted(Comparator.comparing(User::getName))
    .collect(Collectors.toList());

다중 정렬: 실무에서 자주 쓰는 패턴

“나이순 정렬하되, 나이가 같으면 이름으로 정렬해줘” 같은 조건은 실무에서 매우 흔하다.

List<User> sorted = users.stream()
    .sorted(
        Comparator.comparing(User::getAge)
                  .thenComparing(User::getName)
    )
    .collect(Collectors.toList());

Comparator 체이닝을 활용하면 매우 직관적으로 조건을 표현할 수 있다.

null 값이 있는 경우 정렬 예외 발생 방지

만약 정렬 기준 필드에 null이 포함될 수 있다면 예외가 발생할 수 있다. 이를 안전하게 처리하려면 nullsFirst 또는 nullsLast를 활용한다.

List<User> sorted = users.stream()
    .sorted(Comparator.comparing(User::getName, Comparator.nullsLast(String::compareTo)))
    .collect(Collectors.toList());

역정렬: Comparable 기반 객체 정렬 역순

String, Integer와 같이 Comparable을 구현한 객체를 역정렬할 땐 아래처럼 처리한다.

List<String> sortedDesc = names.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());

기본값 기준으로 반대로 정렬하고 싶을 땐 가장 깔끔한 방법이다.

정렬 + 필터 + 매핑 조합 (실무 자주 씀)

정렬은 대개 단독보다는 filter, map과 조합되어 많이 사용된다.

List<String> adultNames = users.stream()
    .filter(user -> user.getAge() >= 20)
    .sorted(Comparator.comparing(User::getAge))
    .map(User::getName)
    .collect(Collectors.toList());

20세 이상 사용자 이름을 나이순으로 정렬해서 추출하는 전형적인 예시다.

실전 예제: 날짜 기준으로 최근 등록순 정렬

public class Post {
    private String title;
    private LocalDateTime createdAt;
}
List<Post> sorted = posts.stream()
    .sorted(Comparator.comparing(Post::getCreatedAt).reversed())
    .collect(Collectors.toList());

날짜 기준 최신순 정렬은 게시판, 공지사항, 로그 등에서 자주 사용된다.

정렬 후 특정 요소만 추출 (top N)

정렬 결과에서 상위 N개만 추출할 때는 limit()을 활용한다.

List<User> top5 = users.stream()
    .sorted(Comparator.comparing(User::getScore).reversed())
    .limit(5)
    .collect(Collectors.toList());

이런 방식은 인기글, 랭킹 페이지 구현 시 활용된다.

주의할 점: 정렬 후 collect()를 호출해야 실제 정렬 반영

Stream은 중간 연산(lazy) 방식이기 때문에, sorted()는 collect()를 만나기 전까지 아무 일도 하지 않는다.

아래 코드는 아무 일도 하지 않음:

users.stream()
     .sorted(Comparator.comparing(User::getAge)); // 아무 동작도 안 함

정리: Stream 정렬은 직관적이지만 디테일이 생명

Java Stream API의 정렬은 단순히 .sorted() 한 줄로 끝나는 듯 보이지만, 실무에서는 필드가 null일 수 있고, 정렬 조건이 여러 개이거나, 성능 이슈도 고려해야 한다. 특히 Comparator 체이닝, null 처리, 역정렬 등을 제대로 알고 쓰면 훨씬 안정적이고 가독성 좋은 코드를 만들 수 있다.

이 글이 Stream 정렬을 처음 접하거나, 실무에서 헷갈리는 분들에게 정리된 기준점이 되기를 바란다.

반응형