2022.08.08/자바 정리/Iterator 패턴/자바 컬렉션 API /입출력 API

Jimin·2022년 8월 8일
1

비트캠프

목록 보기
18/60
post-thumbnail
  • board-app 프로젝트 수행
      1. 데이터 조회 로직을 객체화 하기: Iterator 패턴 적용
      1. 기존 List 구현체를 자바 컬렉션 API로 교체하기: java.util 패키지의 클래스 사용
      1. 입출력 API를 사용하여 데이터를 파일로 저장하기: 바이너리 저장

데이터 조회 로직을 객체화 하기: Iterator 패턴 적용

  • 데이터 조회 규격을 Iterator 인터페이스로 정의한다.
  • List 구현체에서 Iterator 규격을 따르도록 변경한다.
  • DAO 객체에서 목록을 다룰 때 Iterator 구현체를 적용한다.

board-app project

1단계 - Iterator 인터페이스 정의

  • util.Iterator 인터페이스 생성
  • Iterator interface
package com.bitcamp.util;

public interface Iterator<E> {
  boolean hasNext();
  E next();
}

2단계 - List 규격에 Iterator를 리턴하는 규칙을 추가한다.

  • util.List 인터페이스 변경
    • Iterator() 규칙 추가
  • List interface
public interface List<E> {
  ...
  Iterator<E> iterator();
}

3단계 - AbstractList에서 Iterator() 메서드 구현

  • util.AbstractList 클래스 변경
  • AbstractList class
package com.bitcamp.util;

public abstract class AbstractList<E> implements List<E> {

  ...
  
  @Override
  public Iterator<E> iterator() {
    return new Iterator<E>() {
      int index;
      @Override
      public boolean hasNext() {
        return index < AbstractList.this.size();
      }
      @Override
      public E next() {
        return AbstractList.this.get(index++);
      }
    };
  }
}

4단계 - XxxDao 에서 목록을 가져올 때 Iterator를 사용한다.

toArray() 대신, iterator() 사용을 통해 Iterator 객체 사용법 훈련.
(Iterator 사용하는 것이 필수는 아니다.)

  • board.dao.XxxDao 클래스 변경
  • BoardDao class
package com.bitcamp.board.dao;

import com.bitcamp.board.domain.Board;
import com.bitcamp.util.Iterator;
import com.bitcamp.util.LinkedList;
import com.bitcamp.util.List;

// 게시글 목록을 관리하는 역할
//
public class BoardDao {
  List<Board> list = new LinkedList<>();

  ...
  
  public Board[] findAll() {
    Iterator<Board> iterator = list.iterator();

    // 역순으로 정렬하여 리턴한다.
    Board[] arr = new Board[list.size()];

    int index = list.size() - 1;
    while (iterator.hasNext()) {
      arr[index--] = iterator.next();
    }
    return arr;
  }
  • MemberDao class
package com.bitcamp.board.dao;

import com.bitcamp.board.domain.Member;
import com.bitcamp.util.Iterator;
import com.bitcamp.util.LinkedList;
import com.bitcamp.util.List;

// 회원 목록을 관리하는 역할
//
public class MemberDao {
  List<Member> list = new LinkedList<Member>();

	...

  public Member[] findAll() {
    Iterator<Member> iterator = list.iterator();

    Member[] arr = new Member[list.size()];

    int i = 0;
    while (iterator.hasNext()) {
      arr[i++] = iterator.next(); 
    }
    return arr;
  }
}

5단계 - Stack의 toString() 메서드를 구현할 때 Iterator를 사용한다.

  • util.Stack 클래스 변경
    • 제네릭을 적용한다.
    • toString()에 Iterator를 사용한다.
  • Stack class
package com.bitcamp.util;

public class Stack<E> extends LinkedList<E> {

  ...

  @Override
  public String toString() {
    Iterator<E> iterator = iterator();
    StringBuffer buf = new StringBuffer();

    while (iterator.hasNext()) {
      if (buf.length() > 0) { 
        buf.append(" > ");
      }
      buf.append(iterator.next());
    }

    return buf.toString();
  }
}

6단계 - App 클래스에서 Stack을 만들 때 제네릭을 적용한다.

  • board.App 클래스 변경
public class App {

