Java

[Java] 11. 입출력

hyomee2 2024. 8. 21. 18:47

입출력의 정의 및 사용 이유

1. 입출력(IO) 이란?

: 컴퓨터 내부 또는 외부 장치와 프로그램 간의 데이터 연동을 위한 자바 라이브러리

- 단방향 데이터 송수신을 위해 스트림을 활용하여 데이터를 서로 주고 받는다.

 

2. 스트림이란?

: 입출력 장치에서 데이터를 읽고 쓰기 위한 단방향 통로로, 자바에서 제공하는 클래스

- 각각의 장치마다 연결할 수 있는 스트림이 존재하며, 바이트 단위 처리와 문자 단위 처리를 위한 스트림 등이 존재한다.

- 스트림은 기본적으로 1바이트 단위의 데이터만 지나가게 되는데,

 주고 받는 데이터의 기본 단위가 1바이트이므로 단방향 처리만 가능하다.

 따라서 입력 스트림과 출력 스트림을 따로 구성해야 한다.

- 영어, 숫자, 특수기호들은 한 글자가 1바이트이지만, 한글과 같은 언어들은 한 글자를 2바이트 또는 3바이트로 표현한다. 인코딩 방식에 따라 정해지는데, 해당 인코딩 방식에 맞춰서 2바이트 또는 3바이트 단위를 구분하여 입출력하기 위해선 문자 단위를 처리할 수 있는 스트림을 활용해야 한다.

 

3. 입출력을 사용하는 이유

- 입출력을 사용함으로써 사용자로부터 입력을 받거나 화면이나 스피커로 출력해줄 수 있다. 또한 파일 형태로 프로그램의 종료 여부와 상관없이 영구적으로 데이터를 저장할 수도 있다.

 


 

파일 관련 입출력

1. 파일 클래스란?

: 파일 시스템의 파일을 다루기 위한 클래스

- 파일의 크기나 속성, 이름 등의 정보를 확인할 수 있고, 파일 생성 및 삭제 기능 등을 제공한다.

(1) 파일 클래스 관련 메소드

반환 타입 메소드 설명
boolean createNewFile() 새로운 파일 생성
boolean mkdir() 새로운 디렉토리 생성
boolean mkdirs() 경로 상에 없는 모든 디렉토리 생성
boolean delete() 파일 또는 디렉토리 삭제
boolean canExecute() 실행할 수 있는 파일인지 여부
boolean canRead() 읽을 수 있는 파일인지 여부
boolean canWrite() 수정/저장할 수 있는 파일인지 여부
String getName() 파일 이름 반환
String getParent() 부모 디렉토리 반환
File getParentFile() 부모 디렉토리를 File 객체로 생성 후 반환
String getPath() 전체 경로 반환
boolean isDirectory() 디렉토리인지 여부
boolean isFile() 파일인지 여부
boolean isHidden() 숨김 파일인지 여부
long lastModified() 마지막 수정 날짜 및 시간 반환
long length() 파일 크기 리턴
 /* 대상 파일이 존재하지 않더라도 인스턴스를 생성할 수 있다. */
File file = new File("test.txt");

try {
    /* 파일 생성 후 성공 실패 여부 boolean 반환 */
    boolean createSuccess = file.createNewFile();
            
    /* 최초 실행 시 파일이 새롭게 생성 되어 true, 파일이 존재할 경우 새롭게 만들지 않아 false 반환 */
    System.out.println("createSuccess: " + createSuccess);
} catch (IOException e) {
    throw new RuntimeException(e);
}

System.out.println("파일의 크기: " + file.length());
System.out.println("파일의 경로: " + file.getPath());
System.out.println("현재 파일의 상위 경로: " + file.getParent());
System.out.println("파일의 절대 경로: " + file.getAbsolutePath());
        
/*파일 삭제(삭제 후 성공 여부를 boolean으로 반환)*/
boolean deleteSuccess = file.delete();
System.out.println("deleteSuccess: " + deleteSuccess);

 

2.  파일 입출력 관련 기반 스트림의 종류

(1) 바이트 단위(영어, 숫자, 특수기호 사용 시)

1) FileInputStream

 

