JTable은 스윙 ui 중에 JTree와 더불어서 가장 복잡한 컴포넌트이다.

http://java.sun.com/docs/books/tutorial/uiswing/components/table.html


JTable의 api 가 이렇게 복잡한 이유는 JTable이 창구역할( Facade 패턴)과 더불어 MVC 패턴에서 Controller 역할을 하다보니 이것저것 하는 일이 많기 때문이다.

하나의 JTable을 구성하는 요소들을 대강 꼽아보면 TableModel, TableCellRenderer, TableCellEditor, TableColumn, JTableHeader, TableColumnModel 정도가 된다. 아무것도 없는 상태에서 저 수많은 모듈들을 모두 조합해서 JTable을 만들고 TableModel에 bean 인스턴스(Student, Employee 와 같은 클래스)를 등록하고 테이블에 뿌려줘야 한다면 제대로 쓸 수 있는 사람이 별로 없을 것이다.

이때문에 JTable을 보면 각종 Default... 구현체들이 곳곳에 숨어있는데, 처음 JTable을 사용하는 사람들은 맛이나 한 번 보라고(?) 무의미하거나 최소한의 기능만 하는 DefaultTableModel, DefaultTableCellRenderer 등을 조립해서 실행 가능한 JTable 인스턴스를 생성하고 있다.

그러므로 JTable에 대해서 알려면 JTable 자체보다는 JTable을 구성하는 "부품들"의 의미와 사용법을 알아야 한다. 특정 column에 배경색을 주거나, 폰트를 두껍게 하거나 글자 왼쪽에 아이콘을 출력하거나, 오른쪽으로 정렬하는 등 화면 출력을 변경하려고 하면 JTable 인스턴스를 가지고 할 일은 거의 없다. 모두다 JTable을 구성하는 model과 renderer, editor를 다루는 작업들이다.

JTable도 MVC 모델을 충실히 따르고 있기 때문에 Model(모델, 데이터를 관리, 유지하는 모듈)과 View(데이터를 화면에 출력하는 모듈)에 해당하는 요소들을 파악하면 이해가 쉽다.

MVC의 Controller 부분은 각종 리스너 등록을 통해서 구현되는데, 대부분의 구현은 스윙에서 제공하고 있으므로 사용자가 개입할 부분은 그리 많지 않다.

TableModel

MVC 패턴에서 "M(model)" 에 해당하는 javax.swing.table.TableModel 은 데이터를 관리, 유지하는 모듈이다. 원하는 데이터(구체적으로 bean 클래스들)를 TableModel 구현체에 삽입하면 사용자에게 보이지 않는 마술같은(?) 처리 과정(Controller에 해당)을 거쳐서 화면에 출력된다.

javax.swing.table.TableModel은 인터페이스이기 때문에 사용자가 직접 구현해야 하나, 핵심적인 부분은 javax.swing.table.AbstractTableModel 에 구현되어 있어서 대부분의 경우 이를 상속해서 모델 구현체를 만들게 된다.

만들어진 모델 구현체는 아래와 같이 테이블을 초기화할 때 사용한다.

    PageTableModel model = new PageTableModel();
    JTable table = new JTable(model );

그리고 사용자는 model에 값을 추가, 삭제하는 것만으로 JTable의 화면이 자동으로 갱신된다.

    model.addPage(new PageInfo(...... ));

TableColumn

javax.swing.table.TableColumn 은 MVC에서 View에 해당한다. 이 클래스의 구현 내용을 보면 화면에 테이블의 각 컬럼을 그릴때 필요한 column의 너비값과 TableCellRenderer, TableCellEditor 를 제공한다.

TableModel에 데이터를 추가, 삭제할때에는 row 단위로 이루어지지만 이 데이터들을 가져다가 화면에 그릴 때는 column 단위로 이루어진다. 테이블에서 각각의 column은 동일한 맥락의 값들이 모여있다. 예를 들어서 "사원정보"를 나타내는 Employee 클래스가 있을때 이 클래스 안에는 다음과 같은 property가 있을 것이다.
위에서 Employee 클래스에 모두 6개의 프로퍼티가 있으므로 여기에 대응하는 TableColumn도 6개가 있어야 할 것이다. 서로 다른 맥락의 property( firstName, age, email 등)가 모여서 하나의 직원을 나타내므로 각각의 property에 대응하는 column도 자연스럽게 서로 다른 맥락을 내포하게 된다.

