Struts 2는 2007년 2월 초에 시작되었으며 흥미로운 기능을 많이 갖고 있었다. 이 기사에서는 최근에 Struts 2 프레임워크에 추가된 기능 중 하나인 OGNL(Object-Graph Navigation Language)을 자세히 살펴본다. 이 언어는 새로운 형식의 표현식 언어로, 우수한 콜렉션, 오브젝트 그래프 순회 및 인덱싱 기능을 지원한다.
이 기사에서는 계층 구조를 기반으로 하는 웹 페이지를 정의하고 Struts 2 OGNL 프레임워크 코드를 최소한으로 사용하여 데이터를 추출하는 방법을 살펴본다. 또한, 이러한 구문을 사용하여 웹에서 중요한 비즈니스 개념을 구현하는 방법을 검토한다. 더불어 초기 프레임워크(Struts 1.x)에서는 이러한 페이지에서 데이터를 추출하는 작업을 어떻게 처리했는지 그리고 Struts 2의 OGNL 프레임워크로 마이그레이션하는 것이 개발 노력을 최소화하는 데 얼마나 도움이 되는지를 설명한다. 또한, 이와 관련하여 OGNL과 Struts 2의 기본사항을 다루고 OGNL과 Struts 2를 활용하여, 계층 구조 페이지를 처리할 경량 방법론을 개발하는 방법을 설명할 것이다. 또한, 코드가 적은 환경으로 마이그레이션하는 데 도움이 되는, Struts 1.x 및 Struts 2의 기능 비교 결과를 설명할 것이다.
웹 페이지에서 다중 레벨 계층 구조 데이터를 표시하고 데이터를 업데이트하는 시나리오를 생각해 보자. 투자 고문은 클라이언트에게 보고할 보고서를 준비하고 있다. 스크린샷에는 클라이언트의 투자 포트폴리오 데이터를 업데이트하는 화면이 표시되어 있다. 데이터 항목 필드는 포트폴리오의 최상위 레벨 속성이다. 보라색 행은 포트폴리오 아래에 있는 1레벨 데이터 항목이며 Target Investment Amount 및 Target Profit% 필드는 각 1레벨 항목의 속성이다. 마찬가지로 녹색 행은 2레벨 항목이며 이 항목 아래에 있는 필드는 2레벨 항목의 속성이다. 마지막, 노란색 행은 3레벨 항목이다. 이 기사에서는 모든 필드를 업데이트할 수 있으며, 사용자가 해당 페이지를 제출하면 백엔드에 있는 데이터가 적절하게 업데이트된다고 가정한다. 또한, 1레벨, 2레벨, 3레벨 행의 수는 사실상 동적으로 변한다는 점에 유의한다.
그림 1. 묘사된 그림
Struts 1.x의 문제점을 해결하기 위한 현재 구현
목록 1에는 위에 있는 페이지의 데이터 모델이 표시되어 있다.
목록 1. 초기 데이터 구조
public class ClientPortfolioDetailsBean
{
//Domain model for the client portfolio details
//Portfolio level attributes
private String investorName = null;
private String emailId = null;
private String contactNumber = null;
//..... Other portfolio level attributes
//List to hold the details of Stocks / Mutual Funds etc investment category
private List <InvestmentCategoryDetailsBean> invCategoryList = null;
}
public class InvestmentCategoryDetailsBean {
//Bean to hold the information about investment category details
//like Stocks, Mutual Funds etc. Represents the first level rows
private String categoryName = null;
private double targetInvstAmt = 0.0;
private double expectedAnnualReturn = 0.0;
//Other attributes follows
//List of stocks and fund details bean to hold the
//second level items
private List <StocksAndFundDetailsBean> itemList = null;
}
public class StocksAndFundDetailsBean {
//Bean to hold the information about investment category details
//like Stocks, Mutual Funds etc. Represents the second level rows
//Attributes at that level
private String stockFundCategoryName = null;
private double targetInvstAmt = 0.0;
private double targetProfit =0.0;
//Other attribute follows
//List to hold the stock details
private List <StocksDetailsBean> stockDetailsList = null;
}
public class StocksDetailsBean {
//Represents the third level item details
//Details of individual stock/fund details
private String stockFundName = null;
private double noOfStocks = 0.0;
private double buyingPrice =0.0;
private Date buyingDate = null;
}
|
Struts 1.x에서는 첫 번째 사례 연구에 대한 솔루션에서 단조로운 단일 레벨 방식으로 양식 빈을 정의해야 한다. 사용자가 입력한 값을 업데이트한 값을 얻기 위해 목록 2에서는 필수 양식 빈 구조를 제공한다.
목록 2. Struts 1.x 양식 빈의 구조
public class PortfolioDetailsBean extends ActionForm
{
//Top level attributes
private String investorName = null;
private String emailId = null;
//........... Other Top level attributes
//Fields to capture Attributes of First level rows
private double[] firstLevelTargetAmt = null;
private double[] firstLevelExpectedAnnlReturn = null;
//......... Other First Level row attributes
//Fields to capture Attributes of Second level rows
private double[] secondLevelTargetAmt = null;
private double[] secondLevelTargetInvPeriod = null;
//......... Other Second level row attributes
//Fields to capture Attributes of Second level rows
private double[] thirdLevelNoOfStocks = null;
private double[] thirdLevelByingPrice = null;
//......... Other Third level row attributes
}
|
이러한 단일 레벨 양식 빈 요소를 목록 1에 있는 다중 레벨 도메인 모델로 변환하려면 Action 클래스에 목록 3과 같은 코드가 추가로 필요하다. 여기에 적용된 논리는 도메인 모델의 구조에 의존하며 속성의 레벨과 수가 증가함에 따라 Action 클래스에서 변환의 복잡도가 증가한다는 점에 유의한다.
목록 3. 단일 레벨 양식 빈 요소를 다중 레벨 도메인 모델로 변환하는 데 필요한 사용자 정의된 코드
public class UpdatePortfolioAction extends Action
{
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
ActionErrors errors = new ActionErrors();
ActionForward forward = new ActionForward(); // return value
PortfolioDetailsBean portfolioDetailsBean = (PortfolioDetailsBean) form;
try {
//Counter to track index of 2nd level index
int counter2ndLevelIndex= 0;
//Converting the form bean to the domain model
ClientPortfolioDetailsBean clientBean = new ClientPortfolioDetailsBean();
//Setting the top level attributes
clientBean.setEmailId(portfolioDetailsBean.getEmailId());
//.........................................
//Similarly other top level fields are created
//Get the size of first level row sizes from session
int sizeInvestmentCategory = getInvestmentCategorySize
(request.getSession());
clientBean.setInvCategoryList(new ArrayList
<InvestmentCategoryDetailsBean>(sizeInvestmentCategory));
for(int firstLevelIndex=0;firstLevelIndex<
sizeInvestmentCategory;firstLevelIndex++)
{
//Create the updated first level bean
InvestmentCategoryDetailsBean invCatBean =
new InvestmentCategoryDetailsBean();
//Set the investment category Bean in the parent bean
clientBean.getInvCategoryList().add(invCatBean);
//set the updated properties
invCatBean.setExpectedAnnualReturn(
portfolioDetailsBean.getFirstLevelExpectedAnnlReturn()[firstLevelIndex]);
//.........................................
//Set the other properties at first level
//Get the size of 2nd level row sizes from session
int sizeStockAndFundDetails = getStockAndFundDetailsSize
(request.getSession(),firstLevelIndex);
invCatBean.setItemList(
new ArrayList<StocksAndFundDetailsBean>(sizeStockAndFundDetails));
//Fill Up the second level objects
for(int secondLevelIndex=0;secondLevelIndex<
sizeStockAndFundDetails;secondLevelIndex++)
{
StocksAndFundDetailsBean stkFundDetails =
new StocksAndFundDetailsBean();
//Set the object to the parent Bean
invCatBean.getItemList().add(stkFundDetails);
//Set the individual attributes
stkFundDetails.setTargetInvstAmt(
portfolioDetailsBean.getSecondLevelTargetAmt()[counter2ndLevelIndex++]);
//............... Other fields follows
// Setting up of 3rd level list follows here
//......................................
}
}
} catch (Exception e) {
// Report the error using the appropriate name and ID.
}
}
|
번잡한 웹 애플리케이션 개발 태스크 중 하나는 데이터를 양식 빈에서 데이터 빈으로 전송하는 일이다. 문자열을 Java 유형으로 변환하거나 Java 유형을 문자열로 변환하면 복잡도가 증가한다. 문자열 값을 구문 분석하여 double형과 integer형으로 저장해야 할 뿐만 아니라 잘못된 데이터로 인해 발생할 수 있는 예외를 해결해야 한다.
데이터 전송과 유형 변환은 요청 처리 주기의 양단에서 발생한다. 결과가 렌더링되면 데이터가 다시 Java 유형에서 문자열 형식으로 재변환된다. 이러한 프로세스는 해당 도메인의 핵심 부분으로, 웹 애플리케이션의 거의 모든 요청에서 발생한다. 동적 레코드 세트가 있는 동적 양식을 지원하려면 복잡한 코드와 논리가 필요하다. 이러한 논리 코드를 개발하고 유지보수하는 작업은 복잡하다.
데이터 전송과 유형 변환을 자동화하는 기능은 Struts 2의 가장 강력한 기능 중 하나이다. Struts 2 프레임워크를 OGNL과 함께 사용하면 데이터를 List 및 Map 등과 같은 더 복잡한 Java 측 유형으로 전송할 수 있다. 또한, 사용자 정의 변환기를 개발하여 유형 변환 메커니즘을 확장할 수 있으며 사용자 정의된 유형을 포함한 모든 데이터 유형을 처리할 수 있다.
OGNL은 Struts 2 프레임워크 문자열 기반 HTTP 입출력과 Java 기반 내부 처리 간의 인터페이스이다. OGNL에 익숙한 개발자라면 OGNL을 사용하여 효율성을 충분히 개선하면서 유지보수 노력을 줄일 수 있을 것이다.
그림 2. OGNL 스택
개발자의 관점에서 보면 OGNL은 다음과 같은 두 개의 컴포넌트로 구성된다.
- 표현식 언어 — 보통 양식 입력 필드 이름과 JSP 태그에서 사용된다. OGNL 표현식은 Java 측 데이터 특성을 텍스트 기반 뷰 계층의 문자열에 바인드하기 위해 사용된다.
- 유형 변환기 — 데이터 유형 변환을 담당한다. 데이터가 Java 환경으로 이동하거나 Java 환경에서 데이터가 이동할 때마다 HTML 안에 있는 데이터의 문자열 버전과 적절한 Java 데이터 유형 간에 변환이 발생해야 한다. Struts 2 프레임워크는 생각하는 것보다 훨씬 더 많은 것을 처리할 수 있는 내장 변환기를 제공하지만, 원하는 경우에는 개발자가 사용자 정의 변환기를 작성할 수도 있다.
이 프레임워크에서는 데이터를 전송하고 요청 매개변수의 데이터를 변환하는 작업이 자동으로 수행되는데, 그렇다면 데이터는 어디로 이동하며 OGNL은 어떻게
대상을 파악할까? Struts 2의 ValueStack은 단일 가상 오브젝트의 특성으로서 오브젝트의 특성을 모으는 구문이다. 특성이 중복된 경우(employeeId 특성을
갖고 있는 경우)에는 스택에서 가장 상위에 있는 오브젝트의 특성이 사용된다. Struts 2 프레임워크에는 HTTP 원시 문자열과 다음과 같은 Java 유형 목록 간의
변환을 지원하는 기능이 내장되어 있다.
- Array
- boolean/Boolean
- Char/Character
- int/Integer, float/Float, long/Long, double/Double
- Date
- String
- Map
- List
Struts 2 OGNL 표현식을 이용하면 단일 레벨 양식 빈을 다중 레벨 도메인 오브젝트로 변환하기 위해 복잡한 코드를 작성해야 하는 문제점을
줄일 수 있다. Struts 2에서는 HTML 입력 필드 이름을 OGNL 표현식으로 생성할 수 있으며 이렇게 하는 것이 Action 클래스의 복잡한 코드를 제거하는 데
도움이 된다. 예를 들어, 텍스트 상자와 같은 HTML 입력 필드 이름을 clientPortfolioDetailsBean.investorName으로 설정하면 Action 클래스의
getClientPortfolioDetailsBean()이 호출된다. 이 함수는 ClientPortfolioDetailsBean을 리턴하고 이어서 해당 화면의 사용자가 입력한 값을 사용하여
setInvestorName() 메소드를 호출한다. 사용자가 업데이트한 HTML 입력 필드의 이름을 지능적으로 지정하면 Action 클래스에서 복잡한 코드를 작성하지
않아도 된다. 도메인 모델은 Action 클래스 내부에 적절하게 배치된다.
Action 클래스에 get 메소드와 함께 속성 유형 ClientPortfolioDetailsBean을 추가해야 한다. 현재의 Action 클래스 인스턴스에 get 메소드의 인스턴스가
설정되어 있지 않은 경우에는 get 메소드 내부의 세션에서 bean이 리턴되어야 한다. 페이지가 로드되는 동안 clientDetailsBean 인스턴스가 세션 오브젝트에서
유지되어야 한다. 오브젝트가 더 큰 경우에는 getClientDetailsFromSession() 메소드를 데이터베이스에서 데이터를 페치하는 getClientDetailsFromDB()로 대체할 수 있다.
목록 4. 사례 1의 코드 스니펫
public class ClientPortfolioAction extends ActionSupport {
private ClientPortfolioDetailsBean clientDetailsBean = null;
//Accessor for the clientDetailsBean.
public ClientPortfolioDetailsBean getClientDetailsBean() {
if(clientDetailsBean==null)
{
//If the bean is null for this action class instance
//load the data from the session
clientDetailsBean = getClientDetailsFromSession();
}
return clientDetailsBean;
}
}
|
목록 5. 사례 2의 코드 스니펫
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:textfield name="clientDetailsBean.investorName"
value="%{clientDetailsBean.investorName}"/%>
<s:textfield name="clientDetailsBean.emailId"
value="%{clientDetailsBean.emailId}"/%>
<s:textfield name="clientDetailsBean.contactNumber"
value="%{clientDetailsBean.contactNumber}"/%>
<s:iterator value="clientDetailsBean.invCategoryList"
var="invCatDetails" status="firstLevelIdx"%>
<s:property value="%{#invCatDetails.categoryName}"/%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.targetInvstAmt"
value="%{invCatDetails.targetInvstAmt}"/%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.expectedAnnualReturn"
value="{invCatDetails.expectedAnnualReturn}"/%>
<s:iterator value="#invCatDetails.itemList"
var="stkFundDetails" status="secondLevelIndx"%>
<s:property value="%{#stkFundDetails.
stockFundCategoryName}"/%>
<s:textfield name="clientDetailsBean
[%{#firstLevelIdx.index}].itemList[%{secondLevelIndx.index}].
targetInvstAmt" value="%{#stkFundDetails.targetInvstAmt}" /%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].
targetProfit" value="%{#stkFundDetails.targetProfit}" /%>
<s:iterator value="#stkFundDetails.stockDetailsList"
var="stkDetails" status="thirdLevelIndx"%>
<s:property value="%{stkDetails.stockFundName}" /%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%{thirdLevelIndx.index}].noOfStocks" value="%{#stkDetails.noOfStocks}" /%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%{thirdLevelIndx.index}].buyingPrice" value="%{#stkDetails.buyingPrice}"/%>
</s:iterator>
</s:iterator>
</s:iterator>
|
이제 유효한 OGNL 표현식에 따라 입력 텍스트 상자의 이름을 생성하는 JSP 페이지를 생각해 보고 "ABC Incorporated"의 "no of Stocks field"를 OGNL 표현식으로 업데이트하는 방법을 살펴보자.
<s:textfield
name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%thirdLevelIndx.index}].noOfStocks}'/> |
이 행은 다음과 같은 HTML 코드로 변환된다.
<input type="text"
name="clientDetailsBean[0].itemList[0].stockDetailsList[0].noOfStocks"
value="<somevalue>" /> |
OGNL 표현식 clientDetailsBean[0].itemList[0].stockDetails[0].noOfStocks는 Action 클래스 인스턴스에서
getClientDetailsBean().getItemList().get(0).getStockDetailList(0).get(0).setNoOfStocks(<some value>)를 호출하는 것과 동일한 역할을 한다. 마찬가지로
해당 도메인 모델에서 다른 필드를 적절하게 업데이트한다. Action 메소드에서는 getClientDetailsBean()이 호출되면 Struts 2 프레임워크에 의해
모든 필드가 적절하게 업데이트된다. 이렇게 하면 Struts 1 예제에서 살펴본 것처럼 양식 빈을 관련 도메인 모델 오브젝트로 변환하기 위해
Action 클래스에서 복잡한 코드 작업을 하지 않아도 된다.
Struts 2 프레임워크의 이점은 다음과 같다.
- OGNL의 표현식 언어를 이용하면 양식 필드를 Java 측 특성에 맵핑할 수 있다.
- OGNL의 유형 변환기는 요청 매개변수의 문자열 형식 데이터를 실제 Java 유형의 특성으로 자동으로 변환한다.
- 뷰를 렌더링하는 과정에서 다시 표현식 언어와 유형 변환기가 자동으로 Java 유형을 문자열 값으로 변환한다.
- 데이터를 콜렉션과 배열로 변환하거나 그 반대 작업을 수행할 수 있는 유연하고 다양한 변환 세트를 지원한다.
- 복잡한 웹 사이트를 손쉽게 빌드할 수 있게 하며 그 결과 백엔드 코드가 덜 복잡해진다.
- 재사용 가능한 자동 데이터 전송 기능 덕택에 개발 시간이 줄어든다.
- 데이터 변환과 예외 처리를 수행하는 번잡한 코드가 대폭 줄어들기 때문에 개발자가 핵심 비즈니스 논리에 집중할 수 있다.
이 기사에서는 Struts 2의 OGNL 특성과 OGNL이 개발자가 더 능률적인 비즈니스 애플리케이션을 구현하는 데 도움이 된다는 점을 살펴보았다. 다음 기사에서는 개발 관점에서 Struts 2를 검토한다. Struts 2는 계속해서 점진적으로 안정화될 것이므로 필자는 독자가 Struts 2의 강화된 기능 이점을 최대한 활용할 수 있도록 도움을 줄 것이다.
교육
-
Struts 2 및 Internet stats에 관해 배우자.
-
Struts 2.Net을 방문하자.
-
developerWorks podcasts에서 소프트웨어 개발자의 흥미로운 인터뷰와 토론을 확인할 수 있다.
-
developerWorks의 기술 행사 및 웹 캐스트를 통해 최신 정보를 얻을 수 있다.
-
Twitter의 developerWorks 페이지를 살펴보자.
-
IBM 오픈 소스 개발자에게 유익한 컨퍼런스, 기술 박람회, 웹 캐스트 및 기타 행사를 확인하고 참여하자.
-
developerWorks 오픈 소스 영역에서 오픈 소스 기술을 활용하여 개발 작업을 수행하고 이러한 기술을 IBM 제품과 함께 사용하는 데 도움이 되는 사용법 정보, 도구 및 프로젝트 업데이트와 가장 인기 있는 기사 및 튜토리얼을 확인할 수 있다.
-
무료로 제공되는 developerWorks On demand demos를 통해 IBM 및 오픈 소스 기술에 대해 배우고 제품 기능을 익히자.
제품 및 기술
-
DVD로 제공되거나 다운로드할 수 있는 IBM 시험판 소프트웨어를 사용하여 차기 오픈 소스 개발 프로젝트를 구현해 보자.
- IBM 제품 평가판을 다운로드하거나 IBM SOA Sandbox의 온라인 시험판을 살펴보고 DB2®, Lotus®, Rational®, Tivoli® 및
WebSphere®의 애플리케이션 개발 도구 및 미들웨어 제품을 사용해 볼 수 있다.
토론
-
developerWorks 블로그를 통해 developerWorks 커뮤니티에 참여할 수 있다.
-
developerWorks 커뮤니티에 참여하자. 개발자가 운영하고 있는 블로그, 포럼, 그룹 및 위키를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.
Rudranil Dasgupta는 IBM의 준IT 아키텍트이자 공인된 자문 IT 전문가로 전자 산업의 프로젝트 리더로 일하고 있다. 그는 설계 및 개발, 아키텍처 설계, 기술 전략,클라이언트 솔루션에서 자산을 작성하고 활용할 수 있는 솔루션 및 기술 리더십 제공 분야를 거의 7년간 경험했다. 또한, WebSphere 솔루션, SOA, Web 2.0, 컨텐츠 관리 및 Java/J2EE를 많이 다룬 경험이 있다. 그는 IBM developerWorks와 IBM 포럼에 다수의 기사를 기고했다. 또한, 중요한 포럼에서 다수의 Web 2.0과 소셜 협업 관련 세션을 담당하고 있다.
Kaushik Dutta는 애플리케이션 아키텍트로, 현재 GBSC Asset Reuse Enablement Initiative의 기술 아키텍트로 일한다. 그는 소프트웨어 설계, 개발, 분석 및 아키텍처 설계 솔루션 분야에서 10년 이상 전문적인 경험을 했다. 또한, Java 기술, J2EE, EJB, SOA, Web 2.0, WebSphere Adapter, JMS, Web Services, JSP, Servlet,JDBC, RMI, XML, Oracle, HTML 및 JavaScript를 사용한 웹 애플리케이션 개발과 아키텍처 및 시스템 설계 및 분석에 참여했다.