728x90
반응형
SMALL

참고자료

 

김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션 강의 | 김영한 - 인프런

김영한 | I/O, 네트워크, 리플렉션, 애노테이션을 기초부터 실무 레벨까지 깊이있게 학습합니다. 웹 애플리케이션 서버(WAS)를 자바로 직접 만들어봅니다., 국내 개발 분야 누적 수강생 1위, 제대로

www.inflearn.com

 

이전 포스팅에서 여러가지 스트림을 배워봤다. 그리고 마지막 즈음에 말했던 DataStream은 파일에 자바 타입 그대로를 저장할 수 있는 꽤나 신기하지만? 어디에 이게 활용이 될까?싶은 스트림이 있었다. 이 포스팅에서는 이 녀석의 활용처를 알아보고자 한다.

 

일단 그러기에 앞서, 예제를 만들고 왜 이걸 사용하면 좋은지를 먼저 알아보자.

 

회원 관리 예제1 - 메모리

I/O를 사용해서 회원 데이터를 관리하는 예제를 만들어보자.

 

요구사항

  • 회원 관리 프로그램을 작성해라.
  • 회원의 속성은 다음과 같다
    • ID
    • Name
    • Age
  • 회원을 등록하고, 등록한 회원의 목록을 조회할 수 있어야 한다.

 

프로그램 작동 예시

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 1
ID 입력: id1
Name 입력: name1
Age 입력: 20
회원이 성공적으로 등록되었습니다.

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 1
ID 입력: id2
Name 입력: name2
Age 입력: 30
회원이 성공적으로 등록되었습니다.

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 2
회원 목록:
[ID: id1, Name: name1, Age: 20] 
[ID: id2, Name: name2, Age: 30]

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 3
프로그램을 종료합니다.

 

Member

package cwchoiit.io.member;

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

    public Member() {
    }

    public Member(String id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Member{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 

MemberRepository

package cwchoiit.io.member;

import java.util.List;

public interface MemberRepository {
    void add(Member member);

    List<Member> findAll();
}
  • add(): 회원 객체를 저장한다.
  • findAll(): 저장한 회원 객체를 List로 모두 조회한다.
  • Repository는 저장소라는 뜻이다.

MemoryMemberRepository

package cwchoiit.io.member.impl;

import cwchoiit.io.member.Member;
import cwchoiit.io.member.MemberRepository;

import java.util.ArrayList;
import java.util.List;

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;
    }
}
  • 간단하게 메모리에 회원을 저장하고 관리하자.
  • 회원을 저장하면 내부에 있는 members 리스트에 회원이 저장된다.
  • 회원을 조회하면 members 리스트가 반환된다.

MemberConsoleMain

package cwchoiit.io.member;

import cwchoiit.io.member.impl.FileMemberRepository;
import cwchoiit.io.member.impl.MemoryMemberRepository;

import java.util.List;
import java.util.Scanner;

public class MemberConsoleMain {

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

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("1. 회원 등록 | 2. 회원 목록 조회 | 3. 종료");
            System.out.print("선택: ");
            int choice = scanner.nextInt();
            scanner.nextLine();

            switch (choice) {
                case 1:
                    // 회원 등록
                    registerMember(scanner);
                    break;
                case 2:
                    // 회원 목록 조회
                    displayMembers();
                    break;
                case 3:
                    System.out.println("프로그램을 종료합니다.");
                    return;
                default:
                    System.out.println("잘못된 선택입니다. 다시 입력하세요.");
            }
        }
    }

    private static void registerMember(Scanner scanner) {
        System.out.print("ID 입력:");
        String id = scanner.nextLine();

        System.out.print("Name 입력:");
        String name = scanner.nextLine();

        System.out.print("Age 입력:");
        int age = scanner.nextInt();
        scanner.nextLine();

        Member member = new Member(id, name, age);
        repository.add(member);
        System.out.println("회원이 성공적으로 등록되었습니다.");
    }

    private static void displayMembers() {
        List<Member> members = repository.findAll();
        System.out.println("회원 목록");
        for (Member member : members) {
            System.out.printf("[ID: %s, Name: %s, Age: %d]\n", member.getId(), member.getName(), member.getAge());
        }
    }
}
  • 콘솔을 통해 회원 등록, 목록 조회 기능을 제공한다.

