1. 개요

본 문서는 MVC(Model–View–Controller) 기반의 웹 어플리케이션을 Frontend와 Backend로분리된 MSA(Microservice Service Architecture) 어플리케이션으로 리펙토링하는 방안에 대해서 제안한다.
이 방안이 정답이 될 수는 없을지 모르지만 참고할 수 있는 하나의 자료가 되기를 희망한다.

1.1 MVC 모델 개요
MVC(Model–View–Controller, 모델-뷰-컨트롤러)는 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다. 이 패턴은 사용자 인터페이스로부터 비즈니스 로직을 분리하여, 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있도록 한다. MVC에서 모델은 애플리케이션의 정보(데이터)를 나타내며, 뷰는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소를 나타내고, 컨트롤러는 데이터와 비즈니스 로직 사이의 상호동작을 관리한다.

컨트롤러(Controller)
   모델에 명령을 보냄으로써 모델의 상태를 변경하거나, 뷰에 명령을 보냄으로써 모델의 표시 방법을 바꿀 수 있다.
모델(Model)
   모델의 상태에 변화가 있을 때 컨트롤러와 뷰에 이를 통보한다. 이와 같은 통보를 통해서 뷰는 최신의 결과를 보여줄 수 있고, 컨트롤러는 모델의 변화에 따른 적용 가능한 명령을 추가, 제거, 수정할 수 있다.
뷰(View)
   사용자가 볼 결과물을 생성하기 위해 모델로부터 정보를 얻어 온다.

1.2 어플리케이션 유형
예전에는 Java 기반의 웹 어플리케이션은 JSP와 Servlet을 이용하여 개발하였다. 이 시절에는 MVC 모델을 이용하여 하나의 WAS(Web Application Server)안에 동작하도록 만들었다. 그러므로 View (JSP)와 Model (Value Object) 간의 데이터 전달은 VM을 이용하였다.
이러한 구조를 이용한 구성은 두 가지 유형은 존재한다.

[CASE 1]

사용자의 요청은 Controller가 받아 비즈니스 로직을 수행한 후 결과 데이터를 JSP로 전달한다. 이것은 가장 일반적인 형태로 Controller:View = 1:1의 관계를 형성한다.

[CASE 2]

사용자의 요청은 Controller가 받아 비즈니스 로직을 수행한 후 결과 데이터를 JSP로 전달한다. 이때 어떤 JSP로 데이터를 전달 할지는 비즈니스 규칙에 따라 달라진다. 즉 Controller:View = 1:n의 관계를 형성한다.

2. 모놀리식 어플리케이션

2.1 어플리케이션 구성
모놀리식 어플리케이션은 상품을 검색하여 주문하는 간단한 구조의 어플리케이션이다. 업무의 흐름은 다음 그림과 같다.

전형적인 MVC 모델 기반으로 JSP를 뷰로 사용하며, 어플리케이션에 구현된 Endpoint는 다음과 같다.

Endpoint 내용
 /form/{page}  URL에 지정된 페이지로 이동한다.
 /customers/login  로그인을 수행하며, 정상인 경우 search.jsp로 이동하고, 지정된 사용자가 없는 경우 message.jsp로 이동한다.
 /orders/order  선택된 상품을 수량 입력을 위한 화면으로 전달한다.
 /orders/payment  선택된 상품의 결제를 수행한다.
 /orders/list  주문된 상품의 목록을 출력한다.

2.1.1 프로젝트 구성
어플리케이션은 Web Application Server에서 수행이 되므로 “Dynamic Web Module” 특성을 갖는다. 이클립스 기반의 Maven 프로젝트 구성은 다음과 같다.

 com.customer 패키지
   로그인 기능과 관련된 Controller, VO(Value Object) 클래스가 존재한다.
 com.order 패키지
   주문, 결제와 관련된 Controller, VO(Value Object) 클래스가 존재한다.
 com.sp
   페이지 이동을 위한 Controller 클래스가 존재한다.
 resources/config
   데이터베이스 연결, mybatis 사용을 위한 정보가 존재한다.
 resources/mapper
   SQL이 저장된 xml 파일이 존재한다.
 WebContent/WEB-INF/views
   뷰를 역할을 하는 JSP 파일들이 존재한다.

