Jasper를 사용하기 위해 라이브러리를 등록하고 서버를 구동했더니 다음과 같은 에러가 뜬다.
 

com.jaspersoft.jasperserver.api.JSExceptionWrapper: org.xml.sax.SAXParseException: cvc-complex-type.3.2.2: Attribute 'splitType' is not allowed to appear in element 'band'

이 에러는 보통 jrxml이 작성된 iReport와 서버 라이브러리의 버젼 호환이 되지 않은 경우 나오는 에러라고 한다. 주로
서버쪽 jasperreports.jar가 3.5.x 이하의 버젼이 설치된 경우 발생한다.

내 경우 iReport는 최신버젼(3.7.3) 을 사용했는데 서버에 올라가 있던 jasperreports.jar 파일의 버젼은 3.5.3 의 것이었기에 서버의 jasperreports.jar 의 버젼을 3.7.3으로 업데이트 해주었더니 해결..


 

Redmine 백업하기 - MySQL 백업.

개발 이야기/Java | 2010. 3. 5. 19:19
Posted by 시반

Redmine 백업하기 – MySQL 백업.

 

지난시간에는 cwRsync를 이용한 Redmine 파일 시스템 백업에 대하여 살펴보았다면 이번에는 DB백업에 대한 부분을 언급할까 한다.

Redmine을 서비스하기 위해 사용되는 DB PostgreSQL,MySQL,SQLite 세가지지만. 우리 팀에서야 MySQL을 사용하고 있으니MySQL을 중심으로 설명을 진행하도록 하겠습니다.

 

 

위 그림에서도 나와 있지만 대부분의 프로젝트들이 CharacterSet 으로  UTF-8을 사용하고 있지만 Redmine의 경우 기본 설치시

기본 인코딩 타입은 Latin1입니다. 그렇다고 Redmine에서 한글 저장이 안되는 것은 아니지만 DB이전등을 하려고 하는 경우

UTF-8 문자열들이 깨지는 경우가 있다고 하니 처음 설치할 때 utf8로 설정하는 것이 좋을 듯 싶습니다.

 

앞서 말한대로 이전작업을 위한 백업 및 복구 절차를 위한 것이기 때문에 이전시에 인코딩타입도 utf8로 한번 바꿔보았습니다.

 

1. 기존 데이터의 백업

mysqldump를 이용하여 기존 데이터를  old_db.sql이라는 이름으로 백업합니다.

 

c:\redmine\mysql>mysqldump –uroot –p –default-character-set=latin1 bitnami_redmine > old_db.sql

Enter password: ********

 

old_db.sql 파일을 열어 latin1 utf8로 일괄변환한 다음 new_db.sql 로 저장합니다.

 

2. 데이터베이스 생성

이전  서버에 mysql을 설치하고 bitnami_redmine이라는 이름으로 데이타베이스를 생성합니다.

 

c:\redmine\mysql>mysql –uroot –p

Enter password: ******** ~중략~

mysql> drop database bitnami_redmine;

mysql> create database bitnami_redmine COLLATE utf8_general_ci;

 

3. 데이터 복구 

새로만든 bitnami_redmine에 앞서 백업 받은 데이타파일을 기본 charactoer-set을 utf8로 해서 복구합니다.

 

c:\redmine\mysql>mysql –ubitnami –p –default-character-set=utf8 bitnami_redmine < new_db.sql

Enter password: ********

 

 

4. Redmine DB접속정보 변경 

MySQL 설치폴더에서  my.ini 파일을 열어 다음과 같이 수정합니다.

 

[client]
default-character-set=utf8

 

[mysqld]
init_connect=SET collation_connection = utf8_general_ci
init_connect=SET NAMES utf8
default-character-set=utf8
character-set-server=utf8
collation-server=utf8_general_ci

 

[mysql]
default-character-set=utf8

 

show variables like 'c%'; 로 확인해보면  대부분의 characterSet들이 utf8로 변경되어 있슴을 확인할 수 있습니다.

 

 

5. Apache 재시작

http://서버주소/redmine redmine이 정상적으로 보여지는지 확인하면 됩니다.

이 때 redmine은 정상적으로 뜨지만 메뉴 등의 글자가 ???? 등으로 표시된다면 redmine 이하 lang 디렉토리에서 ko.yml 파일을 열어 utf8 형식으로 저장합니다.

아니면  get/redmine 등이 표시되면서 redmine 페이지 자체가 열리지 않는 경우 redmine 이하 config 디렉토리의  database.yml  파일의 production 항목에 encoding:utf8을 추가해 줍니다.

간혹 반대로 database.yml encoding:utf8 이 있는 경우에도 동일 증상이 발생할 수 있습니다.

이 경우에는 오히려 이 구문을 삭제한 후 아파치를 재시작하면 정상적으로 로드되는 것을 확인할 수 있습니다.

 

마치며...

Redmine DB이전작업은 별 내용 없이 MySQL 백업 및 복구에 대한 부분만 처리했습니다. 앞서 말한바와 같이 DB이전 부분은 cwRsyn 이용한 백업 및 복구는 해보지 못했지만 차후 한번 시도해 봐야 할 부분이고 한가지 아쉬웠던 점은 Redmiine버젼의 최신버젼이  0.9X버젼인데 비해 현재 연구소 설치버젼은 0.8X 버젼이라 이번 기회에 업그레이드를 하려했지만  DB 마이그레이션 부분이 가이드만큼 쉽게 진행되지는 않아 동일버젼으로만 이전작업을 마치게 되었다는 점이다.

시간을 내서라도 다시 한번 시도해봐야 할듯 싶다. 일단은 여기서 마무리..

 

 

 

 

 

Redmine 백업하기 – rsync를 이용한 원격백업.

 

얼마전 팀에서 운영하던 이슈추적시스템 중 하나인 Redmine을 다른 서버로 이전하게 되었다.

사실 이전작업이라고 해봤자 첨부파일을 복사하고 백업했던 DB를 복구하기만 하면 되는 것이긴 했지만...

리눅스서버라면 대상소스를 압축하고 ftp로 파일을 옮기고 복구하는 작업을 rsnc를 이용하면 아주 간단하게 처리될 수 있지만

팀내 서버로 윈도우즈 서버를 사용하는 터라 처음엔 그냥 압축한 다음 ftp로 이용하려고 생각했었는데

cwRsyn이라는 윈도우즈용 rsyn도 있다기에 이를 한번 사용해보기로 했다(맥용도 있다는데...쓸일이 없다... -_-).

 

먼저  cwRsyn을 다운로드 받아 설치해보자. 다운로드는 http://sourceforge.net/projects/sereds/files/  에서 받으면 된다.

현재 4.0.3 버젼이 최신버젼이네요...

 

1. cwRsynServer설치하기

 

백업소스가 있는 곳에 cwRsynServer를 설치해야 합니다. 여기서는 윈도우즈용 Redmine이 있던 서버에 설치하는 것으로 진행해보겠습니다.

 

설치시 언제나 나타나는 라이선스 동의.....I Agree 를 클릭..

 

다음엔 설치폴더를 지정합니다. 기본값이 cwRsyn 정도가 되지 않을까 싶었는데 ICW가 기본값이네요...

 

서비스 계정을 등록하는 부분입니다. 리눅스용과는 달리 cwRsynServer는 하나의 서비스 계정만을 제공하는 듯 합니다.

리눅스용처럼 auth_users 등을 설정하여 인증요청시 사용되는 것 같습니다.

기본값으로 계정은 SvcCWRYNC, 패스워드는 7ElpiAHCxi2780 이 지정되지만 

기본값대신 SyncService라는 계정으로 만들어 보았습니다.

 

이제 Install 버튼을 클릭하여 설치를 진행합니다. 끝나면 Close버튼을 클릭하여 설치를 종료합니다.

 

2. 백업소스 설정하기

 

설치가 끝났다면 설정파일(C:\Program Files\ICW\rsynd.conf)을 열어 백업 소스정보를 추가합니다.

 

[redmine_files]

path = /cygdrive/C/redmine/apps/redmine/files

read only = false

transfer logging = yes

  • [redmine_files] : [ ] 안의 이름으로 클라이언트에서 접근합니다. 한마디로 서비스명?
  • path :  실제 백업소스 경로를 지정합니다. 명명규칙에 따르면 c:\work의 경우 /cygdrive/c/work 로 표시합니다.
  • read only = false : 클라이언트에서 업로드를 허용한다는 의미입니다. false로 설정하는 경우 업로드를 위해 Prepare a Directory for Uploading ( 설치폴더/Bin/PreUploadDir.exe )를 설치해야 겠지만 이번엔 이전 작업을 위한 다운로드만을 할 뿐이라 패스...

3. 서비스 구동

 

rsynServer 는 설치를 하게 되면 자동으로 서비스로 등록은 되지만 시작유형이 수동으로 설정되어 있기 때문에 서비스에서 직접 시작을 해주어야 합니다. 주기적으로 백업을 수행하도록 설정한다면야 속성값에서 자동으로 변경해도 상관없을 듯...

 

4. 클라이언트 설치하기

 

이제 백업하고자 하는 곳에 클라이언트를 설치합니다.

여기서는 원격백업을 위해 다른 서버에 설치를 진행하지만 하나의 서버에 설치할 수도 있습니다. (로컬 백업...)

 

마찬가지로 라이선스에 동의....

 

설치할 컴포넌트를 선택합니다. 선택사양이라 해봤자 Secure Channel Wrapper 뿐이네요.

 SSH를 지원하기 위한 컴포넌트인 듯 보입니다.

 

클라이언트 설치폴더가 cwRsync 였네요. Install 버튼을 클릭한 다음  설치를 종료합니다.

 

5. 클라이언트에서 백업 설정하기

 

설치디렉토리의 cwrsync.cmd 라는 파일을 메모장등으로 열어 다음과 같은 형식으로 백업 정보를 저장합니다.

rsync [옵션] [백업소스주소::백업서비스명] [클라이언트 백업 경로]