실행 결과 - 회원 등록

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 1
ID 입력: id1
Name 입력: name1
Age 입력: 20
회원이 성공적으로 등록되었습니다.

 

실행 결과 - 목록 조회

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 2
회원 목록:
[ID: id1, Name: name1, Age: 20]

 

실행 결과 - 종료

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 3
프로그램을 종료합니다.

 

 

문제점

의도대로 잘 구현이 됐다. 그런데 이 프로그램의 문제가 있다. 어떤거냐면, 데이터를 메모리에 보관하기 때문에, 자바를 종료하면 모든 회원 종료가 사라진다. 따라서 프로그램을 다시 실행하면 모든 회원 데이터가 사라진다. 프로그램을 종료하고 다시 실행해도 회원 데이터가 영구 보존되어야 한다.

 

회원 관리 예제2 - 파일에 보관

회원 데이터를 영구 보존하려면 파일에 저장하면 된다. 다음과 같이 한 줄 단위로 회원 데이터를 파일에 저장해보자.

temp/members-txt.dat

id1,member1,20
id2,member2,30
  • 여기서는 문자를 파일에 저장한다. 문자를 다루므로 Reader, Writer를 사용하는 것이 편리하다.
  • 한 줄 단위로 처리할 때는 BufferedReader가 유용하므로, BufferedReader, BufferedWriter를 사용해보자.

FileMemberRepository

package cwchoiit.io.member.impl;

import cwchoiit.io.member.Member;
import cwchoiit.io.member.MemberRepository;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;

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("Fail to write member data to file.", 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("Fail to read member data from file.", e);
        }
    }
}
  • MemberRepository 인터페이스가 잘 정의되어 있으므로, 인터페이스를 기반으로 파일에 회원 데이터를 보관하는 구현체를 만들면 된다.
  • DELIMITER: 회원 데이터는 id1,member1,20과 같이 ,(쉼표)로 구분한다.

참고: 빈 컬렉션 반환

  • 빈 컬렉션을 반환할 때는 new ArrayList()보다는 List.of()를 사용하는 것이 좋다. 그런데, 뒤에서 사용할 ObjectStream 부분과 내용을 맞추기 위해 빈 컬렉션에 new ArrayList()를 사용했다.

회원 저장

bw.write(member.getId() + DELIMITER + member.getName() + DELIMITER + member.getAge());
bw.newLine();
  • 회원 객체의 데이터를 읽어서, String 문자로 변환한다. 여기서 write()String으로 입력받는다. 그리고 DELIMITER를 구분자로 사용한다.
  • bw.write("id1,member1,20")를 통해 저장할 문자가 전달된다.
  • 회원 데이터를 문자로 변경한 다음에 파일에 보관한 것이다.
  • 각 회원을 구분하기 위해 newLine()을 통해 다음 줄로 이동한다. 

회원 조회

  • line = br.readLine()line = "id1,member1,20"과 같이 하나의 회원 정보가 담긴 한 줄 문자가 입력된다.
  • String[] memberData = line.split(DELIMITER) → 회원 데이터를 DELIMITER 구분자로 구분해서 배열에 담는다.
  • members.add(new Member(memberData[0], memberData[1], Integer.valueOf(memberData[2])) → 파일에 읽은 데이터를 기반으로 회원 객체를 생성한다. id, nameString이기 때문에 타입이 같다. age의 경우 문자 20으로 조회했기 때문에 숫자인 Integer로 변경해야 한다.
  • FileNotFoundException e → 회원 데이터가 하나도 없을 때는 temp/members-txt.dat 파일이 존재하지 않는다. 따라서 해당 예외가 발생하기 때문에 이 경우, 회원 데이터가 하나도 없는 것으로 보고 빈 리스트를 반환한다.

try-with-resources

try-with-resources 구문을 사용해서 자동으로 자원을 정리한다. try 코드 블록이 끝나면 자동으로 close()가 호출되면서 자원을 정리한다.

try (BufferedWriter bw = new BufferedWriter(new FileWriter(FILE_PATH, UTF_8, true))) {...}
try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH, UTF_8))) {...}

 

MemberConsoleMain - FileMemberRepository 사용

package cwchoiit.io.member;

import cwchoiit.io.member.impl.FileMemberRepository;
import cwchoiit.io.member.impl.MemoryMemberRepository;

import java.util.List;
import java.util.Scanner;