2.1.2 데이터베이스 모델

3. 리펙토링

1장에서 어플리케이션 유형에 대해 언급하였다. CASE 1의 경우 Controller:View는 1:1의 관계를 가지며, CASE 2의 경우 Controller:View는 1:n의 관계를 갖는다. 2장에서 설명한 어플리케이션의 경우 대부분 1:1의 관계를 가지지만, “/customers/login” 컨트롤러의 경우 message.jsp와 search.jsp로 이동하므로 1:n의 관계를 갖는다.
본 문서에서는 모놀리식 어플리케이션을 JSP를 이용한 Frontend 서비스와 비즈니스 로직, 모델을 이용한 Backend 서비스의 두 영역으로 분리된다.

간단하게 생각하면 컨트롤러의 일부 로직만 분리하여 마이크로서비스(Backend)로 만들고, JSP가 포함된 컨트롤러(Frontend)에서 분리된 서비스를 생각해 볼 수 있다. 이것 또한 가능한 방법이지만 Frontend 컨트롤러의 로직이 남아있는 문제가 발생한다.

여기에서는 Frontend 컨트롤러를 단순하게 만들고 JSP에서 Backend 서비스를 호출하도록 구현하였다. 만일 규모가 크다면 각 영역(Frontend, Backend)을 더 나누어 여러 개의 마이크로서비스로 구성할 수도 있다.

3.1 목표 및 제약사항
3.1.1 목표
본 문서의 목표는 모놀리식 어플리케이션 소스를 최대한 이용하여 Frontend와 Backend 마이크로서비스를 구성하는 하나의 방법을 설명하는 것이다.

3.1.2 제약사항
 JSP에는 비즈니스 로직이 없는 것으로 가정한다. 만일 JSP에 비즈니스 로직이 있는 경우, 이것을 추출하여 Backend 서비스로 보낸다면 단순한 변경이 아닐 수 있기 때문이다.
 Controller에 있는 비즈니스 로직은 재사용한다. 여기서 재사용은 최소한의 변경만을 수행한다는 의미이며, 아무런 변경이 없는 것을 의미하지 않는다.
 데이터베이스는 분리하지 않는다.

3.2 Controller:View = 1:1 리펙토링 방안
1:1의 관계의 MVC 어플리케이션은 다음과 같이 두 개의 마이크로서비스로 분리한다.

하나의 WAS(JVM) 상에서 동작하던 것을 Frontend와 Backend로 분리된 별도의 WAS 상에서 실행되도록 구성을 조정한다. MVC 패턴의 구성 요소는 다음과 같이 분리된다.

 Frontend Controller
   단순하게 화면(JSP) 라우팅 기능만 제공한다. 필요에 따라서는 세션 정보를 함께 전달할 수 있다.
 Backend Controller (REST API)
   MVC 패턴의 Controller에서 비즈니스 로직 만을 수행하도록 분리한다. Frontend 서비스에서 사용하기 위한 Endpoint를 제공하며, 수행결과를 반환한다. 필요에 따라서는 세션 데이터를 함께 보내야 한다.
 View
   MVC 패턴에서 View에서 사용하는 데이터는 JVM Context를 통해서 전달되었다. 하지만 이제는 다른 JVM에서 실행이 되기 때문에 이 방법을 사용할 수 없다. 그러므로 JSP에서는 데이터를 얻기 위한 REST 호출을 수행하여 데이터를 가져와야 한다. 이 과정에서 필요에 따라 세션 데이터를 함께 보내야 한다.
 REST CLIENT
   분리된 비즈니스 로직은 Backend Controller에 존재한다. 이것은 REST API로 제공되므로, JSP에서는 이것을 호출하여 사용한다.
 JSON Model
   컨텍스트로 전달되던 Value Object를 JSON으로 전달한다.

3.3 Controller:View = 1:n 리펙토링 방안
1:n의 관계의 MVC 어플리케이션은 다음과 같이 두 개의 마이크로서비스로 분리한다.

