Java - I/O 활용

INHEES·2025년 4월 20일

Java

목록 보기
12/13

마지막 포스팅에 이어 시간이 조금 지났지만 금일은 자바 입출력의 활용에 대해 알아보는 시간이다.

목차

  • memory, in file
  • DataStream, ObjectStream
  • xml, json, database

코드 구현에 앞서 사용하게 될 회원과 다형성을 활용한 인터페이스를 만들어주자. (기본)생성자와 getter, setter 는 생략하겠습니다.

Member

public class Member implements Serializable {
    private String id;
    private String name;
    private Integer age;

MemberRepository

public interface MemberRepository {
    void add(Member member);

    List<Member> findAll();
}

MemberConsoleMain
다음과 같이 의존성을 주입 받으면서 목차를 따라가보겠습니다.

    private static final MemberRepository repository = new MemoryMemberRepository();
    //private static final MemberRepository repository = new FileMemberRepository();
    //private static final MemberRepository repository = new DataMemberRepository();
    //private static final MemberRepository repository = new ObjectMemberRepository();

Memory

단순하게 함수 구현체를 만들어 보겠습니다.

public class MemoryMemberRepository implements MemberRepository {

    private final List<Member> members = new ArrayList<>();

    @Override
    public void add(Member member) {
        members.add(member);
    }

    @Override
    public List<Member> findAll() {
        return members;
    }
}
  • Memory 는 내부 컬렉션에 저장, 조회하는 방법이다.
  • 당연한 소리지만 프로그램 종료 시에는 모든 회원 데이터가 사라집니다.

Store in File

한 줄 단위로 처리할 때는 BufferedReader 가 유용하므로 BufferedReader , BufferedWriter 를 사용합니다.

public class FileMemberRepository implements MemberRepository {

    private static final String FILE_PATH = "temp/members-txt.dat";
    private static final String DELIMITER = ",";

