💬
목차
< 뒤로가기
인쇄

캐모마일 UI Adapter 가이드

개요

타 프레임워크에서 상용 UI 솔루션과 연동을 위한 모듈을 제공하나, 해당 프레임워크에 종속적인 형태로 개발이 된다.

Chamomile에서는 프레임워크에 대한 종속성을 탈피하기 위해 Spring MVC에서 제공하는 기술들을 이용하여 상용 UI 솔루션과 연동할 수 있는 방법을 제시한다.

즉, 다시말해 백엔드에서 개발되어지는 코드 자체가 프레임워크에 대한 종속성, 상용 UI 솔루션에 대한 종속성이 제거된다. 상용 UI 솔루션을 사용하지 않는 경우에도 동일한 코드로 서비스를 제공할 수 있다. (단, 예외적인 경우도 존재한다.)

지원하는 상용 UI 솔루션은 아래와 같다.

  1. nexacro (투비소프트)
  2. xplatform (투비소프트)
  3. websquare (인스웨이브시스템즈)

흐름도

  • UIAdapter는 상용 UI 솔루션과 연동하기 위한 모듈이다.
  • adapter 들은 java의 SPI (Service Provider Interfaces) mechanism을 이용하여 확장 가능하도록 구성되어져 있다.
  • UIAdapter의 로드는 Java의 java.util.ServiceLoader에 의해 이루어지고, 해당 jar 파일은 아래와 같은 설정 정보를 포함한다.
    1. META-INF/services/net.lotte.chamomile.module.adapter.UiAdapter
    2. net.lotte.chamomile.module.adapter.UiAdapter의 상세한 내용은 java doc을 참고한다.
  • UiAdapter의 설치 및 제거는 클래스패스에 존재하는 경우 자동으로 설치되며, 클래스패스에서 삭제하는 경우 제거 된다.
  • Spring MVC에서 제공하는 기술들을 그대로 사용할 수 있도록 구성된다.

image-20200911093232320
[그림] UIAdapter흐름도

기능 Overview

  • 다중 UI Adapter를 지원한다.
    1. 요청에 따라 동적으로 UI Adapter의 변경을 지원한다.
      예) 하나의 Controller가 존재할 때 여러 UI 솔루션에서 호출할 수 있다.
  • 클래스패스에 존재하는 UIAdapter를 자동으로 로드한다.
  • 상용 UI 솔루션이 제공하는 통신타입 처리를 지원한다.
  • Spring의 Controller 관련 기술 및 annotation을 지원한다.
    1. @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 형태로 변환할 수 있다.
      • 모든 행의 데이터가 변환된다.

image-20200911093709346
[그림] 데이터 변환

데이터타입 매핑

  • 상용 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의 기술을 그대로 이용할 수 있으며, 벤더사에서 제공되는 데이터는 아래와 같은 항목을 통해 변환을 수행한다.

Controller에서 입력 파라미터로 다음과 같은 데이터 형식을 사용할 수 있다.

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의 메서드 레벨에서 사용가능한 추가 타입들은 아래에서 확인할 수 있다.

https://docs.spring.io/spring-framework/docs/5.3.27/reference/html/web.html#mvc-ann-arguments

데이터 출력

  • 처리 된 데이터를 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의 메서드 레벨에서 사용가능한 추가 타입들은 아래에서 확인할 수 있다.

https://docs.spring.io/spring-framework/docs/5.3.27/reference/html/web.html#mvc-ann-return-types

프로젝트 설정

캐모마일 3.0에서는 스프링 부트 기반으로 캐모마일 UIAdapter가 적용된 샘플을 제공한다.

프로젝트 구성에 필요한 빈과 컴포넌트는 AutoConfiguration을 통해 자동으로 주입 되어 개발 시에 신경 쓰지 않아도 된다.

