티스토리 뷰

자바에서 입력을 위해 제공되는 클래스들은 매우 다양합니다. 여러 가지 종류의 입력 클래스가 필요한 이유는 먼저 데이터의 종류에서 출발합니다. 스트림이란 데이터의 연속인데 이것이 문자의 연속이냐 바이트의 연속이냐에 따라 나누어집니다. 대표적인 입력 소스인 파일은 문자 단위의 데이터를 가지는 (그래서 메모장으로 읽을 수 있는) 텍스트 파일이 있고 바이트 단위의 이진 데이터를 가지는 바이너리 파일이 있습니다. 바이너리 파일은 그것을 이해하고 처리할 수 있는 프로그램이 있어야 읽을 수 있습니다. 예를 들어 jpg 파일이나 .doc 파일은 해당 어플리케이션이 있어야 읽어서 내용을 보여줄 수 있습니다. 즉 메모장으로 열 수 없는 파일이죠? 

그러므로 자바에서는 이 두 가지 입력 방식을 InputStream과 Reader라는 다른 클래스에서 상속하는 클래스들을 통해 나타냅니다. 입출력에 관련된 클래스 계층구조는 이 페이지를 참고하기 바랍니다.

예를 들어 정수 123를 프로그램이 입력받을 때 우리는 두 가지 타입의 입력에 대해 다른 방식으로 정수 값을 읽어들이게 됩니다. 우선 텍스트 파일이라면 '1', '2', '8'을 각각 문자(ASCII 코드로 각 한 바이트)로 읽어와서 "128"이라는 스트링으로 보고 십진수 문자열에서 정수값으로 바꾸는 계산을 통해 이진수로 바꾸어 변수(메모리)에 저장하게 됩니다. 반면 바이트스트림으로 입력한다는 것은 정수 값이 이진수로 파일이나 네트워크를 통해 흘러들어오는 경우로 4바이트가 이진수라면 약속에 따라 4개의 바이트를 그대로 정수값에 해당하는 메모리에 저장하면 됩니다. 정수 128은 메모리에 [00000000] [00000000][00000000][10000000]와 같이 저장됩니다.

문자 스트림 [00000001] [00000010] [00001000] '1' '2' '8' "128"을 십진수로 파싱하여 128을 얻고 그것을 이진수로 저장한다.
바이트스트림 [00000000] [00000000][00000000][10000000] 128 (정수) 4바이트를 그대로 메모리에 저장

자리수가 커지는 큰 숫자라면 문자 스트림보다는 바이트 스트림이 훨씬 적은 데이터 양을 가지겠죠? 그리고 스트링을 파싱해서 숫자로 바꾸는 것도 많은 계산을 요하므로 효율성의 측면에서는 바이트스트림이 더 좋습니다. 문자 스트림의 장점이라면 사람이 읽을 수 있는 데이터라는 것이겠죠? 그것 말고도 이진 데이터는 다루기 어렵고 오류가 일어나기도 매우 쉽습니다. 즉 기본적인 신호나 데이터는 0, 1의 이진 데이터인데 그것을 문자 스트림으로 바꾸면 좀더 다루기 쉽고 사람이 이해하기도 쉬워진다고 정리할 수 있습니다.

키보드로부터 입력을 받을 때는 키값이 바이트(이진데이터 신호)의 연속으로 프로그램에 전달됩니다. (System.in은 InputStream 타입의 객체다) 이것이 프로그램에게는 영문자 알파벳이거나 숫자거나 기호로 해석되어야 할 것입니다. 그리고 txt 파일로부터 데이터를 읽을 때도 입력스트림 클래스는 바이트 단위로, Reader라면 문자 단위로 입력을 받게 됩니다. 그렇지만 mp3 파일이나 jpg 파일처럼 그 자체로 이진 데이터를 인코딩하여 가지고 있는 경우는 반드시 바이트 단위로 읽어서 프로그램 안으로 가지고 들어와야 합니다. 이렇게 입력을 문자 단위로 읽느냐(문자 스트림, Reader 클래스) 또는 바이트 단위로 읽느냐(바이트 스트림, Stream 클래스)로 일단 나누어지고 그 두 개의 슈퍼클래스에서 출발해서 여러 가지 하위클래스가 생겨납니다.

먼저 InputStream 클래스를 살펴봅시다. 이것은 기본적으로 다음 세 개의 메소드를 가진 추상 클래스입니다.

  • abstract int read();
  • int read(byte[] b);
  • int read(byte[] b, int off, int len);

