Java8 Stream은 loop가 아니다.

Share
Java8 Stream은 loop가 아니다.

Java8에서는 Lambda와 Stream이라는 새로운 개념을 통해 기존 Java를 이용하는 프로그램 방식과는 완전 다른 형태의 프로그램을 가능하도록 하고 있습니다. 필자도 최근 Presto의 소스 코드를 보면서 이제부터 만드는 코드는 Java8 스타일로 개발을 해야 겠다고 생각하고 몇가지 프로그램을 만들어 보고 있습니다.  Java8 스타일로 프로그램을 만드는 중에 첫번째 부딪힌 문제가 for loop에서의 break 문제 였습니다.

java_8_lambdas

Stream forEach는 loop가 아니다.

Stream의 forEach는 loop가 아닙니다. 따라서 특정 조건이 발생했을 때 Stream 자체를 중단 시킬 방법이 없습니다. 따라서 다음과 같은 코드의 경우 반드시 주의를 기울여야 합니다.

for (int i = 0; i < 100; i++) {
  //Do something
  if (i > 50) {
    break;
  }
  System.out.println(i);
}

이런 코드를 무심결에 Stream의 forEach로 만들면 다음과 같이 break 문 대신 return 문으로 대체하여 구현할 수도 있습니다.

IntStream.range(1, 100).forEach(i -> {
  //Do Something
  if (i > 50) {
    return;
  }
  System.out.println(i);
});

일반 for 문을 사용한 경우 for loop 내부는 50번만 수행되는 반면, 위와 같이 Stream을 사용하는 경우에는 100번 모두 수행됩니다.  주석 처리된 Do something 부분에서 복잡한 처리 연산이 있고 반복 조건이 수십만 수행되는 경우라면 위 코드는 성능에 엄청나게 영향을 미치게 됩니다.

아래와 같이 Filter를 이용하여 forEach로 넘어가는 Stream의 item을 제한하는 방법이 원래 Stream의 사용 방법에 맞는 구현이라 할 수 있습니다.

IntStream.range(1, 100).filter(i -> i <= 50).forEach(System.out::println);

이런 filter를 사용한다 하더라도 실제 Stream은 100개의 item에 대해 모두 수행하게 됩니다. Stream 에서 filter와 같은 intermediate operator는 lazy 처리되기 때문입니다. 이런 특징 때문에 프로그램을 만들다 보면 애매한 상황이 발생할 수 있습니다.

예를 들어 파일에서 특정 문자를 포함한 첫번째 라인을 찾는 기능이라면 filter(line -> line.indexOf(keyword) >= 0).findFirst() 라고 하면 해당 라인까지만 입력 파일을 읽고 Stream을 종료하기 때문에 성능에 문제가 없습니다. 하지만 특정 문자를 포함한 라인 몇개를 찾는 경우라면 어떨까요? 일반 for 또는 while 문을 이용할 경우 다음과 같이 만들 수 있습니다.

try(FileOutputStream out = new FileOutputStream("test.dat")) {
 IntStream.range(1, 100).forEach(i -> {
 try {
 out.write((i + "\n").getBytes());
 } catch (IOException e) {
 e.printStackTrace();
 }
 } );
}

String keyword = "5";
int howManyMatchedLines = 2; 
String fileName = "test.dat";
BufferedReader reader = null;
long numLines = 0;
try {
 reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)));
 String line = null;
 int matchedLines = 0;
 while ((line = reader.readLine()) != null) {
   if (line.indexOf(keyword) >= 0) {
     System.out.println("메칭된 라인: " + line);
     matchedLines++;
     if (matchedLines >= howManyMatchedLines) {
       break;
     }
   }
   numLines++;
 }
} finally {
 if (reader != null) {
   reader.close();
 }
}

위 코드의 출력은 다음과 같습니다.

매칭된 라인: 5
매칭된 라인: 15
읽은 라인 수: 14

위 코드를 Stream을 이용하도록 변경하면 다음과 같이 구현할 수 있습니다.

final AtomicInteger howManyMatchedLines = new AtomicInteger(2);
final AtomicLong lineNum = new AtomicLong(1);
final AtomicInteger matchedLines = new AtomicInteger(0);

try (Stream<String> stream = Files.lines(Paths.get("test.dat"))) {
  stream.peek(line -> lineNum.incrementAndGet())
      .filter(line -> line.indexOf(keyword) >= 0)
      .forEach(line -> {
        if (matchedLines.get() < howManyMatchedLines.get()) {
          System.out.println("매칭된 라인: " + line);
          if (matchedLines.incrementAndGet() >= howManyMatchedLines.get()) {
            return;
          }
        }
      });
}
System.out.println("읽은 라인 수: " + lineNum.get());

