I/O 문자열 다루기

WAS·2025년 2월 3일
0

I/O

목록 보기
3/3

스트림의 모든 데이터는 byte 단위를 사용한다
String 문자열 을 스트림을 통해 파일에 저장하려면 String 문자열byte로 변환한 다음에 저장해야함

다음 예제에서 살펴보자

public class ReaderWriterMainV1 {
	public static void main(String[] args) throws IOException {
		String writeString = "ABC";	
		byte[] writeBytes = writeString.getBytes(StandardCharsets.UTF_8); // 문자열을 바이트로 인코딩
		
		System.out.println("문자열" + writeString);
		System.out.println("바이트배열" + Arrays.toString(writeBytes));
		
		// 파일에서 쓰기
		FileOutputStream fos = new FileOutputStream(TextConst.FILE_NAME);
		fos.write(writeBytes);
		fos.close();
		
		// 파일에서 읽기
		FileInputStream fis = new FileInputStream(TextConst.FILE_NAME);
		byte[] readBytes = fis.readAllBytes();
		fis.close();
		
		String readString = new String(readBytes, StandardCharsets.UTF_8); // 바이트를 문자열로 디코딩
		System.out.println(readString);
	}
}

byte[] writeBytes = 바이트배열.getBytes(UTF_8) : String -> byte로 변환할 때 사용
String readString = new String(readBytes, UTF_8) : byte -> String 변환할 때 사용

하지만 위 예제와 같은 경우 번거로운 변환 과정이 있다


byte 를 문자로 자동으로 바꿔주는 스트림
OutputStreamWriter : 스트림에 byte 대신에 문자를 저장할 수 있게 지원
InputStreamReader : 스트림에 byte 대신에 문자를 읽을 수 있게 지원

String writeString = "ABC";
		
// 파일에서 쓰기
public class ReaderWriterMainV2 {

	public static void main(String[] args) throws IOException {
		String writeString = "ABC";
		
		// 파일에서 쓰기
		FileOutputStream fos = new FileOutputStream(TextConst.FILE_NAME);
		OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
		osw.write(writeString);
		osw.close();
		
		// 파일에서 읽기
		FileInputStream fis = new FileInputStream(TextConst.FILE_NAME);
		InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);  
		
		StringBuilder content = new StringBuilder();
		int ch;
		while((ch = isr.read()) != -1) {
			content.append((char)ch);
		}
		isr.close();
	}
}

(1) : OutputStreamWriter 는 문자를 입력받고, 그 문자를 byte[] 로 변환
(2) : 변환한 byte[]를 전달할 OutputStream 과 인코딩 문자 집합에 대한 정보가 필요
-> new OutputStreamWriter(fos, StandardCharsets.UTF_8);
(3) : 입력한 ABC가 문자 인코딩을 통해 byte[] 로 변환 후, 변환 결과를 FileOutputStream 에 전달
(4) : read() 를 사용해서 문자 하나를 char 형으로 데이터를 받게 된다

이전까지 write() read() 메소드는 byte 단위를 사용하였다
하지만 위 예제는 문자를 다루는 것을 확인할 수 있다
다음은 byte를 다루는 클래스와 문자 를 다루는 클래스를 비교하겠다


byte 다루는 클래스 vs 문자 를 다루는 클래스

  • byte 를 다루는 클래스는 OutputStream InputStream 의 자식이며
    부모 클래스의 기본 기능도 byte 단위를 다루며
    클래스 이름 마지막에 보통 OutputStream InputStream 이 붙어있다 EX) FileOutputStream

  • 문자 를 다루는 클래스는 Writer Reader 의 자식이며
    부모 클래스의 기본 기능도 String Char 같은 문자를 다루며
    클래스 이름 마지막에 보통 Writer Reader 이 붙어있다 EX) OutputStreamWriter
    Writer Reader 은 문자열을 문자 인코딩 표를 가지고 바이트로 편하게 바꿔주는 역할을 함
    FileWriter 은 생성자 내부에서 OutputStreamWriter 를 조금 더 편하게 사용하도록 도와줌

FileOutputStream fos = new FileOutputStream(TextConst.FILE_NAME); //  2줄의 코드를
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);

-> 

FileWriter fw = new FileWriter(TextConst.FILE_NAME, StandardCharsets.UTF_8); // 1줄로 줄일 수 있음