이것은 1:1 리펙토링과 동일하게 분리가 되며(앞 부분의 표 1 참조), Frontend Controller의 기능이 일부 추가된다.

 Frontend Controller
   기본적으로는 화면(JSP) 라우팅 기능만 제공하지만, 여러 개의 JSP 중에서 선택을 해야 하는 경우 기준이 되는 정보를 얻기 위해 REST 호출을 사용할 수 있다. 그리고 필요에 따라서는 세션 정보를 함께 전달한다.
단, frontend/backend 분리 기준이 절대적이지 않으며, 효율적인 리펙토링을 위해 간단한 비즈니스 로직은 Frontend Controller에 존재할 수 있다.
예) CustomerController의 login API
 Backend Controller (REST API)
   MVC 패턴의 Controller에서 비즈니스 로직 만을 수행하도록 분리한다. Frontend 서비스에서 사용하기 위한 Endpoint를 제공하며, 수행결과를 반환한다. 필요에 따라서는 세션 데이터를 함께 보내야 한다.
 View
   MVC 패턴에서 View에서 사용하는 데이터는 JVM Context를 통해서 전달되었다. 하지만 이제는 다른 JVM에서 실행이 되기 때문에 이 방법을 사용할 수 없다. 그러므로 JSP에서는 데이터를 얻기 위한 REST 호출을 수행하여 데이터를 가져와야 한다. 이 과정에서 필요에 따라 세션 데이터를 함께 보내야 한다.
 REST CLIENT
   분리된 비즈니스 로직은 Backend Controller에 존재한다. 이것은 REST API로 제공되므로, JSP에서는 이것을 호출하여 사용한다.
 JSON Model
   컨텍스트로 전달되던 Value Object를 JSON으로 전달한다.

4. Frontend 마이크로서비스 구현

모놀리식 어플리케이션은 스프링 프레임워크를 사용하며, WAS에 배포되어 실행이 된다.
Frontend 서비스는 현재 마이크로서비스 개발에 강점이 있는 Spring Boot를 이용하여 개발한다.

4.1 프로젝트 구성
스프링 부트 기반의 Frontend 서비스의 프로젝트 구성은 다음과 같다.

 src/main/java
   Controller, Value Object, JSP 변경을 단순하게 할 유틸리티 클래스로 구성
 src/main/resources
   서비스 포트, Backend URL 지정
 src/main/webapp/WEB-INF/jsp
   View를 담당하는 JSP 파일로 구성

4.2 환경 설정
4.2.1 Spring Boot Dependencies
 Spring Web
   내장 tomcat을 사용하며, 클라이언트 요청을 처리한다.
 Lombok
   어노테이션으로 Value Object 코드를 간략하게 구성할 수 있는 기능을 제공한다.

4.2.2 pom.xml
스프링 부트에서 JSP를 사용하기 위해서는 다음의 라이브러리가 필요하다.

<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>jstl</artifactId>
</dependency>
<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
	<groupId>jstl</groupId>
	<artifactId>jstl</artifactId>
	<version>1.2</version>
</dependency>

4.2.2 applicaton.properties
이 파일에는 Frontend 서비스의 포트, JSP 파일의 위치 그리고 Backend 서비스 URL을 지정한다.

# PORT
server.port=${SERVER_PORT:8080}

# JSP
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

# Backend Service
backend.service=http://${BACKEND_SERVER:127.0.0.1}:${BACKEND_PORT:8081}/

4.3 Java Source

① 클라이언트 요청을 JSP로 라우팅하는 컨트롤러와 application.properties에 정의된 “backend.service” 속성 정보를 얻기 위한 ConfigBean으로 구성된다.
② PaymentVO는 Backend REST 호출 결과를 저장하는 목적으로 사용되며, 정보는 Context를 통해 JSP로 전달된다.
③ Backend 서비스를 호출하는 기능으로 JSP에서 사용한다. 즉 REST Client 역할을 수행한다. 이것을 통해 JSP 코드 수정을 최소화한다.
위 그림은 Frontend 서비스의 Java 소스이다. 모놀리식과 다르게 각 업무 영역에 존재하는 Controller가 없으며, NavigationController가 기능을 대신한다.
소스에서 중요한 사항에 대한 설명은 다음과 같다.

