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)이나 애플리케이션 간에 데이터를 교환할 때 주로 사용한다.
플랫폼: 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);