Java의 Stream에서 max / min 사용 시 Optional이 등장하는 이유

scminy0527·2026년 3월 4일
post-thumbnail

Java에서 Stream을 사용하다 보면 max()min()을 사용할 때 조금 헷갈리는 부분이 있다.

예를 들어 다음과 같은 상황을 생각해보자.

어떤 Member 객체가 있고, 그 안에 intro라는 String 필드가 있다고 가정하자.

그리고 이러한 Member 객체가 ArrayList에 담겨 있다고 해보자.

ArrayList<Member> memberList = new ArrayList<>();

이때 다음과 같은 문제를 해결해야 한다.

intro 문자열의 길이가 가장 긴 Member를 찾아라.


Stream으로 최대값 찾기

Stream에서는 max()를 이용해서 최대값을 찾을 수 있다.

memberList.stream()
        .max(Comparator.comparingInt(m -> m.getIntro().length()));

여기서 중요한 것은 비교 기준을 정해줘야 한다는 것이다.

우리는 intro의 길이를 기준으로 비교해야 하기 때문에 다음과 같은 Comparator를 사용한다.

Comparator.comparingInt(m -> m.getIntro().length())

즉 의미는 다음과 같다.

intro 길이를 기준으로 가장 큰 객체를 찾는다

그런데 여기서 문제가 발생한다

위 코드의 반환 타입은 다음과 같다.

Optional<Member>

Member가 아니라 Optional<Member>가 반환된다.

처음 Stream을 배울 때 여기서 많이 헷갈리게 된다.

왜 바로 Member를 반환하지 않을까?


Optional이 등장하는 이유

이유는 매우 단순하다.

Stream이 비어있을 수도 있기 때문이다.

예를 들어 다음과 같은 상황이 있을 수 있다.

ArrayList<Member> memberList = new ArrayList<>();

이 리스트가 비어있다면

최대값 자체가 존재하지 않는다

그런데 만약 바로 Member를 반환하도록 설계되어 있다면?

NullPointerException

이 발생할 가능성이 생긴다.

그래서 Java에서는 이러한 문제를 해결하기 위해 Optional 타입을 도입했다.

Optional의 의미는 다음과 같다.

값이 있을 수도 있고
값이 없을 수도 있다

그래서 바로 값을 사용할 수 없다

예를 들어 다음 코드는 사용할 수 없다.

Member member = memberList.stream()
        .max(Comparator.comparingInt(m -> m.getIntro().length()));

왜냐하면 반환 타입이

Optional<Member>

이기 때문이다.

그래서 Optional을 먼저 처리해야 한다.


방법 1 : ifPresent()

Optional에서 가장 많이 사용하는 메서드는 ifPresent()이다.

memberList.stream()
        .max(Comparator.comparingInt(m -> m.getIntro().length()))
        .ifPresent(member -> {
            System.out.println(member.getIntro());
        });

의미는 다음과 같다.

값이 존재한다면 실행해라

즉 Optional 내부에 값이 있을 때만 코드를 실행한다.


방법 2 : orElse()

Optional에서 값을 꺼내고 싶다면 orElse()를 사용할 수 있다.

Member member = memberList.stream()
        .max(Comparator.comparingInt(m -> m.getIntro().length()))
        .orElse(null);

의미

값이 있으면 반환
값이 없으면 null 반환

방법 3 : map()으로 필드만 추출하기

만약 가장 긴 intro 문자열만 출력하고 싶다면 다음과 같이 사용할 수 있다.

String intro = memberList.stream()
        .max(Comparator.comparingInt(m -> m.getIntro().length()))
        .map(Member::getIntro)
        .orElse("");

Stream의 흐름은 다음과 같다.

Stream<Member>
      ↓
max
      ↓
Optional<Member>
      ↓
map
      ↓
Optional<String>
      ↓
orElse
      ↓
String

정리

Stream에서 max() / min()을 사용할 때 기억해야 할 핵심은 다음이다.

1. 비교 기준을 반드시 제공해야 한다.

Comparator.comparingInt()

2. 반환 타입은 Optional이다.

Optional<T>

3. Optional을 처리해야 실제 값을 사용할 수 있다.

대표적인 방법

ifPresent()
orElse()
orElseThrow()
map()

마무리

처음 Stream을 배울 때 가장 헷갈리는 부분은 다음 두 가지이다.

Comparator
Optional

하지만 이 구조만 이해하면

  • 최대값 찾기
  • 최소값 찾기
  • 특정 조건의 객체 찾기

같은 작업을 매우 간결하게 처리할 수 있다.


💡 개인적으로 느낀 Stream의 핵심 흐름은 다음과 같다.

stream()
filter()
map()
max / min
Optional 처리

이 패턴을 이해하면 대부분의 Stream 문제를 해결할 수 있다.

profile
비전공자에서 시작해 Flutter로 실서비스를 만들었고, 더 깊은 개발을 향해 계속 성장 중인 개발자

0개의 댓글