Posts 학원 #47일차: PMS 프로젝트 v31 - 파일 입출력
Post
Cancel

학원 #47일차: PMS 프로젝트 v31 - 파일 입출력

mini pms v.31

31-a: FileReader/FileWriter

Character Stream Class를 활용하여 데이터를 텍스트 포맷으로 파일에 저장하고 파일에서 읽는 것을 연습할 것이다. 텍스트 형식으로 저장할 때 CSV 포맷으로 저장한다.

character stream class

  • 문자 데이터를 출력할 때 UCS2(UTF-16BE) 인코딩 문자를 UTF-8 등과 같은 다른 문자집합의 인코딩으로 자동 변환해준다.
  • 문자 데이터를 읽을 때는 거꾸로 UTF-8 등으로 인코딩 된 데이터를 JVM이 내부적으로 사용하는 UCS2(UTF-16BE) 인코딩으로 자동 변환해준다.
  • 개발자가 인코딩 변환을 위한 코드를 작성할 필요가 없어 편하다.

CSV 파일 포맷

  • Comma-Seperated Values
  • MIME 형식: text/csv
  • 각 레코드(한 단위 값)는 한 줄의 문자열로 표현한다.
  • 한 줄은 줄바꿈 기호(CRLF)로 표현한다.
  • 레코드를 구성하는 필드는 콤마로 구분한다.
  • 각 필드는 큰 따옴표를 쳐도 되고 안 쳐도 된다.
  • 파일에 저장할 때 마지막 레코드는 줄바꿈 기호가 있을 수도 있고 없을 수도 있다.

레코드(record)

  • 컴퓨터 과학에서 한 단위의 정보를 가리키는 용어다.
    • 학생정보, 성적정보, 도서정보, 도서정보, 주문정보, 결제정보, 고객정보 등
  • 한 개 이상의 필드(field)로 구성된다.
    • 학생정보: 이름, 전화번호, 나이, 우편번호 등
  • 객체지향 프로그래밍에서 레코드는 보통 클래스로 정의된다.
  • 필드는 클래스의 인스턴스 필드로 정의한다.

바이너리 포맷 vs 텍스트 포맷

가독성

  • 바이너리 포맷은 사람이 보기가 불편하다.
  • 텍스트 포맷은 사람이 직접 보고 편집할 수 있다.

전용 어플리케이션

  • 바이너리 포맷은 그 포맷을 이해하는 애플리케이션을 이용해야만 읽고 쓸 수 있다.
  • 텍스트 포맷은 메모장과 같은 텍스트 에디터만 있으면 읽고 쓸 수 있다.

파일 크기

  • 바이너리 포맷은 텍스트 포맷에 비해 크기가 작다.
  • 바이너리 포맷은 각 데이터의 크기를 규정해 놓고 쓰고 읽는다.
  • 텍스트 포맷은 메타 데이터(예: 태그)를 이용하여 데이터를 구분한다.
    • 메타 데이터 때문에 파일의 크기가 커진다.

이기종 언어나 플랫폼 간 호환성

  • 일반 바이너리 포맷은 이기종 언어간 교환이 가능하다.

  • 그러나 자바의 serialize와 같은 특정 포맷은 다른 언어에서 읽고 쓸 수 없다.
  • 포토샵 같은 특정 애플리케이션 전용 포맷은 다른 언어에서 읽고 쓸 수 없다.
  • 텍스트 포맷은 이기종 프로그래밍 언어에서도 자유롭게 읽고 쓸 수 있다.
    • 그래서 텍스트 포맷은 이기종 플랫폼(OS)이나 애플리케이션 간데이터를 교환할 때 주로 사용한다.
      • 예) XML, CSV, HTML, => 완벽하게 저장하고 읽어들일 수 있다.
      • 다른 텍스트 포맷은 완벽하지 않을 수 있다.

플랫폼: OS, 하드웨어

