[그림1]

Java Tutorial 에 나오는 JList 의 모습은 왼쪽과 같은데, 분류상 Component 로 되어있기는 하나 동작 방식을 보면 Container 와 거의 흡사하다.

Container와 Component의 가장 큰 차이점이라면 자식 컴포넌트를 포함하는 기능이지만, JList, JTable, JTree같은 컴포넌트는 자체적으로 안에 다른 컴포넌트를 포함하는 식으로 구현되어 있으니 굳이 이름을 붙이자면 TableLayout, TreeLayout, ListLayout이라고 할 만 하다.

[그림1] 은 JList 안에 JLabel 컴포넌트를 그려넣은 화면이지만 JList 안에 그려넣을 수 있는 컴포넌트가 한정되어있지는 않다. 어떤 컴포넌트이든 ListCellRenderer 를 이용하면 자유롭게 JList 안에 그려넣을 수 있다.

이에 관한 자바 튜토리얼을 보면 짤막한 몇 개의 예제가 있기는 한데, 그다지~ 실용성과는 거리가 먼 무미건조한 예제이다보니 큰 도움은 되지 않는 듯 하다. 또한 가장 중요한 부분인 ListCellRenderer에 대한 설명이 너무 부족하다.

여기서는 파일 다운로드 화면을 JList로 구현한 예제를 설명하려고 한다.


파일 다운로드 화면을 보여줄때 보통 다음과 같은 정보가 포함된다.

* 파일의 이름
* 현재 받은 크기 / 파일의 전체 크기
* 다운로드 속도
* 남은 시간

이것을 DownloadInfo 정도로 이름 붙이면 이것이 JLis 안에 보여질 정보를 담은 인스턴스가 된다. 흔히 말하는 MVC 패턴에서 "모델(Model)"에 해당된다. 어느 스윙 컴포넌트를 보더라도 이 구조를 매우 교조적으로 잘 지키고 있는데 JList도 예외는 아니다.

[그림2] 뷰는 신경쓰지 않는다.

모델(Model)은 화면에 보여줄 데이터(E)들을 관리하는 역할을 하는데, MVC 구조에서는 모델에 데이터를 넣으면 사용자의 개입없이 자동으로 뷰(View)가 갱신된다.

새로운 파일을 다운로드 받기 위해서 url을 입력하면, 이에 상응하는 무언가(view)가 화면에 나오기를 기대한다. 그리고 밑단의 소켓 연결을 통해서 데이터가 들어오면 적절히 화면이 갱신될텐데, 이런 일련의 과정(데이터 변경->화면 개인)이 사용자의 전반적인 개입 없이 "모델에 데이터를 넣고 수정하고 삭제하는" 행위만으로 이루어지는 것이 MVC 구조의 장점이다.

1. ListModel 구현

이를 위해서 가장 먼저 할 일은 javax.swing.ListModel 을 구현한 인터페이스를 작성하는 것이다. 각각의 다운로드 작업을 저장, 관리하는 역할을 하는데 사용자가 가장 빈번하게 다루는 대상이다.(가장 중요!)

오히려 java.swing.JList 는 손댈 일이 별로 없을 때가 많다. 왜냐하면 실제 작업은 모델을 통해서 데이터를 관리하는 것이고 화면은 그 결과로 보여지는 것이기 때문이다. javax.swing.JList 는 ListCellRenderer, ListModel, ListUI, 그리고 각종 이벤트 리스너들을 한데 모아서 조립, 초기화하는 창구 역할에 불과하다.(Facade 패턴이라고 하나??)

JList를 쓴다고 할 때에는 "ListModel을 통해서 보여줄 데이터를 관리하고 ListCellRenderer로 화면에 보여준다"로 받아들이면 된다. 단순히 JList 인스턴스 하나 초기화해서 할 일이 그리 많지 않고 그럴 용도로 만들어진 것이 아니다.(ListModel 과 ListCellRenderer 가 포인트!!)

