▸JAVA/라이브러리(API)

java.nio 패키지 사용법(Channel / Buffer / Charset) [1/1]

코데방 2019. 12. 17.
728x90

[ java.nio.channel 패키지 ]

  • 다양한 외부 리소스와 내부의 버퍼를 연결해주는 채널을 생성해주는 클래스들 존재
  • 양방향 채널로 사용할 수도 있고 단방향 채널로 사용할 수도 있음
  • java.io의 스트림 클래스들에 비해 입출력 속도가 빠름
  • java.io의 스트림 클래스들과 다르게 무조건 버퍼를 사용해야 함

[ java.nio 패키지 ]

  • boolean 타입을 제외한 원시타입 별 버퍼 클래스를 제공
  • Buffer 인터페이스를 상속받기 때문에 클래스들의 사용법이 거의 비슷함

파일 채널(FileChannel)과 바이트 버퍼(ByteBuffer) 클래스의 사용법 위주로 정리하겠습니다. 다른 리소스와 연결되는 채널 및 다른 타입의 버퍼 클래스도 거의 사용법은 유사합니다. 특별한 기능을 가진 채널 및 버퍼 클래스는 해당 글에서 추가적으로 다루도록 하겠습니다. 파일 입출력에 대한 전반적인 구조와 개념은 아래 링크글을 참조하시면 됩니다.

 

2019/12/16 - [JAVA/기본 문법] - 외부 데이터 입출력_io / nio / nio2 [1/3]

2019/12/16 - [JAVA/기본 문법] - 외부 데이터 입출력_java.io [2/3]

2019/12/16 - [JAVA/기본 문법] - 외부 데이터 입출력_java.nio [3/3]

 


 

[ 채널(Channel) 생성 ]

  • 정적(static) 메소드를 이용 : open(Path p, Option)
  • java.io 클래스들에서 제공하는 인스턴스 메소드 사용 : getChannel()

 

[ StandardOpenOption ]

  • 채널(Channel) 생성 옵션을 가진 기본 라이브러리 Enum(열거형) 클래스
  • open() 메소드를 이용한 채널 인스턴스 생성 시 옵션은 여러 개 중복으로 넣어줄 수 있음
READ - 읽기용으로 파일을 엶
WRITE - 쓰기용으로 파일을 엶
CREATE - 파일이 없으면 새 파일 생성
CREATE_NEW - 새 파일 생성 (이미 파일이 있으면 예외 발생)
APPEND - 추가 모드로 파일을 엶 (EOF 위치 부터 시작, WRITE / CREATE와 같이 사용)
DELETE_ON_CLOSE - 채널이 닫힐 때 파일을 삭제
TRUNCATE_EXISTING - 파일을 열 때 파일내용을 모두 삭제 (0바이트로 만듦, WRITE와 같이 사용)

 

아래 코드와 같이 여러 개의 옵션을 섞어서 사용할 수도 있고, 조건이 복잡하다면 java.nio.Path/Files 클래스 또는 java.io.File 클래스를 이용해 미리 파일 상태를 확인해서 Path 객체를 생성한 뒤 적절한 옵션을 사용해주면 됩니다. 해당 클래스들은 아래 링크에서 확인하면 됩니다.

 

2019/12/16 - [JAVA/라이브러리(API)] - java.io.File 주요 메소드 [1/1]

2019/12/16 - [JAVA/라이브러리(API)] - java.nio.file.Path 주요 메소드 [1/1]

2019/12/16 - [JAVA/라이브러리(API)] - java.nio.file.Files 주요 메소드 [1/1]

package study.first;

import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class Main {
	public static void main(String[] args) {

		// Path 객체 생성
		Path p = Paths.get("C:\\JAVA\\Test\\input.txt");

		// 채널 열기 (읽기/쓰기, 파일 없으면 새로 생성, 기존에 있는 파일이면 내용을 모두 지워줌
		try (FileChannel ch = FileChannel.open
				(p, StandardOpenOption.WRITE, StandardOpenOption.READ,
						StandardOpenOption.CREATE,
						StandardOpenOption.TRUNCATE_EXISTING)) {

		} catch (Exception e) {

			System.out.println("파일 작업 실패");
		}
	}
}

 


 

[ 버퍼(Buffer) 생성 ]

  • 정적(static) 메소드를 이용 (각 타입 버퍼 클래스 동일)
  • 커널 버퍼 생성(ByteBuffer 클래스에서만 가능) : ByteBuffer.allocateDirect(int capacity)
  • 일반 버퍼 생성 : allocate(int capacity)