  // breadcrumb 메뉴를 저장할 스택을 준비
  public static Stack<String> breadcrumbMenu = new Stack<>();
  
  ...

데이터 조회를 객체화: Iterator 패턴 적용

  • 데이터 조회 객체화: 데이터 조회와 관련된 필드와 메서드를 분리해서 외부 부품(객체, 클래스)으로 만든다.
    ⇒ 객체화 한다.
    ⇒ 유지보수를 위해서!!
    ⇒ 기능 변경 및 교체가 쉽다!
    예; 원래 App 클래스에 모여있던 기능들을 여러개의 클래스로 나누었던 것 또한 객체화한 것이다!
  • 객체, 부품의 기본 단위: 클래스

기존 List 구현체를 자바 컬렉션 API로 교체하기: java.util 패키지의 클래스 사용

1단계 - 목록 관련 클래수나 인터페이스를 java.util 패키지의 멤버로 교체한다.

util.List 인터페이스 삭제
util.AbstractList 추상 클래스 삭제
util.ObjectList 추상 클래스 삭제
util.LinkedList 추상 클래스 삭제
util.Stack 클래스 삭제
dao.BoardDao 클래스 변경
dao.MemberDao 클래스 변경
dao.BoardDao 클래스 변경
dao.App 클래스 변경


입출력 API를 사용하여 데이터를 파일로 저장하기: binary 저장

  • 입출력 스트림 API를 사용하여 데이터를 파일로 저장하고 읽는 방법
  • 바이너리 형식으로 데이터를 입출력하는 방법

board-app project

1단계 - DAO 클래스에 파일에 데이터를 저장하고 로딩하는 메서드를 추가한다.

  • dao.XxxDao 클래스 변경
    • 생성자에 파일 이름을 받는 파라미터를 추가한다.
    • load(), save() 메서드 추가
  • 인스턴스마다 각자 사용할거면 인스턴스 필드로 생성하고 공유할 거면 static으로 생성하면 된다.

S/W 의 분석, 설계, 구현

  • 건축: 설계 → 설계한대로 100% 동일하게 구현
  • S/W: 설계 → 설계도를 참고해서 구현
    ⇒ 설계도에 없는 필드나 메서드가 추가될 수 있다.
    ⇒ 설계도와 다르게 구현될 수 있다. → 구현 완료 후 설계도를 변경

binary data IO(input&output)

  • binary data: byte 형식의 데이터
  1. 데이터 저장
  • FileOutputStream class 도움 받기
  • 객체(인스턴스) → 필드에 저장된 값을 바이트 배열로 변환(바이트/바이트/바이트/...) → 파일에 순차적으로 저장
  1. 데이터 읽기
  • FileInputStream class 도움 받기
  • 객체(인스턴스) ← 바이트 배열 값을 필드에 저장 ← 파일에서 순차적으로 읽기

FileOutputStream, FileInputStream


파일 입출력 예외처리의 주체!


App → Handler → DAO → FileOutputStream → File

  • FileOutputStream을 처리하는 곳에서 오류가 발생!
  • 이때, 예외 처리 장소 선택에는 두 가지 경우의 수가 있다.
  1. 예외 최초 발생지에서 처리하겠다.
    • DAO에서 예외처리를 하겠다.(DAO에서 사용자에게 예외 상황을 알려준다. → 출력한다.)
  2. 예외 발생지의 상위 호출자에게 위임.
    • Handler
    • App
    • JVM
    • 상위 호출자에게 오류를 위임하는 것의 단점
      → 오류 상황을 사용자에게 명확하게 안내하지 못한다.

  • App의 역할: 메인 메뉴 처리
  • BoardDao의 역할: 데이터 처리
  • Handler의 역할: UI 처리
  • 예외처리를 어디서 처리할지?
    → 웹 화면에서 처리할지, console창에서 처리할지는 UI를 처리하는 Handler에서 처리한다.

입출력 API를 사용하여 데이터를 파일로 저장하기: 바이너리 저장 사용

  • 최초 프로젝트 실행시, 오류 출력

1단계 - DAO 클래스 파일에 데이터를 저장하고 로딩하는 메서드를 추가한다.