javax.swing.ListModel 에는 네 개의 메소드가 선언되어 있다.
두 개의 메소드는 이벤트 리스너를 등록, 삭제할때 쓰는데 하나의 모델에 두 개 이상의 뷰를 연결시키는게 아니라면 손댈 일이 거의 없다.(예를 들자면 똑같은 데이터를 서로 다른 형태로 보여주고자 할 때 하나의 모델에 두개 이상의 뷰를 물려서(?) 데이터만 넣으면 뷰들이 알아서 갱신되도록 할 수 있다.) 나머지 두 개의 메소드인 getSize()와 getElementAt(i) 가 외부의 컴포넌트들이 현재 모델에 들어있는 정보를 획득하는 통로가 된다.

실제 JList의 내부 구현을 보면 사용자가 ListModel 에 element를 하나 넣으면(여기서는 DownloadInfo 인스턴스) 등록된 이벤트 리스너들에게 새로 등록된 element의 index 위치 값만 알려준다. 그러면 리스너(구체적으로 ListUI 인스턴스)들은 이 index 값을 가지고 다시 ListModel.getElementAt(index) 를 호출해서 새로 추가된 element를 가져다가 ListCellRenderer 에게 전달해준다.

그리고 ListCellRenderer 가 아래와 같이 메소드의 파라미터(Object value) 로 전달받는다.
Object value, 로 한 것은 사용자가 어떤 모델 데이터를 이용할지 특정지을 수 없기 때문이다. 여기서는 DownloadInfo 인스턴스를 다루므로 ListCellRenderer 인터페이스를 구현할 때 아래와 같이 캐스팅 해서 사용하게 된다. 거의 대부분 이런 식으로 운용된다.

DownloadInfo info = (DownloadInfo) value;

이런 호출 흐름을 완성하기 위해서 가장 먼저 해야할 일이 바로 ListModel 인터페이스를 구현하는 일인데 스윙 api 에서는 사용자를 위해서 매우 간단한 기본 구현체인 DefaultListModel을 제공하고 있다. 단순히 화면에 모델 데이터를 뿌려주기만 할 것이면 이것만으로도 충분하지만, 예를 들어서 모델이 들어있는 데이터들을 정렬하는 등 기능을 추가하려면 기본 구현체를 상속해서 정렬 기능을 삽입하면 된다.
위에서 fireContentsChanged 를 호출할 때 누가 영향을 받을지는 모르지만(?) 리스너가 있다면 현재 정렬을 통해서 모델 데이터에 변경이 생겼음을 알려주는 것이다.(정렬해놓고 알리지 않으면 화면에서는 아무런 변화도 일어나지 않을 것이다.)

element를 새로 하나 넣었을 때, 또는 element를 빼냈을때 또는 위처럼 정렬했을때 모델의 상태에 변화가 생길때마다 리스너에게 이를 알려줘야만 한다. 그래서 API 문서를 보면 다음과 같이 꼭 호출하라는 fire....() 메소드들이 있다.

must-calling

index 값은 변경이 일어난 구간의 범위를 설정할 때 쓰이는데 추가, 삭제처럼 한군데서만 변화가 일어났다면 index를 동일한 값으로 주면 된다. 유의할 점은 위에서 fire...() 를 호출할때 건네주는 index가 javax.swing.ListModel 인터페이스에 선언된 getElementAt(index); 에서 사용될 값이라는 점이다. 2번째 element에서 변경이 일어났는데, index 3을 전달해주면 화면에 엉뚱한게 튀어나온다.(이거 흔한 실수)

직접 javax.swing.AbstractListModel을 상속해서 멋드러진 ListModel을 작성했다면 다음과 같은 테스트 코드로 원하는대로 실행되는지 확인하는 것도 좋다.
<데이터를 추가> 하는 부분이 어떤 경로를 통해서 <리스너 호출>로 이어지는지는 알 수 없으나 어쨋든 잘 작동되는 것을 확인하면 충분하다. (이런걸 블랙박스 테스트라고 하나?) 위와같은 테스트를 통해서 내가 작성한 ListModel이 제대로 동작함을 확인하고 그 다음 단계로 화면에 출력하는 ListCellRenderer로 이어진다.

[JList] 자바 스윙 JList,ListModel, ListCellRenderer 2

'Dev > Java' 카테고리의 다른 글

[JList] 자바 스윙 JList,ListModel, ListCellRenderer 2  (0) 2010.05.12
유틸을 사용한 GridBagLayout 예제  (2) 2010.05.06
GridBagLayout Utility Class  (0) 2010.05.06
Posted by yeori
,