GoF의 디자인 패턴에서는

 

> Inheritance통한 재사용을 white-box reuse

> Composition 통한 재사용을 black-box reuse

 

으로 정의 한다. Inheritance Composition의 장단점을 명확히 파악하고 적절한 곳에 사용하는 것이 중요하다.

 

  

White-box (Inheritance)

투명한 상자 안에 어떤 것이 들어 있는지 밖에서 볼수있다는 .

 

장점

ü  오버라이딩을 통해 슈퍼클래스의 구현을 손쉽게 재정의가 가능하다.

 

단점

ü  슈퍼클래스의 구현이 변경되면 그 하위 클래스 모두를 변경해야한다.

ü  상속시 슈퍼클래스가 하위클래스에  불필요하게 많은 부분이 노출되어 하위클래스가 
슈퍼클래스의 구현에 종속됨으로 써 캡슐화에 위배가 된다.

ü  컴파일 시점에 상속에 의한 객체의 형식이 이미 결정된다 (한개의 부모 클래스만 상속 받기 때문에)

ü  시스템이 진화 할수록 새로운기능이 추가되거나 오류 수정으로 인해 상속관계가 복잡해진다.

 

Black-box (Composition)

상속과 달리 내부가 비치지 않아 밖에서 박스 안을 없다는 .

 

장점

ü  객체합성은 객체가 다른객체의 참조를 얻는 방식으로 런타임 시에 동적으로 이뤄지기 때문에 해당 객체의 인터페이스만을 바라보게 됨으로써 캡슐화가 이루어질 있다.

단점

ü  객체간의 관계가 수평관계가되어 객체나 메소드명이 명확하지 않으면 코드의 가독성이 떨어지고 이해하기 어렵다.

ü  용도에 따라 클래스들을 패키지로 적절하게 분리하고 사용 용도가 명확하게 들어나도록 인터페이스를 설계해야

 

 

 출처 : 자바 I/O & NIO 네트워크 프로그래밍, 김성박.송지훈 공저

 

Posted by Steven J.S Min
,

Observers and Observables

java.util.Observer 인터페이스와 java.util.Observerable클래스는 비교적 단순하지만 Java에서의 디자인패턴의 기본을 엿볼수 있는 모델이 될수 있겠다.

특정 오브젝트나 리소스(Observable)가 변경이 일어났을 경우여러개의 클라이언트(Observer) 들이 변경 내용을 통보 받을수 있도록 한다.

 

다음의 예제는 게시판(MessageBoard) 등록된 학생들에게 변경된 내용을 통보하는 예제이다. 따라서 게시판은 Observable을 상속하였으며 학생들은 Observer 인터페이스를 구현하였다

 