4.3.1 NavigationController.java

public class NavigationController
{
  @RequestMapping(value = "/{bizPart}/{screenId}", method = RequestMethod.GET)
  public String controllerGET(@PathVariable String bizPart, @PathVariable String screenId, HttpServletRequest request)
  {
    if ( "orders".equals(bizPart) ) {
      if ( "list".equals(screenId) ) {
        /*
         * JSP에서 REST CALL 필요한 경우 설정한다.
         */
        request.getServletContext().setAttribute("restCall", 'Y');
      }
    }

    return screenId;
  }
  
  @RequestMapping(value = "/{bizPart}/{screenId}", method = RequestMethod.POST)
  public String controllerPOST(@PathVariable String bizPart, @PathVariable String screenId, HttpServletRequest request)
  {
    if ("customers".equals(bizPart) ) {
      if ("login".equals(screenId)) {
        CustomerDelegation cust = new CustomerDelegation(request);
        screenId = cust.login();
      }
    }
    else {
      /*
       * JSP에서 REST CALL 필요한 경우 설정한다.
       */
      request.getServletContext().setAttribute("restCall", 'Y');
      
      if ("payment".equals(screenId))
        screenId = "detail";
    }
    
    return screenId;
  }
}

 controllerGET
   GET 메소드를 처리하며, /orders/list URL의 경우 JSP에서 REST 호출이 필요하므로 “restCall” 속성을 “Y”로 설정한다. JSP에서는 이 값이 “Y”이면 Backend 서비스를 호출한다.
 controllerPOST
   POST 메소드를 처리하며, JSP에서 REST 호출하기 위해 “restCall” 속성을 “Y”로 설정한다. 그리고 이동할 화면을 지정한다. 현재는 대부분이 URL의 마지막 부분(screenId)을 사용하지만 필요에 따라 다른 화면을 지정할 수 있다. 예) payment -> detail

4.3.2 BaseDelegation.java

public class BaseDelegation
{
  public BaseDelegation(HttpServletRequest req) {
    this.request = req;
    this.request.getServletContext().setAttribute("restCall", "N");
  }
  
  protected String getBackendURL() {
    ApplicationContext context = SellingFrontendApplication.getApplicationContext();
    ConfigBean bean = context.getBean(ConfigBean.class);
    return bean.getBackendURL();
  }
  
  protected byte[] extractBody() {
    try {
      return toByteArray(request.getInputStream());
    } catch (IOException e) {
      e.printStackTrace();
    }
    
    return null;
  }
  
  protected RequestEntity<byte[]> makeNewRequest(String path) throws Exception
  {
    URI uri = new URI(getBackendURL() + path);
    RequestEntity<byte[]> newRequest = new RequestEntity<>(extractBody(), extractHttpHeaders(), extractHttpMethod(), uri);
    return newRequest;
  }
}

 BaseDelegation
   JSP에서 REST 호출을 하는 경우 이 클래스의 인스턴스를 생성하게 된다. 그러므로 “restCall” 속성을 “N”로 설정하여 초기화 한다.

 getBackendURL
   Backend 서비스 URL을 가져온다. 정보는 application.properties에 존재하며, ConfigBean에 존재하므로 실시간으로 Bean을 검색하여 정보를 얻는다.
 extractBody
   웹 페이지에서 전달된 HTTP 요청의 Body 내용을 읽어 들인다. 이렇게 읽은 내용은 Backend 서비스로 전달된다.
 makeNewRequest
   Backend 서비스 요청을 위한 RequestEntiry 인스턴스를 생성한다. 호출 후 결과는 BaseDelegation 클래스를 상속한 클래스에 따라 다르게 처리된다.

4.3.3 CustomerDelegation.java

