본문 바로가기

국비과정

[ANDROID 국비과정] 2023.02.03 - 안드로이드 앱 개발자 과정

Java


File I/O - 바이트스트림

 

기억장치에는 CPU 가 접근하는 주메모리(RAM)와 영구적 저장을 위한 보조기억장치(HDD)가 있습니다. 램의 경우 컴퓨터의 전원을 끄거나 프로그램을 끄면, 데이터가 모두 날아가게 됩니다. 하지만 우리의 목적은 데이터의 영구적인 저장입니다.

그래서 우리는 데이터를 파일로 만들어 저장합니다. Window OS 에서의 보조기억장치인 하드디스크(HDD) 사용법을 기반으로 파일의 영구적 저장방법을 알아보겠습니다.

 

하드디스크는 램과 달리 데이터로 보관하는것이 아니라 파일의 형태로 보관합니다. 또한 파일의 식별자로 파일의 이름과 확장자를 사용할 수 있습니다. 예를 들어 간단한 문자열을 저장하고 싶다면, aaa.txt 와 같이 저장시킬 수 있죠. 이와 같이 파일의 저장과 출력을 담당하는 클래스가 자바에는 존재합니다. 바로 Output 클래스 , Input 클래스 , Reader 클래스 , Writer 클래스 입니다. 아래 그림을 참고하시면 좋습니다.

 

Stream

 

아래 그림은 InputStream 과 OutputStream 을 통해 자바 프로그램과 하드디스크 사이의 파일 입출력을 나타낸 그림 입니다. 한 통로로 Input 과 Output 이 이루어지는것이 아닌 각기 다른 통로를 통해 이루어진다는 점을 알아 둡시다.

 

파일 입출력 도식

 


FileOutput

먼저 파일 출력, 즉, 파일에 데이터를 넘겨주는 과정에 대해 살펴봅시다.

 

파일 생성하기

사용자로부터 데이터를 입력받아 File 에 저장하는 프로그램을 만들어봅시다. File 에 저장하기 위해서는 File 을 제어하는 클래스가 필요하겠죠. File 클래스의 객체를 생성합시다.

 

public class FileOutput {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.print("저장할 데이터를 입력하세요 : ");
        String data = scan.next();

        File file = new File("aaa.txt");
    }
}

 

위 코드에서 볼 수 있듯, 객체의 생성자에는 파라미터로 파일의 경로 및 이름, 확장자를 문자열 데이터로 넘겨줄 수 있습니다. 만일, 위 코드처럼 생성자 매개변수로 파일의 이름과 확장자만 넘겨주었다면, 자동으로 현재 프로젝트에 파일을 생성합니다. 만약, 파일이 없는 경우라면 파일을 자동으로 생성해주며 파일이 있다면 그 파일을 열기만 합니다. 단, 폴더는 자동생성하지 않으며 지정된 경로에 폴더가 없다면 에러를 발생시킵니다.

 

FileOutputStream 을 이용한 데이터 출력

자 이렇게 파일을 생성했다면, 파일 객체가 관리하는 파일(aaa.txt) 에 데이터를 보내야겠죠? 그러려면 일단 OutputStream 을 이용해야 할것 같구요, 파일에 관한 Output 을 해줄 수 있는 클래스가 FileOutputStream 입니다. 객체를 생성해서 정보를 넘겨주는 코드를 작성해봅시다.

 

try {
    FileOutputStream fos = new FileOutputStream(file,true); 
    byte[] bytes = data.getBytes();

    fos.write(bytes);
    fos.flush();

    fos.close();

    System.out.println("파일 저장 성공!");
} catch (FileNotFoundException e) {
	System.out.println("저장될 경로가 존재하지 않습니다...");
} catch (IOException e) {
	System.out.println("쓰기작업 중 오류가 발생했습니다...");
}

 

바이트스트림의 경우 파일의 정보를 바이트 단위로 보냅니다. (그래서 2바이트로 이루어진 한글의 경우, 깨져서 출력이 됩니다.) .getBytes() 메서드를 이용하여 입력받은 데이터를 byte 배열로 바꿔준뒤, 그 바이트 정보를 FileOutputStream 클래스의 write() 메서드로 파일에 작성해줍니다. 또한 출력의 경우 버퍼에 관한 개념이 필요하지만, 현 시점에서는 그냥 아주 작은 임시저장소 정도로 생각합시다. flush() 메소드를 사용해서 버퍼에 남은 찌꺼기들을 내보내주고, 출력이 끝났면 close() 메서드를 이용하여 Stream 을 닫아줍니다. 또한 Stream 을 사용할 경우에는 지정된 경로의 파일을 찾지 못하거나, 예상치못한 입출력 오류가 일어날 가능성이 있으므로 try-catch 문을 통해 예외처리를 해줍시다.

 

