반응형

애플리케이션에서 사용할 데이터 모델 만들기

모델-뷰-컨트롤(MVC) 아키텍처에 익숙하다면 지금까지 만든 것이 애플리케이션의 뷰에 해당한다는 것을 알아차렸을 것이다. 이 부분이야 말로 Jigloo가 진가를 발휘하는 부분이다. 이제는 모델을 만들어야 한다. 다시 말하면 시스템에서 사용할 데이터를 가져오고 사용할 자바 코드를 만들어야 함을 의미한다. Jigloo는 여기서도 역시 빛을 발한다. Jigloo는 단순한 이클립스 플러그인으로 Jigloo를 사용할 때도 직접 이클립스의 강력한 기능들을 힘들이지 않고 바로 사용할 수 있다. 이클립스는 자바 코드 작성과 데이터를 다루는 데 매우 훌륭한 도구이기 때문에 Jigloo 역시 이런 작업을 하기에 적합하다.

스키마 만들기

언급했다시피, XML 형식으로 데이터를 저장하고 JAXB를 사용하여 XML을 읽고 쓰는 작업을 할 것이다. 따라서 스키마를 작성할 필요가 있다.


Listing 1. 워크플로우 XML 스키마
                    
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema" 
  targetNamespace="org:developerworks:workflow" 
  xmlns:dw="org:developerworks:workflow">

     <element name="workflow" type="dw:workflow"/>
     <complexType name="workflow">
          <sequence>
               <element name="user" type="dw:user" minOccurs="0"
maxOccurs="unbounded"/>
               <element name="po" type="dw:purchaseOrder"
minOccurs="0" maxOccurs="unbounded"/>
          </sequence>
     </complexType>
     
     <complexType name="user">
          <sequence>
               <element name="username" type="string"/>
               <element name="role" type="dw:role"/>
          </sequence>
          <attribute name="id" type="integer" use="required"/>
     </complexType>
     <simpleType name="role">
          <restriction base="string">
               <enumeration value="worker"/>
               <enumeration value="manager"/>
          </restriction>
     </simpleType>
     
     <complexType name="purchaseOrder">
          <sequence>
               <element name="priority" type="dw:priority"/>
               <element name="dateRequested" type="date"/>
               <element name="dateNeeded" type="date" minOccurs="0"/>
               <element name="itemName" type="string"/>
               <element name="itemDescription" type="string" minOccurs="0"/>
               <element name="quantityRequested" type="integer"/>
               <element name="url" type="anyURI" minOccurs="0"/>
               <element name="price" type="decimal"/>
               <element name="status" type="dw:orderStatus"/>
               <element name="submittedBy" type="integer"/>
               <element name="processedBy" type="integer" minOccurs="0"/>
          </sequence>
          <attribute name="id" type="integer" use="required"/>
     </complexType>
     
     <simpleType name="priority">
          <restriction base="string">
               <enumeration value="normal"/>
               <enumeration value="high"/>
          </restriction>
     </simpleType>
     
     <simpleType name="orderStatus">
          <restriction base="string">
               <enumeration value="pending"/>
               <enumeration value="approved"/>
               <enumeration value="rejected"/>
          </restriction>
     </simpleType>
</schema>

스키마에 해당하는 XML 파일에 바인딩할 자바 클래스를 만들기 위해 JAXB를 사용할 수 있다. 자바 6을 사용한다면 JAXB가 내장되어 있다. 자바 5를 사용한다면 썬에서 JAXB를 다운로드해야 한다. 여러분은 명령행 도구인 xjc를 사용하길 원할 수도 있다. 예를 들어 xjc workflow.xsd와 같이 사용할 수 있다. 이 명령어를 사용하여 workflow.xsd를 스키마 컴파일러가 파싱하고 클래스를 만들어낼 것이다. 그런 다음 클래스 파일들을 프로젝트에 복사하여 사용할 수 있다. 프로젝트에 XML 디렉터리를 만들고 그 안에 스키마 파일도 복사한다. 이제 예제로 사용할 XML 파일을 만들자.


Listing 2. 초기 XML 데이터
                    
