Java String I/O

정윤수·2023년 6월 21일
2

자바

목록 보기
1/1

목차

  1. Java의 인코딩
  2. Stream
  3. Buffer
  4. Scanner

Java의 인코딩

한 잼민이가 나한테 왔다.

잼민이: Java 어플리케이션을 통해서 제가 입력한 문자열을 출력하고 싶어요 !

나: 아 그래? 그럼 아래처럼 Java언어로 적어봐

br을 이용한 입출력

Scanner을 이용한 입출력

잼민이의 코드를 유심히 보다가 문득 이런 생각이 들었다. 그런데.. Java애플리케이션이 이렇게 Java언어로 작성된 코드를 어떻게 처리 할 수 있지??

컴퓨터 이진코드

  • 모두 알다시피 컴퓨터는 0과 1의 이진코드로 이루어져있다.
  • 그럼 Java언어로 작성된 문자열을 컴퓨터에게 어떻게 이해시킬까??

=> 과거 개발자분들은 아래와 같이 생각했다.

천상계 개발자: 특정한 형식을 만들어서 사용하면 되겠군!

그렇게 탄생한 것이 바로 "인코딩"이다.

인코딩 개념

: 우리가 쓴 글자나 문장을 컴퓨터가 이해(0,1)할 수 있는 코드로 변환

인코딩 방식

  • 컴퓨터가 이해할 수 있도록 컴퓨터가 보는 "인코딩을 위한 표"이다.
  • 인코딩 종류는 아래와 같다.
  • 세 인코딩 방식은 1~127번까지 1:1 매핑되는 10진수 값이 동일하다. 따라서, 아래부터 이해를 돕기 위해 우리에게 익숙한 'ASCII코드'라는 단어를 선택해서 사용하겠다.
    참고) ASCII코드 값이란 파일 인코딩 형식의 아스키코드 값이 10진수로 나오는 것이다.

인코딩 전체 흐름도 in Java Application

  • 전체적인 흐름도만 보기위해 위와 같이 소개한다.
    참고) 후에 자세히 설명을 한다.
  1. 사용자의 입력(Hello)이 나의 노트북으로 UTF-8 형식의 인코딩을 하여 들어온다.
  2. 해당 데이터는 나의 노트북 내 Java Application내로 보내기 위해 UTF-16 형식의 인코딩을 하여 전송된다.
  3. Java Application(S/W)으로 전송되기 위해 Java encoding UTF-16 be 형식의 인코딩하여 전송된다.
  4. Java Application 내 Memory에는 0,1의 이진형태로 저장이 되기 때문에 UTF-16 이진코드 규칙을 지켜 데이터가 저장이 된다.

Stream

  • 사용자의 입력으로부터 화면에 출력되는 과정을 정말 간단하게 보자.
  • Java언어로 작성된 사용자의 입력 데이터는 키보드를 통해 작성되고, 모니터를 통해 출력이 된다.

Stream 개념

: 데이터를 운반하기 위해 사용되는 연결 통로

  • Java 언어로 작성된 코드를 화면에 출력하기 위해 Java Application은 이의 가운데에 놓여서 생각해보자.
    • 입력 데이터를 받는 가장 기본적인 통로: InputStream
    • 출력 데이터가 전송되는 가장 기본적인 통로: OutputStream

Input / Output Stream 특징
1. 단방향 데이터 흐름
2. 1 byte 단위의 데이터가 전송되는 곳

그럼 이제 이런 궁금증이 생길 것이다.
"그럼 Java 애플리케이션이 Java로 작성된 사용자 입력 데이터를 어떻게 통로로부터 수신도 하고, 전송도 시키는 거지??"

표준 입출력

  • 이를 위해 Java Application은 3명의 보디가드를 고용했다.
    • System.in
    • System.out
    • System.err

  • 이 표준 System.in도 내부적으로 InputStream을 통해 들어온다. 즉, 1 byte 단위로 console로부터의 데이터를 다루는 것이다.

그럼 이를 사용해서 사용자의 데이터에 대한 입력과 출력에 대한 간단한 예시를 작성해보자

  • 사용자 정보를 ABC라고 쳤을 때, 이를 그대로 받고 출력하면 65라는 십진수가 나온다.
  • 이것이 A(1 byte)에 대한 아스키코드 표에서 맵핑되는 십진수 값이다.

그럼 왜 십진수 값이 나올까??

.read()

  • 자세히 보면 InputStream.read()를 통해 1 byte 데이터를 받아서 int형으로 widening primitive conversion를 하고 있다.
  • InputStream이라는 통로의 끝에 도달하는 걸 알고 그를 -1(int형, 10진수)로 반환받아 알기 위해 즉, Java 시스템은 InputStream에 대한 안정성을 보장하기 위해 이렇게 지정하였다.

