JTable을 만들어주는 제너릭 GUI(3단계)
데이터 파일로부터 읽어들인 매니저에서 JTable을 이용하여 GUI 화면을 구성하는 프로그램을 재사용가능하게 구현해 보고자 한다.
- 어떤 데이터 요소든 그에 맞게 테이블을 구성하고 열과 테이블 헤더를 구성할 수 있게 한다
- 검색과 수정, 삭제, 추가 기능을 가지는 JTable과 GUI 부분을 데이터가 달라져도 재사용할 수 있게 한다
- 데이터를 입력하고 구동하는 엔진 부분에서 GUIMain 객체를 호출하는 것으로 GUI가 시작되게 한다.
- 한 곡의 정보가 네 개의 항으로 구성되고 이것이 String[] 배열에 의해 데이터 엔진 부분과 연결된다.
이와 같이 동작하도록 작성한 프로그램에서 다음과 같이 두 개의 다른 데이터 파일에 대해 같은 방식으로 화면을 구성하도록 할 수 있다. 이렇게 하기 위해서 재사용 코드를 최대화하는 방식으로 엔진 부분과 GUI 부분을 작성해 보자.
이 프로그램은 다음과 같이 다섯 개의 패키지로 구성된다. 각 패키지의 역할은 다음과 같다.
facade 패키지
- GUI 부분과 엔진 부분 사이를 연결해주는 인터페이스와 구현을 가지는 패키지다. 파사드 패턴에 의해 UI 쪽에서 실제 엔진의 클래스를 알 필요없이 인터페이스만으로 사용할 수 있게 해주는 기능을 제공한다.
- UIData 인터페이스: 요소 타입 클래스가 JTable에서 보여지고 수정할 수 있게 해주는 메소드 인터페이스를 제공한다. 실제 요소 데이터로 표의 한 행의 텍스트를 set/get하는 기능을 가진다.
- IDataEngine 인터페이스: 이 인터페이스는 JTable이 엔진으로부터 JTable을 보여주기 위해 필요한 정보를 전달받기 위한 메소드를 정의한다. 컬럼의 개수와 테이블 헤더를 가져올 수 있고 검색이나 추가/삭제/수정 기능을 엔진에 요청할 수 있다.
- DataEngineImple 추상 클래스: IDataEngine 인터페이스의 공통기능을 구현한 추상 클래스로 실제 구체 엔진 클래스가 상속하게 된다. 여기서는 인터페이스의 기능을 Manageable 또는 UIData 수준에서 구현한 코드를 가진다.
table_demo 패키지
- GUIMain : JFrame을 상속하여 메인 윈도우의 역할을 하는 클래스다.
- TablePanel: 실제 보여질 화면 내용을 가지는 클래스다. 테이블 부분(tableControlloer)과 아래쪽 패널(bottom)을 포함하여 화면을 구성하는 클래스다.
- TableController: JTable을 구성하고 수정/추가/삭제 버튼에 대한 테이블의 처리 기능을 가지는 클래스다.
- BottomPane: 하부의 텍스트 필드와 버튼들을 구성하고 그에 대한 액션리스너를 제공한다.
나머지 패키지
- mgr 패키지 : 변동없음
- song 패키지 SongMgr
- student 패키지 StudentMgr
공통 인터페이스 추출하기
특정 구체 클래스에 의존하지 않는 요소 클래스를 사용할 수 있게 하기 위해서는 Manageable 이외에 GUI와의 인터페이스를 위한 추가적인 기능들이 필요하다.
public interface UIData {
void set(Object[] uitexts);
String[] getUiTexts();
}
이들 메소드는 GUI 에서 호출하여 GUI의 테이블 행과 요소 클래스를 연결하는 기능을 제공한다. GUI의 테이블모델에서 받은 그 행의 데이터 배열을 Object[]로 받아 그것을 구체 클래스의 추가와 수정을 위해 데이터를 설정하는 함수가 set 함수다. 또한 요소 클래스의 데이터를 표에 보여주기 위해 한 행에 들어갈 스트링의 배열로 넘겨받는 함수가 getUiTexts()다.
다음으로 엔진이 제공해야 하는 기능을 모은 인터페이스를 정의할 수 있다. 이것은 Manager와 같이 Manageable을 상속한 요소 타입을 매개변수로 가지는 제너릭으로 정의된다. 여기에는 테이블의 컬럼의 수와 헤더를 가져오는 부분, 그리고 검색과 추가, 수정, 삭제를 요청할 수 있는 메소드들이 제공된다. 이것들은 엔진이 제공하고 GUI 쪽에서 호출하게 되는 메소드들이다.
public interface IDataEngine<T extends Manageable> {
// 이 매니저가 관리하는 데이터를 테이블에 보여주기 위해
// 열제목의 개수와 배열을 반환. 필요한 열의 개수만큼 배열이 반환됨
int getColumnCount();
String[] getColumnNames();
// 키워드에 매치되는 것을 모두 찾아 리스트로 반환
List<T> search(String kwd);
// UI 테이블의 행에 있는 데이터를 스트링 배열로 받아와서 새로운 객체 추가
void addNewItem(String[] uiTexts);
// UI 테이블의 행에 있는 데이터를 스트링 배열로 받아와서 해당 객체 수정
void update(String[] uiTexts);
// UI 테이블의 행의 첫번째 데이터를 키로 받아와 해당 객체를 찾아 삭제
void remove(String kwd);
}
GUI는 이제 특정 요소 클래스(Song 또는 Student)나 특정 관리자 클래스(SongMgr 또는 StudentMgr)에 의존하지 않고 IDataEngine 인터페이스에만 의존하도록 작성할 수 있다.
먼저 GUIMain에서 정의한 engine은 IDataEngine 타입으로 정의된다. 여기서 요소의 구체 클래스를 알지 못하므로 <?>로 처리한 것을 볼 수 있다.
public class GUIMain {
static IDataEngine<?> engine;
public static void startGUI(IDataEngine<?> en) {
engine = en;
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
마찬가지로 TableController에서 정의한 엔진과 요소 타입도 IDataEngine과 UIData 타입으로 바뀌게 된다. 엔진으로부터 검색 결과를 돌려받는 리스트도 List<?>로 선언된다. 여기서 주의할 것은 List<Object>처럼 요소 타입의 슈퍼클래스를 요소로 가지는 리스트는 List<Song>의 슈퍼 타입이 될 수 없다는 점이다. 타입이 다른 경우는 <?>를 써야만 슈퍼타입으로 다형성에 의해 여러 가지 타입의 리스트를 가리키는 타입이 될 수 있다.
public class TableController implements ListSelectionListener {
IDataEngine<?> dataMgr;
...
void loadData(String kwd) {
List<?> result = dataMgr.search(kwd);
tableModel.setRowCount(0);
for (Object m : result)
tableModel.addRow(((UIData)m).getUiTexts());
}
그럼 이제 엔진 부분이 어떻게 바뀌어야 하는지 살펴보자. Song 클래스는 UIData를 구현해야 한다.
public class Song implements Manageable, UIData {
...
@Override
public void set(Object[] row) {
id = Integer.parseInt((String)row[0]);
name = (String)row[1];
title = (String)row[2];
year = Integer.parseInt((String)row[3]);
}
@Override
public String[] getUiTexts() {
return new String[] {""+id, name, title, ""+year, lyric};
}
}
요소 클래스는 이와 같이 UIData의 set과 getUiTexts 메소드를 구현해야 한다. 이것을 통해 GUI 부분에서 호출되는 메소드를 제공할 수 있다.
한편 SongMgr 클래스는 IDataEngine을 구현해야 하는데, 여기서 많은 기능이 구체 클래스와 상관없이 구현가능하다. 그래서 IDataEngine의 메소드들을 구현한 재사용가능한 구현 클래스를 제공한다. DataEngineImpl이라고 하는 클래스를 정의할 수 있다. 이를 위해서 구체 클래스에서 설정한 컬럼 헤더 부분을 가져올 수 있게 labels
public abstract class DataEngineImpl<T extends Manageable> implements IDataEngine<T> {
String[] headers = null;
protected Manager<T> mgr;
public void setManager(Manager<T> mgr) {
this.mgr = mgr;
}
public void setLabels(String[] texts) {
this.headers = texts;
}
그리고 검색, 수정, 삭제 부분은 구체 클래스가 필요하지 않으므로 이 클래스에서 구현할 수 있다.
@Override
public List<T> search(String kwd) {
if (kwd == null)
return mgr.mList;
return mgr.findAll(kwd);
}
@Override
public void update(String[] editTexts) {
// TODO Auto-generated method stub
Manageable s = mgr.find(editTexts[0]);
((UIData)s).set(editTexts);
}
@Override
public void remove(String kwd) {
// TODO Auto-generated method stub
Manageable s = mgr.find(kwd);
mgr.mList.remove(s);
}
IDataEngine 중 addNewItem은 구현되지 않았는데, DataEngineImpl 클래스는 abstract로 선언된 추상클래스여서 미구현된 인터페이스 메소드가 있어도 문제가 되지 않는다.
public class SongMgr extends DataEngineImpl<Song> {
// 테이블의 헤더 데이터 제공 부분
String[] labels = {"랭킹", "이름", "제목", "년도", "가사"};
public SongMgr() {
setLabels(labels);
setManager(new Manager<Song>());
readAll("songs.txt");
}
void readAll(String filename) {
mgr.readAll(filename, new Factory<Song>() {
public Song create() {
return new Song();
}
});
}
@Override
public void addNewItem(String[] editTexts) {
// TODO Auto-generated method stub
Song s = new Song();
s.set(editTexts);
mgr.addElement(s);
}
...
}
여기서 SongMgr는 DataEngineImpl 클래스를 상속하므로 Manager<Song>을 또 상속할 수가 없다. 그래서 mgr 참조 변수를 DataEngineImple 클래스의 필드로 선언하고 매니저 객체를 여기서 생성해서 setManager 해주는 방식을 취한다. 그 결과 Manager에서 상속받았던 메소드의 호출은 다 앞에 mgr를 붙여주어야 한다. DataEngineImple의 mgr는 protected로 선언하여 상속한 클래스에서 사용할 수 있게 하였다.