채널을 하나 만들었으니 버퍼를 생성해줍니다. 채널을 통한 파일 입출력에서는 무조건 버퍼만 사용가능합니다. 일반 배열로 버퍼를 대신할 수도 없습니다. 자주 만들었다 지웠다하는 버퍼라면 그냥 allocate()를, 크게 하나 만들어두고 계속 사용해야한다면 ByteBuffer 클래스의 allocateDirect() 메소드를 사용해 커널 버퍼를 만들어줍니다. 해당 개념은 아래 링크글을 참조하시면 됩니다.

 

2019/12/16 - [JAVA/기본 문법] - 외부 데이터 입출력_java.nio [3/3]

package study.first;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class Main {
	public static void main(String[] args) {

		// Path 객체 생성
		Path p = Paths.get("C:\\JAVA\\Test\\input.txt");

		// 채널 열기 (읽기/쓰기, 파일 없으면 새로 생성, 기존에 있는 파일이면 내용을 모두 지워줌
		try (FileChannel ch = FileChannel.open
				(p, StandardOpenOption.WRITE, StandardOpenOption.READ,
						StandardOpenOption.CREATE,
						StandardOpenOption.TRUNCATE_EXISTING)) {

			// 버퍼 생성 (Capacity가 10인 버퍼 생성)
			ByteBuffer b  = ByteBuffer.allocate(100);
			
			
			
		} catch (Exception e) {

			System.out.println("파일 작업 실패");
		}
	}
}

 


 

[ Capacity, Position, Limit ]

  • Capacity : 버퍼의 전체 크기
  • Position : 현재 버퍼를 쓰거나 읽을 위치, 파일 포인터의 개념과 같은 버퍼 포인터라고 보면 됨
  • Limit : 전체 크기 중에 실제 읽고 쓸 수 있는 위치를 따로 지정한 것 (기본 Capacity와 동일하게 생성)

채널은 버퍼 사용을 기본으로 하고, 파일 입출력을 무조건 버퍼에다가 합니다. 따라서 실제 채널 메소드로 입출력을 해주는 메소드는 버퍼에다가 출력하고 버퍼에서 가져오는 작업입니다. 위의 세 가지 개념만 알고 있으면 크게 어렵지 않습니다.

 

먼저 Capacity는 버퍼의 전체 크기입니다. 버퍼도 배열이기 때문에 배열 생성할 때 지정하는 크기와 같은 개념입니다. Position은 버퍼 포인터라고 생각하면 됩니다. 스트림에서 파일을 읽고 쓰는만큼 파일 포인터의 위치가 이동하는 것과 같이 버퍼에서도 읽고 쓰면 버퍼 포인터가 그만큼 이동합니다. Limit는 전체 버퍼에서 실제로 사용할 수 있는 한계를 설정해둔 포인트입니다. 즉, 실제 버퍼를 읽고 쓰는 범위는 전체(Capacity)중 Position - Limit의 범위입니다.

 

 

 


 

[ java.nio 파일 출력 및 Charset 클래스 ]

 

위의 과정을 통해 파일과 채널을 생성하고 읽고 쓸 수 있는 버퍼 생성을 완료했습니다. 이제 파일을 읽고 쓸 수 있는 상태가 되었습니다. 먼저 프로그램의 문자열을 파일에 쓰는 예제를 통해 파일 출력을 살펴보겠습니다.

 

일단 외부의 문자 데이터를 주고 받을 때는 서로 같은 인코딩 타입을 사용하지 않을 수 있다는 문제가 있습니다. 예를 들어 자바는 문자 인코딩 타입으로 유니코드를 사용하지만 윈도우 메모장은 ANSI 코드를 사용합니다. 유니코드로 출력해봤자 메모장에서는 이게 무슨 문자를 의미하는지 알 수 없고 반대로 메모장의 문자를 바이트 코드로 변환해서 가져와서 디코딩해봤자 무슨 문자인지 알 수가 없게 됩니다. 영문이나 숫자 등 아스키코드는 대부분 공통으로 써서 문제가 별로 없는데 2byte 이상으로 이루어진 한글같은 경우 문제가 됩니다.

 

실제로 바이트 스트림인 FileInputStream을 통해 .txt 파일의 내용을 읽어오면 문자 스트림인 FileReader로 읽어오는 코드와 전혀 다릅니다. 그래서 한글과 같은 다중 바이트 문자는 깨져서 나옵니다. 문자 스트림은 파일을 읽어올 때 내부적인 로직으로 ANSI코드를 유니코드로 변환해서 가져와 주기 때문에 문자를 제대로 읽을 수 있게 됩니다. NIO에서 이러한 문자 스트림 역할을 해주는 클래스가 java.nio.Charset 클래스입니다. 캐릭터셋이라고 부릅니다.

 

 

