반응형
 
태평한 잔치에서 군신이 함께 즐기니 그 모습이 아름답고 좋기가 그지없는 형국입니다
본래 업은 없으나 횡재를 만나 성공할 운세이니 귀인의 도움보다는 나의 노력이 클 것입니다
심신이 다 함께 편하니 집안에 활기가 넘치고 가족 간에 화목이 있으니 어찌 일이 이루어지지 않겠습니까
만일 자식을 낳지 많으면 일신이 영귀해질 것이니 길성이 가까이 하기 때문입니다
자식을 보면 분명 공명의 아들이니 후에 큰 일을 하게 될 것이며 가족이나 친지 중에 새 식구가 들어도 대통의 징조로 삼을 수 있습니다
동서 양방향에서 귀인이 와서 도와주니 근심이 있으면 남과 상의하여 도움을 구하시기 바랍니다
ㅈ과 ㅎ성씨가 해로우니 가까이하는 것을 피하시고 재물에 관한 거래를 하지 않도록 하시기 바랍니다
봉황이 붉은 조서를 머금은 괘를 얻었으니 태을귀인이 임하여 길성을 크게 하고 운수를 대통하게 합니다
십리길 길가에서 관인이 말을 버리니 어려운 사연을 한 번은 겪으시겠군요
마음에 상처가 되는 일은 고민을 길게 하지 마십시오
길고 긴 강물에 돛을 올리니 순풍이 불어오고 바람에 배를 맡기듯 순�! �� 순응하면 얻음이 큰 해임을 명심하여야 할 것입니다
횡재도 노력하지 않으면 얻어질 수 없는 법이니 노력하여 얻은 것을 어찌 횡재라 하겠습니까? 타인이 보기에 질투할 정도의 재물을 얻을 것이니 큰 기쁨이 있을 것입니다
뜻하지 않은 곳에서 예상 밖의 재물이 들어오는 형태이기 때문에 다른 사람이 보기에는 괜한 횡재처럼 보이지만 실상은 자신의 노력의 결과임을 알아야 합니다
다만, 뜻밖에 들어오는 재물이 사연을 품고 있는 재물이 될 수 있으니 얻고도 마음이 편하지 않을 돈은 받지 않는 것이 좋겠습니다
재운이 크게 되어 재물을 구분하여 받아도 끊이지 않을 것이니 정상적이지 않은 돈보다는 복 돈을 받는 것이 좋습니다
재물이 들어오기 시작하면 끊이지 않을 것이니 우선은 자신의 주머니를 먼저 채우는 것이 중요합니다
돈을 쓰는 것은 나중으로 미루어도 문제가 없을 것이니 먼저 예상하여 지출을 하면 후 일에는 얻을 것이 없습니다
들어오게 될 재물에 신경 쓰는 것보다는 이미 들어온 재물을 관리하시는 것이 중요함을 잊지 마시기 바랍니다
작은 노력으로도 큰 것을 취하게 되는 운이 있는 때가 있습니다
올해가 바로 그러한 운세에 진입하였으니 일이 가장 잘 풀려나가는 대통의 운세입니다