<?xml version="1.0" encoding="UTF-8"?>
<dw:workflow xmlns:dw="org:developerworks:workflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="org:developerworks:workflow
workflow.xsd ">
  <user id="0">
    <username>homer</username>
    <role>worker</role>
  </user>
  <user id="1">
    <username>bart</username>
    <role>manager</role>
  </user>
  <po id="0">
    <priority>normal</priority>
    <dateRequested>2001-01-01</dateRequested>
    <dateNeeded>2001-01-01</dateNeeded>
    <itemName>stapler</itemName>
    <itemDescription>A great stapler</itemDescription>
    <quantityRequested>2</quantityRequested>
    <url>http://www.thinkgeek.com/homeoffice/gear/61b7/</url>
    <price>21.99</price>
    <status>pending</status>
    <submittedBy>0</submittedBy>
  </po>
</dw:workflow>

모든 요소들을 추가한 뒤 Package Explorer는 그림 26처럼 보일 것이다(필요하다면 JAXB jar 파일들을 자바 빌드 패스에 추가해야 한다).


그림 26. JAXB 클래스와 XML 파일이 추가된 Package explorer
JAXB 클래스와 XML 파일이 추가된 Package explorer




위로


데이터 접근

Package Explorer에 몇 가지 파일들이 추가된 것을 확인할 수 있을 것이다. 바로 WorkflowDao와 XmlWorkflow다. WorkflowDao는 데이터를 사용할 때 필요로 하는 작업들을 정의한 인터페이스다(Listing 3).


Listing 3. WorkflowDao 인터페이스
                    
package org.developerworks.workflow;

import java.util.List;

public interface WorkflowDao {
     public List<User> getUsers();
     public List<PurchaseOrder> getAllOrders();
     public List<PurchaseOrder> getAllPendingOrders();
     public List<PurchaseOrder> getOrdersForUser(int userId);
     public void saveOrder(PurchaseOrder order);
     public void setOrderStatus(int orderId, OrderStatus status);
}

고전적인 데이터 접근 객체(Data Access Object) 패턴을 사용하고 있다. 간단하게 인터페이스를 정의하고 이 인터페이스에 대한 애플리케이션 코드를 작성한다. XML을 사용하여 JAXB 기반 구현을 할 것이다. 하지만 이 디자인을 사용하여 손쉽게 데이터베이스 기반 구현 같은 기타 구현으로 변경할 수 있다. 이 인터페이스의 구현체가 XmlWorkFlow다.


Listing 4. XmlWorkflow 인터페이스 구현
                    
