프로그래밍 언어 활용/JAVA

IO입출력: 파일에 객체 정보 저장하기, 파일로부터 객체 정보 가져오기

프린이8549 2024. 2. 14. 08:58

0. 들어가기에 앞서

 IO입출력을 통해 파일로 객체를 출력하고 파일로부터 객체의 정보를 읽어오기 위해서는 먼저 파일에 또는 파일로부터의 데이터 입출력이 선행되어야 한다.

 

 따라서 본 고를 통해 객체 정보를 입출력하는 과정을 살펴보기 전에, 단순한 데이터를 입출력하는 과정을 간단하게 확인해보고자 한다.

바이트 기반 스트림을 통해 파일로 데이터 출력하기

프로그램(자바)  -> 외부 매체(파일)로 데이터를 출력하는 것(데이터를 파일로 내보낸다는 의미)

 

1. new FileOutStream("파일명")  통해 해당 파일과의 연결 통로를 만든다.

  • FileOutStream : 파일에 데이터를 1byte 단위로 출력하는 바이트 단위 기반 스트림
  • 해당 파일이 없다면 새로 생성 후 통로 연결함
  • FileOutStream("파일명", true/false)
    • true 미기입 시 해당 파일에 기존 데이터를 덮어씌움(기본값이 false)
    • true 기입 시 해당 파일에 기존의 데이터를 이어서 작성

2. 파일에 데이터를 출력한다.

  • FileOutputStream 클래스의 write() 메소드 사용
  • 출력하는 것이 숫자나 문자에 관계없이 실제로 파일에는 문자로 기록됨(아스키 코드표)
    • 숫자는 0 ~ 127 까지만 기록됨
    • 한글은 2byte 이기에 깨져서 저장됨

3. 스트림을 반납한다.

public void fileSave(){
	// 1. 스트림 객체 생성해 해당 파일과의 연결 통로 만들기
	try(FileOutputStream fos = new FileOutputStream("a_Byte.txt", true) ){
    
    // 2, 파일에 데이터 출력하기
    	fos.write(98);
        fos.write('b');
        
        byte[] arr = {102, 103, 104};
        fos.write(arr);
        
        // new FileOutputStream("파일명").write(byte[], int off, int len)
        // : byte 계열의 off 인덱스부터 len개 만큼 출력
        fos.write(arr,0,1);
        
    } catch(FileNotFoundException e){
    	e.printStackTrace();
    } catch(IOException e){
    	e.printStackTrace();
    }
    
    // 3. 스트림 사용 후 반납하여 닫아주기 fos.close();
    // 단, try catch resource 구문 사용 시 명기하지 않아도 됨
}

 

 바이트 기반 스트림을 통해 파일로부터 데이터 입력하기

외부 매체(파일 등) -> 프로그램(자바) 로 데이터를 입력하는 것(파일로부터 데이터를 읽어오는 것을 의미)

 

1. new FileInputStream("파일명") 통해 해당 파일과 연결 통로 형성한다.

  • FileInputStream : 파일로부터 데이터를 1byte 단위로 입력받는 바이트 단위 기반 스트림

2. 파일로부터 데이터를 읽어온다.

  • FileInputStream 클래스의 read() 메소드 사용
    • 1byte 단위로 읽어오며 이를 정수로 반환
      • 따라서 리턴되는 값을 character형으로 형변환한 후 출력해야 문자로 보임
    • 파일 내용의 끝을 만나는 순간 -1을 반환함
      • 해당 특성을 활용해 while 문을 통해 반복 출력할 수 있음

3. 스트림을 반납한다.

public void fileRead(){
	
    // 1. 스트림 생성
	try(FileInputStream fis = new FileInputStream("a_byte.txt");){
    
    // 2. 스트림 통해 입력받아오기
    // read() 메소드 사용
    	int value = 0;
        while( (value = fis.read()) != -1 ){ // 먼저 value에 넣고 반환하기에 1번씩만 돌아감
        // 아니면 read메소드를 지나갈 때마다 한 줄씩 사라져서 제대로 출력되지 않음
        	System.out.print( (char)value ); // 숫자로 리턴된 값을 캐릭터형으로 변환하여 문자로 출력함
        }
    } catch(FileNotFoundException e){
    	e.printStackTrace();
    } catch(IOException e){
    	e.printStackTrace();
    }
    // 3. 스트림 사용 후 반납하기
}

 

 이상의 과정을 통해 바이트 기반 스트림을 사용하여 파일에 데이터를 출력하고 파일로부터 데이터를 입력하는 방법을 살펴보았다. 

 이를 떠올리면서 이하에서는 파일에 객체 정보를 출력하고 또 파일에 있는 객체 정보를 입력하는 과정을 탐구하고자 한다.

 


1. 파일에 객체 정보를 출력하고, 파일 속 객체 정보를 입력하기

1.1. 객체 클래스 만들기

 객체 정보를 입출력하기 위해서는 먼저 객체를 생성해야 한다. 

 

 객체 생성을 위한 클래스를 구현하는 과정은 이미 확인했기에 넘어가겠으나, 이하 코드에서는 기존에 보지 못했던 인터페이스 "Serializable" 이 등장한다.