  • dao.Xxxao 클래스 변경
    • 생성자에 파일 이름을 받는 파라미터를 추가한다.
    • load(), save() 메서드 추가

load(), save()

  • BoardDao 생성자 호출될 때 마다 load() 호출됨 → File 로딩
    (프로젝트 실행시, 즉, BoardDao 생성자 호출시 File이 없는 경우 없는 경우의 메뉴(핸들러)는 오류가 띄워진다.)
  • BoardDao에서 파일을 수정하는 메서드들(onUpdate(), onDelete(), onInsert()) 호출될 때 마다 save() 호출됨 → File 쓰기

- load(), save() 사용 과정:

App class → BoardHandler class → BoardDao class → File 접근

-- App class

  • main() 전체를 try-catch로 감싸준다.
  • 각 메뉴마다 각 기능에 맞는 파일 이름을 Handler생성자에게 넘긴다.
  Handler[] handlers = new Handler[] {
          new BoardHandler("board.data"), // 게시판
          new BoardHandler("reading.data"), // 독서록
          new BoardHandler("visit.data"), // 방명록
          new BoardHandler("notice.data"), // 공지사항
          new BoardHandler("daily.data"), // 일기장
          new MemberHandler("member.data") // 회원
      };
  • 게시판 메뉴 번호인 1을 입력했다고 가정했을 때, 게시판을 execute() 메서드를 통해 호출한다.
handlers[mainMenuNo - 1].execute();

-- BoardHandler class

  • BoardHandler 생성자
    • App에서 넘겨 받은 fileName을 BoardHandler 생성자에서 boardDao 레퍼런스 객체를 초기화함과 동시에 BoardDao 생성자에게 fileName을 넘겨준다.
    • BoardHandler 생성자에서 boardDao.load() 메서드를 호출한다.
    • load() 메서드와 같은 경우, FileNotFoundException(파일 없음)과 IOException이 발생하게 되는데 이를 BoardDao에서 BoardHandler로 위임하므로 try-catch문으로 오류를 처리해준다.
public class BoardHandler extends AbstractHandler {

  private BoardDao boardDao;

  public BoardHandler(String filename) {
    // 수퍼 클래스의 생성자를 호출할 때 메뉴 목록을 전달한다.
    super(new String[] {"목록", "상세보기", "등록", "삭제", "변경"});

    boardDao = new BoardDao(filename);

    try {
      boardDao.load();
    } catch (Exception e) {
      System.out.printf("%s 파일 로딩 중 오류 발생!\n", filename);
    }
  }
  
  ...

}
  • onInput(), onDelete(), onUpdate()
    파일을 변경시키는 메서드(onInput(), onDelete(), onUpdate())마다 파일을 업데이트 시키기 위해 boardDao.save() 메서드를 호출한다.
    • 이때, save() 메서드 또한 오류를 발생시키는데 이를 throws Exception을 이 메서드들을 호출하는 service() 메서드에 오류 위임한다.
private void onInput() throws Exception {
    Board board = new Board();

    board.title = Prompt.inputString("제목? ");
    board.content = Prompt.inputString("내용? ");
    board.writer = Prompt.inputString("작성자? ");
    board.password = Prompt.inputString("암호? ");
    board.viewCount = 0;
    board.createdDate = System.currentTimeMillis();

    this.boardDao.insert(board);
    this.boardDao.save();

    System.out.println("게시글을 등록했습니다.");
  }

  private void onDelete() throws Exception {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("삭제할 게시글 번호? ");
        break;
      } catch (Exception ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    if (boardDao.delete(boardNo)) {
      this.boardDao.save();
      System.out.println("삭제하였습니다.");
    } else {
      System.out.println("해당 번호의 게시글이 없습니다!");
    }
  }