우연히 주운 돌도 보석의 가치가 있는 시기이니 앉아서 한가롭게 시간을 보내는 일마저도 시간낭비가 되는 셈입니다
외부에서 활동하는 사람은 어느 누구를 만나도 자신을 도와주는 역할을 하게 될 것입니다
시간을 아껴서 최대한 활동영역을 넓히고 대인관계를 확장해야 되는 시기입니다
서비스 업종이나 장사를 하시는 분은 처음 맞이하는 손님을 잘 상대하시기 바랍니다
한 사람의 소중한 인연이 끊이지 않는 인연을 만들어내는 기운이 있으니 한 사람이 열이 되어 돌아오고 점점 더 많은 사람들과의 인연으로 돌아오는 큰 기쁨을 맛보게 될 것입니다
영업을 하시는 분은 성과를 크게 이룰 것이니 큰 계약이 성사되어 회사에서 위치가 크게 올라갑니다
사업을 하시는 분은 한 번의 어려움만 극복하면 귀성이 들어 미래를 준비하는 토대를 마련할 것이니 앞으로 거칠 일이 없을 것입니다
만나는 사람마다 도움을 주고자 하니 덕을 쌓고 마음의 욕심을 버린다면 �! 遲습� 더욱 많아지고 만사가 형통하게 될 대길의 운수입니다
좋은 운을 이끄는 하늘의 기운이 크게 들어오는 시기이고 아주 좋은 귀인을 맞이하는 때이니 인연이 좋지 않을 리가 없습니다
다만 대길의 인연과 크게 흉한 기운의 인연이 같이 들어오니 그것을 구분하는 지혜로움이 필요할 것입니다
하지만 나에게 큰 도움을 주는 귀인은 나에게 바라는 것이 없습니다
그러므로 나에게 무언가를 바라는 인연이라면 좋지 않은 인연으로 봐도 될 것이며 그 연결의 고리도 길지 않을 것입니다
그러나 막역하게 다가서는 인연이라면 길운을 포함한 사람이니 후 일 나에게 큰 도움을 주게 될 것입니다
업무적으로 만나는 사람은 모두 인연이 좋으니 개인적인 만남에서 나에게 바라는 자가 있으면 이를 경계하시기 바랍니다
애정은 각별한 시기이니 반드시 사랑을 하게 될 것입니다
인연이 필요한 사람은 너무도 좋은 시기이지만 그렇지 않은 경우는 좋은 운이 허비되지 않도록 선을 그을 필요가 있습니다
바라면 얻을 수 있는 운수이니 활용을 잘 하셔서 좋은 인연을 많이 만드시기 바랍니다
반응형
IBM 소프트웨어 엔지니어 (마음은 게이머)인 Scott Clee가 테트리스 게임 모델을 재사용 가능한 자바 빈 컴포넌트로 포장하는 간단한 방법을 소개한다. 일단 게임의 구성 요소들이 자바 객체들로 나누어지면 완전한 게임 모델 빈을 형성하도록 재조립될 수 있고, 실제로 어떤 테트리스 GUI에도 결합될 수 있다. forum에서 이 글에 대한 여러분의 생각을 저자와 다른 독자들과 공유하기 바란다.

-참조 : http://www.ibm.com/developerworks/kr/library/j-tetris/

테트리스 게임의 구성 요소들을 자바 객체로 나누어 재사용 가능한 자바 게임 컴포넌트로 만드는 방법

한 친구가 자신은 새로운 프로그래밍 언어를 배울 때마다 그 언어를 사용해 테트리스 게임을 작성하는데 도전한다고 말한 적이 있다. 이러한 전통하에, 내가 처음 자바 언어를 사용한 프로그래밍 법을 배웠을 때 나도 같은 시도를 해 보기로 했다. 나의 첫번째 시도는 하나의 완전한 게임이긴 했지만 매우 간단하고 흉한 것이었다. 시간이 가고 내가 자바 설계와 개발에 더 많은 경험을 얻음에 따라 나는 GUI에서 게임 모델을 분리시켜 (Swing 컴포넌트와 유사하게) 테트리스 빈을 만들 수 있다는 사실을 알았다. 그래서 나는 그것을 해 보기 시작했다.

이 글에서 나는 테트리스 빈을 구축하고 구현하는 방법을 안내하겠다.

테트리스는 자바 객체로 표현될 수 있는 몇 개의 구성 요소를 가지고 있다.:

  • The Tetris pieces 테트리스 조각
  • 조각을 가지고 있는 테트리스 판
  • 판 위의 조각 제어, 점수 관리 등을 하는 게임

이 요소들 각각을 좀 더 자세히 살펴보자.

  • 각 요소는 정확히 네 개의 블록으로 구성되어 있다.
  • 조각 내의 각 블럭은 테트리스 판 내에 (x.y) 좌표를 가지고 있다.
  • 각 조각은 0, 2, 4의 회전율을 가지고 있다.
  • 각 조각은 L, J, S, O, I, Z, 혹은 T 모양을 지닐 수 있다.


그림 1. 각 테트리스 조각에 네 요소가 필요함 : 블록, (x,y) 좌표, 회전율 및 모양
Four elements of each Tetris piece
테트리스 빈으로 무엇을 할 수 있을까?

다음은 테트리스 빈을 만든 후 내가 수행한 몇 가지 일들이다.
  • 빈의 두 인스턴스를 연결시켜 대결 게임을 만들다.
  • Netris라는 이름을 가진, 최초의 Psion Netpad용 테트리스 게임을 만들다
  • 빈을 애플릿에 통합시켜 브라우저와 호환되는 테트리스 게임을 만들다.