추가로 Reader Writer 에도 버퍼 보조 기능을 제공하는 BufferedReader BufferedWriter 가 있다
BufferedReader 을 사용하면 한 줄 단위로 문자를 읽는 기능을 사용할 수 있다 -> readLine()
BufferedReaderreadLine() 메소드는 반환 타입이 String 이며 파일의 끝을 null 로 반환한다

여기서 제일 중요한 점은 모든 데이터는 byte 단위로 저장된다는 점이다
쉽게말해 Writer Reader 이 문자를 저장하는 것처럼 느껴지는 것이지
실제로는 내부에서 다 byte 를 다루는 스트림을 사용하고 있는 것이다


메모리 에 데이터 저장 : 서버를 껐다 키면 데이터가 사라진다는 단점
파일 에 저장 : 데이터는 유지되지만, 각 데이터를 구분자로 구별하고 타입 변경이 귀찮다는 단점

파일 에 저장의 단점을 해결하기 위해서 DataStream 이 등장
DataStream : 데이터 타입을 그대로 사용하며 구분자 사용하지 않아도 됨

DataOutputStream dos = new DataOutputStream(new FileOutputStream("파일경로", true))) 
 { // DataOutputStream 은 보조 스트림으로 혼자 사용 불가능 (대상 스트림을 연결시켜줘야함)
	dos.writeUTF(member.getId()); // 순서를 잘 맞춰야함
	dos.writeUTF(member.getName());
	dos.writeInt(member.getAge());
} catch(
IOException e)
{
	throw new RuntimeException(e);
}

writeUTF() : DataOutputStream 객체에서 문자열 저장할 때 사용
writeInt() : DataOutputStream 객체에서 정수형 저장할 때 사용
readUTF() : DataOutputStream 객체에서 문자열 읽을 때 사용
readInt() : DataOutputStream 객체에서 정수형 읽을 때 사용

그러면 DataStream 어떤 원리로 구분자나 줄 라인 없이 데이터를 저장하고 조회하는 것일까?

writeUTF() 은 UTF-8 형식으로 문자를 저장하는데, 저장할 때 2byte 를 추가로 사용해서 앞의 글자의
길이를 저장해둔다. 예를들어 "id1" 이라는 문자열을 저장을 하면
-> 2byte(문자 길이) + 3byte(실제 문자 데이터) = 3id1 이런식으로 저장이 된다. 3은 3글자를 의미

그 후 readUTF() 로 읽어들일 때 먼저 앞의 2byte 로 글자의 길이를 확인하고
해당 길이만큼 글자를 읽어들인다.
위 예시의 경우 2byte 를 사용해서 3이라는 문자의 길이를 숫자로 보관하고, 나머지 3byte
실제 문자 데이터를 보관한다.

실제 예시
EX) id1name120 -> 3id1 5name1 20(4byte) 로 인식
정수형은 저장 및 읽을 때 4byte 를 사용

DataStream 으로 더 편리하게 저장하고, 구분자가 필요하지 않는 장점이 있지만
위 데이터를 저장할 때, 데이터 필드를 하나하나를 다 조회해서 각 타입에 맞도록 따로 저장해야한다는
단점이 있다 -> dos.writeUTF(member.getId()); dos.writeInt(member.getAge());
이 단점을 해결하기 위해 ObjectStream 이 등장

ObjectStream

ObjectOutputStream 을 사용하면 객체 인스턴스를 직렬화 해서 byte 로 변경 할 수 있음
ObjectInputStream 을 사용하면 byte역직렬화 해서 객체 인스턴스로 만들 수 있음

객체직렬화 : 메모리에 있는 객체 인스턴스를 바이트 스트림으로 변환하여 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 하는 기능으로 나중에 객체의 상태를 유지하여 역직렬화 (바이트 -> 객체) 를 통해
원래의 객체로 복원할 수 있다

객체직렬화 를 사용하려면 직렬화 하려는 클래스는 반드시 Serializeable 인터페이스를 구현해야함
Serializeable 인터페이스는 아무런 기능이 없으며, 직렬화 가능한 클래스라는 것을 표시하기 위한 인터페이스이다. 메서드 없이 단지 표시가 목적인 인터페이스를 마커 인터페이스 라고 한다.

