스트림,이넘문제 (패스)

류한선·2025년 6월 24일

실기연습-2

목록 보기
35/95

좋아요! 🎯
이번엔 enum을 이용한 클래스 설계ordinal() 함정, 필드 초기화 트릭을 섞어서 문제를 내고, 한 줄 한 줄 자세히 풀이해 드릴게요.


🧩 트릭 문제 (enum 설계 + ordinal 함정 + 필드 초기화 트릭)

📜 문제

아래 코드를 보고 출력값을 유추해 보세요.

enum Rank {
    BRONZE(1), SILVER(2), GOLD(3), PLATINUM(4);

    private int level;

    Rank(int level) {
        this.level = level;
        System.out.println(name() + " ordinal=" + ordinal() + ", level=" + level);
    }

    int getLevel() {
        return level;
    }
}

class User {
    private final String name;
    private Rank rank;

    User(String name, Rank rank) {
        this.name = name;
        this.rank = rank;
    }

    void promote() {
        // ordinal() 기준 업그레이드
        int next = rank.ordinal() + 1;
        if (next < Rank.values().length) {
            rank = Rank.values()[next];
        }
    }

    @Override
    public String toString() {
        return name + " is " + rank + " (level=" + rank.getLevel() + ", ordinal=" + rank.ordinal() + ")";
    }
}

public class TestEnumDesign {
    public static void main(String[] args) {
        User u = new User("Alice", Rank.SILVER);
        u.promote();
        System.out.println(u);
    }
}

🧠 디버깅식 풀이

📍 enum Rank

생성자 호출 시점

  • enum 상수(BRONZE, SILVER, ...)가 클래스가 메모리에 로드될 때 한 번 생성됨.
  • 생성자 안에서 ordinal() 호출 → 0부터 시작된 순서 반환.
  • getLevel()은 별도로 우리가 지정한 level 반환.

→ 프로그램 시작 전에 다음 출력이 일어남:

BRONZE ordinal=0, level=1
SILVER ordinal=1, level=2
GOLD ordinal=2, level=3
PLATINUM ordinal=3, level=4

✅ 이렇게 enum 상수를 정의할 때 생성자 호출 및 필드 초기화가 미리 실행됨.


📍 main() 시작

User u = new User("Alice", Rank.SILVER);
  • User 생성자 호출
  • name = "Alice", rank = SILVER

📍 u.promote() 호출

int next = rank.ordinal() + 1;  // SILVER.ordinal() == 1이므로 next = 2
// Rank.values()[2] = GOLD
rank = GOLD;

📍 System.out.println(u); 호출

→ User의 toString():

name = "Alice"
rank = GOLD
rank.getLevel() = 3
rank.ordinal() = 2
→ "Alice is GOLD (level=3, ordinal=2)"

🎯 최종 출력 정리

프로그램을 실행하면, 클래스 초기화 출력 + toString 출력이 나옴:

BRONZE ordinal=0, level=1
SILVER ordinal=1, level=2
GOLD ordinal=2, level=3
PLATINUM ordinal=3, level=4
Alice is GOLD (level=3, ordinal=2)

🔍 포인트 정리

  1. ordinal() 함정
    → ordinal()은 상수의 선언 순서를 반환(0부터).
    → 임의로 level 필드를 둬도 ordinal()은 바뀌지 않음.

  2. 필드 초기화 트릭
    → enum 상수 정의 시 생성자가 한 번 호출됨(프로그램 시작할 때 이미 호출됨).
    → 생성자 안의 출력문은 User 생성 전에 출력됨.

  3. values() 메서드
    → 모든 상수를 배열로 반환, ordinal() 값 기준 인덱스 접근.


💡 ordinal() 대신 필드를 쓰자

실제 개발할 때 ordinal() 대신 level 필드를 쓰는 것이 안전함:

  • ordinal은 상수를 추가하거나 순서 바꾸면 망가지므로
  • level 같은 고유 필드를 이용.

🎯 연습 팁

이런 문제를 연습하면서:
enum 생성자 호출 타이밍
ordinal() vs 고유 필드 차이
values()ordinal() 조합 트릭

이 부분을 반드시 이해해 두면 실기 트릭 문제에서 실수하지 않아요!


맞아요, **ordinal()**은 우리가 따로 정의하지 않았는데도 enum 상수마다 자동으로 존재하는 메서드입니다. 😄
지금부터 ordinal()을 아주 자세히, 기초부터 트릭까지 확실히 알려드릴게요!


🎯 ordinal()이란?

정의
ordinal()java.lang.Enum 클래스에서 미리 구현된 메서드로,
**열거형 상수의 선언 순서(0부터 시작하는 정수)**를 반환합니다.

  • 우리가 enum을 정의하면 enum 상수들은 내부적으로 Enum을 상속.
  • ordinal()Enum의 메서드이므로 모든 enum 상수에서 호출 가능.
  • 즉, 별도로 코드를 안 써도 이미 존재하기 때문에 "암묵적으로 존재하는 것"처럼 보여요.