  private void onUpdate() throws Exception {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("변경할 게시글 번호? ");
        break;
      } catch (Throwable ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    Board board = this.boardDao.findByNo(boardNo);

    if (board == null) {
      System.out.println("해당 번호의 게시글이 없습니다!");
      return;
    }

    String newTitle = Prompt.inputString("제목?(" + board.title + ") ");
    String newContent = Prompt.inputString(String.format("내용?(%s) ", board.content));

    String input = Prompt.inputString("변경하시겠습니까?(y/n) ");
    if (input.equals("y")) {
      board.title = newTitle;
      board.content = newContent;
      this.boardDao.save();
      System.out.println("변경했습니다.");
    } else {
      System.out.println("변경 취소했습니다.");
    }
  }
  • service()
    • save()메서드를 호출해 오류들을 발생시키는 onInput(), onDelete(), onUpdate() 메서드들에 의해
      오류들을 받은 service() 메서드에서는 try-catch 문을 통해 RuntimeException을 service() 메서드를 호출한 BoardHandler class의 수퍼 클래스인 AbstractHandler class로 오류 위임한다.
    • service() 메서드는 AbstractHandler class에 추상 메서드로 선언되어있는데, 여기서 throws를 하지 않기 때문에 이 추상 메서드를 구현한 BoardHandler class에서도 throws로 오류를 위임할 수는 없다. 하지만 RuntimeException을 이용하면 오류를 위임할 수 있다.
@Override
  public void service(int menuNo) {
    try {
      switch (menuNo) {
        case 1: this.onList(); break;
        case 2: this.onDetail(); break;
        case 3: this.onInput(); break;
        case 4: this.onDelete(); break;
        case 5: this.onUpdate(); break;
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

-- AbstractHandler class

  • BoardHandler class에서 위임 받은 오류를 try-catch를 통해 오류 처리한다.
try {
        ...

        // 사용자가 입력한 메뉴 번호에 대해 작업을 수행한다.
        service(menuNo);

        ...

      } catch (Exception ex) {
        System.out.printf("예외 발생: %s\n", ex.getMessage());
      }

-- BoardDao class

  • BoardHandler class에서 보내준 fileName을 생성자에서 초기화 한다.
public class BoardDao {

  ...

  public BoardDao(String filename) {
    this.filename = filename;
  }
  • load(), save() 메서드를 이 클래스에서 생성한다.
  • load() method
    • 오류를 Exception으로 위임한다.
    • FileInputStream을 사용한다.
    • FileInputStream의 내장 함수인 read()를 사용하는데, byte만 읽어들인다.
    • FileInputStream은 항상 사용이 끝나면 close()를 통해 닫아준다.
public void load() throws Exception {
    FileInputStream in = new FileInputStream(filename);
    // FileInputStream 도구를 사용하여 파일로부터 데이터를 읽어 들인다.

    // => 먼저 게시글 개수를 읽는다.
    int size = (in.read() << 24) + (in.read() << 16) + (in.read() << 8) + in.read();

    for (int i = 0; i < size; i++) {

      // => 파일에서 읽은 게시글 데이터를 저장할 객체를 준비한다.
      Board board = new Board();

      // => 저장된 순서로 데이터를 읽는다.
      // 1) 게시글 번호 읽기
      int value = 0;
      value += in.read() << 24; // 예) 12 => 12000000
      value += in.read() << 16; // 예) 34 => 00340000
      value += in.read() << 8;  // 예) 56 => 00005600
      value += in.read();       // 예) 78 => 00000078
      board.no = value;

      // 2) 게시글 제목 읽기
      int len = 0;

      // 출력된 게시글 제목의 바이트 수를 읽어서 int 변수에 저장한다.
      len = (in.read() << 24) + (in.read() << 16) + (in.read() << 8) + in.read();

      // 게시글 제목을 저장할 바이트 배열을 만든다.
      byte[] bytes = new byte[len];

      // 게시글 제목을 바이트 배열로 읽어 들인다.
      in.read(bytes);

      // 바이트 배열을 가지고 String 인스턴스를 생성한다.
      board.title = new String(bytes, "UTF-8");

      // 3) 게시글 내용 읽기
      len = (in.read() << 24) + (in.read() << 16) + (in.read() << 8) + in.read();
      bytes = new byte[len];
      in.read(bytes);
      board.content = new String(bytes, "UTF-8");

      // 4) 게시글 작성자 읽기
      len = (in.read() << 24) + (in.read() << 16) + (in.read() << 8) + in.read();
      bytes = new byte[len];
      in.read(bytes);
      board.writer = new String(bytes, "UTF-8");

      // 5) 게시글 암호 읽기
      len = (in.read() << 24) + (in.read() << 16) + (in.read() << 8) + in.read();
      bytes = new byte[len];
      in.read(bytes);
      board.password = new String(bytes, "UTF-8");

      // 6) 게시글 조회수 읽기
      board.viewCount = (in.read() << 24) + (in.read() << 16) + (in.read() << 8) + in.read();

      // 6) 게시글 등록일 읽기
      board.createdDate = 
          (((long)in.read()) << 56) + 
          (((long)in.read()) << 48) +
          (((long)in.read()) << 40) +
          (((long)in.read()) << 32) +
          (((long)in.read()) << 24) +
          (((long)in.read()) << 16) +
          (((long)in.read()) << 8) +
          ((in.read()));

      //      System.out.println(board);

      // 게시글 데이터가 저장된 Board 객체를 목록에 추가한다.
      list.add(board);

      // 파일에서 게시글을 읽어 올 때는 항상 게시글 번호를 boardNo에 저장한다.
      // 그래야만 새 게시글을 저장할 때 마지막 게시글 번호 보다 큰 값으로 저장할 수 있다.
      boardNo = board.no;
    }

    in.close();
  }
  • save() method
    • 오류를 Exception으로 위임한다.
    • FileOutputStream을 사용한다.
    • FileOutputStream의 내장 함수인 write()를 사용하는데, byte만 읽어들인다.
    • FileOutputStream은 항상 사용이 끝나면 close()를 통해 닫아준다.
public void save() throws Exception {
    FileOutputStream out = new FileOutputStream(filename);

    // 첫 번째로 먼저 게시글의 개수를 4바이트 int 값으로 출력한다.
    out.write(list.size() >> 24);  
    out.write(list.size() >> 16);
    out.write(list.size() >> 8);
    out.write(list.size());

    for (Board board : list) {
      // int ==> byte[] 
      // 예) board.no = 0x12345678
      System.out.println("------------------------");
      System.out.printf("%08x\n", board.no);
      out.write(board.no >> 24); // 0x00000012|345678  
      out.write(board.no >> 16); // 0x00001234|5678
      out.write(board.no >> 8);  // 0x00123456|78
      out.write(board.no);       // 0x12345678|

      // String(UTF-16) => UTF-8 
      System.out.printf("%s\n", board.title);
      // 출력할 바이트 배열의 개수를 먼저 출력한다.
      byte[] bytes = board.title.getBytes("UTF-8"); 
      out.write(bytes.length >> 24);
      out.write(bytes.length >> 16);
      out.write(bytes.length >> 8);
      out.write(bytes.length);
      out.write(bytes);

      System.out.printf("%s\n", board.content);
      bytes = board.content.getBytes("UTF-8"); 
      out.write(bytes.length >> 24);
      out.write(bytes.length >> 16);
      out.write(bytes.length >> 8);
      out.write(bytes.length);
      out.write(bytes);

      System.out.printf("%s\n", board.writer);
      bytes = board.writer.getBytes("UTF-8"); 
      out.write(bytes.length >> 24);
      out.write(bytes.length >> 16);
      out.write(bytes.length >> 8);
      out.write(bytes.length);
      out.write(bytes);

      System.out.printf("%s\n", board.password);
      bytes = board.password.getBytes("UTF-8"); 
      out.write(bytes.length >> 24);
      out.write(bytes.length >> 16);
      out.write(bytes.length >> 8);
      out.write(bytes.length);
      out.write(bytes);

      // int ==> byte[]
      System.out.printf("%08x\n", board.viewCount);
      out.write(board.viewCount >> 24);
      out.write(board.viewCount >> 16);
      out.write(board.viewCount >> 8);
      out.write(board.viewCount);

      // long ==> byte[]
      System.out.printf("%016x\n", board.createdDate);
      out.write((int)(board.createdDate >> 56));
      out.write((int)(board.createdDate >> 48));
      out.write((int)(board.createdDate >> 40));
      out.write((int)(board.createdDate >> 32));
      out.write((int)(board.createdDate >> 24));
      out.write((int)(board.createdDate >> 16));
      out.write((int)(board.createdDate >> 8));
      out.write((int)(board.createdDate));
    }
    out.close();
  }
profile
https://github.com/Dingadung

0개의 댓글