import java.io.Serializable;
//의도했는지 안했는지 확인하기 위한 용도 (실제로 Serializable의 문서를 확인하면 비어있음)
public class Product implements Serializable{ // 직렬화를 위한 인터페이스를 구현하겠다고 선언
	private String name;
	private int price;
	
	public Product() {
		
	}

	public Product(String name, int price) {
		super();
		this.name = name;
		this.price = price;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

	@Override
	public String toString() {
		return "Product [name=" + name + ", price=" + price + "]";
	}
	
	
}

 

해당 인터페이스를 구현하지 않으면, 파일에 객체 정보를 출력하고 파일로부터 객체 정보를 입력하는 데에 있어서 에러가 발생하게 된다.(NotSerializableException 런타임 예외)

 

 그렇다면 해당 인터페이스는 무엇이며 에러가 발생하게 되는 이유는 무엇일까?

1.1.1. 직렬화와 역직렬화

[그림 1] 직렬화와 역직렬화

Serializalbe 인터페이스는 프로그램이 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준으로 사용된다.

  • 실제로 해당 인터페이스의 api를 확인해보면 아무 내용도 없음(이를 마커 인터페이스라고 함)

그렇다면 직렬화란 무엇일까?

  • 직렬화: 객체를 저장, 전송할 수 있는 특정 포맷 상태로 변환하는 것

우리가 번거롭게 객체를 직렬화라는 과정을 거쳐야하는 이유는 데이터를 디스크에 저장하거나 통신할 때는 원시 타입의 데이터만 사용 가능하기 때문이다.

 

 왜 참조 타입의 데이터는 저장하거나 통신할 때 사용할 수 없을까? 참조 타입의 데이터를 사용할 수 없는 이유는 다음과 같다.

 

 아시다시피 참조 형식의 데이터는 실제 데이터 값이 아니라 힙메모리에 할당되어 있는 메모리의 주소값을 가지고 있다.

 

 따라서 이 주소값을 파일에 포함하여 저장한다고 했을 때, 프로그램을 종료하고 다시 실행해서 동일한 주소값을 가져오더라도 기존 객체의 데이터를 가져올 수는 없다.

 

 당연하게도 프로그램이 종료했다면, 기존에 할당됐던 메모리는 해체되고 없어지기 때문이다. 

 

 네트워크 통신도 마찬가지로 각 pc가 사용하는 메모리 공간 주소가 전혀 다르기 때문에 내가 특정 객체를 송신할지라도 이를 수신한 pc에서 해당 메모리 주소는 전혀 다른 값이 존재하게 된다.

 

 따라서 참조 형식 데이터는 직렬화라는 과정을 통해 값 형식 데이터로 변환하게 되는 경우에만, 유의미한 데이터가 되어 저장하거나 통신할 수 있게 된다. 

 

 

만약 파일에 출력할 때 민감한 데이터일 경우 transient 키워드를 통해 직렬화에 제외할 수 있다.

import java.io.Serializable;

public class Product implements Serializable{
	private String name;
	private transient int price; // 직렬화에 제외
	... 생략
	
}

 

 

역직렬화는 직렬화의 반대로, 특정 포맷 상태의 데이터를 다시 객체로 변환하는 것을 의미한다.

1.2. 파일에 객체 정보 저장하기

파일에 객체 정보를 저장하기 위해서는 참조 타입 데이터를 직렬화해줘야 한다.

 

이때 사용하는 것이 ObjectOutputStream 객체의 writeObject() 메소드이다.

public void objectSave(){
	// 출력할 데이터(Product 객체)
    Product phone1 = new Product("아이폰13", 1000000);
    Product phone2 = new Product("갤럭시24", 1500000);
    Product phone3 = new Product("롤리팝", 1200000);
    
    try(ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("d_product.txt") )){
   		// FileOutputStream : 파일과 연결해서 1byte 단위로 출력할 수 있는 기반 스트림
        // ObjectOutputStream : 객체 단위로 출력할 수 있도록 도움을 주는 보조 스트림(ObjectWriter 없음)
        
        oos.writeObject(phone1); // 출력하고자 하는 대상이 객체 단위이기 때문에 직렬화를 해줘야 1byte의 통로를 지나갈 수 있음  
        oos.writeObject(phone2);
        oos.writeObject(phone3);
        
    } catch(FileNotFoundException e) {
   		e.printStackTrace();
    } catch(IOException e) {
    	e.printStackTrace();
    }
}

 

1.3. 파일로부터 객체 정보 가져오기

파일로부터 객체 정보를 가져오기 위해서는 ObjectInputStream의 readObject() 메소드 사용하여 역직렬화를 진행해줘야 한다.

  • 이때 반환 타입은 Object 이기 때문에 본래 객체 타입으로 형변환 진행해줘야 함
public void objectRead(){
	try(ObjectInputStream ois = new ObjectInputStream( new FileInputStream("d_product.txt") )){
        while(true){
        	System.out.println(ois.readObject());
        }
    } catch(EOFException e){
    	System.out.println("파일을 다 읽어왔습니다.");
    } catch(FileNotFoundException e){
    	e.printStackTrace();
    } catch(IOException e){
    	e.printStackTrace();
    } catch(ClassNotFoundException e){
    	e.printStackTrace();
    }
}

 


[그림 1]  출처: https://code-lab1.tistory.com/289 [코드 연구소:티스토리]