Posts 학원 #46일차: 미니 프로젝트와 파일 입출력 API: 바이트 스트림, 제네릭 메서드
Post
Cancel

학원 #46일차: 미니 프로젝트와 파일 입출력 API: 바이트 스트림, 제네릭 메서드

File Format

데이터는 형식을 갖춰서 저장되어 있다.

IMG_0136

Binary

  • 예시: jpeg, gif, wav, mp3, ppt
  • FileInputStream과 FileOutputStream을 통해 데이터를 읽고 쓴다.

Text

  • 예시: txt, java, css, html, md
  • FileReader, FileWriter를 통해 데이터를 읽고 쓴다.
  • FileInputStream과 FileOutputStream도 사용을 할 수 있지만, 문자집합에 대해 신경을 써야 한다. 실행환경(이클립스,Unix: UTF-8, 윈도우: MS949)에 따라 파일을 저장하고, 읽을 때는 ~로 되어 있다고 가정하고 변환한다. FileInputSream/OutputStream은 자동으로 변환해주지 않는다. 따라서 캐릭터 스트림인 FileReader, FileWriter을 쓰는 것이 좋다.

즉 어떤 스트림을 쓸 지는 파일 포맷에 따라 다르다. 바이너리 파일의 경우 byte stream을 사용하고, 텍스트 파일의 경우 character스트림을 사용하는 것이 좋다.

mini pms v.30: 파일 입출력 API를 활용하여 데이터를 읽고 쓰기

IMG_0137

이전 mini pms 버전 30에서는 바로 Character Stream 클래스를 사용하여 CSV 파일로 데이터를 저장하였다. 그러나 바이트 스트림을 어떻게 사용하고 어떤 구조로 되어 있는지, 그리고 바이트스트림과 캐릭터 스트림의 차이와 장단점을 공부하기 위해 버전 30의 단계를 더 쪼개었다.

버전 30 이전에는 사용자가 입력한 데이터를 컬렉션 객체에 저장하였다. 즉 RAM에 데이터가 저장되어 있어서 프로그램을 종료하면 데이터가 지워지는 문제가 있었다. 프로그램을 종료하더라도 데이터가 지워지지 않게 하려면 외부 저장장치(하드디스크, SSD 등)에 저장해야 한다. 즉 데이터를 파일로 출력해야 한다. 이 버전에서는 데이터를 파일로 입출력하는 다양한 도구를 제공하는 파일 입출력 API를 사용하여 입출력 기능을 구현하겠다.

v.30.a: FileIOStream: Data Sink Streaming Class

파일 입출력 API를 사용하여 데이터를 바이너리 포맷으로 변환하여 파일에 입출력하는 기능을 구현한다.

FileIOStream

  • 바이너리 형식으로 데이터를 읽고 쓸 때 사용하는 도구
  • byte stream class

실습

데이터를 저장할 파일 객체 준비

1
static File boardFile = new File("./board.data");

데이터를 파일에 저장하는 기능 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private static void saveBoards() {
  FileOutputStream out = null;
  
  try {
    out = new FileOutputStream(boardfile);
    out.write(boardList.size() >> 24);
    out.write(boardList.size() >> 16);
    out.write(boardList.size() >> 8);
    out.write(boardList.size());
    
    for (Board board : boardList) {
      // => 게시글 번호 출력
      out.write(board.getNo() >> 24);
      out.write(board.getNo() >> 16);
      out.write(board.getNo() >> 8);
      out.write(board.getNo());
      
      // => 게시글 제목 출력: 사이즈가 고정되어 있지 않다.
      // 문자열의 바이트 길이(2바이트) + 문자열의 바이트 배열
      byte[] bytes = board.getTitle().getBytes();
      out.write(bytes.length >> 8);
      out.write(bytes.length);
      out.write(bytes);
      
      // => 게시글 내용 출력
      bytes = board.getContent().getBytes("UTF-8");
      out.write(bytes.length >> 8);
      out.write(bytes.length);
      out.write(bytes);

      // => 게시글 작성자 출력
      bytes = board.getWriter().getBytes("UTF-8");
      out.write(bytes.length >> 8);
      out.write(bytes.length);
      out.write(bytes);
      
      // => 게시글 등록일 출력: 사이즈가 고정되어 있다.
      //    등록일은 무조건 10바이트이기 때문에 바이트 길이 출력X
      bytes = board.getRegisteredDate().toString().getBytes("UTF-8");
      out.write(bytes);

      // => 게시글 조회수 출력
      out.write(board.getViewCount() >> 24);
      out.write(board.getViewCount() >> 16);
      out.write(board.getViewCount() >> 8);
      out.write(board.getViewCount());
    }
    System.out.printf("총 %d 개의 게시글 데이터를 저장했습니다", boardList.size());
  } catch(IOException e) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생!-" + e.getMessage())
  } finally {
    try {
      out.close();
    } catch (IOException e) {
    }
  }
}
  • 문자열의 바이트 길이를 출력하는 이유
    • 사이즈가 고정되어 있지 않는 데이터는 데이터의 실제 값을 출력하기 이전에 데이터의 사이즈를 출력하여, 데이터를 읽을 때 데이터의 사이즈만큼 읽을 수 있도록 한다.
    • Board 객체의 title, content, writer 정보의 경우 사이즈가 정해져 있지 않다. => 사이즈 정보 출력
    • registeredDate 의 경우 yyyymmdd의 10바이트로 사이즈가 고정되어 있다. => 사이즈 정보 출력X
  • 사이즈 정보를 출력할 때 사이즈 정보가 어느 정도 될 지는 개발자가 정할 수 있다.
  • 1~2바이트 정도면 충분하다.
  • 2바이트로 정한 경우 2^16 바이트만큼의 문자열을 저장할 수 있다.