인코딩 타입 간 변환을 위해 일단 Charset 클래스의 인스턴스를 하나 생성해야 합니다.

 

  • Charset.forName("타입") : "유니코드 - 직접입력한 타입" 간 변환을 해주는 객체 생성
  • Charset.defaultCharset() : "유니코드 - OS의 인코딩 타입" 간 변환을 해주는 객체 생성

위와 같이 변환해줄 타입을 지정해주면 됩니다. 텍스트 파일은 윈도우 표준을 따르고 있으므로 일단 두 번째 메소드를 통해 인스턴스를 생성합니다. 그리고 Charset 클래스의 encode() 메소드를 통해 문자열을 바이트 단위의 ANSI코드로 인코딩해서 버퍼에 넣어줍니다. 아래와 같이 메소드를 사용하면 문자열을 모두 인코딩해서 버퍼에 넣어줍니다.

 

  • 버퍼 =  Charset 인스턴스.encode(문자열)
package study.first;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class Main {
	public static void main(String[] args) {

		// Path 객체 생성
		Path p = Paths.get("C:\\JAVA\\Test\\input.txt");

		// 채널 열기 
		// (읽기/쓰기, 파일 없으면 새로 생성, 기존에 있는 파일이면 내용을 모두 지워줌
		try (FileChannel ch = FileChannel.open
				(p, StandardOpenOption.WRITE, StandardOpenOption.READ,
						StandardOpenOption.CREATE,
						StandardOpenOption.TRUNCATE_EXISTING)) {
			
			// 버퍼 생성 (Capacity가 10인 버퍼 생성)
			ByteBuffer b  = ByteBuffer.allocate(100);
			
			// 인코딩 타입 변환을 위한 Charset 객체 생성
			String str = "NIO로 입출력을 해보자..!";
			Charset charset = Charset.defaultCharset();
			b = charset.encode(str); // 문자열 ANSI로 인코딩해서 버퍼에 넣어줌
						
		} catch (Exception e) {

			System.out.println("파일 작업 실패");
		}
	}
}

 


 

이제 정말 입출력을 위한 준비가 끝났습니다. java.io라면 그냥 아무 생각없이 FileReader로 스트림을 생성하고 필터 스트림을 통해 입출력하면 되는데 역시 NIO는 복잡하네요. 이제 버퍼의 내용을 파일에 쓰기만 하면 됩니다. 일단 기본적인 출력 작업은 끝났습니다.

 

  • 채널.write(버퍼) : 버퍼의 내용을 모두 파일에 씀
  • 채널.write(버퍼, p) : 파일의 p번째 위치부터 버퍼의 내용을 모두 파일에 씀
			ch.write(b);

 


 

[ 파일 입력 및 RandomAccessFile 클래스 ]

 

실수를 했습니다.. 한 채널로 파일 읽기/쓰기를 다 해보려고 했는데 그러려면 파일 입력을 먼저 했어야 합니다...! 이유는 위의 코드를 수행하고 나서 파일 포인터가 파일의 가장 마지막(EOF)위치로 가 있기 때문에 파일을 다시 열어주지 않는 이상 파일 포인터를 앞으로 되돌려서 내용을 읽을 방법이 없습니다. 파일 포인터의 위치를 자유 자재로 움직일 수 있는 방법은 "RandomAccessFile" 클래스로 파일을 열어주는 방법밖에 없습니다. 나머지는 전부 파일을 한방향으로만 순서대로 읽게 됩니다. 버퍼 포인터(Position)를 통해 내용을 다시 읽는 것과는 다른 개념이니 유의하셔야 합니다.

 

 

 


 

원래 의도했던대로 동일 채널을 사용해 출력한 파일에서 다시 읽어오는 작업을 수행하기 위해 다시 코드를 조금 바꾸겠습니다. java.io.RandomAccessFile 클래스로 파일을 연 뒤, 채널을 생성해주면 됩니다. java.io에서 파일을 열면 스트림을 통해 파일-프고그램 간 통로가 생성되지만 거기에 필터 스트림을 연결해 사용하는 것처럼 채널을 추가로 연결해 사용할 수 있도록 되어 있습니다. 

 

  • getChannel() : java.io에서 연 파일에 채널 통로를 생성해줌