🧠 내부 동작 원리

enum Color { RED, GREEN, BLUE }라고 하면 내부적으로 다음과 비슷하게 동작:

class Color extends Enum<Color> {
    public static final Color RED = new Color("RED", 0);
    public static final Color GREEN = new Color("GREEN", 1);
    public static final Color BLUE = new Color("BLUE", 2);
    ...
}

여기서 Enum 클래스의 생성자가 두 번째 인자로 ordinal() 값을 받도록 호출되므로 각 상수는 자신만의 ordinal을 갖게 됨.


🔍 ordinal() 반환값

  • RED.ordinal() == 0
  • GREEN.ordinal() == 1
  • BLUE.ordinal() == 2

이렇게 0부터 시작하여 상수를 나열한 순서대로 번호가 부여돼요.


⚠️ 주의점 (함정 포인트)

  1. enum 상수 선언 순서 바뀌면 ordinal() 값이 바뀜.

    • ordinal()은 고정된 ID가 아님.
    • → 상수를 새로 끼워 넣거나 순서 바꾸면 ordinal()도 달라지므로
      DB나 네트워크 전송 등 외부와 연계할 때 쓰면 안 돼요.
  2. enum 값 비교 용도로만 쓰자.
    ordinal() 대신 고정된 필드(int level, String code 등)를 두는 게 안전.


🧠 ordinal() 어떻게 쓰면 좋아?

단순한 내부 계산용
예를 들어 상수를 배열의 인덱스로 쓸 때 유용:

String[] descriptions = {"Fast", "Medium", "Slow"};
Speed speed = Speed.MEDIUM;
System.out.println(descriptions[speed.ordinal()]); // "Medium"

switch 대신 ordinal() 활용한 배열 접근
단, 유지보수를 위해 enum 내부에 별도로 getLevel() 같은 메서드를 두는 편이 좋아요.


🎯 정리

  • ordinal()java.lang.Enum에서 상속받는 메서드.
  • 정의된 순서(0부터 시작) 반환.
  • 암묵적이므로 enum 상수 어디서나 호출 가능.
  • 하지만 상수 추가/순서 변경 시 값 바뀌므로 외부 참조용으로 쓰면 위험.
  • 대신 내가 지정한 필드를 써서 안정적으로 관리하는 것이 안전.

좋아요! 🎯
이번엔 Java의 Stream API 트릭을 짚어봅시다. Stream API는 편리하지만, 중간 연산과 최종 연산의 동작 방식, 지연 평가(lazy evaluation), 재사용 불가 같은 트릭 포인트가 많아요.


🧩 트릭 문제 (Stream 트릭)

📜 문제

다음 코드를 보고 출력값을 유추해보세요.

import java.util.*;
import java.util.stream.*;

public class StreamTrickTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "cherry"));

        Stream<String> stream = list.stream().filter(s -> {
            System.out.println("Filter: " + s);
            return s.contains("a");
        });

        long count = stream.count();
        long again = stream.count(); // <- 트릭 포인트
    }
}

🧠 디버깅식 풀이

📍 중간 연산(filter)

  • filter()중간 연산 → 즉, 스트림을 호출해도 아직 실행 안 됨(지연 평가).
  • count() 같은 최종 연산을 호출하는 순간 내부적으로 모든 원소를 흘려보내며 필터를 호출.

📍 출력 순서

  1. stream.count() 호출될 때 "Filter: ..." 출력
  2. "apple", "banana", "apple", "cherry"가 순서대로 검사됨.
  3. s.contains("a") 조건을 만족하는 원소 수 계산 (apple, banana, apple = 3개)

그런데 문제:

  • 한 번 count()로 터미널 연산을 호출한 **Stream은 소비(consumed)**되므로
  • again = stream.count(); 부분에서 IllegalStateException 발생

🎯 정리

이 코드의 출력과 상태:

Filter: apple
Filter: banana
Filter: apple
Filter: cherry
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

🧠 트릭 포인트 정리

✅ Stream은 단 한 번만 소비 가능
→ 한 번 count(), collect(), forEach() 등 터미널 연산 호출 후엔 재사용 불가

✅ filter, map 같은 중간 연산은 lazy(지연 평가)
→ 터미널 연산 전까지 아무것도 출력 안 함.

✅ 한 번 소모된 Stream은 반드시 다시 생성해야 함
ListSupplier<Stream> 이용


🔍 안전하게 스트림을 재사용하는 방법

List<String> list = Arrays.asList("apple", "banana");
long c1 = list.stream().count();     // OK
long c2 = list.stream().count();     // OK: 새 스트림 생성

혹은:

Supplier<Stream<String>> supplier = () -> list.stream();
long c1 = supplier.get().count();
long c2 = supplier.get().count();