파일에서 데이터를 읽어오는 기능 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private static void loadBoards() {
  FileInputStream in = null;
  try {
    in = new FileInputStream(boardFile);
    
    int size = in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();
    
    for (int i = 0; i < size; i++) {
      Board board = new Board();
      
      // 게시글 번호 읽기
      board.setNo(in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read());
      
      // 문자열을 읽을 바이트 배열을 준비한다.
      byte[] bytes = new byte[30000];
      
      // 게시글 제목 읽기
      int len = in.read() << 8 | in.read();
      in.read(bytes, 0, len);
      board.setTitle(new String(bytes, 0, len, "UTF-8"));
      
      // 게시글 내용 읽기
      int len = in.read() << 8 | in.read();
      in.read(bytes, 0, len);
      board.setContent(new String(bytes, 0, len, "UTF-8"));
      
      // 게시글 작성자 읽기
      int len = in.read();
      in.read(bytes, 0, len);
      board.setWriter(new String(bytes, 0, len, "UTF-8"));
      
      // 게시글 등록일 읽기
      int len = in.read();
      in.read(bytes, 0, len);
      board.setRegisteredDate(Date.valueOf(new String(bytes, 0, len, "UTF-8")));
      
      // 게시글 조회수 읽기 
      board.setViewCount(in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read());
      
      boardList.add(board);
    }
    System.out.printf("총 %d 개의 게시글 데이터를 로딩했습니다.\n", boardList.size());

    } catch (Exception e) {
      System.out.println("게시글 파일 읽기 중 오류 발생! - " + e.getMessage());
      // 파일에서 데이터를 읽다가 오류가 발생하더라도
      // 시스템을 멈추지 않고 계속 실행하게 한다.
      // 이것이 예외처리를 하는 이유이다!!!
    } finally {
      try {
        in.close();
      } catch (Exception e) {
        // close() 실행하다가 오류가 발생한 경우 무시한다.
        // 왜? 닫다가 발생한 오류는 특별히 처리할 게 없다.
      }
    }
  }

실습

v.30.b: DataIOStream: Data Processing Streaming Class

Data Processing Stream Class

  • Decorator 패턴에서 데코레이터 역할을 수행하는 클래스
  • Data Sink Stream Class에 연결하거나 다른 데코레이터에 연결하여 중간에서 데이터를 가공하는 일을 한다.

DataIOStream

  • 문자열이나 자바 원시 타입의 값을 입출력하는 메서드를 구비하고 있다.
  • 이 클래스를 입출력 스트림에 연결하면, 데이터를 읽고 쓰기 편하다.

실습

