cs/Java

[Java] System.in은 한 번만 선언해야 할까?

icodesiuuuu 2025. 4. 29. 21:25

1. Java에서 데이터를 입력받을 때 사용하는 'System.in' 이게 뭘까?

 

System.in은 "표준 입력 스트림"을 의미

  • "표준 입력"이란?
    • 보통 키보드에서 사용자가 입력하는 데이터
  • "스트림(Stream)"이란?
    • 데이터가 한 방향으로 흘러가는 통로를 의미

즉, System.in은 키보드로 입력한 데이터를 '한 방향'으로 읽을 수 있게 하는 통로

 

🧠 쉽게 비유해보자

System.in = 수도관에 흐르는 물줄기

  • 물줄기는 계속 한 방향으로 흐른다. (거슬러 올라갈 수 없음)
  • 한 번 지나간 물은 다시 읽을 수 없다.
  • 물줄기에 컵(BufferedReader, Scanner 등)을 대서 물(데이터)을 받아오는 것.

 

📚 System.in의 구조

  • 타입: InputStream
  • 데이터 단위: Byte(바이트)
  • 방향: 읽기 전용(Forward Only)
InputStream input = System.in;

 

  • System.in은 데이터를 바이트 단위로 읽기 때문에, 사람이 읽기 좋은 형태(문자열)로 만들려면 추가 작업이 필요

그래서 보통은 이렇게 사용함

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

 

 

2. System.in 선언과 데이터 읽기 타이밍

🚨 중요한 오해 바로잡기

System.in을 선언하거나 BufferedReader를 만든 순간에는 입력 데이터를 읽지 않음

  • System.in이나 BufferedReader를 "선언"하는 건 "입력 스트림을 사용할 준비만 하는 것"
  • 실제로 입력 데이터가 프로그램 안으로 들어오는 시점은 read(), readLine() 같은 메서드를 호출할 때

수도꼭지 앞에 컵을 들고 있는 것과 같다.
(아직 물은 안 받음 → readLine()을 해야 물을 받는다.)

 

📋 흐름 정리

(1) System.in 생성 → 스트림 준비만 함
(2) BufferedReader 생성 → System.in을 감쌈 (여전히 읽지 않음)
(3) readLine() 호출 → 이때 진짜로 데이터 읽음

 

 

🤔 구조적으로는 어떤 흐름일까?

[키보드 입력] → [운영체제(OS) 메모리 버퍼] → [System.in 내부 버퍼] → [BufferedReader.readLine()]

 

  • 사용자가 키보드로 입력
  • 운영체제가 입력을 메모리에 저장
  • System.in이 그 메모리 버퍼로부터 데이터를 가져옴
  • BufferedReader가 System.in을 통해 데이터를 읽고, 한 줄 단위로 반환

 

🎯 디테일

  • System.in은 프로그램 시작할 때 딱 하나만 생성된다.
  • 새로 BufferedReader를 만들더라도, System.in은 변하지 않는다.
  • **커서(cursor)**가 한 번 이동하면,
    이미 읽은 데이터는 다시 읽을 수 없다.
  • 남은 데이터가 없으면,
    • readLine()은 null을 반환하고,
    • read()는 -1을 반환한다.

 

3. System.in은 한 번만 사용해야 하는가?

 결론부터 말하면 경우에 따라서는 System.in을 여러 번 감싸서 사용해도 (예: BufferedReader 여러 번 만들기) 시스템이 정상 동작할 수 있다. 하지만 System.in은 한 번만 감싸서 사용하는 것이 가장 안전하고 좋은 습관이다.

 

1. 경우에 따라 System.in을 여러 번 사용해도 "정상 동작"할 수 있다

  • 예를 들어, 남은 데이터가 충분히 있을 때
  • 또는 스트림 커서가 이동했더라도 사용자가 새 입력을 넣어줄 때
  • 프로그램은 멈추지 않고 정상적으로 readLine(), read()를 이어서 수행할 수 있다.

남은 입력이 있거나 새 입력이 들어오면 새로 만든 BufferedReader도 정상적으로 작동할 수 있다.

 

 

 