경로 지정

앞선 예제에서 File 클래스를 이용해서 파일을 만들고, FileOutputStream 클래스를 이용하여 데이터를 저장하는 방법을 알아보았습니다. 다만, 따로 경로를 지정하지는 않았었죠. 그래서 이번에는 경로를 지정하는 방법에 대해 알아봅시다. 

 

Scanner sc = new Scanner(System.in);
System.out.print("데이터 입력 : ");
String data = sc.next();
		
		
File path = new File("C:/Users");
		
if(!path.isDirectory()) {
	path.mkdirs();
}
		
File file = new File(path,"aaa.txt");
		
try {
    FileOutputStream fos = new FileOutputStream(file,true);
    byte[] b = data.getBytes();

    fos.write(b);
    fos.flush();

    fos.close();
			
} catch (FileNotFoundException e) {
	System.out.println("파일을 찾을 수 없습니다.");
} catch (IOException e) {
	System.out.println("입출력 오류");
}

 

  1. File 클래스의 객체하나를 생성해주고, 생성자의 파라미터로 파일을 저장하고자 할 경로인 "C:/Users" 을 전달해주었습니다.
  2. 그리고 if 문의 조건에 path 에 저장된 경로의 파일이 존재하는지 묻고, 존재하지 않는다면 path.mkdirs(); 를 통해 폴더를 생성해주었습니다.

  3. 그 후에 File 클래스의 객체를 생성하여 생성자 파라미터로 path 와 파일이름을 전달해줍니다.

  4. 그리고 나서는 위의 예제와 동일하게 FileOutputStream 클래스를 이용하여 데이터를 저장합니다.

  5. 또한 경로를 지정할 때, 디렉토리를 구분하기 위해 보통 \\ 을 이용합니다. Mac OS 나 리눅스의 경우 / 을 이용하며, 윈도우즈 또한 / 을 이용해도 괜찮습니다.

만일 파일이 확실하게 있는 경우라면 path 와 파일이름을 합쳐서 File 클래스의 객체를 만들때, if 문 없이 바로 만들수도 있겠습니다만, if 문을 지정하는것이 더 안정적이겠네요. 

 


FileInput

다음은 파일 입력에 관한 내용입니다. 즉, 파일의 데이터를 자바 프로그램으로 끌고들어오는 과정에 대해 알아봅시다.

 

파일에서 데이터 불러오기

앞서 파일 출력에서 했던 방식처럼 byte 배열을 통해 파일의 데이터를 읽어와보겠습니다.

 

File file = new File("C:/Users/aaa.txt");

try {
    FileInputStream fis = new FileInputStream(file);

    byte b = (byte)fis.read();

    while(b != -1) { 
        System.out.print((char)b);
        b = (byte)fis.read(); 
    }

    System.out.println("파일 로드가 완료되었습니다.");

    fis.close();
			
} catch (FileNotFoundException e) {
	System.out.println("읽어올 파일을 찾을 수 없습니다.");
} catch (IOException e) {
	System.out.println("읽기 도중 문제가 발생했습니다.");
}

 

  1. 먼저 File 클래스를 이용하여 생성자의 파라미터에 데이터를 가져올 파일의 경로를 넘겨줍니다.

  2. 그리고 FileInputStream 클래스의 생성자 파라미터로 경로의 정보가 저장되어 있는 File 의 객체를 넘겨줍시다.

  3. 그런뒤, read() 메서드로 파일의 데이터를 가져오고 byte 타입 캐스팅을 통해 byte 타입의 변수에 넣어줍시다.

  4. read() 메서드는 1바이트 씩 데이터를 읽어오기 때문에 while 문을 통해서 파일의 데이터를 처리해주어야 합니다. 또한 더 이상 읽어올 데이터가 없다면 read() 메서드는 -1 을 반환합니다. 그래서 -1 을 반환하기 전까지 while 문을 실행시켜 주어야 하며, 하나씩 출력을 해야합니다.

  5. 출력할 때에는 (char) b 를 통해 한 문자로 바꿔준뒤 출력해주어야 합니다.

  6. 그 뒤에 파일 입력이 끝났다면 fis.close() 를 통해 Stream 을 닫아줍시다.

  7. FileInputStream 또한 파일을 찾지 못하거나 입력 오류가 있을 경우 에러를 발생시키므로 예외처리가 필요합니다.

