공부하기/Java

[Java] Stream 함수에서 인덱스 사용하기 (키오스크 과제)

다섯자두 2025. 1. 16. 21:16

💥 Stream 함수에서 인덱스 사용

발단 : forEach() 에서 인덱스 정보가 필요해!

`기존에 생성한 Menu의 MenuItem을 조회 할 때 스트림을 사용하여 출력하도록 수정`하라는 요구사항에 맞게,

`Menu` class의 `printMenuItems()` 메서드를 변경하고자 하였다.

기존 코드

    // List에 들어있는 MenuItem을 순차적으로 보여주는 함수
    public void printMenuItems() {
        for(int i=0; i<menuItems.size(); i++) {
            System.out.print(i+1 + ". ");
            System.out.println(menuItems.get(i));
        }
    }

위 함수를 실행하면 다음과 같이 인덱스와 함께 메뉴 아이템의 정보가 출력된다.

1. ShackBurger        | W 6.9 | 토마토, 양상추, 쉑소스가 토핑된 치즈버거
2. SmokeShack         | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
3. Cheeseburger       | W 6.9 | 포테이토 번과 비프패티, 치즈가 토핑된 치즈버거
4. Hamburger          | W 5.4 | 비프패티를 기반으로 야채가 들어간 기본버거

 

`Menu` class는 필드로 `private final List<MenuItem> menuItems`를 가지고 있다.

menuItems에 대해서 stream을 생성하고 `forEach` 중간 함수를 통해서 `MenuItem 요소`와 요소의 순서를 가져오는 `index 데이터`를 가져와서 출력을 하면 되겠다고 생각했다.

그런데 ... 문제가 발생했다.

wrong number of parameters: expected 1 but found 2

내가 JS의 forEach 함수와 혼동했던 것 ..

Java stream의 forEach 함수의 콜백함수에 들어갈 수 있는 매개변수는 오직 1개, 스트림 요소를 담을 변수뿐이다.

 

근데 인덱스를 사용 못 하는게 말이 돼??? 일단 방법을 생각해봤다.

전개 : index 변수를 선언해서 쓰자

변수 `idx`를 선언하고, forEach 함수 내에서 사용해보려고 했다.

코드는 다음과 같다.

    public void printMenuItemsWrongVer() {
        int idx = 1;
        menuItems.stream().forEach((item)-> {
            System.out.printf("%d. %s\n",idx++,item);
        });
    }

이번엔 다음과 같은 에러가 다시 발생했다.

Variable used in lambda expression should be final or effectively final

람다 표현식 안에서 사용되는 변수는 final이거나 final처럼 동작해야 한다는 것이다.

그래서 함수가 종료됨과 동시에 사라지는 `idx`변수는 람다 표현식 내에서 사용하려고 하면 에러가 발생한다.

 

해결 방법을 찾아보자!

 


해결 ✅ IntStream 사용

스트림을 이용해서 for문처럼 특정 범위동안 반복적인 작업을 하고 싶을 때 IntStream을 주로 사용하는 것 같다.

`IntStream` - 정수 데이터에 대한 stream을 구성할 수 있다.
- `range(start,end)` : start ~ end-1 까지의 정수 데이터로 스트림을 구성한다.
- `rangeClosed(start,end)` : start ~ end 까지의 정수 데이터로 스트림을 구성한다.
- `of(num1, num2, …)` : 지정된 값을 포함하는 정수 스트림을 구성한다.

인덱스로의 활용 방법

IntStream을 구성한 뒤, IntStream의 중간함수 forEach에서 작업을 수행한다.

public void printMenuItems() {
    IntStream.range(0,menuItems.size())
            .forEach(idx -> System.out.printf("%d. %s\n",idx+1,menuItems.get(idx)));
}

이해하기가 쉽고 직관적이라 이 방법을 채택했다!

해결 ✅ AtomicInteger 사용

아까 마주한 에러 문구를 다시 보면, 인텔리제이가 'Convert to atomic'을 하라고 권유하는 것을 볼 수 있다.

눌러보면 코드가 다음과 같이 바뀐다.

    public void printMenuItems() {
        AtomicInteger idx = new AtomicInteger(1);
        menuItems.stream().forEach((item)-> {
            System.out.printf("%d. %s\n", idx.getAndIncrement(),item);
        });
    }

`AtomicInteger` 라는 객체는 람다표현식 내에서 인덱스처럼 사용할 수 있다.

개인적으로 이번에 처음 본 클래스라 조금 더 찾아봤다.

Atomic 클래스

`Atomic클래스` : 멀티쓰레드 환경에서 동시성을 보장해주는 클래스
- CAS(Compare And Swap)방식에 기반하여 동기화 문제 해결
- 변수의 값을 변경하기 전에 확인한 값이 내가 예상하던 값과 같을 경우에만 새로운 값으로 변경해준다.

이렇게 알아서 동시성을 보장해주는 Atomic 클래스의 종류는 다음과 같다.

많구나??

`AtomicInteger` : 멀티쓰레드 환경에서 동시성을 보장해주는 Integer 변수
- `get()` : 메모리에서 값을 가져온다.
- `set()` : 메모리에 값을 저장한다.
- `compareAndSet()` : 메모리에 값을 바꾸는데(저장) 성공하면 true, 실패하면 false를 리턴한다.
- `getAndIncrement()` : 값을 가져온 후 1 증가시킨다.

 

AtomicInteger, AtomicBoolean과 같은 객체 변수는 람다 표현식에서 사용 가능하다.

람다 표현식에서 사용하는 변수는 `final` 혹은 `사실상 final`이어야 하는데, Atomic 클래스 객체는 사실상 final에 속하는 것이다.

 

기존에 사용하던 idx 사용과 가장 유사하게 구현할 수 있지만, 현재 구현에서 동시성 보장 로직을 구태여 실행하게 할 필요는 없는 것 같아 사용하진 않았다.