1
2
3
4
5
6
7
8
9
10
11
<? xml version="1.0"?>
<boards>
  <board>
    <no>1</no>
    <title>aaa</title>
    <content>aaaa</content>
    <writer>okok</writer>
    <registered-date>2020-1-1</registered-date>
    <view-count>11</view-count>
  </board>
</boards>

실습

데이터를 저장할 파일 정보 객체 생성

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

데이터를 쓰는 기능 추가

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 saveBoards() {
  FileWriter out = null;

  try {
    // 파일로 데이터를 출력할 때 사용할 도구를 준비한다.
    out = new FileWriter(boardFile);

    for (Board board : boardList) {
      // 게시글 목록에서 게시글 데이터를 꺼내 CSV 형식으로 출력한다.
      String record = String.format("%d,%s,%s,%s,%s,%d\n", 
                                    board.getNo(),
                                    board.getTitle(),
                                    board.getContent(),
                                    board.getWriter(),
                                    board.getRegisteredDate(),
                                    board.getViewCount());
      out.write(record);
    }
    System.out.printf("총 %d 개의 게시글 데이터를 저장했습니다.\n", 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
31
32
33
34
35
36
37
38
39
40
private static void loadBoards() {
  Scanner in = null;

  try {
    in = new Scanner(new FileReader(boardFile));

    while (true) {
      try {
        String record = in.nextLine();

        String[] fields = record.split(",");

        Board board = new Board();
        board.setNo(Integer.parseInt(fields[0]));
        board.setTitle(fields[1]);
        board.setContent(fields[2]);
        board.setWriter(fields[3]);
        board.setRegisteredDate(Date.valueOf(fields[4]));
        board.setViewCount(Integer.parseInt(fields[5]));

        boardList.add(board);
      } catch (NoSuchElementException e) {
        // Scanner.nextLine()을 사용하다가 파일을 다 읽으면
        // NoSuchElement 예외를 발생시킨다.
        // 이 때 반복문을 나오도록 처리한다.
        break;
      }
    }
    System.out.printf("총 %d 개의 게시글 데이터를 로딩했습니다.\n", boardList.size());

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

31-b. BufferedReader/ BufferedWriter

  • FileReader/FileWriter에 BufferedReader/BufferedWriter 데코레이터를 연결한다.

  • 데이터를 읽고 쓰는 속도를 높인다.

BufferedReader / BufferedWriter

  • 동작원리가 BufferedIOStream과 같다.

실습

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
private static void saveBoards() {
  BufferedWriter out = null;

  try {
    // FileWriter에 데코레이터 BufferedWriter를 연결한다.
    out = new BufferedWriter(new FileWriter(boardFile));

    for (Board board : boardList) {
      String line = String.format("%d, %s, %s, %s, %s, %d\n", 
                                  board.getNo(), 
                                  board.getTitle(),
                                  board.getContent(), 
                                  board.getWriter(), 
                                  board.getRegisteredDate(), 
                                  board.getViewCount());
      out.write(line);
      // 출력할 내용을 버퍼에 저장한다.
      // 버퍼가 꽉 차면 FileWriter를 이용하여 출력한다.
    }
    out.flush(); // 잔여 데이터 출력
    System.out.printf("총 %d 개의 게시글 데이터를 저장했습니다.\n", boardList.size());

  } catch (IOException e) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생! - " + e.getMessage());

  } finally {
    try {
      out.close();
    } catch (IOException e) {
    }
  }
}
  • flush(): 버퍼를 사용하는 경우에는 출력이 끝난 후 잔여 데이터를 출력하도록 하자.

    네트워크 통신에서 데이터 출력할 때 flush()를 안해서 데이터가 상대편에게 넘어가지 않는 경우가 있다. 이런 상황을 고려해서 flush() 호출을 습관들여라.

Scanner 대신 BufferedReader를 사용하자. BufferedReader에는 readLine()이라는 메서드가 있기 때문에 레코드를 한 줄씩 읽을 수 있다. 이때, readLine()은 더 이상 읽을 줄이 없을 경우 예외가 발생하는 것이 아니라 null이 리턴된다.

파일의 데이터를 다 읽었을 때 while문을 빠져나가기 위해서 Scanner를 사용할 때는 try ~ catch 문을 사용하여 NoSuchElement 예외가 나올 때 빠져나갈 수 있도록 처리해주었다. BufferedReader를 사용할 때는 readLine()이 null을 리턴할 때 반복문을 빠져나가도록 처리해야 한다.

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
private static void loadBoards() {
  BufferedReader in = null;

  try {
    in = new BufferedReader(new FileReader(boardFile));

    while (true) {
      String line = in.readLine();      
      // readLine()는 더 읽을 라인이 없을 경우 null을 리턴한다.
      if (line == null) {
        break;
      }
      String[] data = line.split(",");

      Board board = new Board();
      board.setNo(Integer.parseInt(data[0]));
      board.setTitle(data[1]);
      board.setContent(data[2]);
      board.setWriter(data[3]);
      board.setRegisteredDate(Date.valueOf(data[4]));
      board.setViewCount(Integer.parseInt(data[5]));

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

이전 버전에서 Scanner는 FileReader에 연결되어 FileReader의 기능을 확장하였다. 이런 측면에서 Scanner는 Decorator의 기능을 수행한다고 볼 수 있지만, 데코레이터 그 자체는 아니다. 왜냐하면 Scanner는 Reader의 서브 클래스가 아니기 때문이다. 데코레이터는 부모와 동일한 기능을 가져야 한다.

31-c. 리팩토링

  • 객체 생성에 팩토리 메서드 패턴을 응용한다.
  • 객체지향 설계 기법 중에서 GRASP 패턴의 하나인 Information Expert를 응용한다.

팩토리 메서드 패턴

  • new 명령을 사용하여 객체를 생성하기보다는 메서드를 통해 객체를 리턴받는다.
  • 인터페이스로 객체 생성 규칙을 정의하여 프로그래밍의 일관성을 제공한다.
  • 또한 객체 생성의 책임을 인터페이스 구현체에게 떠넘긴다.

GRASP 패턴

  • General Responsibility Assignment Software Patterns의 약자이다.
  • 객체지향 설계를 수행할 때 일반적으로 적용할 수 있는 설계 원칙이다.
  • 객체에 책임(역할)을 부여하는 9가지 원칙을 제안하고 있다.

Information Expert 패턴

  • GRASP 설계 기법 중 하나이다.
  • 데이터를 가지고 있는 객체에게 책임을 부여하는 설계 방식이다.
  • 보통 데이터는 비공개로 감추고, 메서드를 통해 데이터를 노출하는 방식을 취한다.

실습

Information Expert 원칙에 따라 CSV 형식의 문자열을 다루는 코드를 valueOfCsv(), toCsvString() 메서드로 추출하여, 그 값을 다루는 도메인 클래스에 둔다.

1단계: 코드에서 데이터를 CSV 형식의 문자열로 다루는 부분을 메서드로 추출하여 도메인 클래스에 정의한다.

도메인 클래스에 객체의 필드 값을 CSV 문자열로 리턴하는 toCsvString()를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
public String toCsvString() {
  // CSV 문자열ㅇ르 만들 때 줄바꿈 코드를 붙이지 않는다.
  // 줄바꿈 코드는 CSV 문자열을 받아서 사용하는 쪽에서 다룰 문제다.
  // 유지보수에도 더 좋다.
  return String.format("%d, %s, %s, %s, %s, %d", 
                       getNo(), 
                       getTitle(), 
                       getContent(), 
                       getWriter(), 
                       getRegisteredDate(), 
                       getViewCount());
}

도메인 클래스에 CSV 문자열을 가지고 객체를 생성하는 valueOfCsv()를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 팩토리 메서드
public static Board valueOfCsv(String line) {
  String[] data = line.split(",");

  Board board = new Board();
  board.setNo(Integer.parseInt(data[0]));
  board.setTitle(data[1]);
  board.setContent(data[2]);
  board.setWriter(data[3]);
  board.setRegisteredDate(Date.valueOf(data[4]));
  board.setViewCount(Integer.parseInt(data[5]));

  return board;
}

2단계: 도메인 클래스에 정의한 메서드를 사용하여 CSV 데이터를 다룬다.

App클래스의 saveBoards()가 toCsvString()을 호출하도록 만든다.

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() {
  BufferedWriter out = null;

  try {
    out = new BufferedWriter(new FileWriter(boardFile));

    for (Board board : boardList) {
      //out.write(board.toCsvString() + "\n"); 
      // 위의 코드는 임의의 String객체를 만들고, 이는 가비지가 된다.
      // 따라서 다음 두 줄로 String 객체를 만들지 않고 바로 출력하도록 만든다.
      out.write(board.toCsvString());
      out.write("\n")
    }

    out.flush();
    System.out.printf("총 %d 개의 게시글 데이터를 저장했습니다.\n", boardList.size());

  } catch (IOException e) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생! - " + e.getMessage());

  } finally {
    try {
      out.close();
    } catch (IOException e) {
    }
  }
}

loadBoards()가 valueOfCsv()를 호출하여 해당 메서드가 만든 객체를 사용할 수 있게 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  private static void loadBoards() {
    BufferedReader in = null;
    try {
      in = new BufferedReader(new FileReader(boardFile));

      while (true) {
        String line = in.readLine();
        if (line == null) {
          break;
        }
        boardList.add(Board.valueOfCsv(line));
      }
      System.out.printf("총 %d 개의 게시글 데이터를 로딩했습니다.\n", boardList.size());

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

31-d: 리팩토링2

  • 인터페이스를 활용하여 코드를 통합한다.

  • 제네릭을 활용하여 범용 메서드를 만든다.

  • 메서드 레퍼런스 / 생성자 레퍼런스를 활용하여 인터페이스를 구현한다.

인터페이스

  • 객체의 사용 규칙을 정의하는 문법

  • 즉 객체에 대해 메서드를 호출하는 규칙을 정의한다.

인터페이스의 이점

  • 객체를 사용하는 측(client)의 코드 작성과 피사용측 코드 작성을 분리할 수 있다.
  • 즉 코드를 작성하는 개발자들이 서로 영향을 끼치지 않고 프로그래밍을 할 수 있다.
  • 특정 클래스에 종속되지 않기 때문에 구현이 더 자유롭고 객체를 대체하기 더 쉽다.

실습

1단계: 객체에서 CSV 형식의 문자열을 뽑는 것을 인터페이스를 이용하여 규칙으로 정의한다.

객체에서 CSV 문자열을 뽑는 규칙을 정의한다. 이 규칙을 준수하는 객체인 경우 인터페이스에 선언된 메서드 규칙에 따라 호출하면 CSV 문자열을 뽑을 수 있다.

1
2
3
4
5
package com.eomcs.util;

public interface CsvObject<T> {
  String toCsvString();
}

2단계: 도메인 객체가 CsvObject 인터페이스를 구현하게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Board implements CsvObject {
  //..
  @Override
  public String toCsvString() {
    return String.format("%d, %s, %s, %s, %s, %d", 
                         getNo(), 
                         getTitle(), 
                         getContent(), 
                         getWriter(),
                         getRegisteredDate(), 
                         getViewCount());
  }
}

Board 클래스는 CsvObject 규칙에 따라 구현했기 때문에 이 클래스는 toCsvString() 메서드가 있음을 보장한다. 따라서 이 클래스의 객체를 사용하는 측에서는 확실하고 일관되게 메서드를 호출하여 CSV 문자열을 추출할 수 있다.

이제 toCsvString() 메서드는 CsvObject 인터페이스를 통해 규칙으로써 사용될 것이다. 즉 Board 클래스에서 임의로 만든 메서드가 아니다. 인터페이스를 통해 공개된 메서드로 격상되었다.

3단계: Board, Member, Project, Task의 파일 저장 메서드를 통합한다.

메서드에 선언하지 않고 파라미터를 Collection<E>로 선언하면 자바 컴파일러는 E 자체를 클래스로 인식하고 이러한 클래스를 찾으려고 한다. 그러나 그러한 클래스는 없기 때문에 컴파일 오류를 띄운다. 따라서 여기서 E는 타입 파라미터라고 컴파일러에게 알려줘야 한다.

saveObjects()는 두 개의 정보만 있으면 된다. 컬렉션 객체와 그 데이터를 저장할 파일 객체이다. Collection에 담겨 있는 객체는 CsvObject 의 규칙에 따라서 구현되어 있을 거라고 선언했다. 따라서 파라미터로 넘어오는 컬렉션 객체는 toCsvString() 메서드를 가지고 있을 것이다. 따라서 그냥 메서드 안에서 csvObject.toCsvString() 을 호출하면 되었다. 즉 <E extends CsvObject> void saveObjects(Collection<E> list, File file)로 선언한 것은 csvObject 규칙을 준수하고 있는 객체를 담는 컬렉션만 받을 수 있도록 제한조건을 걸어준 것이다.

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
// CsvObject로 구현한 객체를 담은 컬렉션이 아니라면 파라미터로 받지 못한다.
// 파라미터로 올 게 뭔지 모르겠지만, 분명히 CsvObject 인터페이스를 구현했다는 것을 보장한다.
private static <E extends CsvObject> void saveObjects(Collection<E> list, File file) {
  BufferedWriter out = null;

  try {
    out = new BufferedWriter(new FileWriter(file));

    for (CsvObject csvObject : list) {
      out.write(csvObject.toCsvString());
      out.write("\n");
    }

    out.flush();
    System.out.printf("총 %d 개의 객체를 '%s' 파일에 저장했습니다.\n", list.size(), file.getName());

  } catch (IOException e) {
    System.out.printf("'%s'파일 쓰기 중 오류 발생! - %s", file.getName(), e.getMessage());

  } finally {
    try {
      out.close();
    } catch (IOException e) {
    }
  }
}

4단계 - CSV 형식의 문자열을 객체로 변환하는 것을 인터페이스를 이용해 규칙으로 정의한다.

CSV 문자열 받아서 분석하여 객체를 생성해주는 공장에 대한 규칙이다.

1
2
3
public interface CsvObjectFactory<T> {
  T create();
}

5단계: 파일의 데이터를 로딩하는 메서드를 통합한다.

saveObjects()는 두 개의 정보만 있으면 되었지만, loadObjects()는 CSV 문자열을 받아, T 타입의 객체를 생성해주는 공장인 CsvObjectFactory<T> factory까지 파라미터로 넘겨받아야 한다. 이 것은 인터페이스이기 때문에 실제로 넘겨지는 것은 구현체이다. 이 구현체는 직렬화하는 실제 클래스의 정보를 알고 있어야 한다. 그렇다면 어떻게 구현할 수 있을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void loadObjects(Collection<T> list, File file, CsvObjectFactory<T> factory) {
  BufferedReader in = null;
  
  try {
    in = new BufferedReader(new FileREader(file));
    while (true) {
      String record = in.readLine();
      if (record == null) {
        break;
      }
      list.add(factory.create(record));
    }
    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) {}
  }
}

다음과 같이 구현할 수 있다.

5-1. 중첩클래스로 구현한다.

1
2
3
4
5
6
7
class MyBoardFactory<T> implements ObjectFactory<T> {
  @Override
  public T create(String csv) {
    return (T) Board.valueOfCsv(csv);
  }
}
loadObjects(boardList, boardFile, new MyBoardFactory<Board>());

5-2. 익명클래스로 구현한다.

한 번만 생성하여 사용하기 때문에 익명 클래스로 구현해도 상관 없다.

1
2
3
4
5
6
7
ObjectFactory<Board> boardFactory = new ObjectFactory<T>() {
  @Override
  public T create(String csv) {
    return (T) Board.valueOfCsv(csv);
  }
}
loadObject(boardList, boardFile, boardFactory);

5-3. 익명클래스를 파라미터로 바로 넘겨준다.

1
2
3
4
5
6
loadObject(boardList, boardFile, new ObjectFactory<Board>() {
  @Override
  public Board create(String csv) {
    return Board.valueOfCsv(csv);
  }
})

5-4. 익명클래스를 람다문법으로 변경하여 간단하게 만든다.

1
loadObject(boardList, boardFile, csv -> Board.valueOfCsv(csv));

5-5. 메서드 레퍼런스 문법으로 인터페이스를 구현한다.

이때, 구현된 메서드는 그저 이미 존재하는 메서드를 호출하는 일밖에 하지 않는다. 따라서 그 자리에 그 일을 할 메서드가 있다면 구현할 필요 없이 메서드를 바로 주도록 하자. 람다로 객체 만들 필요 없이 기존 메서드를 그대로 줘버리면 간접적으로 인터페이스를 구현하게 된다.

1
loadObject(boardList, boardFile, Board::valueOfCsv);

이제 saveObjects()와 loadObjects()는 컬렉션과 파일 필드를 사용하지 않고, 이를 파라미터로 받는다. 모두가 공유하는 필드보다는 로컬 변수가 안전하다. 따라서 파라미터로 넘겨줄 거면 그냥 로컬 변수로 선언하도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class App {

  public static void main(String[] args) {
    // 스태틱 멤버들이 공유하는 변수가 아니라면 로컬 변수로 만들라.
    List<Board> boardList = new ArrayList<>();
    File boardFile = new File("./board.csv"); // 게시글을 저장할 파일 정보

    List<Member> memberList = new LinkedList<>();
    File memberFile = new File("./member.csv"); // 회원을 저장할 파일 정보

    List<Project> projectList = new LinkedList<>();
    File projectFile = new File("./project.csv"); // 프로젝트를 저장할 파일 정보

    List<Task> taskList = new ArrayList<>();
    File taskFile = new File("./task.csv"); // 작업을 저장할 파일 정보

    //....

6단계: ObjectFactory 구현체로서 생성자 레퍼런스를 전달한다.

현재 loadObjects()는 각 객체의 valueOfCsv() 메서드 레퍼런스를 파라미터로 받고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
  public static Board valueOfCsv(String csv) {
    String[] fields = csv.split(",");

    Board board = new Board();
    board.setNo(Integer.parseInt(fields[0]));
    board.setTitle(fields[1]);
    board.setContent(fields[2]);
    board.setWriter(fields[3]);
    board.setRegisteredDate(Date.valueOf(fields[4]));
    board.setViewCount(Integer.parseInt(fields[5]));

    return board;
  }

그런데 valueOfCsv 메서드는 CSV 문자열을 받아 도메인 객체로 만들어 리턴하는 메서드이다. 이 메서드 대신 csv 문자열을 파라미터로 받아 Board 객체를 생성해주는 생성자로 정의하면 어떨까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 다른 생성자가 있으면 컴파일러가 기본 생성자를 만들어주지 않으니까
// 다음과 같이 별도로 만들어야 한다.
public Board() {}

public Board(String csv) {
  String[] data = csv.split(",");
  
  setNo(Integer.parseInt(data[0]));
  setTitle(data[1]);
  setContent(data[2]);
  setWriter(data[3]);
  setRegisteredDate(Date.valueOf(data[4]));
  setViewCount(Integer.parseInt(data[5]));
}

위와 같이 정의할 수 있다. 이제 loadObjects()를 호출할 때 valueOfCsv() 메서드 레퍼런스 대신 생성자를 참조하는 생성자 레퍼런스를 파라미터로 넘겨주자.

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

💻 HTTP #7: HTTP 압축

코어 자바스크립트 #2.6: alert, prompt, confirm을 이용한 상호작용

Loading comments from Disqus ...