이들은 현재 위치에서 한 바이트 또는 여러 개의 바이트를 읽어들여 돌려주는 메소드들입니다. 하위 클래스는 어떤 스트림을 입력으로 이용할 것인가에 따라 달라지는데 주어진 바이트 배열(ByteArrayInputStream), 파일(FileInputStream), 스트링(StringBufferInputStream), 직렬화된 객체 데이터의 연속(ObjectInputStream) 등 여러 가지 입력 소스에 대해 이 read 메소드들을 오버라이드하여 바이트를 읽어들이는 것을 구현합니다. FilterInputStream은 다른 InputStream을 받아 그것을 내부 입력 스트림(필드 in)으로 저장하고 이것에 대해 입력 기능을 제공하는 클래스들을 나타내게 됩니다.

다음으로 문자의 연속을 입력하는 Reader 클래스를 살펴봅시다. 이것도 InputStream과 마찬가지로 read 메소드를 가지는데, 대신 이것은 byte[]이 아니라 char[]에 입력하여 돌려주는 메소드들이겠죠? 입력을 문자로 보고 미리 정해진 인코딩 방식에 따라 해석하여 그에 맞는 char 타입의 데이터를 돌려주게 되는 것이죠. 

예를 들어 InputStreamReader는 InputStream을 입력으로 받아 생성되는데 연속된 바이트 스트림을 정해진 인코딩 방식에 따라 문자(char) 타입으로 해석한다. 예를 들어 인코딩 방식이 UTF-8이라면 영문자나 숫자는 하나의 바이트로 나타내지고 한글은 여러 개의 바이트로 표시하게 됩니다. 이 경우 한 바이트를 읽어 몇 개의 바이트가 하나의 문자를 만드는지 확인하고 멀티 바이트 문자의 경우는 연속된 바이트를 읽어 하나의 문자를 돌려주는 역할을 합니다. 

이상의 입력 클래스들을 이용해서 파일을 읽어들이는 방법은 여러 가지가 있습니다. File 클래스는 파일의 경로와 이름을 받아 해당 파일을 디렉토리에서 찾을 수 있게 해주는 역할을 합니다.

File f = new File("input.txt");

그럼 이 파일을 이용해 스트림이나 리더를 생성할 수 있겠지요?

InputStream is = new FileInputStream(f);
Reader r = new FileReader(f);

그런데 InputStream이나 Reader 클래스는 사용하기가 상당히 까다롭습니다. InputStream 클래스는 바이트 단위의 이진 데이터를 다루므로 메모리 상의 데이터의 표현을 모두 이해해야 하고 그것이 스트림에 어떻게 저장되어 있는지에 대해 정확히 알아야 사용할 수 있겠지요?. Reader 클래스는 문자 스트림이므로 그것보다는 쉽지만 위에서 본 것처럼 Reader 클래스에서 정수를 읽으려면 '1', '2', '8'을 읽는 것 뿐 아니라 '8'에서 숫자가 끝났다는 것도 알아야 합니다. 즉 토큰을 어디서 끝내야 할지를 알아야 하는 것이죠. 프로그램에서 원하는 타입에 따라 정수, 실수, 스트링 등을 읽으려면 한 글자씩 읽으면서 토큰의 끝을 판별하는 루프를 실행해야 하는데 이것은 난이도도 높을 뿐 아니라 프로그램의 길이와 복잡도가 상당히 길어집니다.

다행히도 이러한 일을 담당해 주는 것이 스캐너(Scanner) 클래스입니다. 즉 초보 프로그래머가 텍스트 형태의 입력을 읽어서 정수나 스트링 같은 데이터를 처리하고자 한다면 스캐너를 이용하는 것이 가장 합리적인 방법입니다. 키보드 입력이나 파일 입력은 대부분의 경우 텍스트 형태의 데이터입니다. 그러므로 Scanner 클래스의 사용법을 잘 이해하는 것이 중요합니다.

스캐너 클래스의 객체 생성은 위의 여러 가지 형태를 이용할 수 있습니다.

(1) Scanner s1 = new Scanner(new FileInputStream("input.txt"));
(2) Scanner s2 = new Scanner(new FileReader("input.txt"));
(3) Scanner s3 = new Scanner(new File("input.txt"));

즉 스캐너는 입력스트림이나 리더 클래스를 이용해서 생성할 수 있으며, 파일 객체를 이용해서 바로 생성할 수도 있습니다. 위의 두 방법은 파일이름으로 파일 객체를 생성하고 그것을 오픈해서 스트림 또는 리더 객체를 생성한 후 그것으로부터 스캐너가 생성된다. 세 번째 방법은 파일에서 바로 스캐너를 생성하는 방법입니다. 파일이 경로와 해당 이름의 파일을 찾는 역할을 해주고 그 파일을 이용해서 스캐너를 생성하는데 이 때 FileNotFoundException이 발생할 수 있고 그것은 checked exception이어서 반드시 캐치되어야 하는 예외입니다. 이것에 대해서는 파일입력 첫번째 포스트에서 다루었으니 참고해 주세요.

