저자 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 액세스에서 공유하는 상수이다.
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점이 그다지 높은 점수가 아니더라도 고득점이 될 가능성은 있다.
서로 다른 사용자로 애플리케이션을 실행해 보고 모두 동일한 고득점이 적용되는지 확인할 수 있다.
어떤 사용자나 시스템에서 다른 사용자 또는 다른 시스템으로 선호 설정을 전송하려는 경우, 해당 사용자/시스템에서 선호 설정을 내보낸 다음 다른 사용자/시스템으로 이를 가져올 수 있다. 선호 설정을 내보내기할 때 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
가 있다. 수신기는 연결된 노드의 모든 변경 사항을 알게 되므로, 다음과 같이 어떤 키-값 쌍이 수정되었는지 확인해야 한다.
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
클래스 쌍 외에도 선호 설정 변경을 알리기 위한 NodeChangeListener
및 NodeChangeEvent
콤보가 있다. 그러나 이는 특정 노드의 값을 변경하는 것이 아니라 알림 노드를 추가하고 제거하는 용도이다. 물론 선호 설정 뷰어와 같은 것을 작성하려면 노드가 나타나는지 여부 및 그 시점을 알 필요가 있으므로 이 클래스도 관심사가 될 수 있다.
전체 선호 설정 API는 애플리케이션의 수명이 끝나더라도 데이터베이스 시스템에 의존할 필요 없이 데이터를 저장하는 매우 편리한 방법이 될 수 있다. API에 대한 자세한 내용은 Sir, What is Your Preference? 영문기사를 참조한다.