반응형

저자 JZ Ventures사의 사장 겸 대표 컨설턴트인 John Zukowski

이 아티클의 영문 원본은
http://java.sun.com/mailers/techtips/corejava/2007/tt0907.html#2
에서 볼 수 있습니다.

이 팁은 Java SE 6을 사용하여 작성되었습니다. 이번 및 향후 테크팁을 사용하기 위해 Java Platform, Standard Edition 6 Development Kit(JDK 6)을 Java SE 다운로드 페이지에서 다운로드할 수 있습니다.

선호 설정(Preferences) API는 표준 플랫폼 1.4버전에 도입된 직후 2003년 7월 15일자 영문기사  선호 설정API에서 맨 처음 다뤄진 바 있다.

이 기사에서는 사용자별 선호 설정을 가져오고 설정하는 방법에 대해 설명했다. 선호 설정 API는 사용자별 설정을 가져오고 설정하는 것에 국한되지 않는다. 시스템 선호 설정, 선호 설정 가져오기 및 내보내기, 선호 설정과 연결된 이벤트 알림도 있다. 선호 설정을 저장하기 위한 사용자 정의 위치를 제공하는 방법도 있다. 언급한 처음 세 옵션에 대해 여기서 설명한다. 사용자 정의 기본 선호 팩토리 만들기는 이후의 팁에서 다루기로 한다.

시스템 선호 설정

선호 설정 API는 서로 다른 두 가지 선호 설정 집합을 제공한다. 첫 번째 집합은 개별 사용자용으로서, 동일한 시스템의 여러 사용자가 서로 다른 설정을 정의할 수 있게 한다. 이를 사용자 선호 설정이라고 한다. 동일한 시스템을 공유하는 각 사용자는 자신의 고유한 값 집합을 선호 설정의 그룹과 연결할 수 있다. 사용자 비밀번호, 시작 디렉토리 등이 그 예이다. 한 시스템의 모든 사용자가 동일한 비밀번호와 홈 디렉토리를 갖는 것은 바람직하지 않다. 독자들도 그렇게 생각하리라 기대한다.

또 다른 선호 설정 형태는 시스템 유형이다. 한 시스템의 모든 사용자가 동일한 시스템 선호 설정 집합을 공유한다. 예를 들어, 설치된 프린터의 위치는 일반적으로 시스템 선호 설정이다. 굳이 사용자별로 다른 프린터 집합을 설치할 필요는 없다. 동일한 시스템을 사용하는 사람이라면 그 시스템을 기준으로 구별되는 모든 프린터를 알고 있을 것이다.

시스템 선호 설정의 또 다른 예로 게임 고득점이 있다. 전체 고득점은 오로지 하나만 존재해야 한다. 여기서 시스템 선호 설정이 사용될 수 있다. 이전 팁에서 userNodeForPackge() 및 그에 이어 userRoot()가 사용자 기본 설정 노드를 가져오는 데 어떻게 사용되는지 확인했다면, 다음 예제에서는 systemNodeForPackage() 또는 루트용 systemRoot()를 사용하여 시스템 기본 선호 트리 중 알맞은 부분을 가져오는 방법을 보여 준다. 알맞은 기본 선호 노드를 가져오는 메소드 호출과 달리 API 사용법은 동일하다.

이번 예제는 간단한 게임이므로 여기서는 게임 용어를 엄격하지 않게 사용하기로 한다. 이 게임에서는 0 ~ 99 범위에서 임의의 수를 선택한다. 그 수가 이전에 저장된 값보다 높으면 "high score"를 업데이트한다. 또한 이 예제에서는 현재의 고득점을 보여 준다. 선호 설정 API 사용법은 간단한 편이다. 여기서는 getSavedHighScore()를 사용하여 저장된 값을 가져오고, 아직 저장된 고득점이 없으면 기본값인 -1을 제공하며, updateHighScore(int value)를 사용하여 새로운 고득점을 저장한다. HIGH_SCORE 키는 새로운 선호 설정 API 액세스에서 공유하는 상수이다.

private static int getSavedHighScore() {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    return systemNode.getInt(HIGH_SCORE, -1);
  }

  private static void updateHighScore(int value) {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    systemNode.putInt(HIGH_SCORE, value);
 }



전체 프로그램은 다음과 같이 된다.