public class MemberConsoleMain {

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

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("1. 회원 등록 | 2. 회원 목록 조회 | 3. 종료");
            System.out.print("선택: ");
            int choice = scanner.nextInt();
            scanner.nextLine();

            switch (choice) {
                case 1:
                    // 회원 등록
                    registerMember(scanner);
                    break;
                case 2:
                    // 회원 목록 조회
                    displayMembers();
                    break;
                case 3:
                    System.out.println("프로그램을 종료합니다.");
                    return;
                default:
                    System.out.println("잘못된 선택입니다. 다시 입력하세요.");
            }
        }
    }

    private static void registerMember(Scanner scanner) {
        System.out.print("ID 입력:");
        String id = scanner.nextLine();

        System.out.print("Name 입력:");
        String name = scanner.nextLine();

        System.out.print("Age 입력:");
        int age = scanner.nextInt();
        scanner.nextLine();

        Member member = new Member(id, name, age);
        repository.add(member);
        System.out.println("회원이 성공적으로 등록되었습니다.");
    }

    private static void displayMembers() {
        List<Member> members = repository.findAll();
        System.out.println("회원 목록");
        for (Member member : members) {
            System.out.printf("[ID: %s, Name: %s, Age: %d]\n", member.getId(), member.getName(), member.getAge());
        }
    }
}
  • MemoryMemberRepository 대신에 FileMemberRepository를 사용하도록 코드를 수정하자.
  • MemberRepository 인터페이스를 사용한 덕분에 구현체가 변경되더라도 클라이언트의 다른 코드들은 변경하지 않아도 된다.

실행 결과

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 1
ID 입력: id1
Name 입력: name1
Age 입력: 20
회원이 성공적으로 등록되었습니다.

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 1
ID 입력: id2
Name 입력: name2
Age 입력: 30
회원이 성공적으로 등록되었습니다.

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 2
회원 목록:
[ID: id1, Name: name1, Age: 20]
[ID: id2, Name: name2, Age: 30]

1.회원 등록 | 2.회원 목록 조회 | 3.종료 
선택: 3
프로그램을 종료합니다.

 

결과 - temp/members-txt.dat

id1,member1,20
id2,member2,30

 

 

문제점

파일에 회원 데이터를 저장한 덕분에, 자바를 다시 실행해도 저장한 회원이 잘 조회된다. 위 문제를 해결한 것이다. 

그러나 아직도 남은 문제가 있다. 결국 파일에서 읽어오는 값은 문자열이기 때문에 멤버 객체에 나이를 저장할 때 문자를 숫자로 변환해줘야 한다. 예) Integer.valueOf(memberData[2])

 

이런 문제를 해결할 수 있는 좋은 방법은 DataStream을 사용하는 것이다.

 

회원 관리 예제3 - DataStream

앞서 배운 예시 중에 DataOutputStream, DataInputStream을 떠올려보자. 이 스트림들은 자바의 데이터 타입을 그대로 사용할 수 있다. 따라서 자바의 타입을 그대로 사용하면서 파일에 데이터를 저장하고 불러올 수 있고, 구분자도 사용하지 않아도 된다. 

DataMemberRepository

package cwchoiit.io.member.impl;

import cwchoiit.io.member.Member;
import cwchoiit.io.member.MemberRepository;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class DataMemberRepository implements MemberRepository {

    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);
        }
    }
}

 

회원 저장

dos.writeUTF(member.getId());
dos.writeUTF(member.getName());
dos.writeInt(member.getAge());
  • 회원을 저장할 때는 회원 필드의 타입에 맞는 메서드를 호출하면 된다.
  • 이전 예제에서는 각 회원을 한 줄 단위로 구분했는데, 여기서는 그런 구분이 필요없다.

회원 조회

Member member = new Member(dis.readUTF(), dis.readUTF(), dis.readInt());
  • 회원 데이터를 조회할 때는 회원 필드의 각 타입에 맞는 메서드를 사용해서 조회하면 된다.

 

MemberConsoleMain 수정 - DataMemberRepository 사용