예) rsync.exe -avzr 127.0.0.1::redmine_files /cygdrive/c/redmine/apps/redmine/files

     --> 백업소스의 모든 파일(하위 디렉토리 포함)을 압축전송을 하여 c:\redmine\apps\redmine\files 디렉토리에 백업하라

저장 후 cwrsync.cmd를 실행하면 다음과 같이 백업소스를 백업하는 것을 확인할 수 있습니다.

 

참고로 옵션은 다음과 같습니다.

 

옵션

설명

-a

아카이브 모드, 심볼릭링크, 속성,퍼미션,소유권  보존

-v

상세보기

-z

전송시 압축

-r

하위 디렉토리 포함

-l

심볼릭 링크 재생성

-p

퍼미션 업데이트

-t

변경시간 보존, 없는 경우 전송시간으로 변경

-g

그룹속성 보존

-u

추가된 파일만 전송

-b

낡은 파일일 경우 ~을 붙임

-- delete

원본서버에 없는 파일은 클라이언트에서도 삭제

--existing

추가된 파일은 제외하고 갱신된 파일만 전송

--exclude ‘PATTERN’

패턴과 맞는 패일을 제외하고 전송

--stats

결과 보고

 

마치며...

 

 http://www.rsync.net 의 튜토리얼을 살펴보면 윈도우즈용 백업 에이전트를 사용하여 파일 시스템 뿐만 아니라  SVN, Git 등의 repository에서부터 MySQL, Postgress 등의 DB백업까지 스케쥴러에 따라 다양한 백업을 하는 방법들을 소개하고 있습니다.  

사실 윈도우즈 서버에서 rsync를 이전작업을 위해 사용해 본것이라 이러한 기능들을 시도해보지는 못하고 첨부파일디렉토리만을 대상으로 해보았지만 차후 백업정책에 따라  충분히 시도해 보아도 될 것 같다는 생각도 드네요.

내용은 별로 없지만 Redmine의 DB (MySQL) 백업 부분은 다음 블로그에 올려야 될거 같습니다.

 

 

 

dom4j를 이용하여 웹서비스에서 데이터를 파싱하는 중에 나온 에러입니다.

JUnit으로 테스트할 때는 이상이 없던터라 데이타가 나오지 않아 깜짝 놀랐네요.

로그를 찾아보니  SAXException: Invalid byte 2 of 2-byte UTF-8 sequence

말 그대로 UTF-8 로 인코딩된 정보가 아니라는 거죠.

당연히 수신측과 송신측의 인코딩 타입은 동일해야 겠죠.

 

그동안은 보내는 쪽 코딩을 주로 했던터라. 저역시 대부분 UTF-8로 변환하여 보냈습니다만

이번에는 수신할 때가 문제였네요. 아마 그쪽에서 인코딩 타입을 EUC-KR 으로 사용했었나 봅니다.

 

  SAXReader xmlReader = new SAXReader();
  xmlReader.setEncoding("EUC-KR");
  Document doc = xmlReader.read(inputstream);

 

이렇게 stream 데이터를 읽기 전에 EUC-KR로 인코딩타입을 명시하니 이상없이 동작하네요.

 

 

 

 

 

Hudson을 설치하고 Hudson관리화면으로 이동하면 Your container doesn't use UTF-8 to decode URLs  이라는 빨간 색 글자가 보이는 경우가 있다. 말 그대로 웹서버 컨테이너가 UTF-8로 지정되어 있지 않습니다. 라고 알려주는 것.

 