FileInputStream fis = null;
try {
    /* 읽어올 대상 파일이 존재하지 않는 경우에 대해서 FileNotFoundException 처리 필요 */
    fis = new FileInputStream("testInputStream.txt");

    /* read() : 파일에 기록 된 값을 순차적으로 읽어오고 더 이상 읽어올 데이터가 없으면 -1 반환 */
    int value;
    while((value = fis.read()) != -1) {
        System.out.print((char)value);
    }

    /* byte 배열을 이용해 한 번에 읽어올 수 있다. */
    byte[] bar = new byte[100];
    fis.read(bar);
    System.out.println(Arrays.toString(bar));

}  catch (IOException e) {
    System.out.println(e.getMessage());
} finally {
    if(fis != null) {
        try {
            // 자원 반납
            fis.close();
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}

 

* 자원 반납이 필요한 이유

1. 장기간 실행 중인 프로그램에서 스트림을 닫지 않으면 누수(leak) 발생 가능성

2. 버퍼의 잔류 데이터가 남은 상태에서 추가로 스트림을 사용하면 데드락(deadlock) 발생 가능성

 

2) FileOutputStream

/* 경로 문제 없이 파일이 존재하지 않을 경우에는 자동으로 파일이 생성 되지만
* 존재하지 않는 경로가 포함 되면 FileNotFoundException이 발생한다.
* 두 번째 인자로 true를 추가하면 존재하는 파일의 맨 끝에서부터 추가 된다. */
try(FileOutputStream fos = new FileOutputStream("testOutputStream.txt", true)) { // try with resource 문
    // write() 메소드는 IOException을 핸들링해야 한다.
    fos.write(97);

    byte[] bytes = new byte[] {98, 99, 100, 101, 102, 10};  // 10은 개행문자(엔터)이다.
    fos.write(bytes);

    fos.write(bytes, 1, 3);

} catch (IOException e) {
    System.out.println(e.getMessage());
}

- testOutputStream.txt의 결과는 아래와 같다.

abcdef
cde

 

(2) 문자 단위(한글까지 사용 시)

1) FileReader

// FileInputStream과 사용법은 거의 동일하나 1byte 단위가 아니라 character 단위로 읽어온다. */

try (FileReader fr = new FileReader("testReader.txt")) {
     int value;
     
     while ((value = fr.read()) != -1) {
          System.out.print((char) value);
    }

    char[] carr = new char[100];
    fr.read(carr);

    for(char c : carr) System.out.print(c);
    
} catch (IOException e) {
    System.out.println(e.getMessage());
}

 

2) FileWriter

try (FileWriter fw = new FileWriter("testWriter.txt")) {
    fw.write(97);
    fw.write('A');
    fw.write(new char[] {'a', 'b', 'c', 'd', 'e', 'f'});
    fw.write("안녕하세요");
} catch (IOException e) {
    System.out.println(e.getMessage());
}

 

3. 파일 입출력 관련 보조 스트림의 종류

- 기반 스트림에 추가되어 스트림의 기능 향상 또는 기능 추가를 위해 사용한다.

- 보조 스트림만으로 는 입출력 처리가 불가능하고, 기반 스트림에 추가로 적용돼야 한다.

(1) 입출력 성능 향상(BufferedReader/BufferedWriter)

- 버퍼 공간을 이용하여 데이터를 쌓아 두었다가 입출력하여 입출력 횟수를 줄이고 성능을 향상시킨다.

1) BufferedReader

try(BufferedReader reader = new BufferedReader(new FileReader("testBuffered.txt"))) {

    /* readLine() 메소드 기능을 추가로 제공하고 있다.
     * 버퍼의 한 줄을 읽어와서 문자열로 반환하며 더 이상 읽어올 값이 없을 경우 null이 반환 된다. */
     String temp;
     while((temp = reader.readLine()) != null) {
        System.out.println(temp);
     }

} catch (IOException e) {
    System.out.println(e.getMessage());
}

 

2) BufferedWriter

BufferedWriter writer = null;

try {
    /* 보조 스트림 객체 생성 시에는 생성자의 인자로 연결 될 기반 스트림을 전달한다. */
    writer = new BufferedWriter(new FileWriter("testBuffered.txt"));

    writer.write("안녕하세요.\n");
    writer.write("반갑습니다.\n");

    /* 버퍼를 이용하는 경우 버퍼가 가득 차면 자동으로 내보내기를 하지만 
    가득 차지 않은 경우 flush() 로 내보내기를 해야 출력이 이루어진다. */
    // writer.flush();

} catch (IOException e) {
    System.out.println(e.getMessage());
} finally {
    if(writer != null) {
        try {
        /* close()를 호출하면 내부적으로 flush()를 실행하고 난 뒤 자원을 반납한다. */
            writer.close();
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}

 

 

(2) 형변환 보조스트림(InputStreamReader/OutputStreamWriter)

-  기반 스트림이 byte 스트림, 보조 스트림이 char 스트림인 경우 사용한다.

   cf. InputStreamReader, OutputStreamWriter는 byte 스트림, BufferedReader, BufferedWriter는 char 스트림이다.

* 표준 스트림

- 자바에서는 콘솔, 키보드 같은 표준 입출력 장치로부터 데이터를 입출력하기 위한 스트림을 System.in, System.out, System.err와 같은 형태로 제공하고 있다. -> 자주 사용되는 자원에 대해 미리 생성해둔 스트림으로, 별도로 생성하지 않아도 된다.

1) InputStreamReader

try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {

    System.out.print("문자열 입력 : ");
    String value = br.readLine();

    System.out.println("입력 받은 문자열 : " + value);

} catch (IOException e) {
    System.out.println(e.getMessage());
}

 

2) OutputStreamWriter