import java.util.*;
import java.util.prefs.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class High {
  static JLabel highScore = new JLabel();
  static JLabel score = new JLabel();
  static Random random = new Random(new Date().getTime());
  private static final String HIGH_SCORE = "High.highScore";

  public static void main (String args[]) {
    /* -- Uncomment these lines to clear saved score
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    systemNode.remove(HIGH_SCORE);
    */

    EventQueue.invokeLater(
      new Runnable() {
        public void run() {
          JFrame frame = new JFrame("High Score");
          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          updateHighScoreLabel(getSavedHighScore());
          frame.add(highScore, BorderLayout.NORTH);
          frame.add(score, BorderLayout.CENTER);
          JButton button = new JButton("Play");
          ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              int next = random.nextInt(100);
              score.setText(Integer.toString(next));
              int old = getSavedHighScore();
              if (next > old) {
                Toolkit.getDefaultToolkit().beep();
                updateHighScore(next);
                updateHighScoreLabel(next);
              }
            }
          };
          button.addActionListener(listener);
          frame.add(button, BorderLayout.SOUTH);
          frame.setSize(200, 200);
          frame.setVisible(true);
        }
      }
    );
  }

  private static void updateHighScoreLabel(int value) {
    if (value == -1) {
      highScore.setText("");
    } else {
      highScore.setText(Integer.toString(value));
    }
  }

  private static int getSavedHighScore() {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    return systemNode.getInt(HIGH_SCORE, -1);
  }

  private static void updateHighScore(int value) {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    systemNode.putInt(HIGH_SCORE, value);
 }
}


그리고 몇 번의 실행 후 화면은 다음과 같이 된다. 61점이 그다지 높은 점수가 아니더라도 고득점이 될 가능성은 있다.

High Score 61













서로 다른 사용자로 애플리케이션을 실행해 보고 모두 동일한 고득점이 적용되는지 확인할 수 있다.
가져오기 및 내보내기

어떤 사용자나 시스템에서 다른 사용자 또는 다른 시스템으로 선호 설정을 전송하려는 경우, 해당 사용자/시스템에서 선호 설정을 내보낸 다음 다른 사용자/시스템으로 이를 가져올 수 있다. 선호 설정을 내보내기할 때 XML 형식의 문서로 내보내며, 독자들이 굳이 알 필요는 없지만 이 문서의 DTD는 http://java.sun.com/dtd/preferences.dtd에서 지정한다. exportSubtree() 메소드를 사용하여 하위 트리 전체를 내보내거나 exportNode() 메소드를 사용하여 단일 노드만 내보낼 수 있다. 두 메소드 모두 OutputStream 인수를 받아 저장 위치를 지정한다. XML 문서는 UTF-8 문자 인코딩 방식이다. 그런 다음 importPreferences() 메소드를 통해 데이터 가져오기가 이루어지는데, 이 메소드는 InputStream 인수를 받는다. API 측면에서 보면 시스템 노드/트리 가져오기와 사용자 노드 가져오기 사이에는 아무런 차이가 없다.

이전 예제에 몇 줄의 코드를 추가하면 새로 업데이트된 고득점을 high.xml 파일로 내보낸다. 추가된 코드 중에는 파일을 저장하고 예외를 처리하는 새로운 스레드를 시작하는 작업이 상당 부분을 차지한다. 단일 노드를 내보내는 부분은 단 세 줄이다.

    Thread runner = new Thread(new Runnable() {
      public void run() {
        try {
          FileOutputStream fis = new FileOutputStream("high.xml");
          systemNode.exportNode(fis);
          fis.close();
        } catch (Exception e) {
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
        }
      }
    });
    runner.start();


내보내기할 때 파일은 다음과 같이 된다.

<?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="system">
    <map/>
    <node name="<unnamed>">
      <map>
        <entry key="High.highScore" value="95"/>
      </map>
    </node>
  </root>
</preferences>

루트 요소에 "system"이라고 말하는 유형 속성이 있다. 이는 그것이 노드의 유형임을 나타낸다. 또한 노드에는 "<unnamed>"라는 값의 이름 속성이 있다. High 클래스는 패키지에 포함되어 있지 않으므로 명명되지 않은 시스템 노드 영역에서 작업해야 한다. 이 항목 속성은 현재의 고득점 값(여기서는 95)을 제공하는데, 실제 값은 다를 수 있다.

이 예제에서는 import 코드를 포함시키지 않겠지만, 가져오기를 수행하려면 선호 설정에 대한 정적 메소드를 호출하고 알맞은 입력 스트림을 전달하면 된다.

FileInputStream fis = new FileInputStream("high.xml");
  Preferences.importPreferences(fis);
  fis.close();



XML 파일은 선호 설정이 시스템 또는 사용자 유형인지 여부에 대한 정보를 포함하므로 가져오기 호출에서 이 정보를 명시적으로 포함할 필요는 없다. 발생 가능한 일반적인 IOExceptions 외에도 가져오기 호출은 파일 형식이 잘못된 경우 InvalidPreferencesFormatException을 throw한다. 또한 내보낼 데이터를 백업 저장소로부터 올바르게 읽을 수 없다면 BackingStoreException을 throw하기도 한다.

이벤트 알림

