최근에 객체지향을 공부하면서 디자인패턴을 같이 공부하는 중이다. 디자인패턴은 '헤드 퍼스트 디자인 패턴'을 위주로 공부하는 중이었다. 좋은 예제도 많고 학습자가 생각하게 만드는 패턴이 괜찮은 책이었다.
각설하고, 옵저버 패턴을 공부하던 중에, 여기에서는 WeatherData를 받아와서 이를 표시해주는 Application으로 옵저버패턴을 연습해보는 예제가 있다. 패턴과는 별개로, 실제 데이터로 뭔가 돌아가게 하는 것이 근사하지 않나? 싶었다.
그래서 실제 날씨를 불러오는 api를 활용해보기로 했다. 잠깐 그런데, api를 불러올려면 api key가 필요할 것이다. 그렇다면 api의 key는 어떻게 관리하는게 좋을까? 여러가지 방법이 있겠지만, 지금까지의 경험으로 보았을 때 .env file과 같은 환경변수로 다루는 게 좋지않을까 싶었다.
그래서 api 매뉴얼을 읽어보기 전에 java에서 env file을 만들고 관리할 수 있는 방법을 찾아보았다.
그래서 .env file을 다룰 수 있게 라이브러리를 찾다가 dotenv-java를 발견했고, 이걸 하고있던 프로젝트에 붙여보았다. pom.xml에 디펜던시를 더하고 아래의 클래스를 구현했다.
package kr.ft.seoul.App.WeatherData;
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry;
public class EnvManager {
private Dotenv dotenv = Dotenv.configure()
.directory("/resource")
.filename("env")
.load();
public EnvManager() {
}
public String getApiKey() {
return this.getValueByKey("API_KEY");
}
public String getValueByKey(String key) {
return this.dotenv.get(key);
}
}
그런데...
왜인지 모르겠는데 java.langNoClassDefFoundError로 고생했다. 라이브러리에서 제시하는 예제와 비슷하게 적용해도 안되고, 다른 분들이 작성하신 코드를 참고해도 잘 안되었다. 디렉토리와 파일, 디펜던시 등등을 체크하고 종속성 체크를 하려다가 문득 이 방법이 주된 것인지 다시 생각해보았다.
'.env'로 관리를 하는 게 모든 언어와 플랫폼에서 주된 방식인지 생각해보았다. Node.js로 개발을 먼저 배운 나에겐 .env 파일을 dotenv로 먼저 관리하는 방법을 접했고 그게 상식이었다. 그 상식이 java에서도 동일할까? 종속성 체크를 위해 해당 라이브러리 정보를 파악하다가 정작 해당 라이브러리의 usages 수치가 낮은 걸 새롭게 알게되었다.
찾아보니 java에서는 환경변수를 관리하는 방법이 다양했고, 이를 직접 구현하기도 쉬웠다. 그 중에서 Properties
클래스를 활용하는 방법을 택했다.
Properties
클래스는 java.util 에서 지원하는 클래스 중 하나이며, 설정정보를 percistant set으로 저장하고 관리할 수 있다.
실제로 만든 EnvManager
클래스의 구현을 통해 어떻게 파일을 읽어오고 생성하며, 저장된 정보를 활용하고, 수정하고, 내용을 별도의 파일에 저장할 수 있는지 알아보자.
public EnvManager() {
String propertiesPath = "src/main/resource/env.properties";
// String propertiesXMLPath = "src/main/resource/env.xml";
this.properties = new Properties();
try {
properties.load(new FileInputStream(propertiesPath));
// properties.loadFromXML(new FileInputStream(propertiesXMLPath));
} catch (IOException e) {
e.printStackTrace();
}
}
Properties
클래스를 인자없이 생성하면 기본적으로 내부에 어떠한 key-value를 저장하지 않은 채로 생성한다. 이후 properties
는 load()
메소드를 통해서 Reader
혹은 InputStream
클래스를 파라미터로써 활용할 수 있다. 즉, Reader
와 InputStream
클래스를 상속받아서 확장한 클래스에도 동작이 가능하다. 활용한 예제의 경우 InputStream
클래스를 상속받은 FileInputStream
클래스를 활용했다. 편한 것을 활용하자.
load()
메소드가 실행되면 파일에서 값을 읽어와 키-값 형식으로 Hashtable에 저장한다. 이는 Properties 클래스가 Hashtable을 상속받아서 구현되었기에 가능하다. 라인 기반으로 읽어와서 관리하기 때문에 아래와 같은 예시도 적용된다.
NAME= jekim
HELLO=world, banana, i'm \
cheki
EMPTY=
Properties.loadFromXML()
메소드를 활용하면 XML 형식으로 만들어진 properties 파일도 파싱하여 안에 있는 데이터를 Properties
클래스에 저장하는 것이 가능하다.
public boolean isKeyExist(String key) {
return this.properties.containsKey(key);
}
public String getValueByKey(String key) {
return this.properties.getProperty(key);
}
간단한 탐색은 Properties.containsKey(String key)
를 이용해서 가능하다. 이름에서 눈치를 채신 분도 있겠지만 해당 메소드는 Hashtable에서 지원하던 걸 상속받아와서 활용한 것이다. 따라서 contains()
와 containsValue()
같은 단순 탐색에서부터, KeySet()
, isEmpty()
등등도 활용 가능하다.
public boolean addProperty(String key, String value) {
return this.properties.putIfAbsent(key, value) == null ? true : false;
}
Properties
는 java.util.Map
인터페이스에서 제공하는 메소드도 구현되어 있어 이를 활용하는 것이 가능하다. 원래는 Hashtable.containsKey()
를 활용해서 값이 있는지 체크하고, 있으면 false를 리턴하고 없으면 키-값을 추가한 뒤 true를 리턴하도록 했으나, 수정된 코드가 더 간단해보여서 위와 같이 수정했다.
public boolean removePropertyByKey(String key) {
return this.properties.remove(key) == null ? false : true;
}
public boolean editPropertyValueByKey(String key, String value) {
return this.properties.replace(key, value) == null ? false : true;
}
마찬가지로 java.util.Map
인터페이스에서 정의되어 있고 Properties
에서 지원하는 remove()
, replace()
를 이용해서 수정과 삭제를 구현했다.
public void savePropertiesFile(String path) {
try {
this.properties.store(new FileOutputStream(path + "_saved"), null);
// this.properties.save() // deprecated
// this.properties.storeToXML(new FileOutputStream(path + "new.xml"), null);
} catch (IOException e) {
e.printStackTrace();
}
}
Properties
클래스는 저장된 키-값을 파일로 저장하는 기능을 지원한다. Properties.save()
와 Properties.store()
메소드가 지원하는데, 위에서 보았던 load()
가 InputStream
의 인스턴스를 매개변수로 받았던 것과 마찬가지로 OutputStream
클래스의 인스턴스를 매개변수로 받는다. 이를 통해 특정 경로에 파일로 저장할 수 있다.
이 두 개의 메소드 중에 save()
는 공식적으로 deprecated 되어있다. save()
는 리턴이 void라서, 해당 메소드를 사용할 때에 키값을 파일로 저장하는 도중에 I/O Error가 발생하면 I/O Exception을 throw하지 않기 때문이다. 가급적이면 store()
을 쓰자. Oracle 공식 메뉴얼에서도 이를 권고하고 있다.
Properties.loadFromXML()
를 이용해서 XML파일을 읽어올 수 있었던 것처럼, Properties.storeToXML()
를 이용하면 Properties
클래스 내의 키-값 데이터를 XML파일로 저장하는 것이 가능하다.
위와 같이 간단하게 EnvManager이라는 클래스를 만들어보았다. 복잡한 것을 추가하거나 하지 않고, 그냥 매뉴얼을 읽고 이미 있는 메소드를 잘 가져와서 쓰자는 생각으로 만들었다.
package kr.ft.seoul.App.WeatherData;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Properties;
import java.util.Set;
public class EnvManager {
private Properties properties;
public EnvManager() {
String propertiesPath = "src/main/resource/env.properties";
this.properties = new Properties();
try {
properties.load(new FileInputStream(propertiesPath));
} catch (IOException e) {
e.printStackTrace();
}
}
public String getValueByKey(String key) {
return this.properties.getProperty(key);
}
public Set<String> getKeyList() {
return this.properties.stringPropertyNames();
}
public boolean editPropertyValueByKey(String key, String value) {
return this.properties.replace(key, value) == null ? false : true;
}
public boolean removePropertyByKey(String key) {
return this.properties.remove(key) == null ? false : true;
}
public void savePropertiesFile(String path) {
try {
this.properties.store(new FileOutputStream(path + "_new"), null);
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean isKeyExist(String key) {
return this.properties.containsKey(key);
}
public boolean addProperty(String key, String value) {
return this.properties.putIfAbsent(key, value) == null ? false : true;
}
}