빌드노트
Optional에 값이 있는데 orElse의 기본값 함수가 실행된다
Optional에 값이 있어도 orElse의 인자로 쓴 함수는 먼저 실행된다. 실제 호출 횟수·반환값·예외를 비교해 orElseGet의 람다 안으로 옮길 작업을 구분하고, 미리 계산한 값을 감싸는 함정을 확인한다.
Java에서 value.orElse(createDefault())를 쓰면 Optional에 값이 있어도 createDefault()가 먼저 실행된다. orElse는 대체값을 만드는 일을 미뤄 주지 않는다. 값이 없을 때만 생성·조회해야 한다면 value.orElseGet(() -> createDefault())처럼 작업을 람다 안으로 옮긴다.
반환값이 같아도 함수 호출 수는 다르다
다음은 JDK 17에서 비교할 수 있는 짧은 코드다. java.util.Optional을 import한 클래스에 아래 두 멤버를 둔다. fallback()은 메모리 카운터만 늘리고 문자열을 반환하며, 실제 조회나 파일 생성은 하지 않는다.
static int calls;
static String fallback() {
calls++;
return "fallback";
}
같은 클래스의 메서드 안에서 다음 두 호출을 비교한다. 각 호출 직전에 카운터를 0으로 맞춘다. 외부 라이브러리는 필요 없다.
var value = Optional.of("saved");
calls = 0;
System.out.println(value.orElse(fallback()) + " calls=" + calls);
calls = 0;
System.out.println(value.orElseGet(() -> fallback()) + " calls=" + calls);
Windows와 OpenJDK 17.0.2에서 실행하니 다음 두 줄이 출력됐다.
saved calls=1
saved calls=0
둘 다 saved를 반환했지만 orElse에서는 대체 함수가 1회 실행됐다. 반환값만 검사하면 불필요한 생성·조회를 놓칠 수 있다. value를 Optional.<String>empty()로 바꿔 같은 비교를 실행했을 때는 두 줄 모두 fallback calls=1이었다.
이 차이는 메서드에 넘기는 대상에서 생긴다. orElse(fallback())에는 함수의 반환값을 넘겨야 하므로 fallback()부터 실행한다. Java 17 언어 명세의 인자 평가 규칙 (새 탭에서 열림)에 따라 메서드 본문에 들어가기 전에 인자를 평가한다. Optional에 값이 있으면 대체값을 반환에 쓰지 않을 뿐, 이미 끝난 호출을 취소하지는 않는다.
orElseGet에는 필요할 때 값을 만드는 Supplier, 즉 공급 함수를 넘긴다. Optional API 문서 (새 탭에서 열림)는 값이 없을 때 공급 함수의 결과를 사용한다고 정한다. () -> fallback()도 인자로서 평가되지만, 그때 람다 본문까지 실행되는 것은 아니다. 람다 실행에 관한 언어 명세 (새 탭에서 열림)가 이 둘을 구분한다.
대체 함수가 실패하면 기존 값도 반환하지 못한다
대체 함수의 return "fallback";을 throw new IllegalStateException("fallback failed");로 바꾼 비교도 실행했다. 카운터는 그대로 먼저 늘리고, 예외가 다음 비교를 막지 않도록 각 호출의 예외를 따로 잡아 확인했다.
- 값이 있는
orElse는 호출 수가 1이 되고IllegalStateException: fallback failed가 발생했다.saved는 반환되지 않았다. - 값이 있는
orElseGet은 호출 수가 0이고saved를 반환했다. - 값이 없는 경우에는 두 메서드 모두 호출 수가 1이 되고 같은 예외가 발생했다.
첫 경우는 orElse 본문이 값을 고르기도 전에 인자 평가가 예외로 끝난 것이다. 값이 있는 Optional도 이를 막아 주지 않는다. 반면 orElseGet은 값이 있으면 대체 함수를 호출하지 않는다. 값이 없어서 공급 함수를 실행한 뒤 생기는 예외까지 처리해 주지는 않는다.
미리 계산한 값을 람다로 감싸도 호출은 남는다
메서드 이름만 orElseGet으로 바꾸고 작업을 앞에 남겨 두면 지연 실행이 되지 않는다. 다시 문자열을 반환하는 fallback()과 값이 있는 value로 다음을 별도 실행했다.
calls = 0;
String computed = fallback();
String result = value.orElseGet(() -> computed);
result는 saved였지만 호출 수는 1이었다. computed를 준비하면서 이미 실행했기 때문이다. 호출 수를 초기화하고 value.orElseGet(() -> fallback())로 바꾼 비교에서는 0이었다. 미룰 대상은 변수 읽기가 아니라 대체값을 만드는 작업이다.
문자열 리터럴이나 이미 준비한 변수를 대체값으로 쓸 때는 value.orElse("guest")처럼 넘길 수 있다. 값이 없을 때만 실행할 생성·조회는 orElseGet의 람다 안에 둔다. 바꾼 뒤에도 함수가 호출된다면 Optional이 실제로 비어 있는지, 함수 호출이 람다 밖에 남아 있는지부터 확인한다. 자기 코드에서 비교할 때도 반환값과 함께 대체 함수에 진입한 횟수나 로그를 확인한다.
직접 실행한 범위는 OpenJDK 17.0.2의 단일 스레드 메모리 카운터, 반환값과 합성 예외다. 다른 JDK 버전이나 실제 DB·파일·네트워크 부작용은 실행하지 않았고, 실행 시간·성능 배수도 측정하지 않았다.