마지막 포스팅에 이어 시간이 조금 지났지만 금일은 자바 입출력의 활용에 대해 알아보는 시간이다.
코드 구현에 앞서 사용하게 될 회원과 다형성을 활용한 인터페이스를 만들어주자. (기본)생성자와 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();
단순하게 함수 구현체를 만들어 보겠습니다.
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;
}
}
한 줄 단위로 처리할 때는 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 파일이 존재하지 않는다. 따라서 해당 예외 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를 추가로 사용해서 앞에 글자의 길이를dis.available() 읽어올 스트림이 있는지 확인하는 코드이다.dos.writeUTF("id1") -> 해당 코드는 3id1(2byte(문자 길이) + 3byte) 다음과 같이 저장되며 문자와 byte 가 섞여 있는 모습이다. (writeInt, readInt)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 를 모두 직렬화해return (List<Member>) findObject 반환 타입이 Object 이므로 캐스팅이 필요합니다. 참고
transient 키워드: transient 가 붙어있는 필드는 직렬화 하지 않고 무시한다.
객체 직렬화는 한계가 존재하기 때문에 사용하지 않는 이유들이 있다.
버전 관리의 어려움
플랫폼 족성성, 성능 이슈, 유연성 부족, 크기 효율성의 문제
해당 소제목은 객체 직렬화를 위한 대안이다.
플랫폼 종속성 문제를 해결하기 위해 2000년대 초반에 XML이라는 기술이 인기를 끌었다.
JSON은 가볍고 간결하며, 웹 API와 RESTful 서비스가 대중화되면서 JSON은 표준 데이터 교환 포맷으로 자리 잡았다.
만약 매우 작은 용량으로 더 빠른 속도가 필요하다면 Protobuf, Avro 같은 대안 기술이 있다
정리하자면 대부분 JSON 만 사용해도 충분하다.
하지만 Json 의 무결성, 관리의 비효율성, 보안문제, 백업과 복구의 문제로 대부분의 현대 애플리케이션에서는 데이터베이스를 사용한다. 데이터베이스는 위의 한계들을
극복하고, 대량의 데이터를 효율적으로 저장, 관리, 검색할 수 있는 강력한 도구를 제공한다.