이렇게 해서 파일의 데이터를 불러왔습니다. 그런데 실행을 시켜보면 알겠지만, 아스키값에 없는 데이터들, 즉 한글같은 경우에는 데이터를 불러올 때 깨집니다. 그 이유는 한글은 2바이트 혹은 3바이트로 이루어진 언어인데 데이터를 불러올 때는 1바이트 씩 불러오기 때문입니다. 그래서 이러한 언어들까지 커버할 수 있도록 데이터를 1바이트 씩 얻어올게 아니라 아예 통째로 받아오는 방법에 대해 알아봅시다.

 

통째로 불러오기

 

File file = new File("C:/Users/aaa.txt");

try {
    FileInputStream fis = new FileInputStream(file);

    byte[] bytes = new byte[1024];
    fis.read(bytes);

    String s = new String(bytes);
    System.out.println(s);

    System.out.println("파일 로드가 완료되었습니다.");

    fis.close();
			
} catch (FileNotFoundException e) {
	System.out.println("읽어올 파일을 찾을 수 없습니다.");
} catch (IOException e) {
	System.out.println("읽기 도중 문제가 발생했습니다.");
}

 

  1. 1바이트씩 데이터를 가져오니 한글과 같은 데이터는 깨지기때문에 우리는 데이터를 통째로 가져와야 합니다. 그리고 그 데이터는 byte 로 가져올 수 있습니다. 그래서 충분한 크기의 빈 byte 배열을 하나 만들어줍시다.

  2. 그리고 read() 메서드를 이용하여 메서드의 파라미터로 배열의 주솟값을 전달해줍시다.

  3. 그러면 배열에 데이터를 저장할 수 있습니다. 그 배열을 String 객체의 생성자 파라미터로 넘겨주면 String 객체로 변환할 수 있습니다.

  4. 그런 뒤 출력해주면 됩니다.

입출력 한꺼번에 처리하기

파일의 데이터의 입력과 출력을 알아보았습니다. 이번에는 파일 복사 프로그램을 만들면서 입력과 출력을 한번에 할 수 있는 방법을 알아봅시다. 먼저 C 드라이브의 Users 파일안에 있는 문서 (aaa.txt) 를 읽어서 데이터를 가져온 뒤, 이클립스 프로젝트 파일로 복사해보겠습니다. 코드부터 봅시다.

 

String scrPath = "C:/Users/aaa.txt"; 

try {
    FileInputStream fis = FileInputStream(scrPath); 
    -- 1

    File path = new File("C:/Users/cskwg/eclipse-workspace");
    if(!path.isDirectory()) path.mkdirs();
    -- 3

    File file = new File(path,"copy.txt");
    FileOutputStream fos = new FileOutputStream(file);
    -- 4

    while(true) {
        byte b = (byte) fis.read();
        if(b == -1) break;
        -- 2

        fos.write(b);
        -- 5
    }

    fos.flush();
    fos.close();
    fis.close();
    -- 6

} catch (FileNotFoundException e) {
	System.out.println("불러올 파일을 찾을 수 없습니다.");
} catch (IOException e) {
	System.out.println("복사중에 오류가 발생했습니다.");
}

 

  1. 먼저, 데이터를 읽어올 파일의 정보를 가져와야겠죠? 그래서 FileInputStream 클래스를 이용하여 객체의 파라미터에 파일의 경로를 넣어줍시다.

  2. 이제 while 문과 FileInputStream 클래스의 read() 메서드를 이용하여 파일(aaa.txt) 의 데이터를 byte 로 읽어와줍니다. 만일 읽어올 데이터가 더이상 존재하지 않는다면, 즉, b 의 값이 -1 이라면 while 문을 종료시킵니다.

  3. 읽어와서 봤더니 데이터를 저장시킬 파일이 필요합니다. File 클래스를 이용해 저장시킬 파일의 겨로를 저장해주고, 폴더를 먼저 생성합시다.

  4. 그리고 경로와 파일을 하나의 File 객체로 묶어준 뒤 그 객체를 FileOutputStream 객체의 생성자 파라미터로 넘겨줍시다.

  5. 아까 2번에서 받아온 데이터를 write() 메서드를 통해 저장합시다.

  6. flush() 메서드를 통해 버퍼에 남은 데이터를 없애주고, close() 메서드를 통해 FileInputStream 과 FileOutputStream 을 닫아줍니다.

 


File I/O - 문자스트림

기존의 바이트스트림으로 byte 배열을 통해 문자열을 출력해보았습니다. 다만, 이 방법으로는 문자열을 한줄씩 읽거나 혹은 한 단어씩 읽고 쓰기에는 무리가 있습니다. 몇바이트를 읽어오고 써내야 한줄 혹은 한단어인지 파악하기가 어렵기 때문이죠. 그래서 좀 더 편하게 문자열 단위로 데이터의 입출력을 처리하는 문자스트림에 대해 알아봅시다.

 