public class CustomerDelegation extends BaseDelegation
{
  public String login() {
    try {
      RestTemplate restTemplate = new RestTemplate();
      ParameterizedTypeReference<CustomerVO> responseType = new ParameterizedTypeReference<CustomerVO>(){};
      ResponseEntity<CustomerVO> res = restTemplate.exchange(makeNewRequest("customers/login"), responseType);

      if ( res.getStatusCode() == HttpStatus.OK ) {
        CustomerVO user = res.getBody();
        
        HttpSession session = request.getSession(true);
        session.setAttribute("user", user);
      }
    }catch(Exception e) {
      e.printStackTrace();
    }
    
    return "search";
  }
  

  public String getLoginId() {
    HttpSession session = request.getSession(false);
    CustomerVO user = (CustomerVO)session.getAttribute("user");
    if ( user != null )
      return user.getUserId();
    else
      return null;
  }
}

 login
   로그인을 수행한다. 이것은 JSP에서 호출되지 않고 NavigationController에 호출된다. 정상적으로 완료되면 사용자 정보를 세션에 저장한다. 그리고 간단한 구성을 위해 WAS의 세션을 사용하였지만, 쿠버네티스에서 실행되는 경우 이것을 사용하면 안된다.
 getLoginId
   세션에서 사용자 ID를 가져온다.

4.3.4 OrderDelegation.java

public class OrderDelegation extends BaseDelegation
{
  public boolean order() {
    boolean rs = false;
    
    try {
      RestTemplate restTemplate = new RestTemplate();
      ParameterizedTypeReference<List<ProductVO>> responseType = new ParameterizedTypeReference<List<ProductVO>>(){};
      ResponseEntity<List<ProductVO>> res = restTemplate.exchange(makeNewRequest("orders/order"), responseType);

      if ( res.getStatusCode() == HttpStatus.OK ) {
        List<ProductVO> result = res.getBody();
        super.request.getServletContext().setAttribute("selectedProduct", result);
      }
      
      rs = true;
    }catch(Exception e) {
      e.printStackTrace();
    }
    
    return rs;
  }
  
  public boolean payment() {
    boolean rs = false;
    
    try {
      RestTemplate restTemplate = new RestTemplate();
      ParameterizedTypeReference<PaymentVO> responseType = new ParameterizedTypeReference<PaymentVO>(){};
      ResponseEntity<PaymentVO> res = restTemplate.exchange(makeNewRequest("orders/payment"), responseType);

      if ( res.getStatusCode() == HttpStatus.OK ) {
        PaymentVO result = res.getBody();
        
        super.request.getServletContext().setAttribute("detailList", result.getProductList());
        super.request.getServletContext().setAttribute("totalPrice", result.getTotalPrice());
      }
      
      rs = true;
    }catch(Exception e) {
      e.printStackTrace();
    }
    
    return rs;
  }
  
  public boolean orderList() {
    boolean rs = false;
    
    try {
      RestTemplate restTemplate = new RestTemplate();
      ParameterizedTypeReference<List<OrderVO>> responseType = new ParameterizedTypeReference<List<OrderVO>>(){};
      ResponseEntity<List<OrderVO>> res = restTemplate.exchange(makeNewRequest("orders/list"), responseType);

      if ( res.getStatusCode() == HttpStatus.OK ) {
        List<OrderVO> result = res.getBody();
        super.request.getServletContext().setAttribute("orderList", result);
      }
      
      rs = true;
    }catch(Exception e) {
      e.printStackTrace();
    }
    
    return rs;
  }
}

 order
   Backend로 분리된 “/orders/order” API를 호출한다. 결과는 ServletCotext에 “selectedProduct”라는 이름으로 저장되며, JSP에서 이것을 이용하여 화면을 구성한다.
 payment
   Backend로 분리된 “/orders/pament” API를 호출한다. 결과는 ServletCotext에 “detailList”, “totalPrice”라는 이름으로 저장되며, JSP에서 이것을 이용하여 화면을 구성한다.
 orderList
   Backend로 분리된 “/orders/list” API를 호출한다. 결과는 ServletCotext에 “orderList”라는 이름으로 저장되며, JSP에서 이것을 이용하여 화면을 구성한다.

4.3.5 ProductDelegation.java