첫번째 두 아이템은 매우 간단한 시스템을 사용해 구현될 수 있다. 각 조각에 대해 중앙 블록을 선택하고 이 블록의 (x,y) 좌표를 저장하면, 조각 내의 나머지 블록들을 이 블록을 중심으로 한 상대 좌표로 저장할 수 있다. 이 방식은 중앙 조각에 대한 상대 블록으로 모양을 저장함으로써 조각의 어떤 형태도 기술할 수 있게 해준다. 중앙 지점은 java.awt.Point로 저장될 수 있고 상대 좌표는 java.awt.Point 배열에 저장될 수 있다. 좌표 계산을 쉽게 하기 위해, 중앙 조각을 (0,0) 상대 좌표를 가진 블록으로 저장할 수 있다.

이 시스템은 또한 조각의 회전을 계산할 때 더 쉽다. 간단한 행렬 조작을 사용하여 단순히 y좌표를 x 좌표로 바꾸고 x 좌표는 y 좌표의 마이너스 값으로 바꾸면 조각을 시계방향으로 90도 회전시킬 수 있다. 우리는 중앙 지점을 중심으로 상대 좌표를 사용하고 있기 때문에, 여기에서도 마찬가지로 할 수 있다:



temp = x;
x = -y;
y = temp;

여러분은 또한 시계방향 회전을 3번 적용하면 조각을 시계 반대 방향으로 90도 회전시킬 수 있다. (믿지 못하겠다면 한 번 해보기 바란다.)

마지막으로, 모든 조각이 동일한 회전율을 가지는 것은 아니므로, 이 문제를 보충하기 위해 회전 기법을 체크해 보아야 할 것이다.

우리는 모든 유형의 조각을 표시하는데 동일한 TetrisPiece 클래스를 사용하기 때문에 이들을 구분할 방법이 필요하다. 이를 위해 우리는 몇 개의 정적인 int 생성자를 사용하여 다른 유형들을 표시하고 지역 변수가 조각의 유형을 저장하도록 한다. 다음은 이 생성자들 중 하나의 예이다:



public static final int L_PIECE = 0;

조각을 테트리스 판 위에서 이동시킬 것이므로 이를 위한 이동 메소드를 제공해야 한다. 몇 가지 이동 (이미 가능한 한 제일 오른쪽 끝에 와 있는데 다시 오른쪽으로 가려는 시도)은 불법적일 수 있다. 따라서 우리는 모든 이동 요청을 확인할 필요가 있다. 우리는 참조를 저장할 테트리스 판에서 이것을 구현할 것이다. 따라서 우리 클래스의 생성자는 여기에서 두 가지 매개 변수를 가질 것이다: 첫번째는 만들어진 조각의 유형이고, 두번째는 테트리스판에 대한 참조이다. 생성자에서 우리는 initalizeBlocks()이라는 private 유틸리티 메소드를 호출할 것인데, 이 메소드는 조각의 상대 좌표값을 각각의 조각 유형에 설정할 것이다.

이동이 합법적인지를 체크하는 간단한 방법은 판에서 조각을 떼내어 원하는 방향으로 이동시킨 후 맞는지 보는 것이다. 맞으면 조각을 보드의 새 위치에 둔다. 그렇지 않으면 이동을 취소하고 원래 있던 자리에 다시 둔다. 반환되는 값은 그 결과에 따라 true (이동이 맞으면)나 false(이동이 맞지 않으면)가 될 것이다.

좀 더 주의를 기울여야 할 이동의 한 유형은 조각이 떨어지는 경우이다. 떨어진다는 것은 조각이 판의 제일 아래쪽으로 바로 내려간다는 의미이다. 이를 위해 우리는 더 이상 움직일 수 없을 때가지 조각을 아래로 계속 이동시키는 while 루프가 필요하다. 그러면 조각은 그 위치에 배치될 것이다.

조각에 적용될 수 있는 다양한 이동들을 구별하기 위해 우리는 다음 예제에 나타난 것과 같이 몇 가지 추가적인 static int 생성자를 사용할 것이다.:



public static final int LEFT = 10;

조각이 맞는지 보기 위해 나중에 willFit() 메소드가 TetrisBoard 클래스에서 구현된 것이다.