참고자료 : Interface as Callback [ http://stevenjsmin.tistory.com/19 ]

 

 

package ch11;

 

import java.util.Observable;

import java.util.Observer;

 

public class MessageBoard extends Observable {

       private String message;

 

       public String getMessage() {

              return message;

       }

 

       public void changeMessage(String message) {

              this.message = message;

              setChanged(); // 만일 너무 자주 변경이 일어나는 경우 문제가 될수 있기때문에 유연성을 제공하기 위한 메소드

              notifyObservers(message);

       }

 

       public static void main(String[] args) {

              MessageBoard board = new MessageBoard();

              Observer bob = new StudentA();

              Observer joe = new StudentA();

              Observer paul = new StudentB();

              Observer steven = new StudentB();

             

              //  Observable 구현한 객체들을 Observable.addObserver(Observer o)메소드를 통하여

              //  모두 등록하고 Observable.notifyObservers(Object arg)에서 등록된 모든 Observer들의 Update() 실행한다.

              /*  다음은  Observable에서 등록된 Observer들의 Update() 실행시키는 코드이다.

               *  for (int i = arrLocal.length-1; i>=0; i--)

               *      ((Observer)arrLocal[i]).update(this, arg);

               */

              board.addObserver(bob);

              board.addObserver(joe);

              board.addObserver(paul);

              board.addObserver(steven);

              board.changeMessage("More Homework!");

       }

} // end of class MessageBoard

 

 

import java.util.Observable;

import java.util.Observer;

 

class StudentA implements Observer {

      

       /**

        * 연락을 보내는 주제 객체(o)와 주제에서전달된 데이터 객체(arg)가 인자로 전달된다.

        *@see java.util.Observer#update(java.util.Observable, java.lang.Object)

        */

       public void update(Observable o, Object arg) {

              System.out.println("StudentA:Message board changed: " + arg);

       }

}

 

 

import java.util.Observable;

import java.util.Observer;

 

class StudentB implements Observer {

      

       /**

        * 연락을 보내는 주제 객체(o)와 주제에서전달된 데이터 객체(arg)가 인자로 전달된다.

        *@see java.util.Observer#update(java.util.Observable, java.lang.Object)

        */

       public void update(Observable o, Object arg) {

              System.out.println("StudentB:Message board changed: " + arg);

       }

}

 

 

결과 : 

    StudentB:Message board changed: More Homework!

    StudentB:Message board changed: More Homework!

    StudentA:Message board changed: More Homework!

    StudentA:Message board changed: More Homework!

 

 

'Java > Core Utilities' 카테고리의 다른 글

Synchronized and Unsynchronized Collections  (0) 2013.11.04
Navigable Collection  (0) 2013.11.04
Logger  (0) 2013.02.09
Preferences & PreferenceChangeListener  (0) 2013.02.09
Properties  (0) 2013.02.09
Posted by Steven J.S Min
,

Logger

Java/Core Utilities 2013. 2. 9. 14:35

Logger를 처음 생성할때 로거 이름을 파라미터로 ‘.’으로  구분을 주거나 클래스를 매개변수로 넘겨 주게된다.

 

    static Logger log = Logger.getLogger("com.oreilly.learnjava.Book");

 

이러한 로거 이름은 계층적으로 관리되어지며 계층적 상위의 설정내용이 하위 설정으로 자동적으로 상속이 되도록 한다.

 

Handlers

실제로 로그메시지가 어느 곳으로 보내어 질지는 Handler 객체에의해 결정되어진다. Handler로서 기본적으로 제공되어 지는 것으로 ConsoleHandler, FileHandler, StreamHandler, SocketHandler 등이 있다. 그 이외에 MemoryHandler가 있는데 순환형태의 버퍼형태로 작동된다. MemoryHandler는 많은 로그정보를 메모리에 담고있다가 특정 트리거에의해 다른 핸들러로 메시지들을 보내(Publish)게 된다.

 

Filters

로그 메시지는 해당 핸들러 또는 상위 부모 핸들러에의해 처리(Publish)되기 이전에 처리되기 위해 로깅 Level이 적절한지를 먼저 체크하며 만일 해당 메시지가 로깅 레벨에 적합하지 않게되면 소스에서 무시하게된다. 이러한 필터는 별도구 구현되어질 수 있으며 런타임시 어플리케이션의 외부에서 적용 가능하다.

 

Formatters

로그 메시는 Formatter를 이용하여 로그 메시지의 출력형태를 설정할 수 있다. 이러한 Formatter의 종류로는 SimpleFormatter, XMLFormatter등이 있으며 별도의 Formatter를 구현할 수있다.

 

Logging Setup Properties

기본 로깅에대한 설정은 jre가 설치된 lib/logging.properties 파일에 정의되어 있지만 JVM 옵션으로 별도의 설정파일을 지정할 수 있다.

> Java –Djava.util.logging.configfile=myfile.properties

와 같이 지정하면 된다.



# Default logging level

.level = INFO

 

# Ouput to file and console

handlers = java.util.logging.FileHandlerjava.util.logging.ConsoleHandler

 

# Configure the file output

java.util.logging.FileHandler.level = FINEST

java.util.logging.FileHandler.pattern = %h/device%u.log

java.util.logging.FileHandler.limit = 50000

java.util.logging.FileHandler.count = 1

java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

 

# Configure the console output

java.util.logging.ConsoleHandler.level = FINEST

 

# Levels for specific classes

#com.level = FINEST


  

로그 레벨은 다음과 같이 계층적 또는 특정 클래스별로 지정이 가능하며 하위 계층에있는 로그레벨이 자동 적용 된다.

> com.level = FINEST

> com.oreilly.level = FINEST

> com.oreilly.LogTest.level = FINEST

 


Log4j 설정파일과 테스트 코드

자바의 기본 내장 Logger와는 조금 다르지만 Log4j기준의 설정파일과 실행코드를 살펴보면 다음과 같다.

 

# 루트 Logger 방식이 아닌 각각의 Logger 방식으로 3가지 Logger 선언

log4j.logger.process.work1 = DEBUG, WORK1

log4j.logger.process.work2 = DEBUG, WORK2

log4j.logger.process.work3 = DEBUG, WORK3

 

# WORK1 Logger 정보를 설정

log4j.appender.WORK1 = org.apache.log4j.DailyRollingFileAppender

log4j.appender.WORK1.File = work1.log

log4j.appender.WORK1.DatePattern = '.'yyyy-MM-dd

log4j.appender.WORK1.Threshold = DEBUG

log4j.appender.WORK1.layout = org.apache.log4j.PatternLayout

log4j.appender.WORK1.layout.ConversionPattern = [%d] %-5p %l - %m%n

 

# WORK2 Logger 정보를 설정

log4j.appender.WORK2 = org.apache.log4j.DailyRollingFileAppender

log4j.appender.WORK2.File = work2.log

log4j.appender.WORK2.DatePattern = '.'yyyy-MM-dd

log4j.appender.WORK2.Threshold = DEBUG

log4j.appender.WORK2.layout = org.apache.log4j.PatternLayout

log4j.appender.WORK2.layout.ConversionPattern = [%d] %-5p %l - %m%n

 

# WORK3 Logger 정보를 설정

log4j.appender.WORK3 = org.apache.log4j.DailyRollingFileAppender

log4j.appender.WORK3.File = work3.log

log4j.appender.WORK3.DatePattern = '.'yyyy-MM-dd

log4j.appender.WORK3.Threshold = DEBUG

log4j.appender.WORK3.layout = org.apache.log4j.PatternLayout

log4j.appender.WORK3.layout.ConversionPattern = [%d] %-5p %l - %m%n

 

 

import org.apache.log4j.Logger;

 

public class LoggerTest {

 

       // 각각의 Logger 가져온다.

       static Logger logger1 = Logger.getLogger("process.work1");

       static Logger logger2 = Logger.getLogger("process.work2");

       static Logger logger3 = Logger.getLogger("process.work3");

 

       /**

        * @param args

        */

       public static void main(String[] args) {

              // 로그 출력

              logger1.error("Hello log4j.");

              logger2.error("Hello log4j.");

              logger3.error("Hello log4j.");

       }

 

}

 

 

 

 

'Java > Core Utilities' 카테고리의 다른 글

Navigable Collection  (0) 2013.11.04
Observers and Observables  (0) 2013.02.09
Preferences & PreferenceChangeListener  (0) 2013.02.09
Properties  (0) 2013.02.09
Collection, Map 클래스의 사용시 성능과 계층구조  (0) 2013.02.09
Posted by Steven J.S Min
,

Preferences

Preferences는 실제로 Tree구조로 저장되고 관리되어지며 Properties와 유사한 형식을 취하고 있지만, Properties String 객체만을 대상으로 한다는 것이 이것과 다르다. Preferences String, int, double, byte[] 등을 이용하여 레지스트리를 관리할 수 있다.

 

다음의 코드는 KEY VALUEPreferences의 형태로 저장하여 파일로 저장하는 코드이다.

 

 

package test;

 

import java.io.FileOutputStream;

import java.util.prefs.BackingStoreException;

import java.util.prefs.PreferenceChangeEvent;

import java.util.prefs.PreferenceChangeListener;

import java.util.prefs.Preferences;

 

public class PrefsDemo {

 

       /**

        * @param args

        */

       public static void main(String[] args) {

              String keys[] = { "company", "copyright", "author" };

              String values[] = { "Google", "copyright 2013", "Steven" };

 

              Preferences prefsdemo = Preferences.userRoot().node("/com/google/book");

              // userNodeForPackage()메소드를 이용하여 클래스를 지정하면

              // 클래스의 Package구조가 '.' '/' 대체되어 구조가 만들어 진다.

              // Preferences prefsdemo = Preferences.userNodeForPackage(Animal.class);

 

              for (int i = 0; i < keys.length; i++) {

                     prefsdemo.put(keys[i], values[i]);

              }

 

              prefsdemo.addPreferenceChangeListener(new PreferenceChangeListener() {

                     @Override

                     public void preferenceChange(PreferenceChangeEvent evt) {

                           System.out.println("Value: " + evt.getKey() + " changed to " + evt.getNewValue());

                     }

              });

              prefsdemo.put("company", "Yahoo");

             

              try {

                     FileOutputStream fos = new FileOutputStream("prefsdemo.xml");

                     prefsdemo.exportNode(fos);

                     // prefsdemo.flush(); // 확실하게 저장소에 저장처리

              } catch (Exception e) {

                     System.err.println("Cannot export nodes: " + e);

              }

 

              try {

                     prefsdemo.removeNode();

              } catch (BackingStoreException e) {

                     System.err.println(e);

              }

 

       }

 

}

 

 

 저장된 결과

 

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<!DOCTYPE preferences SYSTEM "http://java.sun.com/dtd/preferences.dtd">

<preferences EXTERNAL_XML_VERSION="1.0">

  <root type="user">

    <map/>

    <node name="com">

      <map/>

      <node name="google">

        <map/>

        <node name="book">

          <map>

            <entry key="company" value="Yahoo"/>

            <entry key="copyright" value="copyright 2013"/>

            <entry key="author" value="Steven"/>

          </map>

        </node>

      </node>

    </node>

  </root>

</preferences>

 

 

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

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

 

 

package test;

 

import java.util.prefs.PreferenceChangeEvent;

import java.util.prefs.PreferenceChangeListener;

 

public class RootNodeListener implements PreferenceChangeListener {

 

       @Override

       public void preferenceChange(PreferenceChangeEvent evt) {

              System.out.println("Value: " + evt.getKey() + " changed to " + evt.getNewValue());

 

       }

 

}

 

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

 

위의 코드에 앞선 코드에서는 따로 구현하지 않고 직접 내부 클래스로 구현하였다.

 

 

----------------------------------------------------------------------------------------------------------------------

다음 글은 "http://javacan.tistory.com/72 by 최범균"에서 발췌한 것이며 윗 글도 참조하여 작성하였음.

[JAVACAN]

-------------------------------------

 

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 사용해볼 것을 강력하게 권한다.

'Java > Core Utilities' 카테고리의 다른 글

Observers and Observables  (0) 2013.02.09
Logger  (0) 2013.02.09
Properties  (0) 2013.02.09
Collection, Map 클래스의 사용시 성능과 계층구조  (0) 2013.02.09
Collections과 정렬  (0) 2013.02.09
Posted by Steven J.S Min
,