Scanner 객체를 생성하였다면 이제 거기서 숫자나 스트링을 읽는 것은 이제까지 여러번 살펴보았으니 생략하겠습니다. 스트림이나 리더 클래스를 사용하려고 하는 학생들이 꽤 있어서 이 내용을 여기서 다시 정리해 보았습니다.

그럼 언제 스트림이나 리더 클래스를 이용해서 프로그램을 짜게 될까요? 스캐너가 해주는 것처럼 정수나 스트링 같은 데이터를 하나씩 읽어오고 싶다면 스캐너를 쓰는 것이 바람직합니다. 스트림 클래스를 이용하게 되는 대표적인 경우는 암호화/복호화 같은 바이트 스트림을 사용하게 되는 응용 분야나 객체의 직렬화를 할 때 많이 사용합니다. 객체 직렬화에 대해서는 이 글에서 잘 소개하고 있으니 참고하기 바랍니다. 객체 직렬화는 상당히 어려운 주제이고 그 자체로 까다로운 기술이므로 관심있는 사람은 각자 더 공부하기 바랍니다.

다음으로 한글 인코딩 이슈에 대해 살펴볼께요. 파일 입력의 가장 큰 난관이 한글 인코딩인데, 이것이 사실 내용을 이해하기도 어렵고 실제 사용하기도 매우 복잡해서 쉽지 않은 주제죠. 자바 개발에서 한글을 다루는 것이 어려워지는 대표적인 이유는 이클립스 같은 IDE 개발환경이 이를 잘 지원해 주지 못하기 때문입니다. 윈도우 환경은 ANSI, EUC-KR 또는 MS949라고 불리는 한글 인코딩 방식을 사용하고 웹표준에서는 UTF-8 방식을 사용합니다. 이 두 가지 주요 인코딩 방식의 차이 때문에 파일이 읽히지도 않고 한글도 깨지는 현상이 나타나게 됩니다. 그런가하면 자바 언어는 유니코드를 이용한다고도 하죠? 왜 이렇게 복잡한 것일까요?

한글 인코딩 방식은 70년대로 거슬로 올라가는 오랜 역사를 가집니다. 처음 한글을 컴퓨터에 도입하기 위해 조합형과 완성형이라고 하는 두 가지 방식이 있었습니다. 조합형은 자음, 모음 음소 단위로 한 바이트를 할당하여 여러 바이트가 모여서 한글 모아쓰기처럼 한글을 나타내는 방식이고 완성형은 모아쓰기된 글자를 하나의 문자로 보고 두 바이트로 이것을 나타내는 방식이었습니다.

이것이 윈도우 환경에서 사용하던 ASCII 코드와 결합되면서 한 바이트로 한글의 음소를 표현하는 것도 어려운 일이 되었고 그래서 완성형을 도입하여 CP949 표준을 통해 한글을 표현하게 되었습니다. 한글 언어팩이 설치된 윈도우 환경에서 사용되는 방식이죠.

한편 자바 진영을 중심으로 유니코드 2바이트로 세상의 모든 문자를 표현하려는 시도가 있었습니다. 유니코드는 세상의 모든 문자에 대한 코드 체계입니다. UTF-8은 이러한 유니코드를 표현하는 인코딩 방식입니다. 유니코드를 그대로 사용해서 파일이나 데이터를 저장한다면 기존의 ASCII 코드로 작성된 데이터들이 모두 깨질 뿐 아니라 기존에 한 바이트로 표현되던 문자들이 모두 2바이트가 되어야 하므로 저장장소 요구양이 2배가 되는 결과가 초래됩니다. 그래서 유니코드를 좀더 효율적인 방식으로 표현하는 인코딩 기법이 도입되었고 이것이 UTF-8이라고 할 수 있습니다. UTF-8에서는 기존 ASCII 코드는 한 바이트로 표현되고 나머지 글자들은 여러 바이트를 사용하게 됩니다. 한글의 경우는 조합형으로 사용되는데, 이것이 애플 계열의 컴퓨터에서 윈도우즈 환경으로 옮길때 한글이 풀어쓰기 되는 이유입니다. 결과적으로 각 방식에서 한글을 나타내는 방법은 다음과 같습니다.

