2022.08.10/자바 정리/객체 직렬화/인스턴스 텍스트 형식으로 입출력/리팩토링

Jimin·2022년 8월 10일
0

비트캠프

목록 보기
20/60

  • board-app 프로젝트 수행
      1. 자바에서 제공하는 데코레이터를 사용하기
      1. 인스턴스를 통째로 입출력하기: 객체 직렬화
      1. 인스턴스를 텍스트 형식으로 입출력하기: FileReader/FileWriter
      1. 리팩토링: Factory Method 패턴, Information Expert 패턴
  • ObjectOutputStream/ObjectInputStream 사용법

board-app project

자바에서 제공하는 I/O Stream API 사용하기: java.io.*

  • java.io 패키지의 스트리밍 API를 사용하여 Primitive type, String type 값을 입출력한다.

1단계 - 기존의 DataInputStream/DataOutputStream 클래스를 자바 API로 교체한다.

  • com.bitcamp.util.DataOutputStream 클래스 삭제
  • com.bitcamp.util.DataInputStream 클래스 삭제
  • com.bitcamp.board.dao.XxxDao 클래스 변경
    • 자바 stream api로 교체한다.

자바 I/O Streams: java.io.*

  • DataSinkStream
    • Sink: data 저장소
    • 데이터 저장소에 직접 읽기/쓰기를 수행하는 클래스
  • DataProcessingStream
    • Dacorator
    • 중간에서 데이터를 가공하고 일을 하는 클래스

버퍼 사용 이유

  • 1byte씩 바로바로 HDD로 출력하는 것 보다,
    일정량의 바이트들을 메모리에 모았다가 한 번에 출력하는 것이 시간이 덜 소요된다.

객체를 입출력하기

객체 직렬화 방식으로 입출력하기

  • 장점: 입출력 코딩이 쉽다.
  • 단점: 자바가 아닌 다른 언어에서 이 코드를 읽고 쓰기가 힘들다.
    ⇒ 다른 프로그래밍 언어로 읽고 쓰기가 어렵다.

인스턴스를 통째로 입출력하기: 객체 직렬화 → ObjectInputStream/ObjectOutputStream

  • 인스턴스 필드의 값을 통째로 입출력하는 방법

board-app project

1단계 - 도메인 클래스의 인스턴스 값을 직렬화 가능하도록 설정한다.

  • board.domain.Board 클래스 변경

    • java.io.Serializable 인터페이스를 구현한다.
      • 인스턴스를 통째로 입출력 할 수 있도록 표시하는 용도이다.
      • 인터페이스에 추상 메서드가 선언되어 있지 않기 때문에 따로 메서드를 구현할 필요는 없다.
    • serialVersionUID 필드 설정
      • 인스턴스를 저장하고 읽을 때 클래스의 변화 여부를 검증하기 위해 버전 번호를 지정한다.
      • 처음 버전 번호를 부여한 후 클래스에 필드를 추가하거나 삭제할 때 마다 버전 번호를 증가시킨다.
  • domain.Member 클래스 변경

  • Board class

package com.bitcamp.board.domain;

public class Board implements Serializable {
  // java.io.Serializable 인터페이스 구현
  // - Board 클래스의 필드 값을 통째로 입출력 할 수 있다고 설정하는 용도이다.

  private static final long serialVersionUID = 1L;
  // serialVersionUID 필드 설정

	...

}
  • Member class
package com.bitcamp.board.domain;

import java.io.Serializable;

public class Member implements Serializable {
  private static final long serialVersionUID = 1L;

  ...
  
}

2단계 - ObjectInputStream/ObjectOutputStream을 사용하여 객체를 통째로 입출력한다.

  • dao.XxxDao 클래스 변경
    • DataOutputStream/DataInputStream 대신 ObjectOutputStream/ObjectInputStream 클래스를 사용하여 객체를 입출력한다.
  • BoardDao class
// 게시글 목록을 관리하는 역할
//
public class BoardDao {

  ...

