자바 프로그래밍

JTable을 만들어주는 제너릭 GUI(3단계)

plas 2023. 11. 13. 11:08

데이터 파일로부터 읽어들인 매니저에서 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로 선언하여 상속한 클래스에서 사용할 수 있게 하였다.