캐모마일 UI Adapter 가이드
개요
타 프레임워크에서 상용 UI 솔루션과 연동을 위한 모듈을 제공하나, 해당 프레임워크에 종속적인 형태로 개발이 된다.
Chamomile에서는 프레임워크에 대한 종속성을 탈피하기 위해 Spring MVC에서 제공하는 기술들을 이용하여 상용 UI 솔루션과 연동할 수 있는 방법을 제시한다.
즉, 다시말해 백엔드에서 개발되어지는 코드 자체가 프레임워크에 대한 종속성, 상용 UI 솔루션에 대한 종속성이 제거된다. 상용 UI 솔루션을 사용하지 않는 경우에도 동일한 코드로 서비스를 제공할 수 있다. (단, 예외적인 경우도 존재한다.)
현재 가장 많이 사용되는 UI 상용 솔루션인 Nexacro N을 지원한다.
흐름도
UIAdapter는 상용 UI 솔루션과 연동하기 위한 모듈이다.
adapter 들은 java의 SPI (Service Provider Interfaces) mechanism을 이용하여 확장 가능하도록 구성되어져 있다.
UIAdapter의 로드는 Java의 java.util.ServiceLoader에 의해 이루어지고, 해당 jar 파일은 아래와 같은 설정 정보를 포함한다.
UiAdapter의 설치 및 제거는 클래스패스에 존재하는 경우 자동으로 설치되며, 클래스패스에서 삭제하는 경우 제거 된다.
Spring MVC에서 제공하는 기술들을 그대로 사용할 수 있도록 구성된다.
기능 Overview
다중 UI Adapter를 지원한다.
동적으로 UI Adapter의 변경을 지원한다.
예) 하나의 Controller가 존재할 때 여러 UI 솔루션에서 호출할 수 있다.클래스패스에 존재하는 UIAdapter를 자동으로 로드한다.
상용 UI 솔루션이 제공하는 통신타입 처리를 지원한다.
Spring의 Controller 관련 기술 및 annotation을 지원한다.
@RequestParam, @ModelAttribute, @RequestBody, @ResponseBody, HttpEntity, RequestEntity, ResponseEntity
입/출력 데이터 변환, 및 에러처리를 지원한다.
기능설명
데이터 변환
상용 UI 솔루션에서 제공되는 데이터 형식은 아래와 같다.
단일 데이터 (e.g. Variable)
Primitive 형식의 데이터이며, 식별자(name)와 값(value)를 가지고 있다.
2차원 데이터 (e.g. DataSet)
DB의 테이블 형식과 유사한 형태이며, 단일 데이터를 복수개를 가지고 있는 데이터이다.
데이터 형식에 따라 복수의 행을 가지고 있을 수 있다.
websquare의 경우 map 형식과 list 형식으로 구분할 수 있다.
UI 솔루션의 종속성을 탈피하기 위해 Spring의 Controller 레벨에서 데이터 변환을 수행하게 된다. (UI 솔루션 ↔ Java Bean)
단일 데이터 변환
byte[], int, float, double, Boolean (Support wrapper class)
java.lang.String, java.math.BigDecimal, java.util.Date, java.util.Object
UI 솔루션에서 제공되는 타입만을 변환한다.
2차원 데이터
List\<Java Bean \| Map\>
Java Bean (ValueObject) \| java.util.Map
Java Bean으로 변환하게 되는 경우 멤버 변수의 변환 가능한 타입은 단일 데이터 변환 시 지원하는 타입만을 제공한다. (Bean에 getter/setter가 존재 해야 한다.)
2차원 데이터 변환 시 상세한 내용은 아래와 같다.
2차원 데이터의 열의 명칭은 Java Bean의 멤버변수와 대응된다.
Map으로 변환하게 되는 경우 식별자로 사용된다.
첫 번째 행만을 대상으로 하며, 나머지 행의 데이터는 무시된다.
복수개의 행으로 구성되는 경우 List 형태로 변환할 수 있다.
모든 행의 데이터가 변환된다.
데이터타입 매핑
상용 UI 솔루션과 java 간의 데이터 타입에 대한 매핑 정보는 아래와 같다.
nexacro/xplatform의 경우는 지정 된 타입 외에는 데이터 손실의 우려가 있기 때문에 변환하지 않는다.
Websquare의 경우 지정 된 타입 외는 String으로 변환을 수행한다.
java types | nexacro | Xplatform | websquare |
---|---|---|---|
byte[], java.lang.Byte[] | BLOB | BLOB | |
int, java.lang.Integer | INT | INT | int |
long, java.lang,Long | LONG | LONG | |
float, java.lang.Float | FLOAT | FLOAT | float |
double, java.lang.Double | DOUBLE | DOUBLE | double |
boolean, java.lang.Boolean | BOOLEAN | BOOLEAN | |
java.lang.String | STRING | STRING | String |
java.math.BigDecimal | BIG_DECIMAL | BIG_DECIMAL | BigDecimal |
java.util.Date | DATE | DATE | Date |
DATE_TIME | DATE_TIME | ||
TIME | TIME | ||
Java.lang.Object | UNDEFINED | UNDEFINED |
데이터 입력
UI 솔루션에서 요청하는 입력 데이터를 Spring의 Controller 레벨에서 손쉽게 처리 할 수 있는 방법을 제공한다.
Spring MVC의 기술을 그대로 이용할 수 있으며, 벤더사에서 제공되는 데이터는 아래와 같은 항목을 통해 변환을 수행한다.
Argument 타입 | 설명 | 옵션 | 비고 |
---|---|---|---|
@RequestParam | 일반적으로 key/value 형식의 데이터를 획득할 때 사용된다. (단일 데이터 처리) 기본적으로는 어노테이션에 명시 된 이름을 통해 데이터를 획득하고, 이름이 명시되지 않은 경우 파라매터의 타입을 통해 데이터를 획득한다. 옵션 : required와 defaultValue를 추가적으로 설정할 수 있다. | required defaultValue | 어노테이션을 지정하지 않아도 동작한다. 예제 : public void handle(@RequestParam(“userId”) String userId) |
@ModelAttribute | 요청 된 파라매터 정보와 선언 된 객체와의 매핑을 수행한다. (2차원 데이터 처리) @RequestParam과 동일한 이름정책을 가진다. binding을 추가적으로 설정할 수 있다. @Valid 어노테이션과 사용 가능하다. | binding | 메서드 레벨에 선언되는 경우 선처리 기능으로 동작 가능 어노테이션을 지정하지 않아도 동작한다. 예제 : public void handle(@ModelAttribute(“address”) Address address) |
HttpEntity\<T\> or RequestEntity\<T\> | Http 요청의 헤더와 본문으로 구성 된 엔티티이다. (단일/2차원 데이터가 합쳐진 데이터) 내부적으로는 HttpMessageConverter를 이용하여 본문를 구성한다. | 전체 요청 데이터를 하나의 객체로 변환한다. 예제 : public void handle(HttpEntity\<User\> httpEntity) | |
@RequestBody | Http 요청의 본문을 명시 된 파라매터로 구성한다. (단일/2차원 데이터가 합쳐진 데이터) 내부적으로 HttpMessageConverter를 이용하여 본문을 구성한다. @Valid 어노테이션과 사용 가능하다. | required | 전체 요청 데이터를 하나의 객체로 변환한다. 예제 : public void handle(@RequestBody User user) |
Spring에서 제공하는 Controller의 메서드 레벨에서 사용가능한 추가 타입들은 아래에서 확인할 수 있다.
데이터 출력
처리 된 데이터를 UI 솔루션으로 응답하기 위해선 Model, ModelAndView를 이용할 수 있으며, 데이터 자체를 바로 전달 할 수 있다.
ModelAndView를 이용하더라도 View는 요청 된 UI 솔루션을 처리하기 위한 View는 자동으로 지정된다.
Controller에서 출력 파라미터로 다음과 같은 데이터 형식을 사용할 수 있다.
Argument 타입 | 설명 | 비고 |
---|---|---|
Model | Model 인터페이스를 구현한 객체 반환을 허용한다. 추가된 데이터는 primitive 타입인 경우 단일데이터로 판단하고, 그 외 데이터는 2차원 데이터로 판단하여 응답한다. | |
ModelAndView | Model과 동일한형태이며, UI 솔루션을 사용하게 되는 경우 View는 명시적으로 지정할 필요가 없다. 단, UserController와 같은 UI 솔루션과 일반 JSP에서 동일하게 호출한다면 ModelAndView를 사용하여 view를 지정할 수 있습니다. | |
HttpEntity\<T\> or ResponseEntity\<T\> | Http 응답의 헤더와 본문으로 구성 된 엔티티이다. View를 처리하지 않으며, Entity(데이터) 자체를 응답으로 구성한다. Bean으로 반환되는 경우 해당 Bean에 단일데이터와 2차원데이터가 합쳐져 있어야 한다. | |
@ResponseBody | Http 응답의 본문으로 구성한다. View를 처리하지 않으며, 대상 자체를 응답으로 구성한다. Bean으로 반환되는 경우 해당 Bean에 단일데이터와 2차원데이터가 합쳐져 있어야 한다. | List로 반환되는 경우 2차원 데이터로 인식하여 응답한다. |
Spring에서 제공하는 Controller의 메서드 레벨에서 사용가능한 추가 타입들은 아래에서 확인할 수 있다.
프로젝트 설정
캐모마일 3.0에서는 스프링 부트 기반으로 캐모마일 UIAdapter가 적용된 샘플을 제공한다.
프로젝트 구성에 필요한 빈과 컴포넌트는 AutoConfiguration을 통해 자동으로 주입 되어 개발 시에 신경 쓰지 않아도 된다.
따라서, UI Adapter를 사용하기 위해선 다음의 절차를 거치면 된다.
캐모마일 샘플 프로젝트 생성(ex. chamomile-sample-boot-security-adapter-nexacro)
pom.xml 확인
<dependencies> <dependency><!-- 벤더에 맞는 어댑터 적용 --> <groupId>net.lotte.chamomile.module</groupId> <artifactId>chamomile-adapter-nexacro</artifactId> </dependency> <dependency> <groupId>net.lotte.chamomile.module</groupId> <artifactId>chamomile-security-adapter</artifactId> </dependency> </dependencies>application.yml 파일 확인 후 excludePatterns 적용
chmm: uiadapter: excludePatterns: /index.html,/favicon.ico
업무시스템 개발하기
캐모마일 UIAdapter를 사용하면 특정 벤더사의 어댑터에 종속 될 필요 없이 스프링 WebMVC를 사용하여 개발하듯이 개발을 진행하면 된다.
샘플 프로젝트에 나와있는 방식대로 개발을 진행하면 빠르게 개발을 수행할 수 있다.
Controller 구성
demo.user.controller.UserController.java
import com.nexacro.uiadapter.spring.core.data.NexacroResult; import demo.user.controller.dto.SearchRequest; import demo.user.controller.dto.UserRequest; import demo.user.controller.dto.UserResponse; import demo.user.domain.User; import demo.user.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import java.util.List; import java.util.stream.Collectors; @Controller @RequiredArgsConstructor @RequestMapping(path = "/model-attribute") public class UserController { private final UserService userService; @RequestMapping(value = "/user/list") public ModelAndView getUserList(@ModelAttribute(name = "input1") SearchRequest request) { List<User> userList = userService.getUsersBySearchRequest(request); List<UserResponse> results = userList.stream().map(UserResponse::of).collect(Collectors.toList()); ModelAndView mv = new ModelAndView(); mv.addObject("output1", results); return mv; } @RequestMapping(value = "/user/update") public ModelAndView updateUserList(@ModelAttribute(name = "input1") List<UserRequest> request) { userService.updateUserList(request); return new ModelAndView(); } }Service 구성
demo.user.service.UserService.java
import demo.user.controller.dto.SearchRequest; import demo.user.controller.dto.UserRequest; import demo.user.domain.User; import java.util.List; import java.util.Map; public interface UserService { List<User> getUsersBySearchRequest(SearchRequest searchRequest); void updateUserList(List<UserRequest> userRequests); }demo.user.service.UserServiceImpl.java
import com.nexacro.java.xapi.data.DataSet; import demo.user.controller.dto.SearchRequest; import demo.user.controller.dto.UserRequest; import demo.user.domain.User; import demo.user.domain.UserMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; @Override public List<User> getUsersBySearchRequest(SearchRequest searchRequest) { return userMapper.findUsers(searchRequest); } @Override public void updateUserList(List<UserRequest> userRequests) { for (UserRequest userRequest : userRequests) { User user = userRequest.convertToUser(); if (isInsertType(userRequest)) { userMapper.insertUserList(user); continue; } if (isUpdateType(userRequest)) { userMapper.updateUserList(user); continue; } if (isDeleteType(userRequest)) { userMapper.deleteUserList(user); } } } private boolean isDeleteType(UserRequest userRequest) { return userRequest.getRowType() == DataSet.ROW_TYPE_DELETED; } private boolean isUpdateType(UserRequest userRequest) { return userRequest.getRowType() == DataSet.ROW_TYPE_UPDATED; } private boolean isInsertType(UserRequest userRequest) { return userRequest.getRowType() == DataSet.ROW_TYPE_INSERTED; } }VO 구성
demo.user.domain.User.java
import lombok.*; import javax.persistence.*; @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder @EqualsAndHashCode public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; @Column(name = "NAME") private String name; @Column(name = "DESCRIPTION") private String description; @Column(name = "USE_YN") private Boolean useYn; @Column(name = "REG_USER") private String regUser; }Mapper 구성 demo.user.domain.UserMapper.java
import demo.user.controller.dto.SearchRequest; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface UserMapper { List<User> findUsers(SearchRequest request); void insertUserList(User sample); void updateUserList(User sample); void deleteUserList(User sample); }XML 매퍼 구성 resources/sql/service/UserMapper.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="demo.user.domain.UserMapper"> <resultMap id="user" type="demo.user.domain.User"> <id column="ID" property="id"/> <result column="NAME" property="name"/> <result column="DESCRIPTION" property="description"/> <result column="USE_YN" property="useYn"/> <result column="REG_USER" property="regUser"/> </resultMap> <select id="findUsers" parameterType="demo.user.controller.dto.SearchRequest" resultMap="user"> SELECT * FROM USER WHERE 1=1 <if test="keyword != null and keyword != ''"> <choose> <when test="searchType == 'ID'"> AND ID = #{keyword} </when> <when test="searchType == 'NAME'"> AND NAME LIKE #{keyword} </when> </choose> </if> ORDER BY ID ASC </select> <insert id="insertUserList" parameterType="demo.user.domain.User"> INSERT INTO USER ( ID , NAME , DESCRIPTION , USE_YN , REG_USER) VALUES ( #{id} , #{name} , #{description} , #{useYn} , #{regUser}) </insert> <update id="updateUserList" parameterType="demo.user.domain.User"> UPDATE USER SET ID=#{id} , NAME=#{name} , DESCRIPTION=#{description} , USE_YN=#{useYn} WHERE ID = #{id} </update> <delete id="deleteUserList" parameterType="demo.user.domain.User"> DELETE FROM USER WHERE ID = #{id} </delete> </mapper>
특이사항
행의 타입 (RowType)
DataSet에 존재하는 행들의 상태를 말하며, 추가, 수정, 삭제 된 상태를 확인할 수 있다.
데이터셋의 RowType은 nexacro platform에서 transaction 시 입력 데이터셋의 전송옵션이 :U 혹은 :A일 때 행의 타입을 확인할 수 있다. :N 옵션인 경우 Normal 상태이다.
VO 클래스를 생성할 때 DataSetRowTypeAccessor를 구현해야 행의 타입을 확인할 수 있다. com.nexacro.uiadapter17.spring.core.data.DataSetRowTypeAccessor
행 처리타입 예제 코드
demo.user.controller.dto.RowType.java
demo.user.controller.dto.UserRequest.java
대용량 데이터 분할 전송 (firstrow)
대용량 데이터를 전송하기 위해 데이터를 분할하여 전송하는 방식을 지원한다.
즉, 데이터베이스에서 다량의 데이터 조회 시 Out Of Memory 오류를 예방할 수 있다.
Controller에서 입력 파라매터로 데이터 분할 전송을 하기 위한 핸들러를 선언한다.
nexacro : com.nexacro.uiadapter.spring.core.data.NexacroFirstRowHandler
xplatform : com.tobesoft.xplatform.data.XplatformFirstRowHandler
데이터 분할전송을 처리하는 핸들러의 주요내용은 다음과 같다.
전송 규칙은 Variable이 전송 되고 난 뒤 DataSet을 전송한다.
DataSet이 전송되고 난 후 Variable이 전송 될 경우 예외를 발생시킨다.
대용량 처리시 UI에서 에러처리를 하기 위해선 기존의 ErrorCode, ErrorMsg 외에 추가적으로 데이터셋을 통해 에러를 처리해야 한다.
DataSet Schema
DataSet 이름 : FirstRowStatus
Column : ErrorCode, ErrorMsg
대표적인 메서드는 다음과 같다.
void sendPlatformData(PlatformData platformData)
void sendDataSet(DataSet ds)
void sendVariable(Variable var)
void setContentType(String contentType)
PlatformType.CONTENT_TYPE_XML
PlatformType.CONTENT_TYPE_SSV
PlatformType.CONTENT_TYPE_BINARY
UI Adapter 샘플코드
캐모마일 UI Adapter와 넥사크로 UI Adapter를 쉽게 활용하여 프로젝트를 시작할 수 있도록 샘플 코드를 만들어두었다.
캐모마일 IDE의 다음 archetype을 생성하여 프로젝트를 시작하면 넥사크로N 예제를 실행해볼 수 있다.
chamomile-sample-boot-adapter-nexacro
스프링 시큐리티 기반 캐모마일 어댑터를 활용한 넥사크로 프로젝트 샘플
chamomile-sample-boot-nexacro
넥사크로 어댑터를 활용한 캐모마일 넥사크로 프로젝트 샘플
chamomile-sample-boot-security-adapter-nexacro
캐모마일 시큐리티 기반 캐모마일 어댑터를 활용한 넥사크로 프로젝트 샘플
chamomile-sample-boot-security-nexacro
캐모마일 시큐리티 기반 넥사크로 어댑터를 활용한 넥사크로 프로젝트 샘플