  public void load() throws Exception {
    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
      int size = in.readInt();
      for (int i = 0; i < size; i++) {
        Board board = (Board) in.readObject();
        list.add(board);
        boardNo = board.no;
      }
    }
  }

  public void save() throws Exception {
    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
      out.writeInt(list.size());
      for (Board board : list) {
        out.writeObject(board);
      }
    }
  }

 ...
 
}
  • MemberDao class
// 회원 목록을 관리하는 역할
//
public class MemberDao {

  ...

  public void load() throws Exception {
    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
      int size = in.readInt();
      for (int i = 0; i < size; i++) {
        Member member = (Member) in.readObject();
        list.add(member);
      }
    } // try () ==> try 블록을 벗어나기 전에 in.close()가 자동으로 실행된다.
  }

  public void save() throws Exception {
    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
      out.writeInt(list.size());
      for (Member member : list) {
        out.writeObject(member);
      }
    } // try () ==> try 블록을 벗어나기 전에 out.close()가 자동으로 실행된다.
  }

  ...
  
}
  • Serializable 인터페이스 구현을 삭제했을 때 발생하는 오류
    → invalid for deserialization


Serialize와 Deserialize

Serialize

  • Serialize(직렬화)
    • 인스턴스 → 바이트 배열로 변환
    • marshaling
    • 직렬화 후 파일 내부에 포함된 데이터들
      • 클래스 정보(이름, 패키지명...)
      • 버전 번호(serialVersionUID)
      • 필드 값
    ⇒ 직접 필드값을 출력하는 것보다 파일 크기가 더 커지는 단점이 있지만, 코딩이 쉬워진다.

Deserialize

  • Deserialize(역직렬화)
    • 바이트 배열에 있는 data → 인스턴스 생성
    • unmashaling
    • 파일 내부에 포함된 데이터들
      • 클래스 정보(이름, 패키지명...)
      • 버전 번호(serialVersionUID)
      • 필드 값

character stream API 사용법

텍스트 형식의 데이터 특징

  • binary 형식보다 파일 크기가 크다.
  • 인코딩/디코딩에 시간이 소요된다.
    • 저장할 때 UTF-16 → UTF-8로 인코딩
    • 읽을 때 UTF-8 → UTF-16으로 디코딩
  • 텍스트 편집기로 직접 변경이 가능하다. ↔ 바이너리 형식: 전용 APP을 사용해야 한다.
  • 프로그래밍 언어 호환이 잘 된다. ⇒ APP끼리 데이터 교환이 쉽다.

⇒ XML, JSON 형식을 많이 사용한다.


인스턴스를 텍스트 형식으로 입출력하기: FileReader/FileWriter

  • 텍스트로 데이터를 입출력하는 방법

board-app project

1단계 - byte 스트림 클래스 대신 character 스트림 클래스로 교체한다.

  • dao.XxxDao 클래스 변경

    • FileInputStream/FileOutputStream 대신 FileReader/FileWriter로 교체한다.
      • 읽을 때, read()로 한 번에 다 읽지 말고, readLine()으로 한 줄 씩 읽고,
        split() 메서드와 컴마(,)를 이용하여 데이터를 구분한다.
      • 쓸 때, String.format()을 활용한다.
    • 데이터를 입출력 할 때는 CSV(Comma-Seperated Values) 형식을 사용한다.
  • board.App 클래스 변경

    • 파일명의 확장자를 data에서 CVS로 변경
  • BoardDao class

public class BoardDao {

  ...

  public void load() throws Exception {
    try (BufferedReader in = new BufferedReader(new FileReader(filename))) {
      String str;
      while ((str = in.readLine()) != null) {

        String[] values = str.split(",");

        Board board = new Board();
        board.no = Integer.parseInt(values[0]);
        board.title = values[1];
        board.content = values[2];
        board.writer = values[3];
        board.password = values[4];
        board.viewCount = Integer.parseInt(values[5]);
        board.createdDate = Long.parseLong(values[6]);

        list.add(board);
        boardNo = board.no;
      }
    }
  }