public class ProductDelegation extends BaseDelegation
{
  public boolean search() {
    boolean rs = false;
    
    try {
      RestTemplate restTemplate = new RestTemplate();
      ParameterizedTypeReference<List<ProductVO>> responseType = new ParameterizedTypeReference<List<ProductVO>>(){};
      ResponseEntity<List<ProductVO>> res = restTemplate.exchange(makeNewRequest("products/search"), responseType);

      if ( res.getStatusCode() == HttpStatus.OK ) {
        List<ProductVO> result = res.getBody();
        super.request.getServletContext().setAttribute("result", result);
      }
      
      rs = true;
    }catch(Exception e) {
      e.printStackTrace();
    }
    
    return rs;
  }
}

 search
   Backend로 분리된 “products/search” API를 호출한다. 결과는 ServletCotext에 “result” 라는 이름으로 저장되며, JSP에서 이것을 이용하여 화면을 구성한다.

4.4 JSP Source
JSP 파일은 가급적 기존 코드는 변경하지 않고 내용을 추가하는 방법을 선택하였다.

4.4.1 list.jsp

<%@ page import="com.klab.util.*" %>
<%
  if ( "Y".equals(request.getServletContext().getAttribute("restCall").toString()) ) {
    OrderDelegation order = new OrderDelegation(request);
order.orderList();
  }
%>

OrderDelegation 클래스는 Backend 서비스를 호출하고 결과를 ServletContext에 “orderList”라는 이름의 속성으로 저장한다. 이후 JSP 로직은 모놀리식 어플리케이션과 동일하게 동작한다.

4.4.2 detail.jsp

<%@ page import="com.klab.util.*" %>
<%
  if ( "Y".equals(request.getServletContext().getAttribute("restCall").toString()) ) {
    OrderDelegation order = new OrderDelegation(request);
    order.payment();
  }
%>

OrderDelegation 클래스는 Backend 서비스를 호출하고 결과를 ServletContext에 “selectedProduct”, “totalPrice” 라는 이름의 속성으로 저장한다. 이후 JSP 로직은 모놀리식 어플리케이션과 동일하게 동작한다.

4.4.3 order.jsp

<%@ page import="com.klab.util.*" %>
<%
  if ( "Y".equals(request.getServletContext().getAttribute("restCall").toString()) ) {
    OrderDelegation order = new OrderDelegation(request);
     order.order();
  }

  CustomerDelegation cust = new CustomerDelegation(request);
  String userId = cust.getLoginId();
%>

OrderDelegation클래스는 Backend 서비스를 호출하고 결과를 ServletContext에 “selectedProduct”라는 이름의 속성으로 저장한다. 이후 JSP 로직은 모놀리식 어플리케이션과 동일하게 동작한다.

4.4.4 search.jsp

<%@ page import="com.klab.util.*" %>
<%
  if ( "Y".equals(request.getServletContext().getAttribute("restCall").toString()) ) {
    ProductDelegation prod = new ProductDelegation(request);
    prod.search();
  }
%>

ProductDelegation클래스는 Backend 서비스를 호출하고 결과를 ServletContext에 “result”라는 이름의 속성으로 저장한다. 이후 JSP 로직은 모놀리식 어플리케이션과 동일하게 동작한다.

5. Backend 마이크로서비스 구현

모놀리식 어플리케이션은 스프링 프레임워크를 사용하며, WAS에 배포되어 실행이 된다.
Backend 서비스는 현재 마이크로서비스 개발에 강점이 있는 Spring Boot를 이용하여 개발한다. 그리고 Backend는 모놀리식의 로직을 재사용 하지만 몇 가지 변경이 필요하다.

 컨트롤러는 View를 반환하지 않고 데이터를 반환한다.
 데이터베이스 인터페이스는 MyBatis를 사용하는 코드로 변경한다.

5.1 프로젝트 구성
스프링 부트 기반의 Backend 서비스의 프로젝트 구성은 다음과 같다.

 src/main/java
   Controller, Value Object, DAO(Data Access Object) 클래스로 구성
 src/main/resources
   서비스 포트, JDBC 연결정보
 src/main/resources/mapper
   MyBatis에서 사용하는 쿼리 파일