원래 버전의 High 게임은 고득점 선호 설정을 업데이트한 다음 화면의 레이블을 업데이트하도록 명시적으로 호출한다. 이 작업을 수행하는 더 좋은 방법은 선호 설정 노드에 수신기를 추가하는 것인데, 그러면 값이 변경될 때 레이블의 값 업데이트가 자동으로 트리거될 수 있다. 따라서 고득점이 여러 위치로부터 업데이트되더라도 업데이트된 값을 저장한 다음 레이블을 업데이트하는 코드를 추가할 필요가 없다.

다음 두 줄은

 updateHighScore(next);
  updateHighScoreLabel(next);

알맞은 수신기를 추가하여 한 줄로 만들 수 있다.

updateHighScore(next);

그러한 작업에 꼭 알맞은 PreferenceChangeListener 및 그와 연결된 PreferenceChangeEvent가 있다. 수신기는 연결된 노드의 모든 변경 사항을 알게 되므로, 다음과 같이 어떤 키-값 쌍이 수정되었는지 확인해야 한다.

  PreferenceChangeListener changeListener =
        new PreferenceChangeListener() {

      public void preferenceChange(PreferenceChangeEvent e) {
        if (HIGH_SCORE.equals(e.getKey())) {
          String newValue = e.getNewValue();
          int value = Integer.valueOf(newValue);
          updateHighScoreLabel(value);
        }
      }
    };
    systemNode.addPreferenceChangeListener(changeListener);

PreferenceChangeEvent에는 세 가지 중요한 등록 정보가 있다. key, new value 그리고 node 자체이다. 그러나 new value가 선호 설정의 모든 편의 메소드를 포함하지는 않는다. 예를 들어, 값을 int로 검색할 수 없다. 그 대신 값을 수동으로 변환해야 한다. 수정된 High 클래스는 다음과 같이 된다.


import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.prefs.*;
import javax.swing.*;

public class High {
  static JLabel highScore = new JLabel();
  static JLabel score = new JLabel();
  static Random random = new Random(new Date().getTime());
  private static final String HIGH_SCORE = "High.highScore";
  static Preferences systemNode =
  Preferences.systemNodeForPackage(High.class);

  public static void main (String args[]) {
    /* -- Uncomment these lines to clear saved score
    systemNode.remove(HIGH_SCORE);
    */

    PreferenceChangeListener changeListener =
        new PreferenceChangeListener() {

      public void preferenceChange(PreferenceChangeEvent e) {
        if (HIGH_SCORE.equals(e.getKey())) {
          String newValue = e.getNewValue();
          int value = Integer.valueOf(newValue);
          updateHighScoreLabel(value);
        }
      }
    };
    systemNode.addPreferenceChangeListener(changeListener);

    EventQueue.invokeLater(
      new Runnable() {
        public void run() {
          JFrame frame = new JFrame("High Score");
          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          updateHighScoreLabel(getSavedHighScore());
          frame.add(highScore, BorderLayout.NORTH);
          frame.add(score, BorderLayout.CENTER);
          JButton button = new JButton("Play");
          ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              int next = random.nextInt(100);
              score.setText(Integer.toString(next));
              int old = getSavedHighScore();
              if (next > old) {
                Toolkit.getDefaultToolkit().beep();
                updateHighScore(next);
              }
            }
          };
          button.addActionListener(listener);
          frame.add(button, BorderLayout.SOUTH);
          frame.setSize(200, 200);
          frame.setVisible(true);
        }
      }
    );
  }

  private static void updateHighScoreLabel(int value) {
    if (value == -1) {
      highScore.setText("");
    } else {
      highScore.setText(Integer.toString(value));
    }
  }

  private static int getSavedHighScore() {
    return systemNode.getInt(HIGH_SCORE, -1);
  }

  private static void updateHighScore(int value) {
    systemNode.putInt(HIGH_SCORE, value);
    // Save XML in separate thread
    Thread runner = new Thread(new Runnable() {
      public void run() {
        try {
          FileOutputStream fis = new FileOutputStream("high.xml");
          systemNode.exportNode(fis);
          fis.close();
        } catch (Exception e) {
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
        }
      }
    });
    runner.start();
  }
}



PreferenceChangeListener/Event 클래스 쌍 외에도 선호 설정 변경을 알리기 위한 NodeChangeListenerNodeChangeEvent 콤보가 있다. 그러나 이는 특정 노드의 값을 변경하는 것이 아니라 알림 노드를 추가하고 제거하는 용도이다. 물론 선호 설정 뷰어와 같은 것을 작성하려면 노드가 나타나는지 여부 및 그 시점을 알 필요가 있으므로 이 클래스도 관심사가 될 수 있다.

전체 선호 설정 API는 애플리케이션의 수명이 끝나더라도 데이터베이스 시스템에 의존할 필요 없이 데이터를 저장하는 매우 편리한 방법이 될 수 있다. API에 대한 자세한 내용은 Sir, What is Your Preference? 영문기사를 참조한다.

+ Recent posts