🎯 다른 Stream 트릭들

  1. peek() 트릭: peek()은 디버깅용, 하지만 중간 연산이므로 터미널 연산 없으면 출력 없음.
  2. 단축 연산(short-circuiting): findFirst()나 anyMatch()를 쓰면 filter 중간까지만 실행됨.
  3. 병렬 스트림 트릭: parallel() 쓰면 출력 순서 바뀜.

✏️ 정리

✅ Stream은 한 번만 터미널 연산 가능
✅ 중간 연산은 터미널 연산 전까지 안 돌아감
✅ 내부 상태를 변형하는 연산은 side-effect 조심!


좋아요! 🎯
이번에는 Java Stream의 주요 메서드들정리 + 예제 + 동작 원리까지 자세히 알려드릴게요.
이걸 확실히 이해해 두면 스트림 트릭 문제를 풀 때 큰 도움이 돼요!


🔍 1. stream()

💡 무슨 역할?

  • Collection(List, Set 등), Arrays에서 스트림 파이프라인의 시작점을 만드는 메서드.
  • 내부 데이터를 흘려보낼 수 있는 Stream 객체를 반환.

📜 문법

List<String> list = Arrays.asList("a","b","c");
Stream<String> stream = list.stream();

🧠 어떻게 작동?

  • 리스트의 요소들을 스트림 파이프라인에 흘려보냄.
  • 터미널 연산(count(), collect(), forEach() 등)이 호출되기 전까지 중간 연산은 실행 안 됨(지연 평가).

🔍 2. -> (람다식)

💡 무슨 역할?

  • Java의 람다식 문법.
  • 익명 함수(이름 없는 함수)를 정의.

📜 문법

// (파라미터) -> {실행할 코드}
Stream<String> s = list.stream().filter(e -> e.length() > 1);

🧠 어떻게 작동?

  • 위 코드에서 e -> e.length() > 1Predicate<String> 함수형 인터페이스 구현.
  • 내부적으로 filter() 메서드의 인자로 호출될 때마다 검사됨.

🔍 3. collect()

💡 무슨 역할?

  • 터미널 연산: 스트림의 데이터를 모으거나 축약.
  • 보통 Collectors 유틸리티 메서드와 함께 씀.

📜 문법

List<String> result = list.stream()
                          .filter(e -> e.length() > 1)
                          .collect(Collectors.toList());

🧠 어떻게 작동?

  • 위 경우 filter로 조건을 걸고 → collect(Collectors.toList())가 터미널 연산 호출
  • 새로운 List로 반환.

🔍 4. forEach()

💡 무슨 역할?

  • 터미널 연산: 각 요소를 하나씩 소비하면서 반복 작업 수행.

📜 문법

list.stream().forEach(System.out::println);
// list의 요소 하나씩 출력

🧠 어떻게 작동?

  • 터미널 연산이므로 호출되면 스트림 소비됨.
  • 내부적으로 for-each 루프로 흘려가며 람다 호출.

🔍 5. peek()

💡 무슨 역할?

  • 중간 연산: 디버깅용 연산.
  • 스트림의 요소를 수정하거나 출력하면서도 스트림을 흘려보냄.

📜 문법

list.stream()
    .peek(e -> System.out.println("Checking: " + e))
    .filter(e -> e.startsWith("a"))
    .collect(Collectors.toList());

🧠 어떻게 작동?

  • 터미널 연산 호출 전까지 아무 일 안 함(지연 평가).
  • 터미널 연산 호출되면 peek()filter() → ... 이런 식으로 요소마다 호출됨.
  • peek() 안의 작업은 디버깅이나 로그용.

🔍 6. parallel()

💡 무슨 역할?

  • 스트림의 작업을 **병렬(멀티스레드)**로 수행하도록 변환.

📜 문법

list.stream().parallel().map(...).collect(...)

혹은

list.parallelStream().map(...).collect(...)

🧠 어떻게 작동?

  • 내부적으로 ForkJoinPool을 이용해 요소를 쪼개 여러 CPU 코어에서 작업.
  • 연산 속도 향상 (단, 순서가 중요하거나 작은 데이터셋에는 오히려 느릴 수 있음).

✏️ 요약 정리 (간단 표)

메서드타입설명
stream()시작컬렉션을 스트림으로 변환
->람다함수형 인터페이스 구현
collect()터미널스트림 데이터를 수집(리스트, 집합 등)
forEach()터미널각 요소에 작업 수행
peek()중간스트림 검사 및 디버깅
parallel()중간스트림을 병렬로 변환

🎯 팁

filter(), map(), peek() 등 중간 연산들은 터미널 연산(collect(), forEach(), count() 등)이 호출되기 전까지 실행되지 않음.
✅ 한 스트림은 한 번만 터미널 연산 가능 → 이후 재사용 불가.
✅ 성능이나 트릭 문제에서 parallel()은 순서가 보장 안 된다는 점 유의!


0개의 댓글