InputStream / OutputStream

  • 그럼 이제 위의 그림을 이해하기 쉬울 것이다.
  • 단지 표준입출력을 Stream을 통해 데이터가 일정한 형식(1 byte단위, 단방향 보장, 내부적으로 쓰레드 동시성을 보장) 을 지키면서 이동시키도록 하는 것이다.
    => 이로써 65(십진수)가 아닌 H라는 문자가 출력이 될 수 있는 것이다.

한계점

  • 만약 위와 같이 InputStream(1 byte단위)을 통해 한국어를 받고, char형으로 변환하면 우리는 보통 '안'이 출력되는 것을 기대한다.

  • 그런데, 오른쪽 결과를 보면, 중국어도 일본어도 아닌 이상한 라틴어 형태의 i가 출력되는 것을 확인할 수 있다.

왜 이런 일이 일어나는 걸까???

입력 데이터의 전체적인 흐름

  1. 사용자가 "안녕"이라는 문자열을 입력한다.

  2. 컴퓨터는 이를 이해하기 위해 UTF-8 인코딩 테이블을 본다.

    • 참고로 이는 ASCII 코드 표를 확인하는 것이 아니다. 왜냐하면, 아스키 코드 값으로는 숫자나 영어 문자, 특수 문자만을 확인할 수 있다. (ANSI 코드 표로도 확인이 힘들다.)
  3. '안'에 1:1 매핑되는 값을 확인해보니 3 byte로 구성된 236, 149, 128로 되어있다. 그럼 InputStreamReader를 통한 .read()는 이를 통해서는, 1 byte만 읽을 수 있기 때문에 236만이 읽힌다.

    • 참고) 남은 2 byte가 아직 Stream에 남아있는 것이다.
    • 만약, '안'을 완벽하게 다 읽으면 3 byte로써, 이는 표에서 확인할 수 있듯이 C548로 표현된다.
  4. .read()를 통해서 들어온 데이터가 Java Application 내에서는 UTF-16을 쓰기 때문에 236은 EA로 인코딩 된다.

  5. 이는 다시 Java encoding UTF- 16 be에 따라, E: 0xc3 / C: 0xac 로 변환이 되고, 이를 문자로 표현을 하면 라틴어 형태의 단어(i)가 된다.

  1. 이 EA는 Java Memory에 저장되는데, 이에 저장되기 위해 내부적으로 UNICODE 인코딩 형식에 의해 이진형태의 2 byte로 저장이 된다.
    • E: 0xc3 / C: 0xac -> 11000011 10101100

간단히 말하자면, '안' ( 236, 149, 128 ) 모두 읽은, C548이 자바 애플리케이션으로 전달되는 것이 아니라, 메모리에 1 byte만(엉뚱한 데이터)가 저장이 되기 때문에 라틴어 형태의 i가 나오는 것이다.

이는 동시에, Java에서 한 문자(char)는 2byte인데 반면, 바이트 기반 Stream으로 처리하는 것에 한계점이 발생했다. 그럼 어떻게 이를 해결할 수 있을까??

문자 기반 Stream

  • 그를 해결하기 위해 탄생한 것이 바로 문자 기반 Stream인 Reader와 Writer이다.

문자 기반 Stream 특징
1. 단방향 데이터 흐름
2. 2 byte 단위 데이터

그런데, 우리는 현재 함정에 걸렸다!

  • 아래 코드를 보면, Reader는 내부적으로 추상 클래스 이기 때문에 System.in을 항상 표준 입력으로 받아야 한다.

  • Java의 표준 입출력은 System.in, System.out, System.err는 1 byte 형식의 데이터를 다룬다.

이를 어떻게 해결할 수 있을까 후..

InputStreamReader

  • 그런 긴 고민 끝에 나온 것이 바로 우리에게 익숙한 InputStreamReader이다.
  • 이를 통해 한글로 된 데이터를 1 byte가 아닌, 2 byte형태로 받아서 출력한다.

한글은 3 byte인데?? 라는 질문을 받았다. 이에 대해 더 자세히 찾아보니 아래와 같은 내용을 추가적으로 작성했다.

  • 이를 해결하기 위해 Java 내 InputStreamReader는 내부적으로 Charset Class를 둠으로써, 3 byte 데이터를 2 byte 데이터로 자동 변환을 해줌으로써 Reader를 통해 한글로 된 데이터가 알맞게 이동될 수 있게끔 작동한다.

여전히 한계점

  • 아무리 InputStreamReader를 쓴다고 해도, 지금까지의 콘솔 출력은 한 글자씩만 처리가 된다.

아래에서 지금까지 어떤 방법을 쓰고 있고, 이를 위한 해결점이 무엇일지 확인해보도록 하자.

지금까지의 방법 - 하나씩 하나씩..

  • 현재 작동하는 방식은 2byte를 하나씩 시작점(입력)-도착점(Java Application)까지 이동시키는 것과 같다고 상상할 수 있다.