객체직렬화 를 사용하면 객체를 바이트로 변환할 수 있어, 파일에 저장하는 것 뿐 아니라
네트워크를 통해 객체 전송하는 것도 가능하게 된다. 하지만 시간이 지나면서 여러 단점이 생기고
대안 기술들이 생기면서 현재는 객체직렬화 를 거의 사용하지 않는다

객체직렬화 의 문제점

  • 클래스 구조가 변경되면 이전에 직렬화된 객체와의 호환성 문제가 발생 (버전관리가 어려움)
  • 자바 직렬화는 자바 플랫폼에 종속적이라서 다른 언어와의 상호 운영성이 떨어짐
  • 직렬화/역직렬화 과정이 상대적으로 느리고 성능이 좋지 않음

이러한 이유로 최근에는 XML JSON 데이터베이스 로 데이터를 컴퓨터 간에 주고 받는다

XML JSON 데이터베이스

XML : 플랫폼 종속성 문제를 해결하기 위해 등장 (파이썬, 자바, 등등 여러 곳에서 사용 가능)
유연하고 강력하지만, 복잡하고 무겁다는 단점이 있다
-> 태그를 포함한 XML 문서의 크기가 커서 네트워크 전송 비용이 증가

<member>
  <id>id1</id>
  <name>name1</name>
  <age>20</age>
</member>

JSON : 가볍고, 간결하며 자바스크립트와의 호환이 자연스러움
웹 환경에서 데이터를 교환할 때에는 JSON 을 표준 기술로 사용하고 있다

{"member": {"id": "id1", "name": "name1", "age": 20 }}

데이터베이스
위에서 파일에다 저장할 때는 동시 접근성의 문제와 데이터 검색관리의 비효율성
보안의 문제로 인해 나온 서버프로그램이 데이터베이스 이다

참고로 큰 데이터 (이미지, 영상) 은 파일 로 보관하고
그 외의 데이터는 모두 데이터베이스 에 보관한다


자바에서 파일 또는 디렉토리를 다룰 때는 File 또는 Files Path 클래스를 사용하면 된다

File 은 옛날에 사용하는 방식으로 주요기능들은 생략하도록 하겠다 (필요하면 찾아보기)

File file = new File("temp/example.txt");
File directory = new File("temp/exampleDir");

Files
File 클래스를 대체할 FilesPath 가 등장 -> 수 많은 유틸리티 기능이 있다

파일이나 디렉터리의 경로를 나타낼 때는 Path 를 사용
Files 는 직접 생성할 수 없고, static 메서드 를 통해 기능을 제공한다

Path file = Path.of("temp/example.txt");
Path directory = Path.of("temp/exampleDir");

Files 의 주요 기능들

exists() : 파일이나 디렉토리의 존재 여부를 확인 -> Files.exists(file);
createFile() : 새 파일을 생성 -> Files.createFile(file);
createDirectory() : 새 디렉토리를 생성 -> Files.createDirectory(directory);
delete() : 파일이나 디렉토리를 삭제 -> Files.delete(file);
isRegularFile() : 일반 파일인지 확인 -> Files.isRegularFile(file);
isDirectory() : 디렉토리인지 확인 -> Files.isDirectory(directory);
getFileName() : 파일이나 디렉토리의 이름을 반환 -> file.getFileName()
size() : 파일의 크기를 바이트 단위로 반환 -> Files.size(file)

move() : 파일의 이름을 변경하거나 이동

Path newFile = Paths.get("temp/newExample.txt");
Files.move(file, newFile, StandardCopyOption.REPLACE_EXISTING);
System.out.println("File moved/renamed");

getLastModifiedTime() : 마지막으로 수정된 시간을 반환 -> Files.getLastModifiedTime(newFile);

✅ 경로 표시
파일이나 디렉터리는 크게 절대경로정규경로 로 나눌 수 있다

절대경로 : 내가 입력한 경로를 처음부터 끝까지 다 표현한 것
정규경로 : 절대경로 에서 계산이 끝난 실제 경로