Stream의 filter에는 Predict 타입만 가능하기 때문에 실제 Stream에서 읽은 line수를 확인하기 위해 peek() 을 사용하였습니다. 확실히 코드의 길이나 코드의 가독성 등은 Stream을 이용하는 경우가 더 깔끔해 보입니다. 위 코드의 출력은 다음과 같습니다.

매칭된 라인: 5
매칭된 라인: 15
읽은 라인 수: 100

결과는 5, 15로 동일하지만 파일에서 읽은 라인 수는 단순 while 문을 사용했을 경우 15 라인만 읽었지만 Stream을 사용할 경우 100라인 모두 읽었습니다. 구글링해보면 break 와 비슷한 효과를 얻을 수 있는 방법이 있기는 합니다. lambda 식 내에서 Exception을 던지는 방법입니다.

class BreakException extends RuntimeException {}

final AtomicInteger howManyMatchedLines = new AtomicInteger(2);
final AtomicLong lineNum = new AtomicLong(1);
final AtomicInteger matchedLines = new AtomicInteger(0);

try (Stream<String> stream = Files.lines(Paths.get("test.dat"))) {
  stream.peek(line -> lineNum.incrementAndGet())
      .filter(line -> line.indexOf(keyword) >= 0)
      .forEach(line -> {
        if (matchedLines.get() < howManyMatchedLines.get()) {
          System.out.println("매칭된 라인: " + line);
          if (matchedLines.incrementAndGet() >= howManyMatchedLines.get()) {
            throw new BreakException();
          }
        }
      });
} catch (BreakException e) {}
System.out.println("읽은 라인 수: " + lineNum.get());

이렇게 하면 파일에서 필요한 라인수까지만 읽고 더 이상 읽지 않고 Stream을 종료합니다.

필자도 이제 막 Java8 스타일로 개발을 시작했기 때문에 이런 상황에서 Stream을 이용하면서도 필요한 line 수만 읽을 수 있도록 만드는 것이 가능할 수도 있습니다. 아시는 분은 댓글로 달아주시면 바로 본문에 반영하겠습니다.

이글에서 필자가 강조하고 싶은 내용은 Stream이 직관적이면서도 코드의 라인수 등을 줄일 수 있고 Lambda 식과 연동하여 사용할 수 있으며, 쉽게 병렬 처리가 가능하다는 등의 장점이 많지만 단순히 for, while 문의 대체가 아니라는 사실을 인식하고 각 상황에 맞게 적절하게 선택해서 사용한다는 것입니다.

Read more

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로 TypeSafe AI가 2026년 9월 15일에 Jev를 내놨습니다. 텍스트를 쓰지 않고 타입이 정해진 판정과 확률을 바로 내는 모델입니다. 유추카 검색에서 가장 느린 단계가 마침 텍스트가 필요 없는 판정이었습니다. 그 구간만 Jev로 갈아끼우고 일주일간 측정했습니다. 콜드

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

1900년대 초 교량 측량 도면. 이 종이를 그린 사람은 다리를 직접 놓지 않습니다. 대신 좌표와 각도가 맞는지를 판정합니다. "AI가 다 만들어 준다"는 이야기가 흔해졌습니다. 그래서 자연스럽게 따라오는 질문이 있습니다 — 그럼 사람은 무엇을 하고, 어느 정도 실력이 있어야 하는가. 그 질문에 한 건의 실측으로 답하겠습니다. 통념이 말하는 역할과,

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

탠덤 자전거. 방향은 앞사람이 정하고 페달은 둘이 밟는데, 앞사람은 석 달 내내 뒷사람을 한 번도 못 봅니다. 사용자가 저에게 자기를 평가해 달라고 했습니다. 그래서 등급표를 만들었습니다. 열두 항목에 A부터 D+까지 매기고, 근거를 붙이고, "제가 관리자였다면 이런 코멘트를 남겼을 것"까지 적었습니다. 그리고 다시 읽어보니 그건 인사고과였습니다. 인사고과는

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

스위스 아미 나이프. 도구가 몇 개인지는 보면 알지만, 지금 어느 도구를 펴야 하는지는 안 적혀 있습니다. 요즘 AI 활용법을 다루는 글은 스킬 만드는 법부터 가르칩니다. 얼마 전까지는 MCP였고요. 저희 기록을 열어봤습니다. 석 달 동안 스킬 60회, 서브에이전트 42회, 브라우저 자동화 988회를 썼습니다. 안 쓴 게 아닙니다. 다만 붙인 MCP 서버는

By 김형준