조금 바꾼 코드는 아래와 같습니다. java.io편을 보고 오신 분이라면 코드는 간단히 해석 가능하실거라 생각합니다.

package study.first;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;

public class Main {
	public static void main(String[] args) {

		// File 객체 생성
		File p = new File("C:\\JAVA\\Test\\input.txt");
		// Finally 문에서 닫아주기 위해 밖에 생성
		RandomAccessFile file = null;

		// 채널 열기 (RandomAccessFile 클래스로 열기)
		try {
			file = new RandomAccessFile(p, "rw");
			FileChannel ch = file.getChannel();

			// 버퍼 생성 (Capacity가 10인 버퍼 생성)
			ByteBuffer b = ByteBuffer.allocate(100);

			// 인코딩 타입 변환을 위한 Charset 객체 생성
			String str = "NIO로 입출력을 해보자..!";
			Charset charset = Charset.defaultCharset();
			b = charset.encode(str); // 문자열 ANSI로 인코딩해서 버퍼에 넣어줌

			// 버퍼 내용 파일에다가 쓰기
			ch.write(b);

		} catch (Exception e) {

			System.out.println("파일 작업 실패");
		
		} finally {

			try {
				file.close();
			} catch (Exception a) {
				System.out.println("파일 닫기 실패");
			}
		}
	}
}

 


 

먼저, 파일 쓰기에서 버퍼 사용이 끝났으므로 버퍼를 초기화해서 다시 사용할 수 있게 해줍니다. 버퍼 공간에 여유가 있어서 뒤에 이어서 사용해도 되긴 하지만 그냥 초기화를 하도록 하겠습니다. 그리고 파일 내용을 다시 읽기 위해 포인터의 위치를 가장 처음으로 옮겨줍니다. 그리고 입력이 제대로 되는지 확인하기 위해 첫 글자를 소문자로 바꾸고 마지막 느낌표를 골뱅이로 바꿔줍니다. 파일 쓰기를 하면 파일 포인터의 위치가 자동으로 이동하므로 이 점을 항상 유의해야 합니다. 마지막으로는 버퍼를 초기화해줍니다.

 

  • RandomAccessFile 인스턴스.seek(long index) : 파일 포인터를 index로 옮겨줌
  • RandomAccessFile 인스턴스.write(byte b) : 현재 파일 포인터 위치에 내용을 씀 (덮어씀)
  • 버퍼 인스턴스.clear() : Position, Limit의 위치를 초기화해줌 (Position = 0, Limit = capacity)

package study.first;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;

public class Main {
	public static void main(String[] args) {

		// File 객체 생성
		File p = new File("C:\\JAVA\\Test\\input.txt");
		// Finally 문에서 닫아주기 위해 밖에 생성
		RandomAccessFile file = null;

		// 채널 열기 (RandomAccessFile 클래스로 열기)
		try {
			/* 파일 쓰기 */
			file = new RandomAccessFile(p, "rw");
			FileChannel ch = file.getChannel();

			// 버퍼 생성 (Capacity가 10인 버퍼 생성)
			ByteBuffer b = ByteBuffer.allocate(100);

			// 인코딩 타입 변환을 위한 Charset 객체 생성
			String str = "NIO로 입출력을 해보자..!";
			Charset charset = Charset.defaultCharset();
			b = charset.encode(str); // 문자열 ANSI로 인코딩해서 버퍼에 넣어줌

			ch.write(b); // 버퍼 내용 파일에다가 쓰기
			
			
			/* 파일 읽기 */
			String inputStr = "";
			file.seek(0); // 파일 포인터를 가장 처음으로 옮겨줌
			file.write((byte)'n'); // 첫 글자를 소문자로 바꿔줬음 (포인터 이동)
			file.seek(31); // 파일 포인터를 다시 처음으로 옮겨줌
			file.write((byte)'@'); // 느낌표를 골뱅이로 바꿔줌 
			file.seek(0); // 파일 포인터를 가장 처음으로 옮겨줌
			b.clear(); // 버퍼 초기화
	
		} catch (Exception e) {

			System.out.println("파일 작업 실패");

		} finally {

			try {
				file.close();
			} catch (Exception a) {
				System.out.println("파일 닫기 실패");
			}
		}
	}
}

 


 