public class MemberConsoleMain {

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

 

실행 결과는 파일이 잘 보관되고, 파일에는 문자와 byte가 섞여있다. 그래서 파일을 사람이 직접 읽기는 힘들지만, 자바에서 데이터를 타입에 맞게 가져오기엔 아주 최적화되어 있다. 

 

DataStream 원리

근데 궁금하다. 어떤 원리로 구분자나 한 줄 라인 없이 데이터를 저장하고 조회할 수 있을까? 

예를 들어 이렇게 우리가 회원을 저장했다.

dos.writeUTF(member.getId()); // id1
dos.writeUTF(member.getName()); // member1

 

그럼 분명 파일에는 `id1member1` 이렇게 저장이 될텐데, 어떻게 이후에 readUTF()를 호출하면 딱 id1이라는 3글자만 정확히 가져올 수 있을까? 사실은 writeUTF()는 UTF-8 형식으로 문자를 저장하는데, 저장할 때 2byte를 추가로 사용해서 앞에 글자의 길이를 저장해둔다. 그래서 실제로는 이렇게 저장이 된다.

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

기타 타입

dos.writeInt(20);
dis.readInt();
  • 자바의 int(Integer)4byte를 사용하기 때문에, 4byte를 사용해서 파일을 저장하고, 읽을 때도 4byte를 읽어서 복원한다.

 

저장 예시

dos.writeUTF("id1");
dos.writeUTF("name1");
dos.writeInt(20);
dos.writeUTF("id2");
dos.writeUTF("name2");
dos.writeInt(30);

저장된 파일 예시

3id1 (2byte(문자 길이) + 3byte) 
5name1 (2byte(문자 길이) + 5byte) 
20 (4byte)
3id2 (2byte(문자 길이) + 3byte) 
5name2 (2byte(문자 길이) + 5byte)
30 (4byte)
  • 이해를 돕기 위해 각 필드를 엔터로 구분했지만, 실제로는 엔터 없이 한 줄로 연결되어 있다. 
  • 저장된 파일은 실제로는 문자와 byte가 섞여있다.

정리

DataStream덕분에 자바의 타입도 그대로 사용하고, 구분자도 제거할 수 있었다. 추가로 모든 데이터를 문자로 저장할 때보다 저장 용량도 더 최적화할 수 있다. 예를 들어, 숫자 1,000,000,000(10억)을 문자로 저장하게 되면 총 10byte가 사용된다. 왜냐하면 숫자 하나하나를 문자로 저장해야 하기 때문에 ASCII 인코딩을 해도 각각 1byte가 사용된다. 하지만 이것을 자바의 int와 같이 4byte를 사용해서 저장한다면 4byte만 사용하게 된다. 여기서는 writeInt()를 사용하면 4byte를 사용해서 저장한다. 물론 이렇게 byte를 직접 저장하면, 문서 파일을 열어서 확인하고 수정하는 것이 어렵다는 단점도 있지만, 이러한 장점도 있다.

 

문제점

DataStream 덕분에 회원 데이터를 더 편리하게 저장할 수 있는것은 맞지만, 회원의 필드를 하나하나 다 조회해서 각 타입에 맞도록 따로따로 저장해야 한다. 이건 회원 객체를 저장한다기 보다는 회원 데이터 하나하나를 분류해서 따로 저장한 것이다.

dos.writeUTF(member.getId());
dos.writeUTF(member.getName());
dos.writeInt(member.getAge());

 

다시 처음으로 돌아와서 회원 객체를 자바 컬렉션에 저장하는 예를 보자.

MemoryMemberRepository

package cwchoiit.io.member.impl;

import cwchoiit.io.member.Member;
import cwchoiit.io.member.MemberRepository;

import java.util.ArrayList;
import java.util.List;

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;
    }
}

자바 컬렉션에 회원 객체를 저장할 때는 복잡하게 회원의 필드를 하나하나 꺼내서 저장할 필요가 없었다. 단순하게 회원 객체를 그대로 자바 컬렉션에 보관하면 된다. 조회할 때도 마찬가지다. 이렇게 편리하게 회원 객체를 저장할 수 있는 방법은 없을까?

 

회원 관리 예제4 - ObjectStream

회원 인스턴스도 생각해보면 메모리 어딘가에 보관되어 있다. 이렇게 메모리에 보관되어 있는 객체를 읽어서 파일에 저장하기만 하면 아주 간단하게 회원 인스턴스를 저장할 수 있을 것 같다. ObjectStream을 사용하면 이렇게 메모리에 보관되어 있는 회원 인스턴스를 파일에 편리하게 저장할 수 있다. 마치 자바 컬렉션에 회원 객체를 보관하듯 말이다.

 

객체 직렬화