해결방법은 허드슨 톰캣관련 문서에서 확인할 수 있었다.( http://wiki.hudson-ci.org/display/HUDSON/Tomcat )

 

 

i18n
Some versions of Tomcat (such as 5.0.28) uses iso-8859-1 to decode URLs, which is in a clear violation of the relevant RFCs. To fix this problem, add the following URIEncoding attribute to the connector definition in $TOMCAT_HOME/conf/server.xml.
<Connector port="8080" URIEncoding="UTF-8"/>
Other people reported that a presence of RequestDumperValve would also cause a problem.
Related reads
  2. John O'Conner's blog entry about his experiment. See Elliotte's comment in particular (elharo.)
 
 

Tomcat의 경우 인코딩타입을 명백하게 지정하지 않은 경우 기본값으로 iso-8859-1 를 사용하게 되기 때문에 $TOMCAT_HOME/conf/server.xml 의 Connector에 URIEncoding="UTF-8" 이라고 지정해주어야만 한다. 

 

JOTM을 이용한 분산Transaction 처리

 

개요

Spring 등의 오픈소스프레임워크를 이용한 개발 프레젠테이션을 할 때 질문을 받았던 내용 중에 하나가무거운 EJB보다 필요한 기능만을 갖춘 가벼운 Spring등의 프레임워크를 쓰자는 것은 이해가 간다. EJB를 쓰는 주된 이유중에 하나인 분산처리 기능을 Spring등은 어떻게 처리하고 있는가?”

교과서적인 답변이랄까? Spring JTA를 이용한 분산처리 트랜잭션API을 지원하고 있습니다. -_- a

틀린 말은 아니지만 그 분의 질문은 JTA를 어떻게 사용할수 있는가라는 의도였던 것 같다. WAS 를 쓰면서도 EJB 만의 기능(앞서 언급한 분산처리기능)을 구현하거나 필요로 했던 프로젝트는 그다지 많지 않았다. 그래서 Spring등과 톰캣을 이용한 프로젝트들이 많이 생겨난 것이지만 사실 그렇기 때문에 Spring을 통해서도 충분히 분산처리가 가능합니다.라고 고객께 말은 드리지만 정작 분산처리기능이 필요한 경우에는 특히 분산트랜잭션 서비스를 지원하지 않은 서블릿컨테이너(톰캣)을 사용하는 경우에 JTA 를 어떻게 사용해야 하는지 개발자는 조금 더 알아야 하지 않을까?

JOTM(Java Open Transaction Manager)

 

두 개 이상의 자원에 동시에 접근하는데 Transaction이 필요한 경우가 있다. 예를 들어 결재정보를 저장하는 데이터베이스와 구매내역을 저장하는 데이터베이스가 다르다고 할때 이 두 데이터베이스에 접근하기 위한 DataSource는 다르지만 두 데이터베이스에 접근하는 코드는 단일 트랜잭션으로 처리되어야 한다. 웹로직이나 JBoss 등의 WAS는 자체적으로 분산트랜잭션서비스를 지원하지만 톰캣과 같은 서블릿컨테이너는 분산 트랜잭션을 지원하지 않는다. 만약 톰캣과 같은 분산트랜잭션을 지원하지 않은 컨테이너에서 구현해야 한다면 JOTM을 사용하면된다.

 

JOTM(Java Open Transaction Manager) ObjectWeb에서 제공하는 트랜잭션 관리자로서 JTA, JTS 등의 API를 지원하고 있으며 JOTM을 사용하면 기존의 JDBC 기반의 코드에 JTA를 이용한 트랜잭션 처리 코드를 손쉽게 적용할 수 있게 된다..

아래의 항목은 Spring에서 JOTM을 어떻게 사용하는지에 대하여 설명하고 있으며 순서에 따라 진행하면 된다.

 

1. JOTM library download  Jar 파일 설치

  - spring2.5를 다운받았다면 :jotm,jotm_jrmp_stubs.jar,xapool.jar webapplication classpath에 추가

  - JOTM site에서 download받게되면 ow_carol-all.jar를 찾아 webapplication classpath에 추가

 

2.spring에서 제공하는 JotmFactoryBean bean 설정

 

3.각각 사용할 database dataSource org.enhydra.jdbc.pool.StandardXAPoolDataSource를 이용하여 설정한다

 

4.ibatis를 사용할 경우 slqMapClient에 각각 사용할 dataSource property로 가지도록 설정한다

 

spring설정 file(일반적으로 application.xml)

 

  <!-- jotm 설정 -->

    <bean id="jotm"

        class="org.springframework.transaction.jta.JotmFactoryBean" />

 

    <!-- jotm property로 가지는 JtaTransactionManager설정 -->

    <bean id="transactionManager"

        class="org.springframework.transaction.jta.JtaTransactionManager"  p:userTransaction-ref="jotm" />

    <!-- StandardXAPoolDataSource를 이용한 postgresSQL dataSource설정  -->

    <bean id="postgresDBDataSource"  class="org.enhydra.jdbc.pool.StandardXAPoolDataSource"

destroy-method="shutdown">

        <property name="dataSource">

            <bean class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown">

                <property name="transactionManager" ref="jotm" />

                <property name="driverName" value="org.postgresql.Driver" />

                <property name="url" value="jdbc:postgresql://localhost:5432/testDb?charSet=UTF-8" />

            </bean>

        </property>

        <property name="user" value="test" />

        <property name="password" value="test" />

    </bean>

    

    <!-- StandardXAPoolDataSource를 이용한 oracle dataSource설정  -->

    <bean id="oracleDBDataSource"  class="org.enhydra.jdbc.pool.StandardXAPoolDataSource"

        destroy-method="shutdown">

        <property name="dataSource">

            <bean class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown">

                <property name="transactionManager" ref="jotm" />

                <property name="driverName" value="oracle.jdbc.driver.OracleDriver" />

                <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1526:TEST" />

            </bean>

        </property>

        <property name="user" value="test" />

        <property name="password" value="test" />

    </bean>

    

    <!-- postgresSQL dataSource property로 하는 slqMapClient설정 -->

    <bean id="sqlMapClient"  class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">

       <property name="configLocation" value="/WEB-INF/config/sqlmap-config.xml"/>

       <property name="dataSource" ref="postgresDBDataSource"/>

    </bean>

    

    <!-- oracle dataSource property로 하는 slqMapClient설정 -->

    <bean id="oracleDbSqlMapClient"  class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">

       <property name="configLocation" value="/WEB-INF/config/sqlmap-config.xml"/>

       <property name="dataSource" ref="oracleDBDataSource"/>

    </bean>

    

    <!-- postgresSQL dataSource property로 하는 slqMapClient를 가지는 dao -->

    <bean id="postgresDbDao" class="test.dao.PostgresDbDao">

        <property name="sqlMapClient" ref="sqlMapClient" />    

    </bean>

    

    <!-- oracle dataSource property로 하는 slqMapClient를 가지는 dao -->

    <bean id="oracleDbDao" class="test.dao.OracleDbDao">

        <property name="sqlMapClient" ref="oracleDbSqlMapClient" />    

    </bean>

   <!-- The business Logic are autodetected POJOs labeled with the @Service annotaion -->

   <context:component-scan base-package="test.service" scoped-proxy="targetClass"  />

    <context:annotation-config />

    <!-- annotation으로 transcation를 선언 이때 transactionManager jotm transactionManager로 한다  -->

    <tx:annotation-driven transaction-manager="transactionManager"/>

 

iBatis 설정(일반적으로 sqlMap.xml)

 

  <insert id="insertTestInOracleDb" parameterClass="java.util.Map">

        INSERT INTO test_imsi ( name,age) VALUES (#name#,#age#)

    </insert>

    

    <insert id="insertTestInPostgresDb" parameterClass="java.util.Map">

        INSERT INTO test2_imsi ( name,age) VALUES (#name#,#age#)

    </insert>

 

dao (postgresSQL 사용, 여기서는 PostgresDbDao.java)

 

 public class PostgresDbDao extends SqlMapClientDaoSupport

{

    public void insertTestTable(Map<String,String> paramMap)

    {

        getSqlMapClientTemplate().insert("test.insertTestInPostgresDb",paramMap);

    }

}

 

dao (Oracle 사용, 여기서는 OracleDbDao.java)

 

 public class OracleDbDao extends SqlMapClientDaoSupport

{

    public void insertTestTable(Map<String,String> paramMap)

    {

        getSqlMapClientTemplate().insert("test.insertTestInOracleDb",paramMap);

    }

}

 

Service (비즈니스 로직 구현부)

 

 @Service

 @Transactional

 public class TestService

 {

    @Autowired

    private PostgresDbDao postgresDbDao;

    @Autowired

    private OracleDbDao oracleDbDao;

    

    public void jotmTransaction(Map<String,String> paramMap)

    {

        postgresDbDao.insertTestTable(paramMap);

        oracleDbDao.insertTestTable(paramMap);

    }

 }

 

 
프로젝트에서 자주 나오는 요구사항 중 하나가 엑셀저장이다.
이전 블로그내 포스트에서 언급한적이 있지만 내 경우엔 주로 POI를 통해 엑셀파일을 만들고 서블릿을 통해 다운로드 받을 수 있도록 하거나 스프링에서 제공하는 AbstractExcelView(POI 사용시) 또는 AbstractJExcelView(JExcel 사용시) 라는 View클래스를 상속받아 구현하면 되었다.
스트럿츠2에서도 다양한 ResultType 을 지원하고 있지만 excel이 기본 ResultType으로는 없어서 기냥 첫번째 경우로 다운받도록 했는데 마침 행복한 아빠 님의 블로그에서 jXLS를 이용하여 엑셀을 스트럿츠2의 result type으로 지정하는 방법을 설명하고 있는 것을 보고 잠시 소개해 볼까 한다.
 
프로젝트를 하다보면 데이터를 엑셀로 출력해 달라는 요구가 빈번히 발생한다.
간단하게 CSV 파일로 출력할 수도 있으며 Excel이 HTML 형태의 문서양식도 지원하기에
HTML로 작성하고 mine type만 살짝 바꿔주어 엑셀로 읽을 수 있도록 하는 방법이 있다.

물론 CSV로 요구사항을 충분히 만족시킬 수 있을 경우도 있으나 고품질의 엑셀 양식을 요구할 경우 위 두 방법은 웬지 부족함이 있다. 이럴 경우 엑셀 포맷으로 출력할 필요가 있는데 여러가지 솔루션이 있어 어렵지 않게
해결할 수 있는 부분이지만 매번 어떻게 처리할 지 고민을 한다. 여러가지 방법을 사용해 본 후 가장 나은 방법을 추천해 본다.

솔루션 찾기

Java로 엑셀을 다루기 위한 방법은 여러가지 있으며 대표적인 것 3가지만 살펴본다.
Java Excel API은 개발자가 엑셀 스프레드시트를 동적으로 읽고, 쓰고 수정할 수 있도록 하는 성숙한 오픈소스 java API이다. Java 개발자는 간단한 API를 이용하여 엑셀 스프레드시트를 읽고, 수정하고 쓸 수 있다.

장점은 이 패키지는 다른 패키지를 필요로 하지 않고 현재버전(2.6.9.1)의 jar 파일의 크기가 709KB로 부담없이 쓸 수 있다는 것이다. 비교적 간단한 AP를 제공하나 엑셀의 차트나 그래프 매크로 정보를 생성할 수는 없으며 시트에 PNG 이미지만 추가할 수 있다.

 
POI는 Microsoft의 OLE 2 컴포넌트 문서 포맷을 다루기 위한 프로젝트이다. 따라서 POI에는 엑셀 뿐만 아니라 워드문서를 다루는 API 도 제공하고 몇가지 모듈로 나누어져 있다.
이중 HSSF 가 엑셀 파일 포맷을 다루기 위한 자바구현체이다. 오래되었고 다른 것 보다 큰 이상을 가지고 출발한 프로젝트라 다양한 API를 제공한다.

apache 재단에서 진행되는 프로젝트이며 POI 패키지는 여러개의 다른 패키지(commons, log4j 같은..)를 필요로 한다. 풍부한 API를 제공하는 대신에 사용하기 번거로운 점이 있으며 많은 패키지를 필요로 한다는 부담이 있다.

jXLS은 엑셀파일 포맷의 템플릿을 이용하여 엑셀 파일을 손쉽게 생성하기 위한 패키지이다. 또한 XML 설정 파일을 통해 엑셀파일의 데이터를 Java 객체로 읽는 장치도 제공한다.

사실 jXLS은 Javarta POI 패키지를 기반으로 동작한다. 따라서 jXLS을 사용하기 위해서는 POI가 사용하는 많은 다른 패키지를 필요로 한다.

반면 jXLS 자체는 매우 작으며 복잡한 보고서 생성이나 일정한 양식의 엑셀 데이터를 규칙에 따라 읽게 한다는 뚜렷한 목적이 있어 범용성은 약간 떨어지더라도 대부분의 엑셀 관련 작업에 훌륭한 솔루션이 될 수 있다.

우리는 jXLS을 이용하여 위 문제를 풀어본다.

jXLS 맛보기

jXLS은 템플릿을 기반으로 최종 엑셀파일을 생성한다.
JSP나 Velocity 또는 Freemarker 같이 템플릿을 만들고 출력할 데이터를 템플릿을 이용하여 변환하면 템플릿 모양대로 최종결과물이 생성하는 구조다.
여기서 jXLS은 템플릿으로 엑셀파일을 그대로 쓰며 따라서 템플릿 작성이 매우 쉽다. 또한 엑셀 파일을 그대로 사용하므로 엑셀의 서식과 차트등 엑셀 파일의 대부분의 기능을 그대로 사용할 수 있다.
 


간단한 예제로 설명을 한다.
Java 객체
출력할 데이터를 만든다. 아래는 Customer 클래스를 예로 사용했지만 Map도 지원한다. 각 속성에 대한 getter, setter는 존재해야 한다.
public class Customer {
    private Long no;
    private String name;
    private String cellphone;
    private String email;
    public Long getNo() {
        return no;
    }
    ...
}


변환
출력한 데이터를 만든 후 엑셀템플릿과 출력할 데이터 (Java 빈)을 이용하여 변환한다.
        // 출력할 객체를 만든다.
        List<Customer> customers = new ArrayList<Customer>();
        Customer customer = new Customer();
        customer.setNo(1L);
        ...
        customers.add(customer);
        ...
        
        Map<String, Object> beans = new HashMap<String, Object>();
        beans.put("customers", customers);
        XLSTransformer transformer = new XLSTransformer();
        transformer.transformXLS("엑셀템플릿파일이름.xls", beans, "엑셀결과파일이름.xls");


엑셀템플릿
엑셀파일로 다음과 같이 작성한다. 중간에 ${..}로 들어갈 곳은 데이터가 치환되는 부분이다. 위의 경우 "엑셀템플릿파일이름.xls" 파일을 아래와 같이 생성한다.


이제 프로그램을 구동하면 자바객체를 이용하여 엑셀파일을 생성할 것이다.

태그 사용하기
좀 더 세밀한 제어를 위해 jXLS은 여러가지 태그를 제공한다. 위의 예제를 태그로 변경하면 다음과 같다.


jXLS의 자세한 사용법은 jXLS 홈페이지를 참조한다.


 
웹환경 실전에서

우리는 경우에 따라 고객목록을 HTML로 출력하거나 엑셀파일로 다운로드할 것이다. 즉 동일한 데이터가 경우에 따라 표현하는 방법 만 달리하는 경우에 해당한다.
이런 요구사항은 흔히 발생하고 이런 경우 MVC 모델을 응용한 아키텍처를 많이 사용한다.
MVC(Model View Controller) 모델은 많이 들어보았을 것이다. 여기서 우리는 Model과 View를 분리하는 작업을 할 것이다. 위의 경우 Model은 Customer 클래스에 해당하고 View는 엑셀파일에 해당할 것이다.



Struts2 Result 구현
위 구현을 위해 우리는 Struts2를 사용할 것이다. Struts2는 가장 많이 사용하는 프레임워크로 Struts 1에 비해 구조가 많이 개선되었다. 위의 목적을 달성하기 위해 우리는 데이터를 엑셀로 만드는 Result Type만 구현하여 Struts2 프레임워크에 붙여(플러그인)주기만 하면 된다.


Excel Result Type 만들기
Struts2에 새로운 Result Type을 만들기 위해서는 com.opensymphony.xwork2.Result를 구현하면 된다.

엑셀로 변환하기 위해 필요한 정보는 엑셀 템플릿, 엑셀에 출력할 객체들 그리고 다운로드받을 파일이름 정도일 것이다. 이것들을 이용하여 변환을 한다.

JXLSResult.java
아래는 본인이 사용하는 Struts2 엑셀 result tyup 클래스이다.
(아래 소스의 저작권은 넥스트리소프트에 있습니다. 참조하여 사용하는 것은 문제가 없으나 저작권은 반드시 명시하여 주시기 바랍니다.)

public class JXLSResult implements Result {
    /** 엑셀 템플릿 */
    private String template;
    /** 엑셀에 출력할 객체들 */
    private String beans;
    /** 파일이름을 얻어올 키값 */
    private String filenameKey = "filename";

    public void execute(ActionInvocation invocation) throws Exception {
        ActionContext actionContext = invocation.getInvocationContext();
        ServletContext context
            = (ServletContext) actionContext.get(StrutsStatics.SERVLET_CONTEXT);
        HttpServletResponse response
            = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE);
        // 출력할 bean들을 만든다.
        Map<String, Object> beanParams = new HashMap<String, Object>();
        String[] beanNames = splitBeans();
        for (String beanName : beanNames) {
            beanParams.put(beanName, invocation.getStack().findValue(beanName));
        }
        XLSTransformer transformer = new XLSTransformer();
        InputStream is = null;
        HSSFWorkbook workbook;
        String finalTemplate = TextParseUtil.translateVariables(this.template, invocation.getStack());
        try {
            is = readTemplate(finalTemplate, context);
            workbook = transformer.transformXLS(is, beanParams);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    // 무시
                }
            }
        }
        String filename = invocation.getStack().findString(filenameKey);
        if (filename == null)
            filename = "기본파일이름";
        writeWorkbook(filename, response, workbook);
    }
    /** 엑셀에 출력할 객체이름(key)들을 분리한다. */
    private String[] splitBeans() {
        return this.beans.split(",");
    }
    /** 엑셀 결과를 출력한다. */
    private void writeWorkbook(
        String filename, HttpServletResponse response, HSSFWorkbook workbook)
        throws IOException {
        response.setHeader(
            "Content-disposition", "attachment;filename=" + encodeFileName(filename + ".xls"));
        response.setContentType(FileServerUtil.getContentTypeOfFile("xls"));
        workbook.write(response.getOutputStream());
    }
    /** 파일이름 인코딩 */
    private String encodeFileName(String filename) {
        try {
            return URLEncoder.encode(filename, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e.getMessage, e);
        }
    }
    /** 엑셀 템플릿을 읽는다. */
    private InputStream readTemplate(
        String finalTemplate, ServletContext context) throws FileNotFoundException {
        String templateFilePath = context.getRealPath(finalTemplate);
        return new FileInputStream(templateFilePath);
    }
    /**
     * @param template 엑셀 템플릿
     */
    public void setTemplate(String template) {
        this.template = template;
    }
    /**
     * @param beans 엑셀에 출력할 객체들
     */
    public void setBeans(String beans) {
        this.beans = beans;
    }
    /**
     * @param filenameKey 파일이름을 얻어올 키값
     */
    public void setFilenameKey(String filenameKey) {
        this.filenameKey = filenameKey;
    }
}