2. 하지만 "원칙적으로는" 한 번만 사용하는 것이 맞다

  • System.in은 스트림 하나로만 살아있는 구조다.
  • 여러 객체(BufferedReader, Scanner 등)를 만들어도
  • 다 같은 System.in을 공유한다.
  • 스트림은 읽을 때마다 커서가 이동하고,
    다시 돌아갈 수 없기 때문에
  • 여러 번 선언하면 어디까지 읽었는지 관리가 어려워진다.
  • 결국 프로그램이 커서 관리에 실패하면
  • 예상치 못한 null, -1, blocking(입력 대기) 문제가 터질 수 있다.

 

3. 또한 여러번 읽기 객체(BufferedReader 등)를 만들고 동시에 사용하면 버그가 발생할 수 있다.

  • 중복 읽기, 누락, null 반환, blocking 등의 문제

 

예시 코드로 알아 보자


임의로 작성한 코드와 테스트케이스

import java.io.*;
import java.util.*;

public class Main {
    static int N, M;
    static HashSet<String> set = new HashSet<>();
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        init();

        for (int i = 0; i < M; i++) {
            String[] arr = br.readLine().split(",");
            countWord(arr);
        }
    }

    public static void countWord(String[] arr) {
        for(String s : arr) set.remove(s);
        System.out.println(set.size());
    }

    public static void init() throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        for (int i = 0; i < N; i++) set.add(br.readLine());
    }
}

 

5 2
map
set
dijkstra
floyd
os
map,dijkstra
map,floyd

 

 

🔹 STEP 0: 프로그램 시작

  • OS는 대기 중, 입력을 사용자에게 받음
  • 사용자가 전체 입력을 한꺼번에 붙여넣기
  • OS는 입력을 한 줄씩 stdin 버퍼에 저장
  • 자바는 System.in을 통해 이 스트림을 바라봄
[OS stdin 버퍼]
→ 5 2\n
→ map\n
→ set\n
→ dijkstra\n
→ floyd\n
→ os\n
→ map,dijkstra\n
→ map,floyd\n

 

 

🔹 STEP 1: main 함수에서 BufferedReader br1 생성

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  • System.in은 여전히 같은 stdin
  • br1이 생성될 때 내부에 char[] buffer를 가짐 (기본 8192자)
  • 이 시점에는 아직 readLine() 호출 안 했기 때문에 아무 것도 읽지 않음

 

🔹 STEP 2: init() 호출 → BufferedReader br2 생성

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  • 새로운 BufferedReader (br2)가 만들어짐
  • 하지만 System.in은 여전히 같은 스트림
  • 이제 br2.readLine()을 호출하면서 문제가 시작됨

 

🔹 STEP 3: init()에서 br2.readLine() 호출

StringTokenizer st = new StringTokenizer(br.readLine()); // br2
  • 이때 BufferedReader.readLine()은 내부적으로
    • System.in.read()를 호출해서
    • OS stdin에서 여러 바이트를 한꺼번에 읽고
    • char[] buffer에 저장함
  • 문제는 BufferedReader는 보통 8192자까지 미리 읽는다는 것
  • 그래서 이 시점에 br2는 "5 2"뿐만 아니라
    "map\nset\ndijkstra\n..."까지 미리 읽어버릴 수 있음

 

🔹 STEP 4: init()에서 N개 키워드 읽기

for (int i = 0; i < N; i++) set.add(br.readLine());
  • br2는 이미 대부분의 데이터를 내부 버퍼에 담았기 때문에,
  • "map" ~ "os"까지 한 줄씩 소비
  • 남은 줄은:
map,dijkstra\n map,floyd\n

→ 아직 System.in에는 존재할 수 있지만, 버퍼 밖에 있음

 

🔹 STEP 5: main 함수로 돌아감 → br1.readLine() 호출

String[] arr = br.readLine().split(","); // br1
  • 문제는 br1은 이제 막 첫 readLine()을 호출하는데,
  • System.in은 이미 상당량을 br2가 읽어가버렸음
  • br1의 내부 버퍼는 비어 있음
  • 남은 입력이 없거나, 남아 있더라도 System.in.read()가 끝에 도달하면 → null 또는 빈 문자열 반환

'cs > Java' 카테고리의 다른 글

[Java] try-finally보단 try-with-resources?  (0) 2025.05.09