FileIOStream에 DataIOStream을 연결하여 데이터를 저장하고 읽는다. 기존 saveXxx()loadXxx() 메서드를 변경하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static void saveBoards() {
  DataOutputStream out = null;
  
  try {
    out = new DataOutputStream(new FileOutputStream(boardfile));
    
    out.writeInt(boardList.size());
    
    for (Board board : boardList) {
      out.writeInt(board.getNo());
      out.writeUTF(board.getTitle());
      out.writeUTF(board.getContent());
      out.writeUTF(board.getWriter());
      out.writeUTF(board.getRegisteredDate().toString());
      out.writeInt(board.getViewCount());
    }
    System.out.printf("총 %d 개의 게시글 데이터를 저장했습니다", boardList.size());
  } catch(IOException e) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생!-" + e.getMessage())
  } finally {
    try {
      out.close();
    } catch (IOException e) {
      // FileWriter를 닫을 때 발생하는 예외는 무시한다.
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static void loadBoards() {
  DataInputStream in = null;
  try {
    in = new DataInputStream(new FileInputStream(boardFile));
    
    int size = in.readInt();
    
    for (int i = 0; i < size; i++) {
      Board board = new Board();
      
      board.setNo(in.readInt());
      board.setTitle(in.readUTF());
      board.setContent(in.readUTF());
      board.setWriter(in.readUTF());
      board.setRegisteredDate(in.readUTF());
      board.setViewCount(in.readInt());
      
      boardList.add(board);
    }
    System.out.printf("총 %d 개의 게시글 데이터를 로딩했습니다.\n", boardList.size());

    } catch (Exception e) {
      System.out.println("게시글 파일 읽기 중 오류 발생! - " + e.getMessage());
    } finally {
      try {
        in.close();
      } catch (Exception e) {
      }
    }
  }

v.30.c: BufferedIOStream: Data Processing Streaming Class

버퍼를 활용하여 일정 크기의 데이터를 모았다가 한 번에 출력하는 방식으로 입출력 속도를 개선할 것이다.

BufferedIOStream

  • 내부에 바이트 배열을 사용하여 입출력 데이터를 보관한다.
  • 버퍼가 꽉 차면 연결된 출력 스트림으로 내보낸다.
  • 데이터를 읽을 때도 일단 버퍼로 왕창 읽어들인 다음에 1바이트씩 리턴한다.
  • 입출력 속도가 향상된다.

실습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void saveBoards() {
  DataOutputStream out = null;
  
  try {
    out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(boardfile)));
    
    out.writeInt(boardList.size());
    
    for (Board board : boardList) {
      out.writeInt(board.getNo());
      out.writeUTF(board.getTitle());
      out.writeUTF(board.getContent());
      out.writeUTF(board.getWriter());
      out.writeUTF(board.getRegisteredDate().toString());
      out.writeInt(board.getViewCount());
    }
    System.out.printf("총 %d 개의 게시글 데이터를 저장했습니다", boardList.size());
  } catch(IOException e) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생!-" + e.getMessage())
  } finally {
    try {
      out.close();
    } catch (IOException e) {
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static void loadBoards() {
  DataInputStream in = null;
  try {
    // 기존의 스트림 객체에 데코레이터를 꼽아서 사용한다.
    in = new DataInputStream(new BufferedInputStream(new FileInputStream(boardFile)));
    
    int size = in.readInt();
    
    for (int i = 0; i < size; i++) {
      Board board = new Board();
      
      board.setNo(in.readInt());
      board.setTitle(in.readUTF());
      board.setContent(in.readUTF());
      board.setWriter(in.readUTF());
      board.setRegisteredDate(in.readUTF());
      board.setViewCount(in.readInt());
      
      boardList.add(board);
    }
    System.out.printf("총 %d 개의 게시글 데이터를 로딩했습니다.\n", boardList.size());

    } catch (Exception e) {
      System.out.println("게시글 파일 읽기 중 오류 발생! - " + e.getMessage());
    } finally {
      try {
        in.close();
      } catch (Exception e) {
      }
    }
  }

v30.d: ObjectIOStream: Data Processing Streaming Class

직렬화(serialize, marshaling)

  • 인스턴스의 필드 값바이트 배열로 변환하여 출력하는 것이다.
  • 클래스 정보필드의 타입 정보도 포함한다.
  • 한 객체의 메모리에서 표현 방식을 저장 또는 전송에 적합한 다른 데이터 형식으로 변환하는 과정이다.

복원(deserialize, unmarshaling)

  • 직렬화를 통해 저장된 바이트 배열을 자바 객체로 변환하는 것이다.
  • 클래스 정보와 필드 정보를 바탕으로 해당 클래스를 찾아 인스턴스를 자동 생성한다.
  • 생성자 호출 없이 인스턴스의 필드 값을 설정한다.

ObjectIOStream

실습

1단계: 자바 도메인 클래스를 직렬화할 수 있도록 설정

  • Board, Member, Project, Task 클래스
    • java.io.Serializable 인터페이스를 구현한다.
    • private static final int serialVersionUTD 값을 설정한다.
1
2
3
4
public class Board implements Serializable {
  private static final long serialVersionUTD = 1L;
  //..
}

2단계: DataIOStream을 ObjectIOStream으로 교체하여 객체를 저장하고 읽는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void saveBoards() {
  ObjectOutputStream out = null;
  
  try {
    out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(boardfile)));
    
    out.writeInt(boardList.size());
    
    for (Board board : boardList) {
      out.writeObject(board);
    }
    System.out.printf("총 %d 개의 게시글 데이터를 저장했습니다", boardList.size());
  } catch(IOException e) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생!-" + e.getMessage())
  } finally {
    try {
      out.close();
    } catch (IOException e) {
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static void loadBoards() {
  ObjectInputStream in = null;
  try {
    in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(boardFile)));
    
    int size = in.readInt();
    
    for (int i = 0; i < size; i++) {
      boardList.add((Board)in.readObject());
    }
    System.out.printf("총 %d 개의 게시글 데이터를 로딩했습니다.\n", boardList.size());

    } catch (Exception e) {
      System.out.println("게시글 파일 읽기 중 오류 발생! - " + e.getMessage());
    } finally {
      try {
        in.close();
      } catch (Exception e) {
      }
    }
  }