Struts2에 Result Type 추가
struts.xml 에 구현한 result typ을 추가한다. 상세한 result type 구현 및 추가방법은 struts 사이트를 참조한다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
 ...
 <package name="my-default" extends="struts-default">
  <result-types>
   <result-type name="excel" class="com.nextree.fw.commonweb.struts.result.JXLSResult" />
  </result-types>
 ...
 </package>
</struts>



Controller 작성
Struts2의 Action을 controller로 사용하겠다. 아래와 같이 Action을 만든다. 참고로 struts2는 POJO를 그대로 Action으로 사용할 수 있다. 자세한 Struts2 Action 작성방법은 Struts2 site를 참조한다.
public class CustomerListController {
    /** 반환할 값 */
    private List<Customer> customers;

    public String execute() {
        ....
        this.customers = ....;   // 여기서 출력해야 할 데이터를 조회한다.
        return Action.SUCCESS;
    }
    /* 출력할 값의 getter 메소드를 반드시 제공한다. */
    public List<Customer> getCustomers() {
        return new JSONResult(this.zipCodeList);
    }
    /* 위 Excel Result type의 경우 file이름을 controller에서 가져오게 되어 있다. */
    public String getFilename() {
        return "고객목록";
    }
}


Action 정의
Struts2 의 struts.xml에 action을 정의한다. 우리는 excel로 다운로드하기에 앞에 설치한 excel result type을 사용한다.
 
...
<package name="mypackage" namespace="/mypackage" extends="my-default">
  <action name="customers" class="test.CustomerListController" method="execute">
   <result type="excel">
    <param name="template">/customer/CustomerList.xls</param>
    <param name="beans">customers</param>
    <param name="filenameKey">filename</param>
   </result>
  </action>
...

이제 http://hostname:port/context/mypackage/customers.do 로 접속하면 Excel 파일을 다운로드 할 것이다.

결론
위의 방법을 사용하면 그냥 JSP 작성하듯이 엑셀양식을 작성하여 엑셀을 다운로드하는 기능을 구현하기가 매우 쉽다.
엑셀을 그대로 사용하기 때문에 엑셀에 화려한 서식뿐만 아니라 차트나 수식등을 넣을 수도 있어 고품질의 엑셀파일을 만들 수 있다
 
 
 
 

ThreadLocal의 사용법과 활용

개발 이야기/Java | 2009. 4. 3. 13:54
Posted by 시반

예전에 스트럿츠2의 아키텍쳐를 공부하던 중에 "액션 컨텍스트가 메소드 호출에 파라미터로서 전달되지 않도록 쓰레드로컬을 구현하고 있으며 -java.lang.ThreadLocal은 자바 1.2에서 추가된 기능이다- 액션 컨텍스트는 각 쓰레드가 객체 자신의 인스턴스를 소유하는 Thread-specific storage를 제공하며 쓰레드 로컬값을 얻기위한 Access 는 쓰레드 로컬값이 어느 곳에서도 다뤄질 수 있도록 하기 위한 정적메소드이다."라고 했던 문구에서 자바1.2 버전부터 지원했다는 쓰레드 로컬이란 이름이 왜 그리 낯설기만 한지 당시엔 기냥 이름 그대로 쓰레드 단위로 로컬변수를 할당하고 사용한다는 의미로만 이해했다.(나중에 알고보니 틀린말은 아니었지만..자바 개발한지 나름 꽤 오래되었다고 생각했는데 용어자체가 낯설게 느껴진다는게 정말 난 날코더인가 하는 생각이 들었던 순간이었다...).

이후로 기냥저냥 코드를 보고 그냥 저냥 그렇게 쓰이는구나라구 넘어가고 그럭저럭 다른 사람에게 이야기할정도로 자주 사용하게 된 기능이지만 자바캔 블로그 에서 로컬쓰레드에 대해서 설명을 한 글이 있기에 올려본다.

(솔직히 능력이 된다면야 ThreadLocal 에 대해 직접 글을 올렸겠지만 올렸다 하더라도 madVirus님처럼 설명할 자신은 정말 없다. -_- .그래서 더더욱 슬프당.흐미~)

 

쓰레드 단위로 로컬 변수를 할당하는 기능은 ThreadLocal 클래스를 통해서 제공되는데

다음 글에서는 ThreadLocal 클래스의 기본적인 사용방법과 활용 방법을 살펴보도록 한다.

ThreadLocal이란?

일반 변수의 수명은 특정 코드 블록(예, 메서드 범위, for 블록 범위 등) 범위 내에서만 유효하다.

{
    int a = 10;
    ...
   // 블록 내에서 a 변수 사용 가능
}
// 변수 a는 위 코드 블록이 끝나면 더 이상 유효하지 않다. (즉, 수명을 다한다.)

반면에 ThreadLocal을 이용하면 쓰레드 영역에 변수를 설정할 수 있기 때문에, 특정 쓰레드가 실행하는 모든 코드에서 그 쓰레드에 설정된 변수 값을 사용할 수 있게 된다. 아래 그림은 쓰레드 로컬 변수가 어떻게 동작하는 지를 간단하게 보여주고 있다.


위 그림에서 주목할 점은 동일한 코드를 실행하는 데, 쓰레드1에서 실행할 경우 관련 값이 쓰레드1에 저장되고 쓰레드2에서 실행할 경우 쓰레드2에 저장된다는 점이다.


ThreadLocal의 기본 사용법

ThreadLocal의 사용방법은 너무 쉽다. 단지 다음의 네 가지만 해 주면 된다.
  1. ThreadLocal 객체를 생성한다.
  2. ThreadLocal.set() 메서드를 이용해서 현재 쓰레드의 로컬 변수에 값을 저장한다.
  3. ThreadLocal.get() 메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 읽어온다.
  4. ThreadLocal.remove() 메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 삭제한다.
아래 코드는 ThreadLocal의 기본적인 사용방법을 보여주고 있다.

// 현재 쓰레드와 관련된 로컬 변수를 하나 생성한다.
ThreadLocal<UserInfo> local = new ThreadLocal<UserInfo>();

// 로컬 변수에 값 할당
local.set(currentUser);

// 이후 실행되는 코드는 쓰레드 로컬 변수 값을 사용
UserInfo userInfo = local.get();

위 코드만으로는 ThreadLocal이 어떻게 동작하는 지 잘 이해가 되지 않을테니, 구체적인 예제를 이용해서 ThreadLocal의 동작 방식을 살펴보도록 하겠다. 먼저 ThreadLocal 타입의 static 필드를 갖는 클래스를 하나 작성해보자.

public class Context {
    public static ThreadLocal<Date> local = new ThreadLocal<Date>();
}

이제 Context 클래스를 사용해서 쓰레드 로컬 변수를 설정하고 사용하는 코드를 작성할 차례이다. 아래는 코드의 예이다.

class A {
    public void a() {
        Context.local.set(new Date());
       
        B b = new B();
        b.b();

        Context.local.remove();
    }
}

class B {
    public void b() {
        Date date = Context.local.get();

        C c = new C();
        c.c();
    }
}