    @Override
    public void add(Member member) {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(FILE_PATH, UTF_8, true))) {
            bw.write(member.getId() + DELIMITER + member.getName() + DELIMITER + member.getAge());
            bw.newLine();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<Member> findAll() {
        List<Member> members = new ArrayList<>();
        try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH, UTF_8))) {
            String line;
            while ((line = br.readLine()) != null) {
                String[] memberData = line.split(DELIMITER);
                members.add(new Member(memberData[0], memberData[1], Integer.valueOf(memberData[2])));
            }
            return members;
        } catch (FileNotFoundException e) {
            return new ArrayList<>();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • new FileWriter(FILE_PATH, UTF_8, true) append 값을 true 설정을 통해 파일의 경로를 계속 추가해준다.
  • try-with-resources 구문을 사용해서 자동으로 자원을 정리한다. try 코드 블록이 끝나면 자동으로 close()
    가 호출되면서 자원을 정리한다.
  • line = br.readLine() 을 통해 각 회원 하나하나를 불러온다.
  • FileNotFoundException e 회원 데이터가 하나도 없을 때는 temp/members-txt.dat 파일이 존재하지 않는다. 따라서 해당 예외
    가 발생한다. 이 경우 회원 데이터가 하나도 없는 것으로 보고 빈 리스트를 반환한다.

DataStream

  • 자바의 타입을 그대로 사용하면서 파일에 데이터를 저장하고 불러올 수 있고, 구분자
    도 사용하지 않아도 된다.
    private static final String FILE_PATH = "temp/members-data.dat";

    @Override
    public void add(Member member) {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(FILE_PATH, true))) {
            dos.writeUTF(member.getId());
            dos.writeUTF(member.getName());
            dos.writeInt(member.getAge());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<Member> findAll() {
        List<Member> members = new ArrayList<>();
        try (DataInputStream dis = new DataInputStream(new FileInputStream(FILE_PATH))) {
            while (dis.available() > 0) {
                Member member = new Member(dis.readUTF(), dis.readUTF(), dis.readInt());
                members.add(member);
            }
            return members;
        } catch (FileNotFoundException e) {
            return new ArrayList<>();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
  • readUTF() 로 문자를 읽어올 때 어떻게 id1 이라는 3글자만 정확하게 읽어올 수 있는 것일까?
  • writeUTF() 은 UTF-8 형식으로 문자를 저장하는데, 저장할 때 2byte를 추가로 사용해서 앞에 글자의 길이를
    저장해둔다. (65535 길이까지만 사용 가능)
  • dis.available() 읽어올 스트림이 있는지 확인하는 코드이다.
  • dos.writeUTF("id1") -> 해당 코드는 3id1(2byte(문자 길이) + 3byte) 다음과 같이 저장되며 문자와 byte 가 섞여 있는 모습이다.
  • 모든 데이터를 문자로 저장할 때 보다 저장 용량도 최적화 가능하다. (writeInt, readInt)

ObjectStream

ObjectStream 을 사용하면 이렇게 메모리에 보관되어 있는 회원 인스턴스를 파일에 편리하게 저장할 수 있다.

객체 직렬화

Serialization 은 메모리에 있는 객체 인스턴스를 바이트 스트림으로 변환하여 파일에 저장 또는 네트워크를 통해 전송합니다. 반대의 과정에서는 Deserialization 을 통해 원래의 객체로 복원 한다.

때문에 객체 에 implements Serializable 을 추가해준다.

Member 의 컬렉션 자체를 저장해주는 역할을 한다.

    private static final String FILE_PATH = "temp/members-obj.dat";

    @Override
    public void add(Member member) {
        List<Member> members = findAll();
        members.add(member);

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) {
            oos.writeObject(members);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<Member> findAll() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH))) {
            Object findObject = ois.readObject();
            return (List<Member>) findObject;
        } catch (FileNotFoundException e) {
            return new ArrayList<>();
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
  • ObjectOutputStream 를 사용하면 객체 인스턴스를 직렬화해서 byte로 변경할 수 있다.
  • oos.writeObject(members) 를 호출하면 members 컬렉션과 그 안에 포함된 Member 를 모두 직렬화해
    서 byte로 변경한다.
  • return (List<Member>) findObject 반환 타입이 Object 이므로 캐스팅이 필요합니다.

참고
transient 키워드: transient 가 붙어있는 필드는 직렬화 하지 않고 무시한다.

객체 직렬화는 한계가 존재하기 때문에 사용하지 않는 이유들이 있다.

  • 버전 관리의 어려움

    • 클래스 구조가 변경되면 객체와의 호환성 문제가 발생한다.
  • 플랫폼 족성성, 성능 이슈, 유연성 부족, 크기 효율성의 문제


xml, json, db

해당 소제목은 객체 직렬화를 위한 대안이다.

플랫폼 종속성 문제를 해결하기 위해 2000년대 초반에 XML이라는 기술이 인기를 끌었다.

  • 하지만 복잡성과 무거움이라는 문제가 존재한다.

JSON은 가볍고 간결하며, 웹 API와 RESTful 서비스가 대중화되면서 JSON은 표준 데이터 교환 포맷으로 자리 잡았다.

만약 매우 작은 용량으로 더 빠른 속도가 필요하다면 Protobuf, Avro 같은 대안 기술이 있다

  • 이런 기술은 호환성은 떨어지지만 byte 기반에, 용량과 성능 최적화가 되어 있으므로 매우 빠르다.
  • 다만 byte 기반이므로 JSON처럼 사람이 직접 읽기는 어렵다.

정리하자면 대부분 JSON 만 사용해도 충분하다.


하지만 Json 의 무결성, 관리의 비효율성, 보안문제, 백업과 복구의 문제로 대부분의 현대 애플리케이션에서는 데이터베이스를 사용한다. 데이터베이스는 위의 한계들을
극복하고, 대량의 데이터를 효율적으로 저장, 관리, 검색할 수 있는 강력한 도구를 제공한다.

profile
이유를 찾아보자

0개의 댓글