Writer

먼저, 문자열 데이터를 저장하는 방법에 대해 알아봅시다.

 

Scanner sc = new Scanner(System.in);
File path = new File("C:\\Users\\cskwg\\eclipse-workspace");
if(!path.exists()) path.mkdirs();
-- 1

File file = new File(path,"README.txt");
-- 2

while(true) {
    System.out.print("데이터 입력(exit를 입력하면 종료) : ");
    String data = sc.next();
    -- 3

    if(data.equalsIgnoreCase("exit")) break;
    -- 4

    try {
        FileWriter fw = new FileWriter(file,true);
        // writer(data); 짜증!
        -- 5

        PrintWriter printW = new PrintWriter(fw);
        -- 6

        printW.println(data);
        -- 7

        printW.flush();
        printW.close();
        -- 8
        
	} catch (IOException e) {
		System.out.println("입출력 오류");
	} -- 9
}

 

  1. File 클래스를 이용하여 데이터를 저장할 파일의 경로를 지정해주고, if 문을 통해 없는 폴더가 경로에 지정되었다면 폴더를 생성해줍시다.

  2. File 클래스를 이용하여 경로와 저장할 파일을 하나의 객체로 합쳐줍니다.

  3. 사용자로부터 데이터를 입력받습니다.

  4. 입력받은 데이터가 exit 라면(대문자 포함) while 문을 종료시킵니다.

  5. FileWriter 객체의 생성자 파라미터로 file 객체를 넘겨줍시다. 또한 true 값을 파라미터로 넘겨주면 데이터의 지속적인 추가가 가능합니다. write() 메서드를 통해 데이터를 저장하려니 줄바꿈문자도 신경써줘야 하고, 특정 포맷의 출력은 하기 쉽지 않습니다. 그래서 보조스트림을 사용합시다.

  6. Scanner 클래스를 사용할 때, 키보드의 입력을 받는 객체가 System.in 이었던것과 비슷하게 파일의 출력 또한 보조스트림 클래스인 PrintWriter 클래스를 통해 편리하게 처리할 수 있습니다.

  7. 보조스트림 PrintWriter 의 객체의 println() 메서드를 통해 데이터를 저장합니다.

  8. 파일 출력이 모두 끝났으면 flush() 메서드를 통해 찌꺼기를 처리하고, close() 메서드로 스트림을 닫아줍시다. PrintWriter 스트림만 닫아주더라도 FileWriter 스트림은 자동으로 닫히게 됩니다.

  9. try-catch 문을 통해 파일 입출력 때 발생할 수 있는 에러에 대한 예외처리를 해줍니다.

 

Reader

ArrayList<String> datas = new ArrayList<>();
-- 1

try {
    FileReader fr = new FileReader(file);
    // int a = fr.read(); 한문자씩.. 짜증!
    -- 2

    BufferedReader br = new BufferedReader(fr);
    -- 3

    while(true) {
        String line = br.readLine();
        if(line == null) break;
        -- 4

        datas.add(line);
        -- 5
    }
    System.out.println(datas);
    -- 6

    br.close();
    -- 7 
    
} catch (FileNotFoundException e) {
	System.out.println("파일을 찾을 수 없습니다.");
} catch (IOException e) {
	System.out.println("입출력 오류");
} -- 8

 

  1. 데이터를 저장시킬 컬렉션 하나를 ArrayList 로 만들어 줍니다.
  2. 우리는 앞선 예제에서 만든 데이터를 불러올 예정이니, FileReader 객체의 생성자 파라미터로 앞선 Writer 예제에서 만들었던 file 객체를 넘겨줍시다. 그리고 read() 메서드로 한문자씩 데이터를 가져오려니 짜증이 납니다. 그래서 보조스트림 클래스인 BufferedReader 클래스를 이용하겠습니다.

  3. BufferedReader 객체의 생성자 파라미터로 FileReader 의 객체를 넘겨줍시다.

  4. String 참조변수 하나를 만들어 readLine() 메서드로 한줄씩 문자열을 받아옵시다. readLine() 메서드는 줄바꿈 문자는 제외되고 한줄씩 읽어올 수 있습니다. 만약, 받아온 데이터가 없다면 반복문을 종료합니다.

  5. String 변수로 한줄씩 받아온 데이터를 ArrayList 의 요소로 추가시킵니다.

  6. ArrayList 의 요소값을 출력합니다.

  7. 파일의 입력이 끝났다면 close() 메서드를 통해 스트림을 닫아줍시다.

  8. try-catch 문을 통해 파일 입출력시 나타날 수 있는 에러들의 예외처리를 해줍니다.