본문 바로가기
빌드노트 검색

빌드노트

Stream의 count()는 값을 반환했는데 peek() 로그가 없다

count()가 정상 반환해도 peek()는 생략될 수 있다. 호출 카운터로 로그 설정 문제와 구별하고, 필터 위치에 따른 실제 차이와 결과 목록 뒤 명시적 반복으로 필수 처리를 옮기는 방법을 확인한다.

Java의 List에서 만든 Stream에 peek()로 로그를 넣고 count()를 호출하면, 개수는 나오는데 로그는 한 줄도 없을 수 있다. 원본의 크기만으로 개수를 구할 수 있으면 Stream 구현이 peek()를 포함한 원소 순회를 생략할 수 있기 때문이다. 종료 연산이 끝났다는 사실이 모든 중간 람다의 실행을 뜻하지는 않는다.

자신의 파이프라인에서 먼저 볼 곳은 peek() 안이다. 값 확인용 로그는 생략될 수 있지만, 저장이나 상태 갱신처럼 반드시 수행할 처리를 여기에 두었다면 옮겨야 한다. 선택 결과와 개수를 구하는 일, 각 값에 필요한 동작을 실행하는 일을 분리한다.

로그가 빠졌는지, 람다가 빠졌는지 확인한다

다음 입력은 로그 설정과 무관한 카운터로 peek() 호출 여부를 확인한다. JDK 17 이상이 설치된 터미널에서 java -version으로 버전을 확인하고 jshell --feedback silent를 연 뒤 입력한다. 외부 라이브러리는 필요 없다. 카운터는 이 진단에서만 쓴다.

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
var values = List.of(1, 2, 3, 4);
var calls = new AtomicInteger();
long count = values.stream().peek(n -> calls.incrementAndGet()).count();
System.out.println("count=" + count + ", peekCalls=" + calls.get());

Windows 11, OpenJDK 17.0.2에서 직접 출력한 결과는 count=4, peekCalls=0이었다. 개수는 네 개인데 람다는 한 번도 호출되지 않았다. peek()는 원소 수를 바꾸지 않으므로 이 목록의 크기를 알면 각 값을 방문하지 않고도 답을 낼 수 있다.

Java 17 count()의 API Note (새 탭에서 열림)와 Java 21의 같은 설명 (새 탭에서 열림)은 이런 실행 생략을 허용한다. 자신의 환경에서 peekCalls=4가 나왔다면 그 실행에서는 호출된 것이다. 모든 JVM·파이프라인이 같은 횟수를 보장하는 것은 아니다. 자신의 코드에서 카운터는 증가하는데 로그만 안 보인다면 로그 레벨과 출력 대상을 확인한다.

별도 새 Java 프로세스에서 같은 네 정수로 조건을 비교했다. 짝수만 남기는 filter(n -> n % 2 == 0) 뒤에 peek()를 두면 결과 개수 2, 호출 2회였고, 필터 앞으로 옮기면 개수는 2인 채 호출은 4회였다. 필터가 있으면 원본 크기 4를 그대로 답으로 쓸 수 없고, 필터 앞에서는 제외될 값도 지나간다. 반환된 개수와 중간 지점의 호출 횟수는 다른 값이다.

이 차이를 보고 필수 처리를 실행시키려고 filter()를 덧붙이지 않는다. peek() 문서 (새 탭에서 열림)는 디버깅 용도를 설명하면서, 원소 생성이 최적화로 생략되면 해당 동작도 호출되지 않는다고 밝힌다. 위 횟수는 이 입력과 구현에서 얻은 관찰이다.

필수 처리는 결과를 만든 뒤 실행한다

선택된 값과 개수가 모두 필요하다면 목록을 먼저 만든 뒤 명시적으로 반복할 수 있다. 다음 입력은 같은 JShell 세션에서 짝수를 모으고 각 값의 처리 위치를 출력으로 확인한다. 앞서 count()를 호출한 Stream은 끝났으므로 values.stream()에서 새로 시작한다.

var result = values.stream().filter(n -> n % 2 == 0).toList();
for (int n : result) {
    System.out.println("process=" + n);
}
System.out.println("count=" + result.size());

같은 환경에서 process=2, process=4, count=2가 차례로 출력됐다. 필요한 처리는 반복문 안의 출력 자리에 둔다. 개수는 선택 결과인 result.size()로 읽는다. 원본의 values.size()는 4이므로 선택된 두 값의 개수를 대신하지 못한다.

Stream 패키지의 부수효과 설명 (새 탭에서 열림)은 계산 결과에 영향을 주지 않는 단계가 생략될 수 있다고 명시한다. 반복문으로 옮기면 필요한 동작이 중간 연산의 최적화에 달리지 않게 된다. 다만 처리 중 예외가 발생하면 뒤의 원소까지 완료된 것은 아니며, 외부 저장·발송의 성공을 이 코드만으로 보장하지는 않는다.

확인한 실행 범위는 네 정수와 OpenJDK 17.0.2의 순차 Stream이다. 조건별 새 Java 프로세스와 위 JShell 입력을 실행했다. Java 21은 공식 문서만 읽었으며, 해당 버전의 실제 실행·병렬 Stream·큰 입력의 성능·외부 저장이나 발송은 검증하지 않았다.

← 전체 글 보기