class C {
    public void c() {
        Date date = Context.local.get();
    }
}

위 코드를 보면 A, B, C 세 개의 클래스가 존재하는데, A.a() 메서드를 호출하면 다음 그림과 같은 순서로 메서드가 실행된다.


위 그림에서 1~10은 모두 하나의 쓰레드에서 실행된다. ThreadLocal과 관련된 부분을 정리하면 다음과 같다.
  • 2 - A.a() 메서드에서 현재 쓰레드의 로컬 변수에 Date 객체를 저장한다.
  • 4 - B.b() 메서드에서 현재 쓰레드의 로컬 변수에 저장된 Date 객체를 읽어와 사용한다.
  • 6 - C.c() 메서드에서 현재 쓰레드의 로컬 변수에 저장된 Date 객체를 읽어와 사용한다.
  • 9 - A.a() 메서드에서 현재 쓰레드의 로컬 변수를 삭제한다.
위 코드에서 중요한 건 A.a()에서 생성한 Date 객체를 B.b() 메서드나 C.c() 메서드에 파라미터로 전달하지 않는다는 것이다. 즉, 파라미터로 객체를 전달하지 않아도 한 쓰레드로 실행되는 코드가 동일한 객체를 참조할 수 있게 된다.

ThreadLocal의 활용

ThreadLocal은 한 쓰레드에서 실행되는 코드가 동일한 객체를 사용할 수 있도록 해 주기 때문에 쓰레드와 관련된 코드에서 파라미터를 사용하지 않고 객체를 전파하기 위한 용도로 주로 사용되며, 주요 용도는 다음과 같다.

  • 사용자 인증정보 전파 - Spring Security에서는 ThreadLocal을 이용해서 사용자 인증 정보를 전파한다.
  • 트랜잭션 컨텍스트 전파 - 트랜잭션 매니저는 트랜잭션 컨텍스트를 전파하는 데 ThreadLocal을 사용한다.
  • 쓰레드에 안전해야 하는 데이터 보관

이 외에도 쓰레드 기준으로 동작해야 하는 기능을 구현할 때 ThreadLocal을 유용하게 사용할 수 있다.

ThreadLocal 사용시 주의 사항

쓰레드 풀 환경에서 ThreadLocal을 사용하는 경우 ThreadLocal 변수에 보관된 데이터의 사용이 끝나면 반드시 해당 데이터를 삭제해 주어야 한다. 그렇지 않을 경우 재사용되는 쓰레드가 올바르지 않은 데이터를 참조할 수 있다.

 

2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어