package org.developerworks.workflow;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class XmlWorkflow implements WorkflowDao {
     private static final String DATA_FILE = "data.xml";
     private static XmlWorkflow instance;
     
     private Workflow workflow;
     
     private XmlWorkflow() {
          try {
               JAXBContext ctx = this.getContext();
               Unmarshaller unm = ctx.createUnmarshaller();
               File dataFile = this.getDataFile();
               InputStream inputStream;
               if (dataFile.exists() && dataFile.length() > 0){
                    inputStream = new FileInputStream(dataFile);
               } else {
                    inputStream = 
Thread.currentThread().getContextClassLoader().getResourceAsStream("xml/"+DATA_FILE);
               }
               JAXBElement element = (JAXBElement) unm.unmarshal(inputStream);
               this.workflow = (Workflow) element.getValue();
          } catch (JAXBException e) {
               e.printStackTrace();
               throw new RuntimeException("Failed to read data file",e);
          } catch (FileNotFoundException e) {
               e.printStackTrace();
               throw new RuntimeException("Could not open data file", e);
          }
     }
     
     public static XmlWorkflow getInstance(){
          if (instance == null){
               instance = new XmlWorkflow();
          }
          return instance;
     }

     public List<PurchaseOrder> getAllOrders() {
          return this.workflow.getPo();
     }

     public List<PurchaseOrder> getAllPendingOrders() {
          List<PurchaseOrder> allOrders = this.getAllOrders();
          List<PurchaseOrder> pending = new ArrayList<PurchaseOrder>();
          for (PurchaseOrder order : allOrders){
               if (order.getStatus().equals(OrderStatus.PENDING)){
                    pending.add(order);
               }
          }
          return pending;
     }

     public List<PurchaseOrder> getOrdersForUser(int userId) {
          List<PurchaseOrder> allOrders = this.getAllOrders();
          List<PurchaseOrder> userOrders = new ArrayList<PurchaseOrder>();
          for (PurchaseOrder order : allOrders){
               if (order.getSubmittedBy().intValue() == userId){
                    userOrders.add(order);
               }
          }
          return userOrders;
     }

     public List<User> getUsers() {
          return this.workflow.getUser();
     }

     public void saveOrder(PurchaseOrder order) {
          int index = 0;
          for (PurchaseOrder po : this.workflow.getPo()){
               if (po.getId().intValue() == order.getId().intValue()){
                    this.workflow.getPo().set(index, order);
                    this.saveData();
                    return;
               }
               index++;
          }
          // add new order
          order.setId(new BigInteger(Integer.toString(this.workflow.getPo().size())));
          this.workflow.getPo().add(order);
          this.saveData();
     }
     
     public void setOrderStatus(int orderId, OrderStatus status) {
          for (PurchaseOrder po : this.workflow.getPo()){
               if (po.getId().intValue() == orderId){
                    po.setStatus(status);
                    this.saveData();
                    return;
               }
          }
     }

     private void saveData(){
          File dataFile = this.getDataFile();
          try {
               JAXBContext ctx = this.getContext();
               Marshaller marshaller = ctx.createMarshaller();
               FileOutputStream stream = new FileOutputStream(dataFile);
               marshaller.marshal(this.workflow, stream);
          } catch (JAXBException e) {
               e.printStackTrace();
               throw new RuntimeException("Exception serializing data file",e);
          } catch (FileNotFoundException e) {
               e.printStackTrace();
               throw new RuntimeException("Exception opening data file");
          }
     }
     
     private File getDataFile() {
          String tempDir = System.getProperty("java.io.tmpdir");
          File dataFile = new File(tempDir + File.separatorChar + DATA_FILE);
          return dataFile;
     }

     private JAXBContext getContext() throws JAXBException {
          JAXBContext ctx = JAXBContext.newInstance("org.developerworks.workflow");
          return ctx;
     }
     
     public static void main(String[] args){
          XmlWorkflow dao = XmlWorkflow.getInstance();
          List<User> users = dao.getUsers();
          assert(users.size() == 2);
          for (User user : users){
               System.out.println("User: " + user.getUsername() + " ID:" + user.getId());
          }
          List<PurchaseOrder> orders = dao.getAllOrders();
          assert(orders.size() == 1);
          for (PurchaseOrder order : orders){
               System.out.println("Order:" + order.getItemName() + "
ID:" + order.getId() + " Status:" + order.getStatus());
          }
          PurchaseOrder order = orders.get(0);
          order.setStatus(OrderStatus.APPROVED);
          order.setProcessedBy(new BigInteger("1"));
          dao.saveOrder(order);
     }
}

위에서 만들었던 예제 파일을 초기에 읽어 들이는 것을 확인할 수 있지만 시스템 임시 폴더에 data.xml로 변경사항을 저장한다. 데이터를 저장하기에 가장 안전한 장소는 아니지만 예제 애플리케이션에 사용하기에는 적절하다. 이 클래스 파일 안에 간단한 main 메서드가 있는 것 또한 볼 수 있다. JAXB가 동작하는 것을 간단하게 단위 테스트하기 위한 용도로 만들었다. 만약에 자바 5를 사용한다면 JAXB jar 파일을 프로젝트의 클래스패스에 추가해야 한다. 계속해서 작업하려면 그 파일들을 복사하여 프로젝트에 추가하거나 프로젝트 외부에 그 파일들이 위치한 장소를 참조할 수 있도록 해야 한다.




위로


애플리케이션 초기화하기

애플리케이션과 인터랙트하기 전에 모든 요소를 초기화해야 한다. 먼저 애플리케이션에서 사용할 모델 객체를 몇 개 선언해야 한다. Listing 5에 있는 코드를 추가하여 WorkflowMain 멤버 변수를 추가한다.


Listing 5. 모델 객체 선언
                    
     // Data Model Objects
     private java.util.List<User> users;
     private User user;
     
     // Service Object
     private WorkflowDao dao = XmlWorkflow.getInstance();

코드에 접근하려면 Workflow.java 파일을 마우스 오른쪽 클릭을 하고 Open With > Java Editor를 선택한다.

애플리케이션의 initGUI() 메서드 코드를 수정한다. 사용자 명단을 초기화하기 위해 private 메서드를 만들겠다.


Listing 6. 사용자 메서드 만들기
                    
     private void initUserList(){
          this.users = dao.getUsers();
          for (User u : users){
               this.userListCombo.add(u.getUsername());
          }
     }

userListCombo를 정의한 후 initGUI()에서 이 메서드를 호출한다.


Listing 7. 사용자 메서드 호출
                    
                {
                    userListCombo = new Combo(this, SWT.NONE);
                    userListCombo.setText("Users");
                    userListCombo.setBounds(28, 35, 105, 21);
                    this.initUserList();
               }




위로


이벤트를 사용하여 뷰와 모델을 엮기

뷰와 모델을 만들었다. 이제 그 둘을 엮어 보자. 컨트롤러가 필요하다. SWT(와 스윙)는 모든 UI 프레임워크에서 사용하는 간단한 기술을 사용한다. 바로 이벤트 주도 시스템이라는 것이다. 이벤트를 사용하여 언제 모델에 있는 작업을 호출하고 뷰를 수정할지 알려줄 수 있다.

이제 다시 비주얼 디자이너로 돌아가자. 모델과 엮기를 원하는 첫 번째 UI 이벤트는 사용자 콤보 리스트에서 사용자를 선택했을 때다. 콤보 컨트롤을 선택하고 GUI 속성 뷰에서 Event 탭으로 변경하면 그림 27에 보이는 것과 같은 화면을 확인할 수 있을 것이다.


그림 27. 콤보 컨트롤 이벤트에 접근하기
콤보 컨트롤 이벤트에 접근하기

몇몇 리스너들을 콤보 컨트롤에서 볼 수 있다. SelectionListener를 선택한다. 이 리스너는 콤보 컨트롤에서 무언가를 선택할 때마다 SelectionEvent를 발생시킨다. 이 이벤트를 다룰 것을 익명 메서드로 그것을 인라인에서 다룰 수도 있고이 이벤트를 다룰 메서드를 정의할 수도 있다. 후자를 선택하겠다. 이렇게 하면 userListComboWidgetSelected라는 메서드가 생성된다. 이벤트를 다루기 위한 코드는 Listing 8에 나와있다.


Listing 8. 사용자 콤보 리스트 선택 코드
                    
     private void userListComboWidgetSelected(SelectionEvent evt) {
          int index = this.userListCombo.getSelectionIndex();
          if (index >= 0){
               this.user = this.users.get(index);
               System.out.println("User selected="+this.user.getUsername());
               purchaseOrderTable.removeAll();
               java.util.List<PurchaseOrder> orders;
               boolean isManager = this.user.getRole().equals(Role.MANAGER);
               if (isManager){
                    orders = dao.getAllPendingOrders();
               } else {
                    orders = dao.getOrdersForUser(this.user.getId().intValue());
               }
               this.approveButton.setVisible(isManager);
               this.rejectButton.setVisible(isManager);
               for (PurchaseOrder order : orders){
                    displayPurchaseOrder(order);
               }
          }
     }

여기서 살펴봐야 할 것들이 많다. 먼저 사용자가 관리자인지 확인한다. 관리자가 아니면 사용자의 모든 구매 주문 목록을 보여준다. 관리자가 맞다면 대기중인 주문 목록만을 보여준다. 다음으로 관리자가 아니면 승인/취소 버튼이 보이지 않도록 한다. 마지막으로 purchaseOrderTable에 있는 모든 주문 데이터들을 데이터 접근 객체를 사용해 가져와 보여준다.

이제 Approve 버튼에 이벤트를 추가한다. 비주얼 디자이너에 보이는 Approve 버튼을 선택하고 GUI 속성 창에 있는 이벤트 탭으로 이동한다. 버튼을 선택했을 때 실행되어야 하기 때문에 그림 28에 보이는 것처럼 여기서도 selection event를 사용할 것이다.


그림 28. Approve 버튼 선택 이벤트 설정
Approve 버튼 선택 이벤트 설정

그리고 나서 이 이벤트를 다룰 코드를 추가한다.


Listing 9. Approve 버튼 선택 이벤트 코드
                    
     private void approveButtonWidgetSelected(SelectionEvent evt) {
          TableItem[] selected = this.purchaseOrderTable.getSelection();
          if (selected != null){
               for (TableItem item : selected){
	this.dao.setOrderStatus(Integer.parseInt(item.getText(4)), OrderStatus.APPROVED);
                    item.setText(3, OrderStatus.APPROVED.toString());
               }
          }
     }

reject 버튼도 이와 매우 비슷하게 할 수 있다. 선택 이벤트 리스너를 추가하고 비슷한 코드를 실행한다. 유일한 차이가 있다면 주문 상태를 APPROVED가 아닌 REJECTED로 바꾼다는 것이다.


Listing 10. Reject 버튼 코드
                    
     private void rejectButtonWidgetSelected(SelectionEvent evt) {
          TableItem[] selected = this.purchaseOrderTable.getSelection();
          if (selected != null){
               for (TableItem item : selected){
	this.dao.setOrderStatus(Integer.parseInt(item.getText(4)), OrderStatus.REJECTED);
                    item.setText(3, OrderStatus.REJECTED.toString());
               }
          }
     }

이제 남은 건 Submit 버튼이다. 이 버튼을 사용하여 새로운 구매 주문을 추가할 수 있어야 한다. 이 버튼 역시 다른 버튼들과 유사하다. 선택 이벤트에 대한 이벤트 핸들러를 그림 29처럼 추가한다.


그림 29. PO 버튼 선택 리스너 추가
PO 버튼 선택 리스너 추가

이벤트를 다룰 코드를 추가한다.


Listing 11. PO 버튼 이벤트 핸들러 코드 추가
                    
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.GregorianCalendar;

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

...

     private void addButtonWidgetSelected(SelectionEvent evt) {
          try {
               this.addPurchaseOrder();
          } catch (Exception e) {
               throw new RuntimeException("Exception adding purchase order",e);
          }
          this.formItemText.clearSelection();
          this.formPriceText.clearSelection();
          this.formQuantityText.clearSelection();
     }
     
     private void addPurchaseOrder() throws Exception{
          String item = this.formItemText.getText();
          String priceString = this.formPriceText.getText();
          String quantityString = this.formQuantityText.getText();
          BigDecimal price = new BigDecimal(priceString);
          BigInteger quantity = new BigInteger(quantityString);
          PurchaseOrder po = new PurchaseOrder();
          int num = this.dao.getAllOrders().size();
          String numString = Integer.toString(num);
          BigInteger newId = new BigInteger(numString);
          po.setId(newId);
          po.setItemName(item);
          po.setPrice(price);
          po.setQuantityRequested(quantity);
          po.setPriority(Priority.NORMAL);
          po.setStatus(OrderStatus.PENDING);
          po.setSubmittedBy(this.user.getId());
          GregorianCalendar cal = (GregorianCalendar) GregorianCalendar.getInstance();
          DatatypeFactory factory = DatatypeFactory.newInstance();
          XMLGregorianCalendar now = factory.newXMLGregorianCalendar(cal);
          po.setDateRequested(now);
          this.dao.saveOrder(po);
          this.displayPurchaseOrder(po);
     }

	private void displayPurchaseOrder(PurchaseOrder order) {
		String[] row = new String[] 
		         {order.getItemName(), order.getPrice().toString(), 
				order.getQuantityRequested().toString(),
order.getStatus().toString(), order.getId().toString()};
		TableItem tableItem = new TableItem(purchaseOrderTable,0);
		  tableItem.setText(row);
		  this.purchaseOrderTable.showItem(tableItem);
	}




위로


GUI 테스트

GUI를 테스트해야 할 시간이다. 그림 30처럼 클래스를 오른쪽 클릭을 한 후 Run As > SWT Application을 선택한다.


그림 30. 애플리케이션 실행
애플리케이션 실행

이렇게 하면 애플리케이션이 실행된다.


그림 31. 워크플로우 애플리케이션
워크플로우 애플리케이션

기본 데이터로 대기중인 구매 주문을 넣어뒀기 때문에 사용자 bart를 선택한 뒤 구매 주문을 승인할 수 있다.


그림 32. 주문 승인
주문 승인

사용자를 homer로 변경한 뒤 구매 주문의 상태를 확인해 보자.


그림 33. 사용자가 자기 PO 보기
사용자가 자기 PO 보기

새로운 주문을 추가할 수도 있다.


그림 34. PO 추가
PO 추가

Add PO를 클릭하여 테이블에 구매 주문을 추가할 수 있다.


그림 35. 새 PO 추가
새 PO 추가

다시 bart로 변경하여 승인 또는 취소할 수 있다.


그림 36. 업데이트된 관리자 화면
업데이트된 관리자 화면 

+ Recent posts