5.2 환경 설정
5.2.1 Spring Boot Dependencies
 Spring Web
   내장 tomcat을 사용하며, 클라이언트 요청을 처리한다.
 Lombok
   어노테이션으로 Value Object 코드를 간략하게 구성할 수 있는 기능을 제공한다.
 JDBC API, MySQL Driver
   데이터베이스 연결 기능을 제공한다.

5.2.2 pom.xml
MyBatis, Swagger 사용을 위해 다음의 라이브러리가 필요하다.

<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis</artifactId>
	<version>3.4.2</version>
</dependency>
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-spring</artifactId>
	<version>1.3.2</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>2.6.1</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>2.6.1</version>
</dependency>

5.2.2 applicaton.properties
이 파일에는 Backend 서비스의 포트, MySQL 연결 정보를 지정한다.

# PORT
server.port=${SERVER_PORT:8081}

# JDBC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://${DB_SERVER:127.0.0.1}:${DB_PORT:3306}/prodb?serverTimezone=UTC&useSSL=false&useUnicode=yes&characterEncoding=UTF-8
spring.datasource.username=msa
spring.datasource.password=passw0rd

5.3 Java Source

① MyBatis 및 Swagger 설정 파일
② MyBatis에서 쿼리를 수행하는 인터페이스
위 그림은 Backend 서비스의 Java 소스이다. 모놀리식과 비슷하게 Controller, Service가 동일한 패키지에 존재한다. 하지만 화면을 담당하는 JSP 설정은 존재하지 않는다. 특히 모놀리식에서는 XML 파일과 SqlSessionFactory 클래스를 이용하여 데이터베이스를 사용하였다. 이것은 iBatis에서 사용하는 일반적인 방법이다. 하지만 MyBatis에서는 이러한 방법도 사용할 수 있지만, 좀 더 간편한 방법으로 데이터베이스를 사용할 수 있다.

