주요글: 도커 시작하기
반응형
자바 1.4에 새롭게 추가된 Preference API의 사용법 및 특징에 대해서 살펴본다.

Preference API 사용하기

자바 1.4에는 설정 정보를 보다 쉽게 관리할 수 있는 Preference API가 추가되었다. Prefenrece API는 java.util.Properties 클래스와 마찬가지로 설정 정보를 읽거나 저장할 수 있는 기능을 제공하는데 그 사용방법은 매우 간단하다. 먼저 어떤 설정 정보를 저장하고 싶다면 다음과 같이 하면 된다. 이 코드는 이름이 "Port"인 키에 int 값 8080을 할당하고, "Document Root"인 키에 String 값 "/usr/local/apache/docs"를 할당한다.

  Preferences prefs = Preferences.userNodeForPackage(SomeClass.class);
  prefs.putInt("Port", 8080);
  prefs.putString("Document Root", "/usr/local/apache/docs");

Preference API에서 실제 설정 정보를 저장하고 읽어오는 기능을 제공하는 클래스는 java.util.prefs.Preferences로서, Preferences 클래스의 인스턴스는 위 코드에서 볼 수 있듯이 Preferences 클래스의 정적 메소드인 userNodeForPackage()를 사용해서 구할 수 있다. 이때, Preferences.userNodeForPackage() 메소드에 파라미터로 Class 인스턴스를 전달하는데, 이 Class 인스턴스를 전달하는 이유에 대해서는 'Preferences API의 트리 구조' 부분에서 설명하도록 하겠다.

설정 정보를 읽어오는 것 또한 간단하다. 앞 코드에서 봤던 putXXX 메소드 대신에 getXXX 메소드를 사용하면 된다. 예를 들면 다음과 같다.

  Preferences prefs = Preferences.userNodeForPackage(SomeClass.class);
  int portNo = prefs.getInt("Port", 8080);
  String documentRoot = prefs.getString("Document Root", "/usr/local/apache/docs");

위 코드에서 Preferences.getInt() 메소드는 인자를 두개 전달받는데, 첫번째 인자는 키값이고, 두번째 인자는 기본값이다. 예를 들어, 키값에 해당하는 값이 존재하지 않는다거나, (getInt() 메소드의 경우) 정수형으로 변환할 수 없다거나, 또는 설정 정보를 저장하고 있는 저장소(파일이나 데이터베이스 등)에 접근할 수 없는 경우에 getInt() 메소드는 두번째로 전달받은 기본값을 리턴하게 된다. 따라서 다음 코드에서 getInt() 메소드는 9090을 리턴한다.

  prefs.putInt("Port", 8080);
  int portNo = prefs.getInt("port1", 9090);

지금까지 살펴본 코드에서 눈여겨 볼 부분은 java.util.Properties 클래스와 달리 String 뿐만 아니라 int 타입도 설정 정보로 사용할 수 있다는 점이다. 실제, Preferences 클래스는 다음과 같은 타입에 대한 설정 정보를 사용할 수 있도록 지원하고 있다.

타입 읽기 메소드 쓰기 메소드
String get(String key, String def) put(String key, String val)
boolean getBoolean(String key, boolean def) putBoolean(String key, boolean val)
int getInt(String key, int def) putInt(String key, int val)
long getLong(String key, long def) putLong(String key, long val)
float getFloat(String key, float def) putFloat(String key, float val)
double getDouble(String key, double def) putDouble(String key, double val)
byte[] getByteArray(String key, byte[] def) putByteArray(String key, byte[] val)

허용되는 키와 허용되는 값

Preferences 인스턴스에 설정 값을 저장하거나 읽어올 때 키를 사용하는데, 이때 키의 이름에는 특별한 제한이 없다. 알파벳만을 쓰던, 한글명을 쓰던, 아니면 숫자와 문자를 복합적으로 사용하든 자바의 String 객체이기만 하면 키로 사용할 수 있다. 단, 주의할 점은 키의 길이가 Preferences.MAX_KEY_LENGTH 보다 작아야 한다는 것이다. 참고로, Preferences.MAX_KEY_LENGTH의 값은 80이다.