따라서, UI Adapter를 사용하기 위해선 다음의 절차를 거치면 된다.

  1. 캐모마일 샘플 프로젝트 생성(ex. sample-rest-chamomile-security-adapter-nexacro)

  2. pom.xml 확인

    <dependencies>  
    .
    .
    .
    <dependency>  
        <groupId>net.lotte.chamomile.boot</groupId>  
        <artifactId>chamomile-boot-starter</artifactId>  
    </dependency>
    <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>
  3. application.yml 파일 확인 후 excludePatterns 적용

    .
    .
    .
    chmm:
    uiadapter:  
        excludePatterns: /index.html,/favicon.ico
    .
    .
    .

업무시스템 개발하기

캐모마일 UIAdapter를 사용하면 특정 벤더사의 어댑터에 종속 될 필요 없이 스프링 WebMVC를 사용하여 개발하듯이 개발을 진행하면 된다.

샘플 프로젝트에 나와있는 방식대로 개발을 진행하면 빠르게 개발을 수행할 수 있다.

  1. 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();  
    }  
}
  1. 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;  
    }
}
  1. 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;  
}
  1. 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);
}


5. XML 매퍼 구성
resources/sql/service/UserMapper.xml
```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>

벤더사별 특이사항

  • TOBESOFT (nexacro, xplatform)
    • 데이터 처리
    • 행의 타입 (RowType)
    • DataSet에 존재하는 행들의 상태를 말하며, 추가, 수정, 삭제 된 상태를 확인할 수 있다.
    • 데이터셋의 RowType은 nexacro platform에서 transaction 시 입력 데이터셋의 전송옵션이 :U 혹은 :A일 때 행의 타입을 확인할 수 있다. :N 옵션인 경우 Normal 상태이다.
    • VO 클래스를 생성할 때 DataSetRowTypeAccessor를 구현해야 행의 타입을 확인할 수 있다.
      • nexacro : com.nexacro.uiadapter17.spring.core.data.DataSetRowTypeAccessor
      • Xplatform : com.tobesoft.xplatform.data.DataSetRowTypeAccessor

행 처리타입 예제 코드

demo.user.controller.dto.RowType.java

import com.nexacro.uiadapter.spring.core.data.DataSetRowTypeAccessor;  
import lombok.Getter;  
import lombok.NoArgsConstructor;  
import lombok.Setter;  

@Getter  
@NoArgsConstructor  
public class RowTypeVO implements DataSetRowTypeAccessor {  
    private int rowType;  

    @Override  
    public void setRowType(int rowType) {  
        this.rowType = rowType;  
    }}

demo.user.controller.dto.UserRequest.java

import demo.user.domain.User;  
import lombok.Getter;  
import lombok.Setter;  

import java.util.ArrayList;  
import java.util.List;  
import java.util.Map;  
import java.util.stream.Collectors;  

@Getter  
@Setter  
public class UserRequest extends RowTypeVO {  
    private Long id;  
    private String name;  
    private String description;  
    private String useYn;  
    private String regUser;  

    public User convertToUser() {  
        return User.builder()  
                .id(id)  
                .name(name)  
                .description(description)  
                .useYn(convertUseYn())                .regUser(regUser)  
                .build();    }  
    public static List<UserRequest> ofList(List<Map<String,Object>> request) {  
        List<UserRequest> list = new ArrayList<>();  
        for (Map<String, Object> map : request) {  
            UserRequest userRequest = new UserRequest();  
            userRequest.setId(Long.parseLong((String)map.get("id")));  
            userRequest.setName((String) map.get("name"));  
            userRequest.setDescription((String) map.get("description"));  
            userRequest.setUseYn((String) map.get("useYn"));  
            userRequest.setRegUser((String) map.get("regUser"));  
            int dataSetRowType = (int)map.get("DataSetRowType");  
            userRequest.setRowType(dataSetRowType);  
            list.add(userRequest);  
        }        return list;  
    }  
    public Boolean convertUseYn() {  
        return useYn.equals("Y");  
    }}
  • 대용량 데이터 분할 전송 (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
다음 이메일