자바 객체 직렬화(Serialization)는 메모리에 있는 객체 인스턴스를 바이트 스트림으로 변환하여 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 하는 기능이다. 이 과정에서 객체의 상태를 유지하여 나중에 역직렬화(Deserialization)를 통해 원래의 객체로 복원할 수 있다. 객체 직렬화를 사용하려면 직렬화하려는 클래스는 반드시 Serializable 인터페이스를 구현해야 한다.

package java.io;

public interface Serializable {
}
  • 이 인터페이스는 아무런 기능이 없다. 단지 직렬화 가능한 클래스라는 것을 표시하기 위한 인터페이스일 뿐이다. 
  • 메서드 없이 단지 표시가 목적인 인터페이스를 마커 인터페이스라 한다.

Member - Serializable 추가

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

    public Member() {
    }

    public Member(String id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
	
    ...
}
  • Member 클래스에 Serializable을 구현했다.
  • 이제 이 클래스의 인스턴스는 직렬화될 수 있다.

만약, 해당 인터페이스가 없는 객체를 직렬화하면 다음과 같은 예외가 발생한다.

java.io.NotSerializableException: io.member.Member

 

ObjectMemberRepository

package cwchoiit.io.member.impl;

import cwchoiit.io.member.Member;
import cwchoiit.io.member.MemberRepository;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class ObjectMemberRepository implements MemberRepository {

    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로 변경할 수 있다.
  • 우리는 회원 객체 하나가 아니라 회원 목록 전체를 파일에 저장해야 하므로, members 컬렉션을 직렬화해야 한다. 
  • oos.writeObject(members)를 호출하면 members 컬렉션과 그 안에 포함된 Member를 모두 직렬화해서 byte로 변경한다. 그리고 oos와 연결되어 있는 FileOutputStream에 결과를 출력한다. 
  • 참고로, ArrayListjava.io.Serializable을 구현하고 있어서 직렬화할 수 있다.
  • ObjectInputStream을 사용하면, byte를 역직렬화해서 객체 인스턴스로 만들 수 있다. ois.readObject()를 사용하면 역직렬화가 된다. 이때 반환 타입이 Object이므로 캐스팅해서 사용해야 한다. 
  • 그리고, 파일이 없는 경우 FileNotFoundException이 발생하기 때문에 빈 배열을 반환해줘야 하는데 이 경우 List.of()가 더 효율이 좋다. Immutable이기 때문에 ArrayList와 달리 가변 메모리를 사용하지 않기 때문에 메모리 최적화가 가능하기 때문이다. 그러나 여기서는 new ArrayList<>();를 반환했다. 그 이유는 add() 메서드에서 findAll()을 통해 빈 배열을 받은 경우든 아니든 새로운 멤버를 추가해줘야 하는데 Immutable이면 리스트에 요소를 추가할 수 없기 때문이다. 

MemberConsoleMain 수정 - ObjectMemberRepository 사용

public class 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();
    
    ...
    
}

 

실행 결과 - temp/members-obj.dat

파일이 정상 보관된다. 문자와 byte가 섞여 있다.

 

 

정리

객체 직렬화 덕분에 객체를 매우 편리하게 저장하고 불러올 수 있었다. 객체 직렬화를 사용하면 객체를 바이트로 변환할 수 있어, 모든 종류의 스트림에 전달할 수 있다. 이는 파일에 저장하는 것은 물론, 네트워크를 통해 객체를 전송하는 것도 가능하게 한다. 이러한 특성 때문에 초기에는 분산 시스템에서 활용되었다. 그러나 객체 직렬화는 1990년대에 등장한 기술로, 초창기에는 인기가 있었지만 시간이 지나면서 여러 단점이 드러났다. 또한 대안 기술이 등장하면서 점점 그 사용이 줄어들게 되었다. 현재는 객체 직렬화를 거의 사용하지 않는다. 

 