Files 에서 경로 표시하는 예제

 public static void main(String[] args) throws IOException {
 	Path path = Path.of("temp/..");
 	System.out.println("path = " + path); // 상대경로
 	System.out.println("Absolute path = " + path.toAbsolutePath()); // 절대 경로
 	System.out.println("Canonical path = " + path.toRealPath());  // 정규 경로
      // 출력결과
     //path = temp/..
 	 //Absolute path = /Users/yh/study/inflearn/java/java-adv2/temp/..
	 //Canonical path = /Users/yh/study/inflearn/java/java-adv2
    
 	Stream<Path> pathStream = Files.list(path); // 현재 경로에 있는 모든 파일 또는 디렉토리를 반환
 	List<Path> list = pathStream.toList(); 
    pathStream.close();
    for (Path p : list) {
      System.out.println((Files.isRegularFile(p) ? "F" : "D") + " | " + p.getFileName());
   }
 }

✅ Files로 문자 파일 읽기
기존에 파일을 저장할 때는 버퍼도 만들고, Reader, Writer 등 복잡하게 만들어야 했다
하지만 아주 간편하게 파일을 저장 및 읽을 수 있는 방법이 있다

writeString("경로", "저장할문자열", "인코딩"); : 파일에서 쓰기
readString("경로", "인코딩"); : 파일에서 모든 문자 읽기

private static final String PATH = "temp/hello2.txt";
String writeString = "abc\n가나다";
Path path = Path.of(PATH);
Files.writeString(path, writeString, UTF_8);  // 파일에 쓰기
String readString = Files.readString(path, UTF_8)  // 파일에서 읽기

이번 예제는 한줄(라인 단위)로 읽는 예제를 보여주겠다

방법은 두가지
Files.readAllLines(path) : 파일을 한번에 다 읽은 후, 라인 단위로 List 에 나누어 저장하고 반환

  Path path = Path.of(PATH);
  Files.writeString(path, writeString, UTF_8);  // 파일에 쓰기	
  List<String> lines = Files.readAllLines(path, UTF_8);  // 파일에서 읽기
  for (int i = 0; i < lines.size(); i++) {
	 System.out.println((i + 1) + ": " + lines.get(i));
  }

Files.lines(path) : 파일을 한 줄 단위로 나누어 읽는다, 용량이 큰 파일을 처리하려면 이것 사용

 try(Stream<String> lineStream = Files.lines(path, UTF_8)){
    lineStream.forEach(line -> System.out.println(line));
}

✅ 파일 복사 최적화

(1) 첫 번째 방법

public static void main(String[] args) throws IOException {
		
	FileInputStream fis = new FileInputStream("temp/joo.dat"); // 읽을 파일 대상
	FileOutputStream fos = new FileOutputStream("temp/new_joo.dat"); // 새로운 파일 생성
		
	byte[] bytes = fis.readAllBytes(); // 파일을 읽어서 바이트 배열로 모두 저장
	fos.write(bytes); // 새로운 파일에 그 값들을 넣음 (복사)
		
	fis.close();
	fos.close();
}
  • 파일(joo.dat) -> 자바(byte) -> 파일(new_joo.dat) 과정을 거친다

위 예제처럼 InputStream 으로 들어온 데이터를 OutputStream 으로 데이터 내보내는 방법을
자바에서 더 쉽게 제공하는 메소드가 존재한다 -> transferTo()

(2) 두 번째 방법
transferTo() 사용

public static void main(String[] args) throws IOException {

	FileInputStream fis = new FileInputStream("temp/joo.dat"); // 읽을 파일 대상	
	FileOutputStream fos = new FileOutputStream("temp/new_joo.dat"); // 새로운 파일 생성
	fis.transferTo(fos); 
	fis.close();
	fos.close();
}
  • InputStream 에 존재하는 메소드 (자바9)
  • InputStream 을 읽어서 OutputStream 으로 내보내는 기능
  • 성능최적화가 되어있어서, 속도가 빠른 편임

(3) 세번째 방법
Files.copy() 사용

public static void main(String[] args) throws IOException {
	Path source = Path.of("temp/joo.dat"); // 복사할 원본
	Path target = Path.of("temp/new_joo.dat"); // 복사할 대상
	Files.copy(source, target);
}

이 방법은 1,2 방법 처럼 자바에 파일 데이터를 불러오지 않고
운영체제 의 파일 복사 기능을 이용해서 속도가 3가지 방법 중 가장 빠르다

profile
우측 상단 햇님모양 클릭하셔서 무조건 야간모드로 봐주세요!!

0개의 댓글

관련 채용 정보