  public void save() throws Exception {
    try (FileWriter out = new FileWriter(filename)) {
      for (Board board : list) {
        out.write(String.format("%d,%s,%s,%s,%s,%d,%d\n",
            board.no,
            board.title,
            board.content,
            board.writer,
            board.password,
            board.viewCount,
            board.createdDate));
      }
    }
  }

  ...
  
}
  • MemberDao class
public class MemberDao {

  ...
  
  public void load() throws Exception {
    try (BufferedReader in = new BufferedReader(new FileReader(filename))) {
      String str;
      while ((str = in.readLine()) != null) {

        String[] values = str.split(",");

        Member member = new Member();
        member.no = Integer.parseInt(values[0]);
        member.name = values[1];
        member.email = values[2];
        member.password = values[3];
        member.createdDate = Long.parseLong(values[4]);

        list.add(member);
      }
    }
  }

  public void save() throws Exception {
    try (FileWriter out = new FileWriter(filename)) {
      for (Member member : list) {
        out.write(String.format("%d,%s,%s,%s,%d\n",
            member.no,
            member.name,
            member.email,
            member.password,
            member.createdDate));
      }
    }
  }

  ...
  
}
  • App class
public class App {

  ...

  public static void main(String[] args) {
    try {
      // 핸들러를 담을 레퍼런스 배열을 준비한다.
      Handler[] handlers = new Handler[] {
          new BoardHandler("board.csv"), // 게시판
          new BoardHandler("reading.csv"), // 독서록
          new BoardHandler("visit.csv"), // 방명록
          new BoardHandler("notice.csv"), // 공지사항
          new BoardHandler("daily.csv"), // 일기장
          new MemberHandler("member.csv") // 회원
      };

      ...
      
  } // main
  
  ...

}


리펙토링: Factory Method 패턴(GOF의 디자인 패턴), Information Expert 패턴(GRASP 패턴)

  1. CSV Data → Board 객체 생성

    객체 생성은 그 객체를 가장 잘 아는 객체에게 맡긴다.


2. Board 객체 → CSV Data 변환

  • 구조 변경 이전에는, 만약 Board 클래스에 필드가 추가되거나 삭제된다면 BoardDao 클래스를 변경해야 한다.

    정보를 다루는 것은 그 정보를 갖고 있는 전문가에게 맡기는 것이 좋다.

board-app project

  • CSV 데이터를 가지고 Board객체를 생성하는 일은 Board 클래스에 맡긴다.

    객체 생성은 메서드를 통해 수행한다. → Factory Method 패턴

  • Board 객체의 값을 CSV 형식으로 변환하는 일은 Board 클래스에 맡긴다.

    CSV 데이터 생성은 Board 메서드를 통해 수행한다. → Information Expert 패턴

1단계 - Board 클래스에 팩토리 메서드를 추가한다.

  • domain.Board 클래스 변경
    • 팩토리 메서드 create()를 추가한다.
    • 즉, CSV 데이터를 가지고 객체를 생성하는 복잡한 로직을 캡슐화 한다.
  • GoF의 Factory Method 패턴
    → 객체 생성 과정이 복잡할 때 별도의 메서드로 캡슐화 한다.
  • Board class
public class Board implements Serializable {
  
  ...
  
  // GoF의 Factory Method 패턴
  // - 객체 생성 과정이 복잡할 때 별도의 메서드로 캡슐화 한다.
  // 
  public static Board create(String csv) {
    String[] values = csv.split(",");

    Board board = new Board();
    board.no = Integer.parseInt(values[0]);
    board.title = values[1];
    board.content = values[2];
    board.writer = values[3];
    board.password = values[4];
    board.viewCount = Integer.parseInt(values[5]);
    board.createdDate = Long.parseLong(values[6]);

    return board;
  }
}

2단계 - CSV 데이터를 가지고 객체를 처리할 Board 클래스를 사용한다.