마지막으로 TetrisPiece 클래스를 포장하기 위해 우리는 중앙 지점과 상대 좌표와 같은 몇 가지 변수에 대한 getters와 setters, 그리고 무작위 유형의 TetrisPiece 인스턴스를 반환할 getRandomPiece()라는 정적인 메소드를 가진다.

TetrisPiece 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

테트리스 판은 빈 블록과 색깔 있는 블록을 가지고 있는 2D 격자판으로 생각할 수 있다. 다양한 유형의 테트리스 조각들이 int 생성자에 의해 구별되기 때문에, 우리가 해야 할 일은 빈 블록의 값을 정의하는 것 뿐이고 우리는 판을 2D int 배열로 저장할 수 있다. 이 방식을 사용하면 판 내의 빈 블록은 다음에 의해 표시될 것이다.:



public static final int EMPTY_BLOCK = -1;

유연성을 유지하기 위해 판의 크기를 가변적으로 하겠지만 이것을 생성자 내에 정의할 것이다. 따라서 생성자는 열과 행의 수를 나타내는 두 ints를 받아들일 것이다. 그리고 나서 2D 배열 내의 모든 값을 기본적으로 빈 블록으로 만드는 resetBoard() 메소드를 호출할 것이다.

조각들이 판에 추가되고 제거되기 때문에 우리는 addPiece()removePiece()메소드를 제공한다. addPiece() 메소드는 TetrisPiece()를 취하고 판에서 이 메소드가 차지하는 모든 위치의 값을 자신의 유형으로 설정함으로써 작동한다. removePiece() 메소드는 판의 값이 빈 블록의 값으로 설정된다는 점을 제외하면 비슷하다.

판에 변화가 있을 때 사용자가 알 수 있도록 하기 위해, 조각이 추가되거나 이동되었을 때 BoardEvent가 구동될 것이다. 이 이벤트를 듣는 클래스들에 대해 우리는 BoardListener 인터페이스가 필요한데, 이벤트가 구동되었을 때 이 인터페이스의 boardChange()메소드가 호출된다. 이 이벤트들은 화면 수정이 필요할 때 통지되도록 테트리스 판 GUI에 의해 사용될 수 있다. listener를 저장하기 위해 우리는 java.util.Vector를 사용할 것이고, listener를 추가/삭제하고 이벤트를 구동시키기 위한 관련 메소드를 제공할 것이다.

때때로 여러분이 조각을 추가하고 삭제할 때 BoardEvents를 구동시키는 것이 부적절할 수 있다. 조각이 떨어져야 할 때 (이 이동은 조각을 떨어뜨리기 위해 while 루프를 사용한다는 것을 기억하라)를 예로 들 수 있다. 이 경우 조각이 바닥에 부딪쳤을 때만 이벤트가 필요하다. 이를 용이하게 하기 위해 우리는 boolean 매개변수를 취하도록 addPiece() 메소드를 만들어 값이 true일 경우에만 이벤트가 구동되도록 할 것이다.

테트리스 게임의 중요 요소 중 하나는 한 행이 완료되면 그 행은 사라지고 그 위의 모든 행들이 내려온다는 것이다. 이를 위해 우리는 삭제될 행의 지수를 매개변수로 취하는 removeRow() 메소드를 제공할 것이다. 행이 없어진 후에 BoardEvent가 구동될 것이다.

private 변수들에 접근하기 위해 필요한 getter와 setter들 외에도, 우리는 하나의 메소드가 더 필요하다. 앞에서 설명한 willFit()가 그것이다. 이 메소드는 TetrisPiece를 매개변수로 취해 그 조각이 판에 맞는지 결정하기 위한 boolean 값을 돌려준다. 맞는다는 것은 그 조각이 판의 경계 안에 있고 판에서 그 조각이 맞춰질 곳에 있는 블록의 값이 비어 있다고 설정되어 있음을 의미한다. 이런 경우 true 값이 반환된다.

이제 TetrisBoard 클래스가 완성되었다.이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

100 피트 벽을 가진 테트리스?

어느날 나는 이 빈을 타워 블록의 점등 시스템에 연결시키고 빌딩의 측면을 따라 내려가면서 테트리스 게임을 하고 싶어졌다. 나는 누군가가 이렇게 했다는 것을 신문에서 읽은 후 내내 이것을 하고 싶어해 왔다.

이제 테트리스 게임에서 사용되는 두 개의 주 컴포넌트를 만들었으므로, 이들을 모아 게임 로직을 만들면 된다.