v30.e: 리팩토링: 메서드 파라미터에 제네릭 적용

리팩토링

  • 소프트웨어를 보다 쉽게 이해할 수 있고, 적은 비용으로 수정할 수 있도록 겉으로 보이는 동작의 변화 없이 내부 구조를 변경하는 것
  • 중복된 코드를 제거하여 설계(design)를 개선시킨다.
  • 코드를 더 이해하기 쉽게 만든다.
  • 버그를 찾기 쉽게 해준다.
  • 프로그램을 빨리 작성하도록 도와준다.

리팩토링을 해야 할 때

  • 비슷한 코드를 중복해서 작성해야 할 때
  • 기능을 추가할 때
  • 버그를 수정할 때
  • 코드 리뷰(code review)를 수행할 때

실습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static <E extends Serializable> void saveObjects(Collections<E> list, File, file) {
  ObjectOutputStream out = null;
  
  try {
    out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
    
    out.writeInt(list.size());
    
    for (E obj : list) {
      out.writeObject(obj);
    }
    out.flush();
    // close()가 호출되면 flush()가 자동 호출된다.
    // 그러나 가능한 버퍼를 사용할 때,
    // 출력한 후에 flush()를 호출하는 것을 습관들이자.
    
    System.out.printf("%총 %d 개의 객체를 '%s' 파일에 저장했습니다",  list.size(), file.getName());
  } catch(IOException e) {
    System.out.printf("객체를 '%s' 파일에 쓰기 중 오류 발생!-", file.getName(), e.getMessage());
  } finally {
    try {
      out.close();
    } catch (IOException e) {
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SuppressWarnings("unchecked")
private static <E extends Serializable> void loadBoards(Collection<E> list, File file) {
  ObjectInputStream in = null;
  try {
    in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(boardFile)));
    
    int size = in.readInt();
    
    for (int i = 0; i < size; i++) {
      list.add((E)in.readObject());
    }
    System.out.printf("'%s' 파일에서 총 %d 개의 객체를 로딩했습니다.\n", file.getName(), list.size());

    } catch (Exception e) {
      System.out.printf("'%s' 파일 읽기 중 오류 발생! - %s\n", file.getName(), e.getMessage());
    } finally {
      try {
        in.close();
      } catch (Exception e) {
      }
    }
  }

이제 다음과 같이 모든 타입에 대해 사용할 수 있다.

1
2
3
4
saveObjects(boardList, boardFile);
saveObjects(memberList, memberFile);
saveObjects(projectList, projectFile);
saveObjects(taskList, taskFile);
1
2
3
4
loadObjects(boardList, boardFile);
loadObjects(memberList, memberFile);
loadObjects(projectList, projectFile);
loadObjects(taskList, taskFile);
This post is licensed under CC BY 4.0 by the author.

💻 HTTP #6: HTTP 메시지

코어 자바스크립트 #2.2 엄격모드

Loading comments from Disqus ...