firstName과 email을 보면 똑같은 String 타입이지만 의미가 다르다. 의미가 다르다는 것은 각 column을 그리는 방식, 편집 후 데이터 검증 방식이 다름을 의미한다. ( age가 0보다 작다면 취소 시켜야 할 것이다.)

그렇기때문에 각각의 TableColumn은 여기에 속한 cell이 그려지거나 편집될때 사용될 TableCellRenderer, TableCellEditor 를 별도로 관리하며 ui 레이어에서 테이블을 화면에 그릴 때 요청이 들어오면 renderer와 editor 구현체를 전달해서 그리기 작업을 간접적으로 지원한다.

이렇게 bean 클래스의 각각의 프로퍼티에 대응하는 TableColumn이 여러개 존재하면 이것들을 관리해줄 무언가가 필요한데, 스윙에서는 javax.swing.table.TableColumnModel 을 통해서 관리하도록 요구한다.

TableModelTableColumnModel을 혼동하면 안된다.

TableModel은 데이터를 관리하는 M(모델)에 속하고 TableColumnModel은 javax.swing.table.TableColumn(그리기 작업을 지원하는 역할) 들을 관리하는 V(뷰)에 속한다. 양쪽 모두 이름에 Model 이라고 쓰여있지만 속한 영역이 다르다.

TableColumnModel 은 인터페이스이고 스윙에서 DefaultTableColumnModel 구현을 제공하고 있다.

TableColumn을 관리하는 단순한 기능을 하다보니 대부분의 경우에 스윙에서 제공하는 기본 구현체만으로 충분한 경우가 많다.

JTable 인스턴스를 생성할 때 별도의 TableColumnModel 인스턴스를 제공하지 않으면 TableModel에서 column 크기를 읽어서 model.getColumnCount() 열의 수만큼 무의미한 TableColumn을 생성한다.(테이블 헤더에 A, B, C, D와 같은 값이 출력되는 것도 이때문이다.)

위의 직원 데이터에 대한 테이블 헤더를 구성하려면 아래와 같이 TableColumnModel을 만들어서 JTable 인스턴스를 생성하면 된다.

TableCellRenderer, TableCellEditor

TableCellRenderer는 각각의 cell을 그릴때 사용되는데 마치 도장 찍듯이 하나의 cell renderer 인스턴스를 반복적으로 사용해서 cell을 화면에 그려나간다. ( JList,ListModel, ListCellRenderer 2 참조)

화면을 그릴때에는 ui 레이어가 JTable의 getCellRenderer(int, int); 를 호출해서 renderer를 얻어내고 이것을 붓처럼 이용해서 그리기 작업을 한다.

여기서 renderer를 등록하는 두가지 방법이 있는데 하나는 위에서 설명했듯이 TableColumn에 renderer를 등록해놓는 것이다. TableColumn에 renderer 를 등록해놓으면 ui 레이어가 table.getCellRenderer(row, col); 을 호출해서 좌표값에 해당하는 TableColumn을 찾고, 다시 TableColumn.getCellRenderer()를 호출해서 등록된 renderer를 얻어낸다.

만일 TableColumn에 renderer가 등록되어 있지 않다면 이번에는 TableModel.getColumnClass(col) 을 호출해서 해당 cell에 보여질 property(위에서 firstName, email 등) 값의 Class 정보를 얻어낸다.

Class 정보를 얻어내서 주어진 Class에 대응하는 기본 renderer를 이용해서 그리기 작업을 지원한다. JTable안을 보면 빈번하게 사용되는 타입에 대한 cell renderer가 구현, 등록되어 있다. ( DateRenderer, DoubleRenderer, IconRenderer, NumberRenderer 등 )

하지만 위에서 Employee의 salary 같이 사용자가 만든 타입은 이것을 제대로 그려줄 renderer가 없기 때문에 Money의 toString() 을 그대로 출력한다.(즉, 사용자가 정의한 타입에 대해서는 별도로 renderer를 구현해 주어야 제대로 보여질 것이다.)

TableCellEditor는 특정 cell을 수정하고 업데이트할 목적으로 사용되는데, cell에 출력된 내용을 모델 데이터에 반영하기 위해서 javax.swing.event.CellEditorListener 를 등록해서 수정된 값을 읽어들인다.

스윙에서는 JTextField를 이용해서 특정 셀을 수정하는 DefaultCellEditor 구현을 제공하고 있다. 아래 코드는 여기에 리스너를 붙여서 셀의 값을 편집하고 TableModel에 갱신된 값을 써넣는 코드이다.

[Dev/Java] - [Swing JTable] JTable 다루기 2

Posted by yeori
,