그래서, 결국 지금까지의 흐름을 살펴보면, 처음에는 스트림을 사용해서 문자를 바이트로 변환해서 파일에 저장했고, 그 과정에서 파일에 있는 데이터를 문자로 변환했을 때 객체로 변환하려면 타입을 변환해주어야 하고 구분자를 통해 각 필드를 뽑아내야 하는 번거로움이 있었다. 그래서 FileWriter, FileReader의 불편함을 극복하고자, DataStream을 사용하게 됐다. 이 스트림은 자바의 데이터 타입을 그대로 사용해서 파일에 저장할 수 있었다. 그래서 파일의 데이터를 읽어올 때 그 데이터 타입 그대로로 가져오기 때문에 형변환과 같은 작업이나 구분자가 따로 필요하지 않아 더 편리했지만, 객체를 저장하는 느낌이 아니라 객체의 각각 필드를 하나씩 나누어 저장하는 느낌이 강했다. 그래서 이것을 해결하고자 ObjectStream을 사용하게 됐다. 객체 인스턴스 그 자체를 바이트로 변환할 수 있게 직렬화 가능한 객체로 변환하고 그 오브젝트를 그대로 바이트로 변환해서 파일에 집어넣을 수 있었다. 

 

 

 

그런데, 위에서 직렬화는 이제 거의 사용하지 않는다고 했는데 그 이유는 무엇이고 그럼 어떤 방식을 사용할까?

XML, JSON, 데이터베이스

회원 객체와 같은 구조화 된 데이터를 컴퓨터 간에 서로 주고 받을 때 사용하는 데이터 형식이 어떻게 발전해왔는지 알아보자.

객체 직렬화의 한계

  • 버전 관리의 어려움: 클래스 구조가 변경되면 이전에 직렬화된 객체와의 호환성 문제가 발생한다.
  • 플랫폼 종속성: 자바 직렬화는 자바 플랫폼에 종속적이어서, 다른 언어나 시스템과의 상호 운용성이 떨어진다.
  • 성능 이슈: 직렬화/역직렬화 과정이 상대적으로 느리고 리소스를 많이 사용한다. 

 

객체 직렬화 대안1 - XML

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

플랫폼 종속성 문제를 해결하기 위해 2000년대 초반에 XML이라는 기술이 인기를 끌었다. 하지만 XML은 매우 유연하고 강력했어도 복잡성무거움이라는 문제가 있었다. 태그를 포함한 XML 문서의 크기가 커서 네트워크 전송 비용도 증가했다.

 

객체 직렬화 대안2 - JSON

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

JSON은 가볍고 간결하며, 자바스크립트와의 자연스러운 호환성 때문에 웹 개발자들 사이에서 빠르게 확산되었다. 2000년대 후반, 웹 API와 Restful 서비스가 대중화되면서 JSON은 표준 데이터 교환 포맷으로 자리잡았다. XML이 특정 영역에서 여전히 사용되고는 있으나, JSON이 현대 소프트웨어 개발의 주류로 자리 잡았다. 지금은 웹 환경에서 데이터를 교환할 때 JSON이 사실상 표준이다.

 

데이터베이스

앞서 설명한 것처럼 회원 객체같은 구조화된 데이터를 주고받을 때는 JSON 형식을 주로 사용한다. 그러나 어떤 형식이든 데이터를 저장할 때, 파일에 데이터를 직접 저장하는 방식은 몇가지 큰 한계가 있다.

  • 첫째, 데이터의 무결성을 보장하기 어렵다. → 여러 사용자가 동시에 파일을 수정하거나 접근하려고 할 때, 데이터의 충돌이나 손상 가능성이 높아진다. 이러한 경우, 데이터의 일관성을 유지하는 것이 매우 어렵다.
  • 둘째, 데이터 검색과 관리의 비효율성이다. → 파일에 저장된 데이터는 특정 형식 없이 단순히 저장될 수 있기 때문에, 필요한 데이터를 빠르게 찾는데 많은 시간이 소요될 수 있다. 특히 데이터의 양이 방대해질수록 검색 속도는 급격히 저하된다.
  • 셋째, 보안 문제이다. → 파일 기반 시스템에서는 민감한 데이터를 안전하게 보호하기 위한 접근 제어와 암호화등이 충분히 구현되어 있지 않을 수 있다. 결과적으로 데이터의 유출이나 무단 접근의 위험이 커질 수 있다.
  • 넷째, 대규모 데이터의 효율적인 백업과 복구가 필요하다.

이러한 문제점들을 하나하나 해결하면서 발전한 서버 프로그램이 바로 데이터베이스이다.

 

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

네트워크 1  (0) 2024.10.15
File, Files  (0) 2024.10.14
IO 기본 2  (0) 2024.10.11
IO 기본 1  (0) 2024.10.11
문자 인코딩  (0) 2024.10.07

+ Recent posts