try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out))) {
    bw.write("출력 테스트");
} catch (IOException e) {
    System.out.println(e.getMessage());
}

 

(3) 기본 자료형 데이터 입출력 스트림(DataInputStream/DataOutputStream)

1) DataInputStream

try (DataInputStream dis = new DataInputStream(new FileInputStream("score.txt"))) {
    while(true) {
        /* 파일에 기록한 순서 대로 읽어오지 않는 경우 에러가 발생하거나 의미 없는 데이터를 읽어오게 된다. */
        System.out.println(dis.readUTF());
        System.out.println(dis.readInt());
        System.out.println(dis.readChar());
    }

/* read자료형() 메소드는 파일에서 더 이상 읽어올 값이 없는 경우 EOFException(End Of File) 발생 */
} catch (EOFException e) {
    System.out.println("파일 읽기가 완료 되었습니다.");
} catch (IOException e) {
    System.out.println(e.getMessage());
}

2) DataOutputStream

try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("score.txt"))) {
    dos.writeUTF("홍길동");
    dos.writeInt(95);
    dos.writeChar('A');

    dos.writeUTF("유관순");
    dos.writeInt(85);
    dos.writeChar('B');

    dos.writeUTF("이순신");
    dos.writeInt(75);
    dos.writeChar('C');

} catch (IOException e) {
    System.out.println(e.getMessage());
}

 

(4) 객체 자료형 데이터 입출력(ObjectInputStream/ObjectOutputStream)

- 객체 입출력 처리를 위해서는 반드시 직렬화 처리를 해주어야 한다. 직렬화객체를 바이트 스트림으로 변환하는 것이다. (직렬화 처리를 해주지 않으면 NotSerializableException이 발생한다.)

- 직렬화 대상 클래스에 Serializable 인터페이스만 구현하면 직렬화가 필요한 경우 데이터 직렬화 처리가 된다. (implements Serializable)

/* 객체 입출력 처리를 위해서는 반드시 직렬화 처리를 해야 한다.
* 직렬화 대상 클래스에 Serializable 인터페이스만 구현하면 직렬화가 필요한 상황인 경우 데이터 직렬화 처리가 된다. */
public class MemberDTO implements java.io.Serializable {

    private String id;
    private String pwd;
    private int age;
    private char gender;
    /* 해당 필드는 직렬화 처리에서 제외하겠다는 의미 */
    private transient double point;

    public MemberDTO(){}

    public MemberDTO(String id, String pwd, int age, char gender, double point) {
        this.id = id;
        this.pwd = pwd;
        this.age = age;
        this.gender = gender;
        this.point = point;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public char getGender() {
        return gender;
    }

    public void setGender(char gender) {
        this.gender = gender;
    }

    public double getPoint() {
        return point;
    }

    public void setPoint(double point) {
        this.point = point;
    }

    @Override
    public String toString() {
        return "MemberDTO{" +
                "id='" + id + '\'' +
                ", pwd='" + pwd + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", point=" + point +
                '}';
    }
}


public class Application4 {
    public static void main(String[] args) {
        MemberDTO[] members = {
                new MemberDTO("user01", "pass01", 25, '남', 95.7),
                new MemberDTO("user02", "pass02", 30, '여', 84.2),
                new MemberDTO("user03", "pass03", 35, '여', 72.6),
        };

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("member.txt"))) {

            for(MemberDTO member: members) {
               // MemberDTO 타입 객체를 하나씩 출력
                oos.writeObject(member);
            }

            // MemberDTO[] 타입 객체를 출력
            // oos.writeObject(members);

        } catch (IOException e) {
            e.printStackTrace();
        }

        MemberDTO[] inputMembers = new MemberDTO[members.length];

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("member.txt"))) {

            for(int i = 0; i < inputMembers.length; i++) {
                inputMembers[i] = (MemberDTO) ois.readObject();
            }

        }  catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            /* 역직렬화(바이트 스트림 -> 객체) 시 해당하는 클래스가 없으면 실패하므로 Exception handling이 필수이다. */
            e.printStackTrace();
        }

        for(MemberDTO member: inputMembers) {
            System.out.println(member);
        }

    }
}