해결 방법 방법 - 한 번에!

  • 그런데, 문자열을 출력시키기 위해 굳이 그렇게 해야할까??
  • 그렇게 해서 입력받을 때 한번에 받고 출력할 때 한번에 출력함으로써 보다 많은 데이터를 한번에 처리할 수 있게 하였다. 이 때 사용되는 것이 바로 Buffer라는 개념이다.

Buffer

: Stream을 위해 데이터를 저장해두는 임시공간

  • Stream으로 들어오는 데이터를 Java Application 내 Buffer라는 공간을 두어, 이에 데이터를 차근차근 쌓음으로써, 한 번에 입력받고 한 번에 출력 시킬 수 있다.
  • Buffer는 Array형태로 이루어져 있으며, FIFO 구조로 먼저 들어온 것이 먼저 나가는 성질을 지니고 있다.

Stream 정리

  • 이제 지금까지 설명한 Stream에 대한 내용들을 정리 해보겠다.
  • console에 입력되는 데이터를 받고 이를 console로 출력하기 위해 받기 위해 표준 입출력을 사용
  • 표준 입출력으로 데이터(1 byte)를 받아, 이를 문자형태로 변환하기 위해 InputStreamReader를 사용
  • 문자들을 한 번에 모으고, 또 출력하기 위해 Java Application의 내부적으로 Buffer라는 공간을 사용하여, 문자를 문자열 형태로 입력 또는 출력이 가능

Scanner

: br과 양대산맥인 Scanner에 대해서도 잠깐 알아보자

  • 놀랍게도 Scanner는 생성자로써 InputStreamReader를 사용하고 있는 걸 확인할 수 있다!
    * 1 byte 단위로 데이터가 단방향으로 이동

  • WHITESPCAE_PATTERN이라는 정규표현식 자체를 static 변수(=class 변수)를 두어 대부분의 Scanner에서 사용하고 있는 걸 확인할 수 있다.

Buffer 사용

  • 또한, Scanner에서도 Buffer라는 걸 기본적으로 사용하고 있는 걸 확인할 수 있는데, Buffer의 데이터를 스페이스 바 단위로 token을 나누어서 사용하고 있다. 이 과정 또한 정규표현식이 사용됨을 알 수 있다.

그럼 Scanner는 언제 쓸까??

Scanner의 복잡한 연산 처리

  • 아래는 Scanner 클래스의 .nextInt()에서 사용중인 메서드이다.
  • 보기만 해도 많고, 또 복잡한 정규 표현식이 내부적으로 쓰이는 것을 확인할 수 있다. 이로써, br과 달리 복잡한 연산을 더 명시적으로 구분 짓고 효율적으로 처리할 수는 있지만, 정규표현식을 이렇게 많이 쓰면 CPU 처리 속도가 느려질 수 있다.
    * br과 Scanner의 속도 차이는 대략 10배 정도라고 한다

br과 Scanner 차이점

  • 해당 내용을 끝으로 설명을 마치고자 한다.

1. 정규 표현식 사용

2. Buffer 크기

  • 8배 차이 나는 걸 확인할 수 있듯이 br은 Java 애플리케이션을 통틀어 단순히 문자열을 읽고 반환하기 위한 기본적인 Stream의 역할을 제공해주기 위해 그에 걸맞는 Buffer 크기를 가지고 있다고 생각한다.

감사합니다.

참고
https://www.javatpoint.com/java-bufferedreader-class
https://stlee321.tistory.com/entry/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C%EB%A5%BC-UTF-8%EB%A1%9C-%EC%9D%B8%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=76083001&partner=googlek&BSCPN=ORM&BSPRG=ADWORDS&BSCCN1=89221&utm_source=ADWORDS&utm_medium=cpc&utm_term=JAVA%C0%C7%C1%A4%BC%AE&gad=1&gclid=CjwKCAjwv8qkBhAnEiwAkY-ahtQREnpP7bAIlvb0KKL6mB33QK0VUMo_5GWCG7bh6IFHrknqThHCohoC4acQAvD_BwE
https://hyeunjae.tistory.com/78
https://reinvestment.tistory.com/44
https://jeongdowon.medium.com/unicode%EC%99%80-utf-8-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-b6aa3f7edf96
https://jjeong.tistory.com/696
https://daniiieee.tistory.com/4
https://velog.io/@seungje89/Java-Scanner-InputStream-InputStreamReader-BufferedReader-%EA%B0%80%EB%B3%8D%EA%B2%8C-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0

profile
https://www.youtube.com/watch?v=whoDs0KRc7k&t=1s

2개의 댓글

comment-user-thumbnail
2023년 7월 11일

역시 infj스럽게 글에서 소소한 웃음 포인트가 돋보이는군요... 👍🏻

1개의 답글