gson 라이브러리를 사용한 파일 입출력 기능 구현하기
프로젝트에 파일 입출력 기능을 추가하자!
이전에 살펴본 todo-list-console 프로젝트에서 가장 아쉬운 점은 정보가 메모리 상에서만 저장되기 때문에 프로그램이 종료되면 실행 간에 작업했던 내용을 모두 잃는다는 점 입니다. 그래서 이러한 문제점을 해결하기 위해서 파일 입출력을 사용하여 프로그램을 실행하는 중에 작업한 내용을 파일 형태로 저장하고, 프로그램을 재실행했을 때 이 파일을 읽어서 원래 작업 내용을 불러오는 기능을 구현해보겠습니다.
작업한 내용을 파일 형태로 저장한다고 하면 일반적으로 가장 쉽게 생각할 수 있는 방법이 txt 파일로 작업한 내용을 저장하는 것 입니다. 그것이 이전에 C언어 파일 입출력을 공부하면서 경험해본 방법이기도 하고, 파일 입출력에서 가장 기본적으로 다루는 것이 txt 파일이기 때문입니다.
하지만 현재 상황에서 txt 파일의 형태로 작업내용을 저장하는 것은 상당히 많은 문제가 있습니다. 그 중에서도 가장 큰 문제는 현재 프로젝트에서 작업내용을 저장하기 위해서는 프로그램 내에서 생성되는 Todo object를 저장해야 하는 데 이를 단순히 txt 파일로 저장하는 것이 꽤 복잡한 문제라는 것 입니다. Todo object는 아래와 같은 data field를 가집니다.
private long id;
private String content;
private List<Todo> parents;
private LocalDateTime createAt;
private LocalDateTime updateAt;
private LocalDateTime finishAt;
하나의 Todo object가 위와 같이 6개의 data field를 가져야하고, 이러한 Todo Instance 여러 개가 모여서 하나의 프로젝트 data가 되는 것인데 이 각각에 대한 정보를 단순히 text로 저장한다고 하면 어떨까요? 저장은 어느정도 할 수 있다고 하지만 해당 파일을 읽어와서 객체를 다시 생성하고 하는 과정에서 굉장히 많은 수고가 따르게 됩니다. txt 파일 내용 하나하나를 일일히 parsing 해야하고 그걸 객체에 할당해서 다시 생성하고... 보통 복잡한 과정이 아닙니다.
그래서 해당 todo-list 프로젝트를 txt 파일을 통해서 파일 입출력 기능을 구현하는 것은 다소 무리가 있습니다. 그래서 문제에 대해서 좋은 솔루션이 될 수 있는 것이 바로 json 파일을 사용하는 것 입니다. json은 Java Script Object Notation의 약자로서 데이터를 효율적으로 표현하기 위해서 사용되는 data format 이라고 보시면 됩니다. 그래서 위에서 살펴본 Todo Object 1개를 json으로 표현한다고 하면 아래와 같이 표현할 수 있습니다.
{
"id": 1,
"content": "test1",
"parents": [],
"createAt": {
"date": {
"year": 2021,
"month": 7,
"day": 12
},
"time": {
"hour": 20,
"minute": 23,
"second": 36,
"nano": 218000000
}
}
},
json은 애초에 java object를 파일로 저장하고 이를 읽어와서 다시 object를 생성하는 데 사용하기 위한 목적으로 사용되기 때문에 이전에 위에서 살펴봤던 문제에 대해서 좋은 솔루션이 될 수 있습니다. 그리고 java에서 object를 쉽게 json으로 저장하고, 이 json을 읽어서 다시 object를 생성하는 과정을 쉽게 수행할 수 있도록 해주는 라이브러리가 "gson" 이라고 합니다.
gson을 사용하면 이전에 우리가 고민했던 Object를 파일로 어떻게 저장하고, 이 파일을 읽어와서 어떻게 다시 Object를 생성할 지에 대한 내용들을 아주 쉽게 몇줄의 코드만으로 처리할 수 있게 됩니다. 그렇기 때문에 이 gson의 사용법에 대해서 공부할 가치는 충분합니다.
gson을 사용하기 위해서는 gson 라이브러리를 프로젝트에 import 시켜주는 과정이 필요합니다. maven이나 gradle을 사용한다면 단순히 몇줄의 코드를 추가해주면 되고, gson 라이브러리를 직접 다운로드해서 프로젝트 라이브러리에 추가하는 것도 가능합니다. 저 같은 경우에는 프로젝트에서 사용하는 gradle이 gson을 정상적으로 import하는 문제가 있어서 해당 라이브러리를 직접 다운로드해서 추가하였습니다.
위와 같이 gson 라이브러리가 정상적으로 추가되었다면 이제 gson을 프로젝트 내에서 사용할 수 있게 됩니다. 이론적인 측면 보다는 이 gson을 실용적으로 해당 문제를 해결하는 데 어떻게 사용할 수 있는 지에 집중해서 gson 라이브러리를 살펴볼 수 있도록 하겠습니다.
파일 입출력 기능을 구현하기 위해서는 다음 두 가지 기능을 구현해야 합니다.
1. 현재까지 저장된 Todo Object들을 json 파일로 저장
2. 저장된 json 파일을 읽어서 저장했던 Todo Object를 프로그램 내에서 생성
그렇다면 지금부터 gson을 사용해서 다음 기능들을 구현할 수 있는 지 순서대로 살펴보도록 하겠습니다.
우선 기본적으로 다음 파일 입출력 기능을 구현하기 위해서 실제로 프로젝트에서 사용자가 입력한 Todo를 저장하는 TodoRepository class 내에서 가장 많은 작업을 수행하였습니다.
Todo Object 들을 json 파일로 저장
우선 현재 프로그램 상에서 유지하고 있는 Todo Object 들을 파일로 저장하는 기능을 구현하기 위해서 TodoRepository class 내에 "save"라는 method를 정의하였습니다. 구현한 save method의 코드는 다음과 같습니다.
public String save() {
try (Writer writer = new FileWriter("backup.json")) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
gson.toJson(TodoRepository.getInstance().findAll(), writer);
writer.close();
return "Success!";
}
catch(Exception e) {
return "Failed...";
}
}
위의 코드만을 가지고 Todo Object를 json으로 저장하는 기능을 구현할 수 있습니다. 우선 기본적으로 파일 입출력에 대한 부분이기 때문에 try-catch 문 구조를 사용하고 있습니다. 그렇다면 순차적으로 각 코드를 분석해보도록 하겠습니다.
우선 FileWriter Object를 생성합니다. 이 때, 내용을 저장할 파일의 경로를 "backup.json"으로 지정했습니다. 이는 프로젝트 root에서 backup.json 이라는 파일에 어떤 내용을 쓰겠음을 의미합니다. 만약 해당 파일이 존재하지 않느다면, 해당 파일을 생성한 후 해당 파일에 내용을 기록하게 됩니다. 또한 FireWriter의 Constructor를 사용 시 두 번째 parameter를 true로 할당하게 되면 기존에 저장되어 있는 내용을 유지하면서 그 뒤에 내용을 추가하게 되는 데, 이번 기능에서는 사용자가 저장기능을 호출한 당시에 프로그램에서 저장하고 있는 내용만을 저장하는 것이기 때문에 기본값인 false를 사용했습니다.
그래서 만약 정상적으로 File이 open 되면, try문의 내부 로직을 수행하게 되고, 어떤한 문제로 오류가 발생하게 되면 catch 문 내의 로직을 수행하여 "Failed..."라는 메시지를 출력하는 것을 확인할 수 있습니다.
Writer writer = new FileWriter("backup.json")
파일을 정상적으로 열었다면 이번에는 Gson Object를 생성합니다. 그냥 단순히 Gson Object를 생성하는 것이 목적이라면 new Gson()과 같이 Gson class의 constructor를 호출하면 되지만, 아래의 코드에서는 Gson Class의 Constructor가 아닌 GsonBuilder Class의 Constructor를 호출하고 있는 것을 확인할 수 있습니다.
GsonBuilder Class를 사용하는 이유는 해당 class를 사용하면 Gson class의 instance를 생성할 때 여러 세팅들을 사용해서 생성하는 것이 가능하기 때문입니다. 그렇다면 여기서 무슨 세팅들을 하기 위해서 이러한 선택을 한 것인지 설명해보도록 하겠습니다.
아래의 코드에서 확인할 수 있듯이 GsonBuilder Class의 Constructor에서 "setPrettyPrinting" method를 호출한 것을 확인할 수 있습니다. 이는 json 파일에 내용을 저장할 때 아래와 같이 가독성이 좋은 형태로 저장할 수 있도록 해주는 옵션에 해당합니다. 만약 해당 옵션을 사용하지 않으면 json 파일은 마치 코드 난독화 작업을 해둔 것처럼 쭉 한줄로 저장되어 가독성이 매우 떨어지고 보기 좋지 않습니다. setPrettyPrinting 옵션은 아주 유용하게 사용되는 옵션이니 잘 기억해두고 활용해보시기 바랍니다.
그리고 최종적으로 create() method를 통해서 해당 옵션들이 적용된 Gson Object를 생성합니다.
Gson gson = new GsonBuilder().setPrettyPrinting().create();
이제 다음으로 현재 프로그램에서 유지하고 있는 Tdoo Object들을 모두 json 파일로 저장하는 코드입니다. 실제로 아래 한줄의 코드를 통해서 json 파일로의 저장작업을 처리하게 됩니다.
아래의 코드에서 확인할 수 있듯이 toJson method를 사용하고 있는 것을 볼 수 있습니다. 이름 그대로 toJson method는 인자로 전달받은 Object를 json의 형태로 변환해주는 기능을 수행합니다. 그래서 첫번째 parameter로 TodoRepository Instance에 저장되어 있는 Todo Instance의 List를 .findAlll() method를 통해서 불러오는 모습입니다.
여기서 TodoRepository Instance에서 getInstance() method를 사용한 이유는 TodoRepository의 Instance를 프로젝트 내의 다른 class끼리도 공유해서 사용하기 위해 Singleton Pattern을 사용하여 프로그램을 작성했기 때문입니다. 즉, getInstance method의 역할은 현재 프로젝트 내에서 사용하는 TodoRepository Instance를 얻어오는 역할을 수행하는 부분이라고 보시면 됩니다.
그리고 이어서 두번째 parameter로 이전에 생성한 writer를 전달함으로서 처리한 내용을 backup.json 파일로 저장하게 됩니다. 해당 코드가 실행되면 아래와 같이 프로젝트의 root에 backup.json 파일이 생성되고, 프로그램에서 유지중이던 TodoRepository의 내용을 json 파일로 저장한 것을 확인할 수 있습니다.
gson.toJson(TodoRepository.getInstance().findAll(), writer);
이렇게 정말 몇줄의 코드만으로 간단하게 java 프로그램에서 유지 중인 Object를 json 파일로 저장할 수 있습니다.
저장된 json 파일을 읽어서 저장했던 Todo Object를 프로그램 내에서 생성
그렇다면 이번에는 이렇게 저장한 json 파일을 프로그램에서 읽어서 Object를 다시 생성하는 load 기능을 마찬가지로 gson을 사용하여 구현해보도록 하겠습니다. json 파일로부터 정보를 읽어서 Object를 프로그램 내에서 생성하는 기능은 TodoRepository Class에서 "load" 라는 method를 통하여 구현되었습니다. load method의 전체 코드는 아래와 같습니다.
public String load() {
Gson gson = new Gson();
try {
String path = "backup.json"; // file path for reading
String jsonData = readFileAsString(path); // load the json file content
JsonParser parser = new JsonParser(); // 파싱을 위한 객체를 생성
JsonArray jsonArray = (JsonArray)parser.parse(jsonData);
// access each todo
for(int i=0 ; i<jsonArray.size() ; i++) {
JsonObject object = (JsonObject)jsonArray.get(i);
Todo todo = gson.fromJson(object, Todo.class);
TodoRepository.getInstance().add(todo);
}
idGenerator.changeStartPoint(jsonArray.size()+1);
return "Success!";
}
catch(Exception e) {
System.out.println(e);
return "Failed...";
}
}
우선 아래와 같이 path라는 String에 읽어올 file의 path를 미리 저장해둡니다.
String path = "backup.json";
String jsonData = readFileAsString(path);
그리고 이어서 readFileAsString이라는 method의 parameter로 이 path를 전달해서 저장된 json 파일의 모든 내용을 String으로 읽어오는 것을 확인할 수 있습니다. 여기서 호출한 readFileAsString method는 아래와 같이 정의되어 있습니다. 결과적으로 위의 코드를 실행하게 되면 jsonData 변수는 저장된 json 파일에 들어있는 모든 내용을 String의 형태로 저장하게 됩니다.
public static String readFileAsString(String file)throws Exception
{
return new String(Files.readAllBytes(Paths.get(file)));
}
그리고 이어서 JsonParser, JsonArray Object를 생성합니다. JsonParser class는 말그대로 읽어온 json string을 적절히 parsing 할 때 사용하는 class 이고, JsonArray는 json 파일 내부적으로 존재하는 여러 개의 object들을 하나의 Array로 표현하는 데 사용하는 class라고 이해하시면 됩니다. 여기서도 json array를 사용하는 이유는 우리가 이전에 저장한 내용도 Todo Object의 List이기 때문입니다.
그리고 이어서 생성한 JsonArray instance에 JsonParser의 parse method를 호출해서 이전에 생성한 json string을 parsing하는 작업을 수행합니다. 이렇게 함으로써 JsonArray에 Todo Object가 Array의 형식으로 담기게 되고 Array indexing을 하는 것처럼 각각의 Todo Object에 접근할 수 있게 됩니다. 어떻게 접근이 이뤄지는 지는 뒤에서 확인해보도록 하겠습니다.
JsonParser parser = new JsonParser();
JsonArray jsonArray = (JsonArray)parser.parse(jsonData);
그리고 위에서 생성한 JsonArray를 기반으로 각 하나하나의 Object에 접근하게 됩니다. 아래의 코드를 통해서 볼 수 있듯이 .get(index) method를 통해서 JsonArray에 저장되어 있는 각각의 Object에 접근하는 것을 확인할 수 있습니다. 그리고 이렇게 읽어온 Object를 JsonObject라는 Class의 instance로 담는 것을 확인할 수 있습니다. 그리고 여기서 주목할 점은 "fromJson"이라는 method의 사용입니다.
이름에서 알 수 있듯이 fromJson method는 json 파일로부터 java object를 생성하는 데 사용합니다. 우선 첫번째 인자로 이전 단계에서 생성한 JsonObject를 넘겨줍니다. 그리고 두번째 인자로 Todo.class를 넘겨주는 것을 볼 수 있는 데 이는 Gson을 통해서 class를 생성할 때 참조의 기준이 설정하기 위함입니다. 따라서 Todo.class를 인자로 넘겨주는 것은 이전에 첫번째 인자로 넘겨준 JsonObject를 Todo class의 instance로 생성하라는 것을 의미합니다.
결과적으로 아래의 코드에서 todo instance에는 json 파일로부터 읽어온 정보를 가지는 Todo instance가 됩니다. 그리고 마지막으로 TodoRepository에 이를 add 함으로서 이전의 기록을 유지하게 되는 것입니다. 놀랍게도 그 어느 복잡한 parsing의 과정도 없이 너무나도 간단하게 Object를 생성할 수 있습니다. 심지어 Todo Object는 자신의 data field에도 Object를 가지고 있음에도 불구하고, 너무나도 간단하게 Object를 생성할 수 있습니다.
이러한 부분에 있어서 gson이 가지는 힘은 강력 합니다. 다소 복잡할 수 있는 Object를 너무나도 쉽게 json 파일로 저장하고, 이를 읽어와서 다시 Object를 생성하는 과정을 너무나도 간단하게 몇 줄의 코드만으로 이를 구현할 수 있습니다.
그렇다면 여기서 드는 의문점이 있습니다.
물론 이 질문처럼 한다면 훨신 더 편하게 코드를 구성하는 것이 가능할 것 입니다. 하지만 이렇게 하지 않고 JsonArray를 통해서 일일히 하나씩 Todo Object를 생성한 이유는 이전의 작업에서 TodoRepository를 singleton pattern으로 정의했기 때문입니다. Singleton pattern을 사용하면, getInstance method를 통해서 얻어오는 instance 외에는 TodoRepository의 instance를 생성할 방법이 없습니다.
for(int i=0 ; i<jsonArray.size() ; i++) {
JsonObject object = (JsonObject)jsonArray.get(i);
Todo todo = gson.fromJson(object, Todo.class);
TodoRepository.getInstance().add(todo);
}
그래서 어쩔 수 없이 for 반복문과 JsonArray를 통해서 Todo Instance를 각각 생성하고 이를 TodoRepository에 지속적으로 추가한 것 입니다.
'Web Backend > Java' 카테고리의 다른 글
Collection Framework 이란? (0) | 2022.03.26 |
---|---|
콘솔 상에서 비밀번호 처리하기 (0) | 2021.08.29 |
JVM의 메모리 구조 (0) | 2021.07.03 |
Variable 과 Method (0) | 2021.06.29 |
Class와 Object (0) | 2021.06.29 |
댓글
이 글 공유하기
다른 글
-
Collection Framework 이란?
Collection Framework 이란?
2022.03.26 -
콘솔 상에서 비밀번호 처리하기
콘솔 상에서 비밀번호 처리하기
2021.08.29 -
JVM의 메모리 구조
JVM의 메모리 구조
2021.07.03 -
Variable 과 Method
Variable 과 Method
2021.06.29