MS949 방식
EUC-KR 또는 ANSI
영문자와 숫자는 ASCII로 한 바이트로 표현
한글은 완성형의 확장판인 CP949 인코딩을 사용
한글 언어팩이 설치된 윈도우 환경에서 사용됨
유니코드 2바이트로 할당된 한글 2만자를 표현하는 코드 모든 초성, 중성, 종성의 결합 글자에 대해 코드가 할당됨
UTF-8 한글을 자음 모음 조합형으로 표현하는 인코딩방법 풀어쓰기 형태로 여러 바이트로 표현됨

그럼 실제로 자바 개발에서 한글 파일의 입력 문제는 어떻게 해결되어야 할까요? 여기서 생기는 문제는 윈도우 환경과 텍스트파일/메모장 프로그램, 그리고 이클립스 환경이 사용하는 인코딩의 관계에서 발생한다는 점을 먼저 이해해야 합니다. 이클립스는 버전에 따라 디폴트 인코딩을 MS949를 사용하는 경우도 있고 UTF8만 사용하는 경우도 있습니다.

운영체제 입력파일/메모장 이클립스 프로젝트 소스코드 나타나는 결과
윈도우
(MS949)

ANSI MS949 ANSI 문제없음. 소스의 한글도 잘 보이고 파일입력도 잘됨
ANSI UTF-8 UTF-8 소스의 한글은 잘 보이지만 파일 입력이 안됨
UTF-8 MS949 ANSI 소스의 한글은 잘 보이지만 파일 입력이 안됨
UTF-8 UTF-8 UTF-8 문제없음. 소스의 한글도 잘 보이고 파일입력도 잘됨
  UTF-8 ANSI 소스의 한글이 깨진다.

간단히 말해서 이클립스 환경과 입력파일의 인코딩이 일치해야 된다는 것이죠. 이것이 불일치한다면 가장 좋은 방법은 프로젝트의 인코딩에 맞게 입력파일의 인코딩을 바꾸는 것입니다. (메모장으로 파일을 열어 다른이름으로 저장 -> 인코딩 변경 -> 저장)  프로젝트 생성할 때 설정되는 디폴트 인코딩은 프로젝트메뉴 -> Properties -> General -> Workspace -> encoding에서 바꿀 수 있습니다.

위 표의 마지막 경우는 사용중이던 프로젝트에 대해 인코딩을 변경하였을 때 소스가 깨지는 현상입니다. 즉 한글 코멘트를 포함한 소스 파일을 가진 이클립스 프로젝트의 인코딩을 바꾸면 소스의 한글이 모두 깨지게 됩니다(이것은 사실은 이클립스의 버그입니다). 그러므로 소스 코드를 포함한 프로젝트의 한글 인코딩을 바꾸는 것은 바람직하지 않겠지요? 이클립스 프로젝트의 인코딩은 파일을 읽어 들일 때 작동하고 Ctrl-V에서는 자동으로 현재의 인코딩에 맞추므로 복사 붙여넣기는 한글이 깨지지 않습니다.

그러므로 팁이라면 이클립스에서 프로젝트의 디폴트 인코딩이 무엇인지 확인하는 것이 좋습니다. 프로젝트를 만들 때 UTF-8로 설정하고 시작하거나(Project->Properties 화면의 인코딩변경) 이클립스의 환경 설정(Project/properties/ General/ Workspace)에서 프로젝트 생성시 UTF-8을 사용하게 세팅하는 것이 좋은 방법입니다. 이전 버전의 이클립스는 MS949를 디폴트로 사용하였으나 최근의 이클립스 환경은 UTF-8을 디폴트로 사용하도록 바뀌었으므로 프로젝트는 UTF-8로 유지하고 입력 파일이 맞는지 확인하는 것이 좋은 습관입니다. 

마지막으로 UTF-8 인코딩의 입력 파일을 읽을 때 첫번째 바이트가 BOM 바이트여서 Scanner의 next()나 nextInt()가 잘 동작하지 않는 문제가 있습니다. BOM 바이트는 UTF-8 인코딩에서 바이트의 순서(Byte Order Marker)를 정해주는데, 이 바이트 때문에 첫번째 단어나 숫자를 읽을 때 오류가 발생합니다. 이것은 nextLine()을 통해 한 줄을 읽어서 버리는 것으로 해결해야 합니다. (최근 이클립스에서는 BOM 바이트 파일 입력은 해결되었습니다.)

한가지 팁으로 Note++에서 BOM 바이트 없이 UTF-8 인코딩으로 저장할 수 있는 기능을 제공합니다. 

이렇게 바꾸고 저장하면 위에서 얘기한 BOM 바이트의 처리를 위한 nextLine()을 생략할 수 있습니다. 

가장 중요한 사항은 먼저 이클립스의 인코딩을 확인하고 그에 맞게 입력파일 인코딩을 저장하는 것입니다. 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함