게임의 흐름을 제어하기 위한 좋은 방법은 이것을 java.lang.Thread를 확장하는 내부 클래스에 내장시키는 것이다. 이 방식의 한 가지 장점은 게임 속도를 제어하기 위해 스레드 sleep 호출을 추가할 수 있다는 것이다. 또 다른 장점은 현재 주 애플리케이션 스레드가 자유롭기 때문에 하나의 GUI가 첨부될 때 색칠 문제가 없어진다는 것이다. 이 문제는 주 스레드가 계속 묶여 있어 색칠할 시간이 없을 때 때때로 발생할 수 있다.

스레드 내의 로직은 run() 메소드 내의 while 루프 속에 구현될 것이다. 루프는 계속해서 조각을 만들어 내고 조각을 더 이상 맞출 수 없을 때까지 이들을 게임판으로 떨어뜨릴 것이다. 이 때 fPlaying이라는 지역 boolean 변수가 false로 설정되어 루프를 끝내고 GameEvent를 구동시켜 게임이 종료되었음을 표시할 것이다.

while 루프 내에 fPaused의 boolean 값을 체크하는 if 절이 있다. 이 값이 true로 설정되었을 경우 루프는 계속 실행되겠지만 모든 게임 로직이 무시되어 종료되는 느낌을 줄 것이다. Boolean이 false로 다시 바뀌면 게임이 계속될 것이다.

우리는 한 번에 하나씩 떨어지는 조각에만 관심을 가지고 있으므로, 여기에 대한 참조를 저장할 fCurrPiece라는 변수를 만들 것이다. 이 변수가 null 값으로 설정되면 이전의 조각이 더 이상 아래로 내려갈 수 없으며 판의 최종 위치에 도착했음을 의미한다. 이 때 우리는 새 조각을 만들어 판의 맨 위 중앙에 둔다. fCurrPiece 변수가 null값이 아닌 모든 경우에 우리가 해야 할 일은 그 것을 한 위치로 떨어뜨리고 주어진 시간 동안 스레드를 휴면 상태로 만드는 것이다.

한 조각이 더 이상 움직일 수 없게 되었을 때 우리는 행이 완성되었는지 보아야 한다. 이를 위한 손쉬운 방법은 한 쌍의 중첩 for 루프를 사용하는 것이다. 이 for 루프의 바깥 쪽 루프는 행의 지수를 따라 작업하며, 안쪽의 루프는 지수 전체에 걸쳐 확인 작업을 수행한다. 만일 우리가 완성된 행을 발견하면, TetrisBoard 클래스에 구현된 removeRow() 메소드를 호출하며 완성된 행의 지수를 전달할 수 있다. 이제 제거된 행 위의 모든 행들이 하나씩 내려올 것이기 때문에 우리는 이들을 다시 체크해야 할 것이다. 여러 행을 한번에 완성하도록 장려하기 위해 우리는 완성된 행의 개수를 저장하고 각각 더 높은 점수를 줄 것이다.

테트리스 게임의 또 다른 주요 요소는 더 많은 행이 완성될수록 조각이 더 빨리 내려온다는 것이다. 이 기능은 지금까지 완성된 행의 개수를 체크하고 이에 따라 스레드의 휴면 주기를 점차 감소시켜가는 방식으로 구현될 수 있다.

GameThread 내부 클래스를 만들기 위해 필요한 것은 이것이 전부지만, 구현해야 할 또 다른 내부 클래스가 있다. 다수의 이벤트가 구동될 것이고 이들은 listerners가 저장되도록 요구할 것이므로, 이들을 모두 한 장소에 두는 것이 좋을 것이다. 우리는 EventHandler 내부 클래스를 사용하여 이를 수행할 것이다.

EnvetnHandler 내부 클래스
이 클래스는 우리가 구동하는 이벤트에 관심이 있는 listener들에 대한 참조를 저장할 것이다. listener들에 대한 addremove 메소드를 제공할 뿐 아니라 이벤트들을 구동하기 위한 유틸리티 메소드도 있을 것이다.

이 클래스는 다음 유형의 이벤트들을 다룬다. :

  • GameEvent : 게임이 시작되거나 멈출 때마다 구동된다. 게임 START 혹은 END를 표시하기 위한 값을 가지고 있다.

  • BoardEvent : 게임판에 변경 사항이 있을 때 구동된다. EventHandler 클래스에서 추가/삭제 listener 호출이 TetrisBoard 클래스로 전달된다.

  • ScoreEvent: 점수가 바뀔 때 구동된다.