5.3.1 DataAccessConfig.java

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
  sessionFactory.setDataSource(dataSource);
  sessionFactory
      .setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
  return sessionFactory.getObject();
}

 classpath:mapper/*.xml
   MyBatis에서 수행할 쿼리를 저장한 XML 파일의 위치를 지정한다. 이것은 클래스 패스에 있는 mapper 폴더 하위의 모든 xml 파일을 대상으로 한다.

5.3.2 CustomerController.java

@RestController
@RequestMapping("/customers")
public class CustomerController
{
  @Resource(name="customerService")
  private CustomerService customerService;

  @RequestMapping(value = "/login", method = RequestMethod.POST)
  public ResponseEntity<CustomerVO> login(@ModelAttribute CustomerVO userInfo)
  {
    CustomerVO user = customerService.getUser(userInfo.getUserId());
    if ( user != null ) {
      return new ResponseEntity<>(user, HttpStatus.OK);
    }
    else {
      return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
    }
  }
}

 @RestController
   모놀리식에서는 @Controller를 사용했지만 스프링 부트에서는 @RestController를 사용한다. ServletContext로 데이터를 전달하지 않으므로 모놀리식과 다르게 ModelMap이 없다. 그리고 OrderController, ProductController 모두 동일한 규칙이 적용된다.
 ResponseEntity
   @Controller는 View를 선택하기위해 ID를 리턴 하였다. 하지만 Backend 서비스는 View가 없으므로 실행 결과만을 반환한다. 즉 데이터베이스에서 사용자 정보를 가져와 CustomerVO에 저장하여 반환한다. 그리고 모놀리식에서 사용자 정보를 세션에 저장하는 부분은 Frontend에서 수행하므로 Backend 에서는 삭제하였다.

5.3.3 CustomerService.java

public class CustomerService
{
  @Autowired
  private CustomerDAO customerDAO;
  
  public CustomerVO getUser(String userId) {
    Map<String, Object> parm = new HashMap<>();
    parm.put("userId", userId);

    return customerDAO.selectUser(parm);
  }
}

 CustomerDAO customerDAO
   모놀리식에서 DAO는 SqlSessionFactory를 이용한 클래스로 구현이 되었다. 하지만 MyBatis에서는 인터페이스를 이용하여 구현할 수 있다. 즉 SqlSessionFactory를 이용하는 부분을 MyBatis 내부에서 자동으로 수행해 준다.

5.3.4 CustomerDAO.java

public interface CustomerDAO
{
  public CustomerVO selectUser(Map<String, Object> parm);
}

 selectUser
   DAO는 SQL 실행을 위한 메소드를 정의한다. 실제 이 메소드가 수행할 SQL은 mapper 폴더에 XML 파일로 정의한다. 동일한 방법으로 OrderDAO, ProductDAO를 정의한다.

5.4 MyBatis Query XML
모놀리식에서는 iBatis 방식으로 데이터베이스를 사용하였다. 다음은 모놀리식에서 사용하는 XML 파일이다.

<mapper namespace="CUSTOMER">
  <select id="selectUser" parameterType="hashmap" resultType="com.klab.customer.CustomerVO">
    <![CDATA[
    select  user_id as userId
    from    cust_mst
    where   user_id = #{userId}
    ]]>
  </select>
</mapper>

SqlSessionFactory를 이용하는 경우 select 메소드의 SQL id로 “CUSTOMER.selectUser”를 전달한다. 그리고 쿼리의 파라미터는 “parameterType”으로 지정하고, 결과로 반환되는 “resultType”으로 지정한다.

다음은 MyBatis에서 사용하는 XML 파일이다.

<mapper namespace="com.klab.dao.CustomerDAO">
  <select id="selectUser" parameterType="hashmap" resultType="com.klab.customer.CustomerVO">
    <![CDATA[    
    select  user_id as userId
    from    cust_mst
    where   user_id = #{userId}
    ]]>
  </select>
</mapper>

MyBatis를 사용하는 경우 이 XML은 약간의 변경이 필요하다. 그것은 mapper 부분에 인터페이스로 정의한 DAO를 지정하는 것이다. 그리고 다음의 규칙을 준수한다.
 DAO 인터페이스에는 XML에 정의된 SQL id와 동일한 메소드가 있어야 한다.
 각 메소드의 파라미터는 XML에서 parameterType로 지정한 클래스와 동일해야 한다. 만일 파라미터가 없는 경우 생략이 가능하다.
 각 메소드의 리턴값은 XML에서 resultType로 지정한 클래스와 동일해야 한다. 만일 void인 경우에는 생략이 가능하다. 그리고 DAO가 List를 반환하는 경우에 List를 XML에 List를 명시하지 않아도 된다. MyBatis가 자동으로 처리해 준다.
모놀리식에서는 iBatis를 사용하는 방식처럼 MyBatis를 사용하였다. 실제 오래된 버전의 스프링과 iBatis를 사용하는 경우와는 다를 수 있다. 하지만 이것을 MyBatis로 변경하는 경우 규칙성이 존재하므로 약간의 노력(?)으로 일부 자동화 할 수도 있을 것이다.

6. 테스트


테스트는 두 단계로 진행된다.

 로그인 -> 상품검색 -> 수량입력 -> 결제
 주문내역 조회

1) 로그인

2) 상품검색

3) 수량입력

4) 결제

5) 주문내역 조회

6. 결론

본 문서에서는 모놀리식 어플리케이션을 두 개의 마이크로서비스로 분리하였다. 물론 업무 영역별로 분해하는 방법도 있지만, 여기에서는 다음의 부분에 초점을 맞추었다.

 코드 재사용
   JSP, Controller 소스를 변경을 최소화하고 기존 비즈니스 로직을 최대한 재사용하여 모놀리스와 동일한 동작을 구현한다.
 최신 기술 적용
   클라우드 상에서 동작하는 서비스 개발에 많은 강점을 갖고 있는 스프링 부트를 이용한다.

여기서 제시한 방법이 정답이 아닐 수도 있다. 하지만 보다 나은 방법을 고민하는 하나의 단서가 되기를 바라는 마음이다.


소스코드

  • 모놀리식 : https://github.ibm.com/choies/selling-project
  • Frontend : https://github.ibm.com/choies/selling-frontend
  • Backend : https://github.ibm.com/choies/selling-backend