  • dao.BoardDao 클래스 변경
    • load() 메서드에서 CSV 데이터를 처리할 때 팩토리 메서드를 호출한다.
  • BoardDao class
public class BoardDao {

  ...

  public void load() throws Exception {
    try (BufferedReader in = new BufferedReader(new FileReader(filename))) {
      String str;
      while ((str = in.readLine()) != null) {
        Board board = Board.create(str);
        list.add(board);
        boardNo = board.no;
      }
    }
  }

  ...
  
}

3단계 - Board 클래스에 CSV 데이터를 생성하는 메서드를 추가한다.

  • domain.Board 클래스 변경
    • toCsv()를 추가한다.
      • 필드를 알고 있는 것은 Board 클래스이기 때문에 필드의 값을 가지고 CSV형식의 데이터를 생성하는 것도 Board 클래스가 할 일이다.
      • GRASP 패턴의 Information Expert 패턴이다.
  • GRASP 패턴의 Information Expert 패턴
    → 정보 생성은 그 데이터를 갖고 있는 전문가에게 맡긴다.
  • Board class
public class Board implements Serializable {
  
  ...

  // GRASP 패턴의 Information Expert 패턴
  // => 정보 생성은 그 데이터를 갖고 있는 전문가에게 맡긴다.
  public String toCsv() {
    return String.format("%d,%s,%s,%s,%s,%d,%d",
        this.no,
        this.title,
        this.content,
        this.writer,
        this.password,
        this.viewCount,
        this.createdDate);
  }
}

4단계 - CSV 데이터를 출력할 때 Board 클래스를 사용한다.

  • dao.BoardDao 클래스 변경
    • save() 메서드 변경
  • BoardDao class
public class BoardDao {

  ...

  public void save() throws Exception {
    try (FileWriter out = new FileWriter(filename)) {
      for (Board board : list) {
        out.write(board.toCsv() + "\n");
      }
    }
  }

  ...
  
}

5단계 - Member와 MemberDao도 변경한다.

  • domain.Member 클래스 변경

    • 팩토리 메서드 create()를 추가한다.
    • toCsv()를 추가한다.
  • dao.MemberDao 클래스 변경

    • save() 메서드 변경
  • Member class

public class Member implements Serializable {
  
  ...
  
  //GoF의 Factory Method 패턴
  // - 객체 생성 과정이 복잡할 때 별도의 메서드로 캡슐화 한다.
  // 
  public static Member create(String csv) {
    String[] values = csv.split(",");

    Member member = new Member();
    member.no = Integer.parseInt(values[0]);
    member.name = values[1];
    member.email = values[2];
    member.password = values[3];
    member.createdDate = Long.parseLong(values[4]);

    return member;
  }

  // GRASP 패턴의 Information Expert 패턴
  // => 정보 생성은 그 데이터를 갖고 있는 전문가에게 맡긴다.
  public String toCsv() {
    return String.format("%d,%s,%s,%s,%d",
        this.no,
        this.name,
        this.email,
        this.password,
        this.createdDate);
  }
}
  • MemberDao class
public class MemberDao {

  ...

  public void load() throws Exception {
    try (BufferedReader in = new BufferedReader(new FileReader(filename))) {
      String str;
      while ((str = in.readLine()) != null) {
        list.add(Member.create(str));
      }
    }
  }

  public void save() throws Exception {
    try (FileWriter out = new FileWriter(filename)) {
      for (Member member : list) {
        out.write(member.toCsv() + "\n");
      }
    }
  }
  
	...

}

Wrapper 객체

  • 랩퍼 객체 사용 이유
    → primitive 타입별로 메소드 여러개만들지 말고 Object로 하나로 담으려고,
    클래스타입이어야 Object에 담을 수 있으니까.
Integer a = new Integer(100);  <- deprecated
->
Integer a = Integer.valueOf(100);

Wrapper 클래스의 가장 큰 목적!
primitive 값을 포함하여 모든 값을 쉽게 주고 받기 위함이다.

profile
https://github.com/Dingadung

0개의 댓글