구동될 수 있는 많은 다른 유형의 이벤트들이 있지만, 간편성을 위해 나는 위에서 설명한 이벤트들만 사용하였다. 우리가 구현할 수 있는 다른 이벤트에는 LineEvent가 있는데, 한 행, 혹은 여러 행이 완성되었을 때 구동되고 화면 애니메이션을 일으키는데 사용될 수 있다.

TetrisGame 마무리하기

이제 내부 클래스들을 완성하였으므로 TetrisGame 클래스의 나머지 부분을 설명해야 한다. 모든 자바 빈과 마찬가지로 우리는 매개 변수 없는 생성자가 필요하다. 이 생성자에서 우리는 EventHandler 클래스와 TetrisBoard 클래스의 인스턴스를 만들 것이다. TetrisBoard 클래스 10x20이라는 기본 사이즈를 가질 것이다.

게임 상태를 제어하기 위해 우리는 개시, 중지, 일시 중지 메소드를 사용할 것이다. startGame() 메소드는 모든 게임 변수를 리셋하고 ScoreEvent (이제 0으로 리셋됨)와 GameEvent (START라는 매개 변수 유형을 가짐)를 구동시킬 것이다. 또한 GameThread를 생성하여 개시할 것이다. stopGame() 메소드는 fPlaying 변수를 false로 바꾸어 GameThread가 끝나도록 하고 END라는 매개변수 유형으로 GameEvent를 구동시킨다. setPause() 메소드는 한 게임을 일시 중지시키는 역할만 한다.

필요한 모든 getters와 setters와 별도로, 구현할 메소드가 하나 더 있는데, move() 메소드가 그것이다. 이 메소드는 이동 방향을 매개변수로 취하는데, 이것은 TetrisPiece 클래스에서 나오는 생성자이다. move() 메소드는 게임이 진행중이며 일시 중지 상태가 아니라고 가정하고 이동하려고 시도한다. 그 이동이 아래로 떨어지는 요청인데 성공하지 못한다면 fCurrPiece가 null 값으로 설정되고 조각은 게임판 내의 현재 위치에 남아 있을 것이다. 그러면 GameThread는 새로운 조각을 생성한다.

TetrisGame에 대해서는 이게 전부이다. 이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드 받을 수 있다.

이제 완벽함을 위해 우리는 BeanInfo 클래스를 만들고 이 클래스들을 적절한 파일들로 채울 수 있지만, 여기에서는 그럴 필요가 없다. 우리가 필요한 것은 우리의 빈을 테스트할 간단한 GUI이고 참고 자료에 하나가 제공되고 있다. 이것은 테트리스 게임판을 그리기 위해 한 개의 내부 클래스를 사용하고 키 조작을 조정하기 위한 몇 가지 로직을 포함하고 있는 간단한 클래스이다. GUI는 javax.swing.JFrame에 표시되며, 선택적으로 java.applet.Applet이 될 수도 있다.

우리의 테트리스 빈을 테스트하기 위해 소스 파일을 푸는데, 디렉토리 구조를 그대로 두기 바란다. (왜냐하면 내가 빈 클래스들이 TetrisBean 디렉토리에 있도록 이들을 TetrisBean 패키지에 두었기 때문에 때문이다.) 여러분이 소스 파일을 푼 경로를 자바 클래스 경로에 추가하고 파일을 컴파일한다. 이제 여러분이 해야 할 일은 "java Scottris"를 실행시키는 것 뿐이다.

나는 이러한 테트리스 빈을 구현할 수 있는 많은 방법이 있다는 것을 알고 있다. 내가 이 글에서 소개한 것은 아주 간단한 방법이며, 이것이 여러분의 창조력에 불을 붙일 수 있기를 바란다. 나는 여러분이 이것을 자유롭게 개선시키기 바란다.




위로


참고자료




위로


필자소개

Photo of Scott Clee

Scott Clee는 현재 IBM의 CICS 제품에 대한 FV Tester로 일하고 있다. 4년간 자바 프로그래머로 일했으며 자바와 관련된 재미있는 프로젝트를 취미 삼아 수행하는 것을 즐긴다.





위로
반응형

저자 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