앞의 표에서 Preferences는 String, byte[], int, long, float, double, boolean 타입을 지원한다고 했는데, 이중 기본 데이터 타입에 해당하는 int, long, float, double, boolean 에는 값에 제한이 없고, String과 byte[]의 경우에는 값이 null이어서는 안 된다. 값의 크기는 바이트로 Preferences.MAX_VALUE_LENGTH (8196 바이트;8K)보다 작아야 한다.

String과 byte[] 배열을 저장할 때는 주의할 점이 있다. 먼저 자바의 String 객체는 한글자가 2 바이트를 차지하는 유니코드를 사용하기 때문에 실제 String 객체로 저장할 수 있는 값의 총 길이는 4K가 되는 셈이다. byte[]의 경우는 RFC2045에 지정되어 있는 Base64 인코딩 방식을 사용하여 인코딩된다. Base64 인코딩을 사용하여 인코딩된 결과는 실제 배열의 길이보다 그 길이가 길기 때문에 배열의 길이만으로는 8K 제한을 확인할 수 없다. 따라서 8K의 3/4 정도 되는 길이의 byte[] 까지만 값으로 사용하는 것이 좋다.


값의 반복검색 및 제거

Preferences 인스턴스가 저장하고 있는 모든 <키, 값> 쌍을 구하고 싶다면 keys() 메소드를 사용하면 된다. key() 메소드는 키값을 갖는 String 배열을 리턴한다. 다음은 keys() 메소드를 사용해서 모든 <키, 값> 쌍을 출력하는 예이다.

  Preferences pref = Preferences.userNodeForPackage( someClass.class);
  try {
      String[] keys = pref.keys();
  
      for (int i = 0 ; i < keys.length ; i++) {
        System.out.println(keys[i] + " = " + pref.get(keys[i], "none");
      }
  } catch(BackingStoreException ex) {
      ....
  }

Preferences 인스턴스에 있는 <키, 값> 쌍을 지울 때는 remove() 메소드나 clear() 메소드를 사용하면 된다. 먼저 remove() 메소드는 다음과 같이 사용한다. 이 코드는 이름이 "Some Key"인 키에 해당하는 쌍을 삭제한다.

  Preferences pref = Preferences.userNodeForPackage( someClass.class);
  pref.remove("Some Key");

remove() 메소드가 특정 키에 대한 삭제 처리를 한다면 clear() 메소드는 모든 키에 대한 삭제 처리를 해준다. 따라서, 모든 키에 대한 값을 제거하고 싶다면 clear() 메소드를 사용하면 된다.

사용자 설정 정보와 시스템 설정 정보

윈도우 2000이나 XP를 보면 사용자에 상관없이 시스템 전체에 적용되는 환경 변수를 지정할 수 있고 또한 각 사용자마다 별도의 환경 변수를 지정할 수 있도록 되어 있는 것을 알 수 있다. 비슷하게 리눅스나 유닉스 같은 시스템도 마찬가지로 전체 시스템에 적용되는 시스템 환경 변수와 각 사용자마다 다르게 적용되는 환경 변수를 지정할 수 있다. Preference API도 이러한 다중 사용자 시스템과 마찬가지로 시스템 전체에 영향을 미치는 설정 정보와 사용자 각각에 영향을 미치는 설정 정보를 별도로 처리할 수 있도록 하고 있으며, 이들을 각각 시스템 설정 정보(system Preference)와 사용자 설정 정보(user Preference)라 부른다.

Preference API는 사용자 설정 정보를 담고 있는 Preference 인스턴스와 시스템 설정 정보를 담고 있는 Preference 인스턴스를 리턴해주는 static 메소드를 제공하고 있으며, 이들 메소드는 다음과 같다.

  • Preference static userRoot()
  • Preference static userNodeForPackage(Class c)
  • Preference static systemRoot()
  • Preference static systemNodeForPackage(Class c)
위의 메소드 중에서 userRoot()와 userNodeForPackage(Class c)는 사용자 설정 정보를 담고 있는 Preference 인스턴스를 리턴하고, systemRoot()와 systemNodeForPackage(Class c)는 시스템 설정 정보를 담고 있는 Preference 인스턴스를 리턴한다.

트리로 구성된 설정 정보

앞에서 사용자 Preference 인스턴스를 구하는 메소드에는 userRoot()와 userNodeForPackage(Class c)가 있다고 했는데, 이때 Root와 Node라는 이름에서 Preference 인스턴스가 트리 형태로 구성되어 있다고 유추해볼 수 있을 것이다. 실제로 Preference API는 트리 형태로 preference 인스턴스들을 구성하고 있다. 각각의 Preference 인스턴스는 트리의 한 노드가 되며, 이 트리의 계층 구조는 패키지 이름을 통해서 결정된다. 다음 그림은 Preference 인스턴스의 트리가 어떻게 구성되는 지를 보여주고 있다.


타원-노드, 사각형-<키,값> 쌍위 그림이 사용자 Preference 인스턴스 트리를 보여주고 있다고 가정할 경우 위 그림에 있는 각각의 키에 대하여 값을 읽어오는 방법은 다음과 같다.

  Preferences rootPref = Preferences.userRoot();
  String val = rootPref.getString("somekey", "default value");
  
  Preferences javacanPref = Prefereces.userNodeForPackage(com.javacan.Description.class);
  val = javacanPref.getString("key", "val");
  
  Preferences mailPref = Preferences.userNodeForPackage(com.javacan.mail.MailManager.class);
  String smtpServer = mailPref.getString("smtp", "mail.javacan.com");
  
  Preferences dbPref = Preferences.userNodeForPackage(com.javacan.db.PoolManager.class);
  String driver = dbPref.getString("driver", "Oracle");
  String pools = dbPref.getString("pools", "test");

그림과 위 코드를 통해서 알 수 있는 것은 각 노드마다 그에 알맞은 Preferences 객체가 존재한다는 것이다. 그리고 각 Preferences 노드는 자신들만의 <키, 값> 목록을 갖고 있다. 따라서 같은 이름을 갖는 키라도 그 키가 어떤 노드에 포함되느냐에 따라서 다른 값을 가질 수 있게 된다.

노드의 경로 및 노드 처리

트리상의 각 노드는 경로를 사용하여 표현할 수 있다. 예를 들어, 앞의 그림에서 이름이 mail인 노드의 경로는 '/com/javacan/mail'로 표현되며, 루트 노드의 경우는 '/'로 표현된다. 이때 경로명은 절대 경로와 상대 경로로 표현할 수 있다. 먼저, 절대 경로는 루트 경로부터 차례대로 표시한다. 즉, mail 노드를 '/com/javacan/mail'로 표시하는 것이 바로 절대 경로이다. 반면에 상대 경로는 특정 노드에 대해서 상대적인 경로를 나타낸다. 예를 들어, '/com' 노드 입장에서 마지막에 있는 mail 노드는 'javacan/mail'과 같이 표현할 수 있다. 상대 경로를 표현할 때는 하위 트리에 포함되어 있는 노드만 표현할 수 있다. 예를 들어, '/com' 노드에서 상대 경로를 사용하여 '/org/jcore' 노드를 표현할 수는 없다는 얘기다.

Preferences 클래스는 node(String pathname) 메소드를 제공하는데, 이 메소드는 경로를 사용하여 다른 Preferences 인스턴스를 구해준다. 예를 들어, 다음과 같이 node() 메소드에 상대 경로 또는 절대 경로를 전달하여 그에 알맞은 Preferences 인스턴스를 구할 수 있다.

  Preferences rootPref = Preferences.userRoot(); // 루트('/') 노드
  Preferences comPref = rootPref.node("com"); // '/com' 노드
  Preferences mailPref = comPref.node("javacan/mail"); // '/com/javacan/mail' 노드
  Preferences jcore = comPref.node("/org/jcore"); // '/org/jcore' 노드

또한, node() 메소드를 사용하면 존재하지 않는 노드를 생성할 수도 있다. 예를 들어, '/child/pref' 노드가 존재하지 않는 상황에서 comPref.node("/child/pref")를 실행하면 '/child/pref' 노드가 생성된다.

일단 노드를 구하면 parent() 메소드를 사용하여 부모 노드를 구할 수 있다.

  // '/com/javacan/db' 노드
  Preferences node = Preferences.userNodeForPackage(com.javacan.db.Pool.class);
  
  Preferences parent = node.parent(); // '/com/javacan' 노드

자식 노드의 경우는 앞에서 설명한 node() 메소드를 사용하여 구할 수 있다는 것을 이미 알고 있을 것이다. Preferences 클래스는 자식 노드의 이름을 알 수 있도록 childrenNames() 메소드를 제공하고 있는데, 이 메소드는 자식 노드의 이름을 String 배열로 리턴한다.

  try {
      Preferences root = Preferences.userRoot();
      String[] childNames = root.childrelNames();
    
      for (int i = 0 ; i < childNames.length ; i++) {
          Preferences child = root.node(childNames[i]);
          ...
      }
  } catch(BackingStoreException ex) {
      ...
  }

removeNode() 메소드를 사용하면 노드를 삭제할 수도 있다. 단, 하위 노드까지 모두 삭제된다. 예를 들면 다음과 같다.

  try {
      Preferences comNode = Preferences.userRoot().node("/com");
      comNode.removeNode();
  } catch(BackingStoreException ex) {
      ...
  }

위와 같이 하면 /com 노드를 비롯한 그 하위에 있는 /com/javacan 을 비롯한 모든 노드를 삭제한다.

flush()/sync() 그리고 이벤트 리스너

flush()와 sync()

Preferences API는 내부적으로 사용하는 설정 정보 저장 장치에 상관없이 동작하도록 설계되었다. 즉, Preferences API는 노드 정보와 <키, 값> 정보를 저장하기 위해 데이터베이스를 사용하든 파일을 사용하든, 또는 원격 서버와 통신을 하든 사용자에게는 동일하게 동작한다. 그런데, 만약 putXXX() 계열의 메소드를 사용하여 설정 정보를 지정할 때마다 또는 새로운 노드를 생성할 때 마다 매번 데이터베이스, 파일, 또는 원격 서버에 그 결과를 저장한다면 성능상에서 문제가 발생할 수 있을 것이다.

그래서 Preference API는 노드의 변화나 키, 값의 변화가 발생할 때 마다 매번 저장하지 않고 대신 변경된 내용을 메모리상에 저장해두었다가 flush() 메소드를 사용하여 한번에 저장하도록 하고 있다.

  Preferences pref = Preferences.userRoot();
  
  pref.putInt("X", 95);
  pref.putInt("Y", 100);
  
  ... // 변경 내용이 아직 저장장소에 적용되지 않는다.
  pref.flush();
  
  // 변경 내용이 저장장소에 적용되었다.

단, 주의할 점은 반드시 flush() 메소드를 사용해야만 저장되는 것은 아니라는 점이다. 예를 들어, 어떤 JDK 구현체는 쓰레드를 사용하여 일정 주기로 변경된 데이터를 실제 저장소에 반영하도록 구현되어 있을 수도 있다. 중요한 건 flush() 메소드를 사용하면 확실하게 저장소에 변경된 내용이 반영된다는 점이다. 변경된 내용이 곧바로 반영되지 않는다는 것은 서로 다른 어플리케이션이 동시에 같은 시스템 노드에 접근할 때 데이터가 동기화되지 않는다는 것을 의미한다.

비슷하게, getXXX() 메소드를 호출할 때에도 데이터를 읽어오기 위해 매번 저장소에 접근하는 것은 아니기 때문에 다른 어플리케이션에서 flush() 메소드를 호출하여 변경 내용을 저장소에 반영한다 하더라도 반영된 데이터를 읽어오지 못하는 경우가 발생한다. Preference API는 반영된 내용을 getXXX() 메소드를 사용할 때 읽어올 수 있도록 하기 위해서 sync() 메소드를 제공한다. sync() 메소드를 수행하면, 다음번에 데이터를 읽어올 때 저장소에 있는 실제 내용을 읽어올 수 있게 된다. 즉, flush() 메소드와 sync() 메소드는 서로 보완적인 기능을 제공하고 있는 것이다.

flush() 메소드와 sync() 메소드를 호출할 때 알아야 할 점은 Preferences 인스턴스가 나타내는 노드 및 그 하위 노드에 대해서만 적용된다는 점이다. 예를 들어, '/com/javacan' 노드에 해당하는 Preferences 인스턴스의 flush() 메소드를 호출하였다면, '/com' 노드의 변경된 <키, 값> 쌍은 적용되지 않고 '/com/javacan' 노드나 '/com/javacan/mail' 노드와 같이 서브트리에 속해 있는 노드에 대해서만 변경된 결과가 적용된다. sync() 메소드도 마찬가지로 Preferences 인스턴스 노드를 상위노드로 하는 서브트리에 대해서만 변경된 데이터를 읽어온다.

이벤트 리스너

Preference API는 키값이 변경되거나 자식노드가 추가 또는 삭제될 경우 이벤트를 발생하여 어플리케이션에서 알맞게 처리할 수 있도록 해 주고 있다. 이 두 이벤트는 PreferenceChangeEvent와 NodeChangeEvent이며, 이 두 이벤트는 PreferenceChangeListener와 NodeChangeListener에 전달된다.

PreferenceChangeListener

PreferenceChangeListener는 설정 정보가 새롭게 추가, 제거 또는 변경되는 경우에 알맞은 처리를 하고 싶을 때 사용된다. (AWT의 이벤트 위임 모델과 같은 이벤트/리스너 방식을 사용한다.) 각각의 노드마다 리스너를 등록할 수 있는데, 이는 노드마다 별도로 이벤트 처리를 할 수 있다는 것을 의미한다.

이벤트 처리를 하려면 먼저 PreferenceChangeListener 인터페이스를 구현한 클래스를 작성하고, preferenceChange() 메소드를 알맞게 구현하면 된다. 다음은 PreferenceChangeListener 인터페이스를 구현한 예를 보여주고 있다.

  class RootNodeListener implements PreferenceChangeListener {
      public void preferenceChange(PreferenceChangeEvent pcs) {
          System.out.println(pce.getKey() + " = " + pce.getNewValue());
      }
  }

위 코드에서 보듯이 PreferenceChangeListener 인터페이스를 구현한 클래스는 preferenceChange() 메소드를 구현해주어야 한다. PreferenceChange() 메소드는 PreferenceChangeEvent 객체를 파라미터로 전달받는다. PreferenceChangeEvent 객체는 변경된 키와 그 값을 저장하고 있으며, 키와 값은 각각 getKey() 메소드와 getNewValue() 메소드를 사용하여 구할 수 있다. 위에서 작성한 이벤트 리스너는 다음과 같이 Preferences.addPreferenceChangeListener() 메소드를 사용하여 등록할 수 있다.

  Preferences userPref = Preferences.userRoot();
  userPref.addPreferenceChangeListener(new RootNodeListener());

  userPref.put("someKey", "someValue");
  userPref.put("somekey", "value2");
  userPref.remove("someKey");

위 코드를 수행했을 때 출력되는 결과는 다음과 같다. 이 출력 결과를 보면 삭제된 키에 대한 값은 null이라는 것을 알 수 있다.

  someKey = someValue
  somekey = value2
  someKey = null

PreferenceChangeEvent 클래스는 또한 getNode() 메소드를 제공하는 데, 이 메소드는 이벤트가 발생한 노드의 Preferences 인스턴스를 리턴한다.

NodeChangeListener

NodeChangeListener는 자식 노드가 추가되거나 삭제될 때 알맞은 처리를 할 수 있도록 해 준다. 다음은 NodeChangeListener 인터페이스를 구현하여 이벤트를 알맞게 처리할 수 있도록 해주는 클래스의 한 예이다.

  class RootNodeListener implements NodeChangeListener {
      public void childAdded(NodeChangeEvent nce) {
          System.out.println(
              nce.getParent().getName()+" 노드에 자식 추가:"+nce.getChild());
      }
      
      public void childRemoved(NodeChangeEvent nce) {
          System.out.println(
              nce.getParent().getName()+" 노드의 자식 저게:"+nce.getChild());
      }
  }

NodeChangeEvent 클래스는 getParent() 메소드와 getChild() 메소드를 제공하는데, getParent() 메소드는 제거되거나 삭제된 노드의 부모 노드에 해당하는(즉, NodeChangeListener를 등록한) Preferences 인스턴스를 리턴하고, getChild() 메소드는 추가되거나 삭제된 노드에 해당하는 Preferences 인스턴스를 리턴한다.

NodeChangeListener를 등록할 때는 다음과 같이 addNodeChangeListener() 메소드를 사용하면 된다.

  Preferences pref = Preferences.userRoot();
  pref.addNodeChangeListener(new RootNodeListener());
  
  ...

NodeChangeListener와 관련해서 알아두어야 할 점은 바로 하위 노드의 추가/제거에 대해서만 이벤트가 발생한다는 점이다. 에를 들어, 다음 코드를 보자.

  Preferences rootNode = Preferences.userRoot();
  rootNode.addNodeChangeListener(new RootNodeListener());
  
  // /madvirus 노드가 추가되므로
  // RootNodeListener의 childAdd() 메소드가 호출
  Preferences madvirusNode = pref.node("madvirus");
  
  // /madvirus/homepage 노드가 추가되지만,
  // rootNodeListener의 childAdd() 메소드는 호출되지 않음
  Preferences homepageNode = madvirusNode.node("homepage");

위 코드에서 루트 노드에 NodeChangeListener를 등록하였는데, 루트 노드의 바로 하위 노드인 '/madvirus'를 추가할 때는 NodeChangeListener가 사용되지만 바로 하위 노드가 아닌 '/madvirus/homepage'가 추가될 때는 NodeChangeListener가 사용되지 않는다.

결론

이 글에서 우리는 자바 1.4에 새롭게 추가된 Preferences API를 통해서 설정과 관련된 보다 다양한 기능을 사용할 수 있다는 것을 배웠다. Preferences API가 기존의 java.util.Properties 클래스에 비해 더욱 다양한 기능을 제공하게 된 이유는 Preferences API를 설계할 때 다음과 같은 것들을 목적으로 삼았기 때문이다.

  • 트리 구조의 데이터를 저장하는 계층적 구조를 제공해야 한다.
    계층적 구조를 사용함으로써 개발자들은 보다 유연하고 체계적으로 설정 정보를 사용할 수 있게 된다.

  • 파일의 위치를 기억할 필요가 없어야 한다.

  • 설정 정보 저장소가 사용 가능하지 않더라도 동작해야 한다.
    예를 들어, 설정 정보를 데이터베이스에 저장했을 때 데이터베이스가 동작을 멈춘다 해도 설정 정보를 사용하는 어플리케이션은 알맞게 수행되어야 한다.

  • 사용자 데이터와 시스템 데이터를 구분할 수 있어야 한다.

  • 다른 패키지 내지 어플리케이션의 데이터와 독립적이어야 한다.

  • 명시적으로 데이터를 저장하거나 읽을 필요가 없어야 한다.
    즉, 설정 정보를 저장하고 있는 파일의 위치를 기억할 필요가 없으며, 데이터의 읽고 쓰기가 API에 의해 알맞은 때 수행된다.
실제로 필자는 현재 개발하고 있는 일부 모듈에서 Preferences API를 사용하여 설정 정보를 활용하고 있는데, 위에서 언급한 설계 목적이 그대로 설정 정보 관리의 편리함으로 이어지고 있음을 느끼고 있다. 특히, 패키지이름을 사용하여 계층 구조로 설정 정보를 관리할 수 있다는 것은 설정 정보를 유연하면서도 체계적으로 관리할 수 있게 해준다. 게다가 java.util.Properties가 String 타입만 가능한 것에 비해 Preferences API는 기본 데이터 타입도 사용할 수 있다는 장점을 갖고 있다. 만약 위에서 언급한 설계 목적이 여러분이 필요로 하는 설정 정보 처리 시스템에 부합된다면, Preferences API를 사용해볼 것을 강력하게 권한다.

관련링크:

+ Recent posts