(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자

"학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

 그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다.

아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을

사용할 수 있게 되었다는 점입니다. 학습 곡선 일지 시리즈의 1편, 2편 및 3편은 컴파일러 기반 버전의

언어 사용 방법을 보여주기 위해 업데이트 되었습니다.

최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

4편은 시리즈의 2편에서 시작된 JavaFX 이미지 검색 애플리케이션을 완결하는 새로운 부분입니다.

학습 곡선 일지의 앞 부분에서는 JavaFX 스크립트를 사용하여 Flickr에서 이미지를 검색하는

기존 이미지 검색 애플리케이션의 사용자 인터페이스(UI)를 재현했습니다.

결과 JavaFX 스크립트는 원래 UI와 완전히 똑같진 않지만 상당히 근접했습니다.

그림 1은 JavaFX 스크립트 구현으로 생성된 기본 UI를 보여줍니다.

 

그림 1. JavaFX 이미지 검색 애플리케이션 UI

 UI 구축에는 계층 구조적인 Swing 기반 접근 방법을 따랐습니다.

이 시리즈의 1편에서 언급한 것처럼 JavaFX 개발자는 앞으로 노드 기반 접근 방법을 사용할 것입니다.

이제 애플리케이션의 JavaFX 스크립트 버전을 완료하고 사용자가 Flickr 웹 사이트에서 이미지를 검색, 나열 및

표시할 수 있도록 하겠습니다. 완성된 JavaFX 스크립트 애플리케이션을 위해 NetBeans 프로젝트 다운로드를 받을 수 있습니다.


이미지 검색

구 현할 첫 번째 동작은 이미지 검색입니다.

사용자가 UI의 검색 필드에 검색어를 입력하면 애플리케이션은 Flickr 웹 사이트에서 이미지 검색을 시작합니다.

검색어와 매칭되는 이미지가 있으면 애플리케이션은 최대 100개의 매칭되는 축소판 이미지의 목록을 로드하여

UI의 Matched Images 영역에 선택 가능한 목록으로 표시합니다. 또한 진행 표시줄이 이미지 검색을 추적합니다.

기 존 이미지 검색 애플리케이션은 Matched Images 영역에 반환된 축소판 이미지의 목록을 제목과 함께 표시합니다.

우리는 JavaFX 스크립트 애플리케이션 목록에서 매칭되는 이미지의 반환된 제목만 보여주도록 단순화하겠습니다.

JavaFX 스크립트의 ListItem 구성요소는 현재 icon 속성을 갖지 않습니다.

따라서 현재 ListListItem 구성요소가 생성하는 목록은 이미지를 포함할 수 없습니다.

이미지 검색을 위해 ImageSearcherPhoto의 두 가지 JavaFX 스크립트 클래스를 생성했습니다.

또한 진행 표시줄을 위한 별도의 클래스도 생성했습니다. JavaFX 스크립트 패키지에 ProgressBar 구성요소는 아직 없습니다.

임시 해결책으로 학습 곡선 일지 시리즈에서 구축한 이미지 검색 UI에 Swing JProgressBar 구성요소로부터

JavaFX 스크립트 진행 표시줄을 생성하는 createProgressBar 함수를 포함했습니다.

우리는 이 함수를 자체 파일의 자체 클래스로 이동하기로 결정했습니다.

이는 애플리케이션의 주요 부분을 깔끔하게 만드는 이점이 있습니다.

따라서 애플리케이션에는 이제 4개의 파일이 있습니다.

  • Main.fx: 애플리케이션의 주요 부분을 제공합니다. UI를 표시하고 이미지 검색 및 가져오기를 호출합니다.
  • ImageSearcher.fx: 이미지 검색을 수행합니다.
  • Photo.fx: Flickr 사진을 나타냅니다.
  • TempProgressBar.fx: 진행 표시줄을 생성합니다.

다음은 이미지 검색을 호출하는 Main.fx의 코드입니다.

  var searcher = ImageSearcher {
      callback: function(photos:Photo[]):Void {
          thumbnailList.items = for(photo in photos) {
              ListItem {
                  text: photo.title
                  value: photo
              }
          };

          matchedImagePB.indeterminate= false;
      }
  };

  var search = function():Void {
      System.out.println("searching... {searchTextField.text}");
      matchedImagePB.indeterminate = true;
      searcher.search(searchTextField.text);
  };

  searchTextField.action=search;

사용자가 UI의 검색 필드에 검색어를 입력하면 ImageSearcher 클래스 내의 search 함수를 호출하는 작업을

트리거하고 검색어를 search 함수로 전달합니다.

변수에 함수가 지정된 것에 유의하십시오.

  var search = function():Void {...}
 

JavaFX 스크립트에서 함수는 변수에 지정되거나 매개 변수로써 다른 함수에 전달될 수 있는 1급 개체(first-class object)입니다.

또한 함수는 matchedImagePB의 indeterminate 등록 정보를 true로 설정합니다. 

  var matchedImagePB = TempProgressBar { };
  matchedImagePB.indeterminate = true;

TempProgressBar 클래스는 matchedImagePB 변수에 지정됩니다.

따라서 matchedImagePB의 indeterminate 등록 정보를 설정하는 것은 실질적으로

TempProgressBar 개체의 indeterminate 등록 정보를 설정하는 것입니다.

그 영향을 이해하기 위해 TempProgressBar 클래스를 살펴보겠습니다.


TempProgressBar 클래스

다음은 TempProgressBar 클래스입니다.

 package javafxscriptimgsearch2;

  import javafx.ext.swing.*;

  public class TempProgressBar extends Component {
      protected function createJComponent():javax.swing.JComponent {
          return new javax.swing.JProgressBar();
      }
      public attribute indeterminate:Boolean = false on replace {
          var prog = this.getJComponent() as javax.swing.JProgressBar;
          prog.setIndeterminate(indeterminate);
      }
}
클래스에는 createJComponent 함수와 indeterminate 속성이 있습니다. 
replace
트리거가 indeterminate 속성에 연결되어 있습니다.
      public attribute indeterminate:Boolean = false on replace {
          var prog = this.getJComponent() as javax.swing.JProgressBar;
          prog.setIndeterminate(indeterminate);
      }
 

애플리케이션의 주요 부분이 속성을 true로 설정하는 것과 같이 속성 값이 변경되면 트리거는 진행 표시줄을 생성합니다.

특히 트리거는 getJComponent() 함수를 사용하여 JavaFX 스크립트 구성요소로 캡슐화된 Swing JProgressBar

구성요소를 생성합니다. 트리거는 또한 JProgressBar 구성요소의 indeterminate 등록 정보를

true로 설정하여 검색 진행 중에 진행 표시줄이 계속 움직이도록 합니다.

트 리거는 JavaFX 스크립트의 강력한 기능 중 하나입니다. 트리거는 특정 조건 충족 시 코드 블록을 실행하도록 합니다.

또한 기존 Swing 구성요소의 재사용이 얼마나 쉬운지에도 유의하십시오.

Swing 구성요소를 사용하려면 JavaFX 스크립트로 간단한 래퍼(wrapper)를 작성하기만 하면 됩니다.


ImageSearcher 클래스

다음은 ImageSearcher 클래스입니다.


   package javafxscriptimgsearch2;

import javax.xml.parsers.*;
import org.xml.sax.helpers.DefaultHandler;
import java.lang.System;
import java.lang.Thread;
import java.lang.Runnable;
import javax.swing.SwingUtilities;

public class ImageSearcher {
public attribute callback: function(photos:Photo[]):Void;

public function search(search:String) {
var thread = new Thread(Runnable {
public function run():Void {
var photos:Photo[];

var handler = DefaultHandler {
public function startDocument() { }
public function startElement(uri:String, localName:String, qName:String , attributes:org.xml.sax.Attributes ) {
if(qName == "photo") {
var photo = Photo {
id: attributes.getValue("id")
server: attributes.getValue("server")
farm: attributes.getValue("farm")
title: attributes.getValue("title")
secret: attributes.getValue("secret")
};
insert photo into photos;
}
}
public function endElement(uri:String , localName:String , qName:String ) { }
public function endDocument() { }

};

var SEARCH_URL = "http://api.flickr.com/services/rest/?" +
"method=flickr.photos.search";
var key = "339db1433e5f6f11f3ad54135e6c07a9";
var MAX_IMAGES = 100;
var searchUrl = "{SEARCH_URL}&api_key={key}&per_page={MAX_IMAGES}&text={search}";
var url = new java.net.URL(searchUrl);
var is = url.openStream();
var factory = SAXParserFactory.newInstance();
var saxParser = factory.newSAXParser();
saxParser.parse(is, handler);

SwingUtilities.invokeLater(Runnable {
public function run():Void {

if(callback != null) {
callback(photos);
}
}
});
}
});
thread.start();
}
}
 

ImageSearcher 클래스는 애플리케이션에서 이미지 검색을 수행하는 search 함수의 래퍼(wrapper)입니다.

새 스레드를 시작하여 search 함수가 시작됩니다. 스레드 내에서 함수는 검색어와 매칭되는 이미지(Flickr에서는 사진)를 가져오기

위해 Flickr 사진 검색 웹 서비스를 호출합니다. 다음은 Flickr 서비스를 호출하는 코드입니다. 

   var SEARCH_URL = "http://api.flickr.com/services/rest/?" +
          "method=flickr.photos.search";
  var key = "339db1433e5f6f11f3ad54135e6c07a9";
  var MAX_IMAGES = 100;
  var searchUrl = "{SEARCH_URL}&api_key={key}&per_page={MAX_IMAGES}&text={search}";
  var url = new java.net.URL(searchUrl);
  var is = url.openStream();

호출은 Flickr API의 REST(Representational State Transfer) 버전을 사용합니다

(Flickr는 XML-RPC 및 SOAP 버전도 제공합니다). API에 전달되는 인수에는 API 키, 반환되는 이미지의 최대 갯수,

사용자가 지정한 검색어가 포함됩니다. 애플리케이션에서 최대 100개의 매칭되는 이미지 목록을 다운로드하고 싶으므로

이미지 최대 갯수를 100으로 설정했습니다. 사진 검색 서비스는 사진을 XML 문서로 반환하므로 문서를 분석하는 기법이 필요합니다.

이 애플리케이션에서는 분석 수행을 위해 SAX DefaultHandler를 사용했습니다.

   var handler = DefaultHandler {
public function startDocument() { }
public function startElement(uri:String, localName:String, qName:String , attributes:org.xml.sax.Attributes ) {
if(qName == "photo") {
var photo = Photo {
id: attributes.getValue("id")
server: attributes.getValue("server")
farm: attributes.getValue("farm")
title: attributes.getValue("title")
secret: attributes.getValue("secret")
};
insert photo into photos;
}
}
public function endElement(uri:String , localName:String , qName:String ) { }
public function endDocument() { }

};

 

각 Flickr 사진에 대해 DefaultHandler사진 개체를 인스턴스화하고 속성 값을 Flicker 사진의 해당 속성 값으로 설정합니다.

DefaultHandler는 그 다음 각 Photo 개체를 photos라는 시퀀스에 추가합니다.

학습 곡선 일지 3편: JavaFX 스크립트 함수에서 시퀀스는 동일한 유형을 갖는 개체의 순서별 목록을 나타냈습니다.

search 함수는 그 다음 callback을 호출하는 다른 스레드를 시작합니다.

 SwingUtilities.invokeLater(Runnable {
          public function run():Void {

              if(callback != null) {
                  callback(photos);
              }
          }
  }
 

SwingUtilities.invokeLater 메소드는 스레드의 Runnable 작업을 이벤트 디스패치 스레드에 놓습니다.


Callback 함수

ImageSearcher 클래스는 속성 유형이 함수인 callback이라는 속성을 갖습니다. 함수는 photos 시퀀스를 매개 변수로

받고 아무 것도 반환하지 않습니다.

   public attribute callback: function(photos:Photo[]):Void;

ImageSearcher 클래스가 인스턴스화되면(애플리케이션의 주요 부분에 발생) callback 함수는 UI의 Matched Images

영역에 표시하기 위해 제목의 목록을 구축합니다. 목록의 각 항목은 속성 값이 반환된 사진의 제목인 text 속성과

속성 값이 Photo 개체인 value 속성을 갖습니다. 다음 코드는 애플리케이션의 주요 부분 내에서

 ImageSearcher 클래스를 인스턴스화하고 callback 함수를 제공합니다. 

  var searcher = ImageSearcher {
       callback: function(photos:Photo[]):Void {
          thumbnailList.items = for(photo in photos) {
              ListItem {
                  text: photo.title
                  value: photo
              }
          };
           matchedImagePB.Indeterminate= false;
      }
  };

매칭되는 사진 목록을 구축한 후 callback 함수는 Matched Images 진행 표시줄의 indeterminate 등록 정보를

기본값인 false로 다시 설정합니다.


이미지 검색 수행

이미지 검색을 다루는 코드를 검토했으니 이제 작업을 살펴봅시다. 그림 2는 사용자가 검색어를 입력한 후 Search 필드와

Matched Images 진행 표시줄의 상태를 보여줍니다.


그림 2.
검색어 입력

애플리케이션이 매칭되는 이미지를 검색하는 동안 검색어를 기반으로 검색 중임을 나타내는 메시지가 나타납니다.

이 예제에서는 "searching... polar bear" 메시지가 나타납니다.

그림 3은 매칭되는 이미지의 반환된 제목 목록 일부를 보여줍니다.


그림 3. 이미지 검색 결과
검색 함수가 올바르게 작동합니다!
 
이미지 표시

구현할 다음 동작은 이미지 표시입니다.

사용자가 반환된 이미지 제목 목록에서 제목을 선택하면 애플리케이션은 해당 이미지를 Flickr 사이트에서 가져와

UI의 Selected Image 영역에 표시해야 합니다.

표시를 위해 이미지를 가져오도록 onChange 위임을 추가하는 List 클래스의 사용자 정의 하위 클래스인

PhotoList라는 클래스를 추가했습니다. 다음은 Main.fx 파일에서 PhotoList 가 인스턴스화되는 방법을 보여줍니다.


   class PhotoList extends List {
       public
attribute onChange:function(photo:Photo);
       public
attribute selectedPhoto:ListItem = bind selectedItem on replace {
           var
photo = selectedPhoto.value as Photo;
           if
(onChange != null) {
               onChange(
photo);
           
}
       
}
   
}
 
   var
thumbnailList = PhotoList {
       preferredSize:[
300, 230]
       hmax:
Layout.UNLIMITED_SIZE
       vmax
: Layout.UNLIMITED_SIZE
   
};


사용자가 목록에서 항목을 선택하면 애플리케이션은 PhotoList 개체의 selectedPhoto 속성을 선택된 항목으로 설정합니다.

이는 PhotoList 개체의 selectedPhoto 속성이 다음 표현식의 결과에 바인딩되었기 때문입니다. 

  selectedItem on replace {
      var photo = selectedPhoto.value as Photo;
  }
 

바인딩은 표현식의 결과를 변수와 연관시키는 것을 의미합니다. 표현식이 변경되면(이 경우 selectedItem가 변경됨) 변수 값은

변경됩니다(이 경우 PhotoListselectedPhoto 속성이 자동으로 업데이트됨).

바인딩은 JavaFX 스크립트의 또 다른 강력한 기능입니다. 바인딩을 사용하여 애플리케이션의 부분을 직접적이고

우아하게 동기화할 수 있습니다.

또한 사용자가 목록에서 항목을 선택하면 애플리케이션은 PhotoList 개체의 onChange 속성과 연관된 이미지 로더 함수를 호출합니다.

이 함수는 선택된 진행 표시줄의 indeterminate 등록 정보를 true로 설정하여 이미지를 가져오는 동안 계속 진행되도록 합니다.

그 다음 선택된 Photo 개체에서 해당 이미지를 가져오기 위해 loadFullImage 함수를 호출합니다.

 class PhotoList extends List {
      public attribute onChange:function(photo:Photo);
      ...
          if(onChange != null) {
              onChange(photo);
       }

  // configure the image loader
  var imageLoader = function(photo:Photo):Void {
      if(photo != null) {
          selectedImagePB.indeterminate = true;
          photo.loadFullImage(function():Void{
              selectedImageDisplay.icon = Icon { image: photo.fullImage };
              selectedImagePB.indeterminate = false;
          });
      }
  };

thumbnailList.onChange = imageLoader;
 

이미지 로더는 Selected Image 영역에 표시하기 위해 이미지를 설정하고 이미지를 가져온 후

선택된 진행 표시줄의 indeterminate 등록 정보를 다시 false로 설정합니다. 

  selectedImageDisplay.icon = Icon { image: photo.fullImage };
  selectedImagePB.indeterminate = false;
 


Photo 클래스

Photo 클래스를 살펴봅시다.

 

    package javafxscriptimgsearch2;

import javafx.scene.image.*;
import java.lang.*;
import javax.swing.SwingUtilities;
import javax.imageio.ImageIO;

public class Photo {
public attribute id:String;
public attribute server:String;
public attribute farm:String;
public attribute title:String;
public attribute secret:String;

private attribute image:Image = null;

public attribute fullImage:Image = null;

public attribute fullImageURL = bind "http://static.flickr.com/{server}/{id}_{secret}.jpg";

public function loadFullImage(
callback:function():Void
):Void {

if(image == null) {
var thread = new Thread(Runnable {
public function run():Void {
var strImageUrl = "http://static.flickr.com/{server}/{id}_{secret}.jpg";
System.out.println("loading: {strImageUrl}");
var buffImg = ImageIO.read(new java.net.URL(strImageUrl));

SwingUtilities.invokeLater(Runnable {
public function run():Void {
image = Image.fromBufferedImage(buffImg);
fullImage = image;
if(callback != null) {
callback();
}
}
});

}});
thread.start();
} else {
callback();
}

}
}
 

Photo 클래스는 Flickr에 저장된 사진과 연관시키는 XML 속성에 해당하는 몇 가지 속성을 갖습니다.

예를 들어 Photo 클래스의 id 속성은 Flickr에 저장된 사진의 id 속성에 해당합니다. 또한 클래스는 Flickr에서 사진을 가져오는

loadFullImage 함수도 제공합니다.

새 스레드를 시작하여 loadFullImage 함수가 시작됩니다.

스레드 내에서 함수는 특정 사진에 대해 사진 소스 URL을 구성한 다음 사진을 가져옵니다.

다음은 사진 소스 URL을 구성하고 사진을 가져오는 코드입니다

   var strImageUrl = http://static.flickr.com/{server}/{id}_{secret}.jpg
   var buffImg = ImageIO.read(new java.net.URL(strImageUrl));

loadFullImage 함수가 URL 구성을 위해 Photo 개체의 server, idsecret 속성을 사용하는 것에 유의합니다.

또한 이미지 로드를 위해 자바 imagio 패키지의 ImageIO.read 메소드를 사용했습니다.

함수는 JavaFX 스크립트 Image 클래스의 fromBufferedImage 메소드를 사용하여 가져온 사진을 Photo 개체의

image 속성에 지정합니다. 그런 다음 호출자(이 경우 애플리케이션의 주요 부분에 있는 이미지 로더)에게의 callback

호출하는 다른 스레드를 시작합니다.

   SwingUtilities.invokeLater(Runnable {
       public function run():Void {
           image = Image.fromBufferedImage(buffImg);
           fullImage = image;
           if(callback != null) {
               callback();
           }
       }
   }); 

SwingUtilities.invokeLater 메소드는 스레드의 Runnable 작업을 이벤트 디스패치 스레드에 놓습니다.


목록에 이미지 표시

애플리케이션의 이미지 표시 부분을 테스트해봅시다. 그림 4는 Matched Images 영역에서 이미지를 선택한 후

Selected Image 진행 표시줄의 상태를 보여줍니다.


그림 4. 표시할 이미지 선택

애플리케이션이 이미지를 가져오면 이미지의 사진 소스 URL을 식별하는 메시지를 표시합니다.

이 예제에서는 "loading: http://static.flickr.com/3234/2595583764_a2e6661d3a.jpg" 메시지가 나타납니다.

이미지 가져오기가 끝나면 애플리케이션은 "loaded" 메시지를 표시합니다.

그림 5는 UI의 Selected Image에 선택한 이미지가 표시된 완전한 UI를 보여줍니다.


그림 5. 표시된 이미지

애플리케이션의 이미지 가져오기 부분이 작동합니다. 애플리케이션도 의도대로 작동합니다.

완성된 JavaFX 스크립트 애플리케이션을 위해 NetBeans 프로젝트 다운로드를 받을 수 있습니다.

 

요약

이 학습 곡선 일지 시리즈에서는 JavaFX 스크립트 프로그래밍 언어

(이 시리즈에서는 JavaFX 스크립트라고 줄여서 부름)의 사용을 시작하는데 도움이 되는 몇 가지 기본 개념 및 기법을 소개했습니다.

 

시리즈의 1편에서는 간단한 JavaFX 애플리케이션을 만드는 방법을 소개했습니다.

2편에서는 풍부한 UI 생성을 위해 사용 가능한 JavaFX 라이브러리 내의 일부 클래스와 언어의 선택적 구문을 설명했습니다.

UI 구축에는 계층 구조적인 Swing 기반 접근 방법을 따랐습니다.

이 시리즈의 1편에서 언급한 것처럼 앞으로 JavaFX 개발자는 노드 기반 접근 방법을 사용할 것입니다.

노드 기반 접근 방법에서는 UI 구축을 위해 다른 JavaFX 라이브러리를 사용할 것입니다.

3편에서는 뷰와 데이터 모델 등의 애플리케이션 부분을 동기화하기 위해 사용 가능한 JavaFX 스크립트의 바인딩 기능과

JavaFX 스크립트 함수를 다뤘습니다.

4편(본 기사)에서는 웹 서비스 액세스를 위한 JavaFX 스크립트 사용 방법을 보여주었습니다.

그 과정 중에 FX 스크립트에서 Swing 클래스와 같은 자바 기술 슬래스의 액세스가 얼마나 쉬운지도 보여주었습니다.

또한 이 시리즈를 통해 코드 완성과 같은 기능이 어떻게 NetBeans IDE 6.1에서 JavaFX 애플리케이션의 빌드 및 실행을

단순화하는지도 볼 수 있었습니다. JavaFX Preview SDK를 사용하여 명령줄에서 JavaFX 애플리케이션을

빌드 및 실행할 수도 있습니다.JavaFX 포함 NetBeans IDE 6.1을 다운로드하여 설치하거나

JavaFX Preview SDK를 다운로드하여 JavaFX 스크립트를 시작하십시오.


자세한 정보
이 글의 영문 원본은 Learning Curve Journal, Part 4: Accessing a Web Service에서 보실 수 있습니다.
 
2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어
(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자
"학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다.
아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을
사용할 수 있게 되었다는 점입니다. 이전의 학습 곡선 일지에서는 인터프리터 기반 버전 사용에 대해 설명했습니다.

업데이트된 학습 곡선 일지에서는 컴파일러 기반 버전의 언어 사용법을 보여줍니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

지난 학습 곡선 기사에서는 간단한 사용자 인터페이스(UI)를 구현하고 UI 시험이 성공적이었음을 확인했습니다.

결과 JavaFX 스크립트는 Flickr에서 이미지를 검색하는 이미지 검색 애플리케이션 구축을 위한

원래 자바 프로그래밍 언어 UI와 비슷하게 보였습니다. 그림 1은 결과적인 기본 프레임을 보여줍니다.

 


그림 1. 원래 UI를 복제한 JavaFX 이미지 검색 애플리케이션

선언적 JavaFX 스크립트 구문을 사용한 결과 코드는 자바 언어 UI의 이식을 위한 괜찮은 시작이었습니다.

그러나 유휴 상태의 프레임, 응답하지 않는 검색 필드, 비활성 진행 표시줄, 빈 목록 상자 및

빈 이미지 레이블에 대한 추가 작업이 필요합니다. 여기에서는 아무 일도 발생하지 않습니다.

현재까지는 활성 데이터모델에 아무 UI 요소도 연결되지 않았으며 사용자 상호작용에도 응답하지 않습니다.

예를 들어 Search 텍스트 필드는 입력한 문자를 받아서 보여주지만 아직 아무 것도 하지 않습니다.

이 골격 뿐인 UI에는 추가 작업이 필요합니다. 이를 위해 비활성 애플리케이션에서 필요한 작업을 수행하도록 하는 함수가 필요합니다.


함수

JavaFX 스크립트 함수는 자바 프로그래밍 언어 메소드와 비슷합니다.

이들 메소드와 마찬가지로 함수는 매개 변수와 반환 값을 갖습니다.

또한 속성 및 변수와 if-then, while 루프, for 루프 및 기타 조건문도 가질 수 있습니다. 다음은 몇 가지 유효한 함수의 예입니다. 

function z(a,b) {
      var x = a + b;
      var y = a - b;
      return sq(x) / sq (y);
 }

 function sq(n) {return n * n; }

 function main() {
      return z(5, 10);
  }

  function min(x1 : Number, x2 : Number ): Number {
      if (x1 < x2) {
        return x1;
      }else {
        return x2;
     }
 }



반환 값 유형이나 매개 변수 유형을 선언할 필요가 없습니다.

그러나 자바 언어 프로그래머인 저에게는 min 함수에서와 같이 유형을 사용하는 것이 익숙하므로

가능한 경우에는 항상 사용하곤 합니다. 매개 변수 유형을 선언하는 것은 명확성에 도움이 됩니다.

따라서 z 함수를 다음과 같이 다시 작성하겠습니다.


 

function z(a: Number, b: Number): Number {
   var x: Number = a + b;
   var y: Number = a - b;
   return sq(x) / sq (y);
}

return 키워드는 선택 사항입니다.

 function sq(n) {n*n;}

 

위 함수는 다음과 같습니다.

 function sq(n) {return n*n;}
함수는 function 키워드로 시작합니다. 그 뒤에는 함수 이름과 매개 변수 목록이 나옵니다. 

JavaFX 스크립트에서는 유형이 변수나 함수 이름 다음에 나옵니다.

예를 들면 b: Number 매개변수는 b 라는 인수의 유형이 Number라는 의미입니다.

마지막으로 함수는 Number를 반환하므로 이를 매개 변수 목록 뒤에 선언할 수 있습니다.

함수의 본문 앞뒤에는 자바 언어에서 메소드 본문을 둘러싸는 것과 같이 괄호가 있습니다.

함수는 매개 변수나 기타 참조 변수가 변경될 때마다 반환 값을 재평가합니다.

이 기능은 개체를 자주 변경될 수 있는 특정 값에 바인드하려는 경우 유용합니다.

바인딩에 대해서는 나중에 자세히 설명하겠습니다.

클래스에 대해 함수가 정의될 수도 있습니다. 다음은 Friends 클래스에 대해 정의된 함수의 예입니다.

import java.lang.System;
  import javafx.lang.Sequences;

  class Friends {
      attribute knownNames: String[];
      function sayHello(name: String): String {
          var index = Sequences.indexOf(knownNames,name);
          if (index >= 0) {
              return "Hello, {name}!";
              } else {
              return "Sorry, I can't talk to strangers.";
              }
      }
  }

  var buddies = Friends {
      knownNames: ["John", "Robyn", "Jack", "Nick", "Matthew",
      "Tressa", "Ruby"]
  };

  var greeting = buddies.sayHello("John");
  System.out.println(greeting);

이 작은 프로그램은 sayHello 메소드에 아는 이름을 입력하면 "Hello"라고 응답합니다.

그렇지 않은 경우엔 "can't talk to strangers"라고 합니다.

Friends 클래스에는 한 가지 속성과 속성을 사용하여 메시지를 반환하는 함수가 포함되어 있습니다.

Sequences 클래스를 사용하는 것에 유의합니다. 이 클래스는 시퀀스 조작을 위한 다양한 함수를 포함합니다.

시퀀스는 이 예제에서 아는 이름의 목록과 같이 개체의 순서별 목록을 나타냅니다.

SequencesindexOf 함수는 지정된 시퀀스에서 같은 값을 갖는 개체를 검색합니다.

여기에서 indexOf는 아는 이름 시퀀스에서 지정된 이름(이 예제에서는 "John")과 매칭되는 개체를 검색합니다.


반응적 UI 요소

UI 요소(JavaFX 스크립트 라이브러리에서는 노드)는 키 입력이나 마우스 클릭과 같은 사용자 상호작용에 응답할 수 있습니다.

위젯은 action, onMouseClicked, onKeyTyped 및 기타 이벤트 기반 속성을 갖습니다. 이들 속성과 함수를 연관시킬 수 있습니다.

예를 들어 함수를 TextFieldaction 속성과 연관시키면 해당 함수는 필드 내에서 Enter를 누를 때 실행됩니다.

Button 위젯의 동일한 action 속성은 사용자가 클릭할 때마다 활성화됩니다.

JavaFX 이미지 검색 애플리케이션과 관련된 함수가 필요하므로 UI 요소에 대한 이벤트 핸들러를 작성하기로 했습니다.

다음 애플리케이션은 두 개의 버튼과 하나의 레이블을 생성합니다.

Bigger 버튼을 누르면 레이블의 글꼴 크기가 증가하고 텍스트가 변경됩니다.

Smaller 버튼을 누르면 레이블의 글꼴 크기가 감소하고 텍스트가 변경됩니다.

이 애플리케이션에서는 계층 구조적인 Swing 기반 접근 방법을 따릅니다.

이 시리즈의 1편에서 언급한 것처럼 앞으로는 JavaFX 스크립트 개발자가 노드 기반 접근 방법을 사용하도록 할 것입니다.

 

 import javafx.ext.swing.SwingFrame;
  import javafx.ext.swing.BorderPanel;
  import javafx.ext.swing.FlowPanel;
  import javafx.ext.swing.Button;
  import javafx.scene.Font;
  import javafx.scene.HorizontalAlignment;
  import javafx.ext.swing.Label;

  var font = Font { size: 18 };

  class FontDataModel {
      attribute text: String;

      function increaseFontSize() {
         font = Font { size: font.size + 1 };
         text= "Font Test ({font.size})";
         }
      function decreaseFontSize() {
         font = Font { size: font.size - 1 };
         text= "Font Test ({font.size})";
      }
  }

  SwingFrame {
       var myFont = FontDataModel {
           text: "Font Test (18)"
        }

      content:
      BorderPanel {
          top: FlowPanel {
              alignment: HorizontalAlignment.LEADING
              content: [
                  Button { text:"Bigger"
                      action :
                      function() {
                           myFont.increaseFontSize();
                      }
              },

              Button { text:"Smaller"
                  action : function() {
                      myFont.decreaseFontSize();
                  }
              }

              ]
          }
          center:
              Label {
                  width: 200
                  font: bind font
                  text: bind myFont.text

          }

       }
       visible: true
  }

 

 

 이 코드를 잘라내어 JavaFX 애플리케이션에 바로 붙여넣을 수 있습니다. Preview 버튼을 사용하면 그림 2의 결과가 보입니다.


그림 2. 미리보기 기능을 사용하여 JavaFX 스크립트 언어를 대화형으로 시험

이 Bigger-Smaller 글꼴 애플리케이션은 텍스트 문자열 속성을 갖는 FontDataModel을 생성합니다.

애플리케이션은 FontDataModel 인스턴스를 생성하고 text 속성을 초기화합니다.

FontDataModel에도 increaseFontSizedecreaseFontSize라는 두 가지 함수가 있습니다.

이들 함수는 텍스트 속성을 변경하고 Font 클래스의 인스턴스를 생성하며 인스턴스의 글꼴 크기를 업데이트합니다.

왜 글꼴을 FontDataModel의 속성으로 지정하고 텍스트 속성과 같은 방법으로 함수에서 업데이트하지 않는지 궁금할 지도 모릅니다.

이러한 접근 방법은 Font가 변경할 수 없는 개체, 즉 원래 개체의 속성을 변경할 수 없기 때문에 사용할 수 없습니다.

개체의 인스턴스에서만 속성을 변경할 수 있습니다.

각 버튼은 연관된 함수를 포함하는 action 속성을 갖습니다.

예를 들어 Bigger 레이블이 있는 버튼은 myFont 변수의 increaseFontSize 함수를 호출합니다.

 Button { text:"Bigger"

       action :
      function() {
           myFont.increaseFontSize();
      }
   
}
 

버튼을 누를 때마다 글꼴 크기와 텍스트가 변경되는 것을 볼 수 있습니다.

그림 3은 Bigger 버튼을 두 번 눌렀을 때의 결과를 보여줍니다.


그림 3. 버튼을 클릭하면 텍스트와 글꼴 크기 변경

분명히 increaseFontSize 메소드는 글꼴과 텍스트를 변경합니다. 이를 위해 바인딩이라는 JavaFX 스크립트 기능을 사용했습니다.


뷰와 모델 바인딩
 

JavaFX 스크립트에는 하나의 속성이 다른 속성의 변경을 추적하도록 하는 bind 연산자가 있습니다.

하나의 속성을 다른 속성에 바인딩한다는 것은 바인딩된 속성이 대상 속성의 변경 사항을 항상 인지한다는 것을 의미합니다.

이 Bigger-Smaller 글꼴 애플리케이션에서는 Label이 글꼴과 텍스트의 변경 사항을 추적하도록 하려고 합니다.

이를 위해 Labelfont 속성을 Font 변수에 바인드하도록 bind 연산자를 사용했습니다.

또한 Labeltext 속성을 FontDataModeltext 속성에 바인드하도록 bind 연산자를 사용했습니다.

다음은 Label 선언입니다.

  Label {
      width: 200
      font: bind font
      text: bind myFont.text

  }
 

bind 연산자는 함수에도 사용할 수 있습니다. 함수는 인수나 참조 변수가 변경될 때마다 결과를 업데이트하므로

함수에의 바인딩은 단일 속성에의 바인딩과 마찬가지로 작용합니다.

사실 함수는 실제로 바인딩과 함께 사용하도록 설계되었습니다.

본문 내의 매개 변수 및 참조 변수를 모두 포함하는 모든 종속성을 자동으로 추적하는

재사용 가능한 하위 루틴으로의 바인딩을 리팩터링하기 위해 이들을 사용할 수 있습니다.

표 1에서 코드의 일부를 참조하십시오.

 

표1. 함수와 함께/함수 없이 바인드 사용
 함수 없이 바인드  함수와 함께 바인드
 import java.lang.System;

   class Data {

       attribute foo: Number;
       attribute baz: Number;
   }

   var data = Data {
       foo: 4
       baz: 7
   };

   var zoo = bind data.foo +
           data.baz + 10;
   System.out.println(zoo);
   data.baz = 12;
   System.out.println(zoo);
 import java.lang.System;

   class Data {
       attribute foo: Number;
       attribute baz: Number;
       function add(x, y, z): Number {
           return x+y+z;
       }
   }

   var data = Data {
       foo: 4
       baz: 7
   };

   var zoo = bind data.add(data.foo, data.baz, 10);
   System.out.println(zoo);
   data.baz = 12;
   System.out.println(zoo);
 출력:  출력:
21.0
26.0
21.0
26.0
 
요약

JavaFX 애플리케이션에 동작을 추가하기 위해 함수를 사용합니다. UI 구성요소의 속성은 함수에 매핑됩니다.

action, onMouseClickedonKeyTyped와 같은 UI 이벤트 처리를 위해 이들 함수를 정의할 수 있습니다.

함수에는 함수 본문 내에서 매개 변수 및 참조 변수를 재평가하는 추가적인 등록 정보가 있습니다.

bind 연산자를 사용하여 하나의 속성을 다른 속성에 연결할 수 있습니다.

이는 UI 위젯이 모델 속성을 추적하도록 할 때 특히 유용합니다.

뷰 속성을 모델 속성에 바인딩한다는 것은 모델과 뷰가 동일한 데이터로 항상 동기화됨을 의미합니다.

JavaFX 이미지 검색 애플리케이션의 UI에 아직 기능을 추가하지 않았지만 함수가 필요하다는 것을 알게 되었습니다.

검색 텍스트를 가져오기 위해서는 거의 확실히 함수를 사용해야 합니다.

같은 함수가 이미지를 가져오기 위해 Flickr 사이트에도 액세스할 것입니다.

또한 뷰를 기본 모델에 연결하기 위해 bind 연산자도 사용해야 할 것입니다.

이제 UI를 일부 기본 작업 및 함수에 연결하는 방법을 알게 되었습니다.

또한 UI를 기본 모델에 연결하기 위해 bind 연산자도 사용할 수 있습니다.

 

자세한 정보
이 글의 영문 원본은 Learning Curve Journal, Part 3: JavaFX Script Functions에서 보실 수 있습니다.
 
블로그 이미지

시반

시반(詩伴)이란 함께 시를 짓는 벗이란 뜻을 가지고 있습니다. 함께 나눌수 있는 그런 공간이길 바라며...

카테고리

분류 전체보기 (233)
개발 이야기 (73)
Java (22)
VoIP (19)
이클립스 (22)
ORM (6)
MINA (4)
WEB2.0 (57)
DB2 (24)
MySQL (6)
오라클 (26)
기타 (44)
취미 (0)
잡담 (2)