파일 내용 수정을 좀 하고 버퍼를 초기화했으니 이제 파일의 내용을 버퍼로 가져오고 문자열에 넣어주면 됩니다. 출력 때와 마찬가지로 메모장의 ANSI코드로 된 바이트 데이터를 가져왔으므로 다시 Java의 유니코드로 변환해서 문자열에 넣어줘야 합니다. 문자열의 문자를 바이트 코드로 바꿔서 메모장에 주는 작업이 "인코딩"이었다면, 메모장에서 받아온 바이트 코드를 자바의 문자열로 바꿔주는 작업이므로 이번에는 "디코딩"을 해주면 됩니다.

 

  • 채널 인스턴스.read(버퍼) : 현재 Position - Limit이 허용하는 크기만큼 파일을 읽어서 버퍼에 저장
  • 버퍼.flip() : 버퍼의 Limit을 현재 Position 위치로 이동시키고 Position 위치를 0으로 이동시킴
  • Charset 인스턴스.decode(버퍼).toString() : 버퍼의 내용을 디코딩해서 문자열로 변환해줌

flip() 메소드와 clear() 메소드의 차이점은 Limit의 위치입니다. clear()메소드를 실행하면 Limit은 가장 기본 위치인 Capacity 위치와 동일합니다. 하지만 flip()은 Limit의 위치를 현재 Position 위치로 옮겨줍니다. 파일의 내용을 버퍼에 다 쓰고나면 Position위치는 내용의 가장 마지막위치 다음에 있으므로 flip()을 실행하면 Position - Limit의 범위는 딱 내용이 있는 범위만 가지게 됩니다. 만약 아래 코드에서 flip()대신 clear()를 써줘도 같은 내용이 출력되지만 버퍼의 남는 공간만큼 모두 공백으로 출력될 것입니다.

 

사실 flip()은 아래 두 코드로 나눠서 구현할 수도 있습니다. Limit값과 Position값은 현재 값을 추출할 수도 있고 인자값에 숫자를 넣어주면 해당 숫자값으로 변경할 수도 있습니다. 

b.limit(b.position());  // limit값을 현재 position값으로 변경
b.position(0);          // position값을 0으로 설정

 

package study.first;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;

public class Main {
	public static void main(String[] args) {

		// File 객체 생성
		File p = new File("C:\\JAVA\\Test\\input.txt");
		// Finally 문에서 닫아주기 위해 밖에 생성
		RandomAccessFile file = null;

		// 채널 열기 (RandomAccessFile 클래스로 열기)
		try {
			/* 파일 쓰기 */
			file = new RandomAccessFile(p, "rw");
			FileChannel ch = file.getChannel();

			// 버퍼 생성 (Capacity가 10인 버퍼 생성)
			ByteBuffer b = ByteBuffer.allocate(100);

			// 인코딩 타입 변환을 위한 Charset 객체 생성
			String str = "NIO로 입출력을 해보자..!";
			Charset charset = Charset.defaultCharset();
			b = charset.encode(str); // 문자열 ANSI로 인코딩해서 버퍼에 넣어줌

			ch.write(b); // 버퍼 내용 파일에다가 쓰기
			
			
			/* 파일 읽기 */
			String inputStr = "";
			file.seek(0); // 파일 포인터를 가장 처음으로 옮겨줌
			file.write((byte)'n'); // 첫 글자를 소문자로 바꿔줬음 (포인터 이동)
			file.seek(31); // 파일 포인터를 다시 처음으로 옮겨줌
			file.write((byte)'@'); // 느낌표를 골뱅이로 바꿔줌 
			file.seek(0); // 파일 포인터를 가장 처음으로 옮겨줌
			b.clear(); // 버퍼 초기화
			
			ch.read(b); // 파일 내용 다 읽어서 버퍼에 저장
			b.flip();   // 버퍼의 Position과 Limit을 내용 범위로 변경 
			inputStr = charset.decode(b).toString(); // 문자열 넣어줌
			System.out.println(inputStr);

		} catch (Exception e) {

			System.out.println("파일 작업 실패");

		} finally {

			try {
				file.close();
			} catch (Exception a) {
				System.out.println("파일 닫기 실패");
			}
		}
	}
}

 


 

위 과정을 통해 정상적으로 파일 입출력을 완료했습니다. 그런데 만약에 버퍼 사이즈보다 파일의 크기가 클 경우는 위와 같은 코드로 파일 내용을 모두 가져와서 문자열에 넣어줄 수 없습니다. 그럴 때는 위의 코드에서 read() 메소드부터 문자열에 넣어주는 부분까지를 반복문으로 수행해주시면 됩니다. 간단한 응용이니 코드는 생략하겠습니다!

728x90

댓글

💲 추천 글