https://velog.io/@hsw0194/Spring-MVC-HandlerMapping의-동작방식-이해하기-1편
저 **RequestMappingHandlerAdapter**에게 **@RequestMapping**을 인식시켜야 한다.
(applicationContext-web.xml에서 HandlerAdapter bean에 이미 등록되어 있음)
```xml
<!-- default adapter(org/springframework/web/servlet/DispatcherServlet.properties) -->
<bean id="httpRequestHandlerAdapter" class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" />
<bean id="simpleControllerHandlerAdapter" class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
<bean id="annotationMethodHandlerAdapter" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
<bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
<bean id="requestMappingHandlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonMessageConverter"/>
</list>
</property>
</bean>
<bean id="requestMappingHandlerMapping" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="order" value="3"/>
</bean>
```
애너테이션 인식/활성화
<context:component-scan> 으로 bean 을 생성하여, @Controller, @RequestMapping 등의 MVC @(Annotation) 이 인식되면서 스프링 컨테이너에 등록되도록 한다. ( 다른 방법으로는, annotation-driven, annotation-config 태그 등을 이용하여 MVC 컴포넌트를 활성화 하는 방법도 있다 )
응답 형식 정형화
Client에게 주는 응답을 정형화하기 위해 일반적으로 ResponseDTO 등의 Java객체로 생성해 전달하는데, 코어에서 이미 구현되어 있는 ExternalApiResult 가 있으므로 사용 ( Builder 혹은 도메인 객체 치환 메서드 추가로 구현하면 좋을듯 )
@ResponseBody
3-1. @Responsebody 을 사용하면, return시 ViewResolver 대신에 MessageConverter가 동작 한다. 따라서, View 페이지가 아닌 반환값 그대로 클라이언트한테 return 하고 싶은 경우 사용하며 HTTP body에 자바 객체를 치환한 JSON 형식으로 전달할 수 있다.
3-2. JSON 치환은 applicationContext-web.xml에서 “requestMappingHandlerAdapter”에 “messageConverters”로 등록된 “jacksonMessageConverter”가 수행해준다. ( MappingJackson2HttpMessageConverter 인데, Spring Boot에서는 기본값이며 솔루션에서도 명시하여 설정되어 있다. )
ExternalApiResult … private T result;
new ExternalApiResult() HashMap형태로 응답 데이터를 작성한다.
- 제네릭 타입을 String으로 설정한 경우 예시
**2. 응답 형식 정형화** → 에서 해당 객체를 이용한 응답을 하려고 하는데, “HashMap param”을 String으로 변환하고 싶다면, jackson 라이브러리로 Stringify (문자열로 치환)하여 “result”에 넣어줄 수 있다. **new ExternalApiResult<String>()인 경우, result의 타입도 String**
jackson 라이브러리의 [ObjectMapper](https://interconnection.tistory.com/137) ,[Jackson라이브러리 이해하기](https://mommoo.tistory.com/83) ,[Jackson ObjectMapper Config](https://kwonnam.pe.kr/wiki/java/jackson)
요약: Java Object ( Collection ) ↔ JSON 으로 매핑 기능을 제공함.
DTO로 응답하기
응답할 객체를 조작/유효성 검사/불필요(비밀)정보 제외 등의 필요가 있다면, builder 패턴의 DTO 객체를 작성후 사용하면 된다.
여기서는 HashMap에 필요한 데이터만 담고 있으므로 생략
Path Variable과 Query Parameter는 언제 사용해야 할까?
요약: resource를 식별하고 싶으면Path Variable을 사용하고, 정렬이나 필터링을 한다면Query Parameter를 사용
/users # 사용자 목록을 가져온다.
/users?occupation=programer # 프로그래머인 사용자 목록을 가져온다.
/users/123 # 아이디가 123인 사용자를 가져온다.
기존 URL Mapping 변환 예시
1. /destinyAPI.do?method=getServerStatus&userCode=007 -> /api/servers?userCode=007
2. /destinyAPI.do?method=getFolderList&account=admin&OID=E000 -> /api/folders/E000?account=admin
3. /destinyAPI.do?method=getDocumentList&account=admin&OID=1O8W6k020Fc¤tPage=1&pageUnit=10
->
/api/folders/1O8W6k020Fc?account=admin¤tPage=1&pageUnit=10
4. /destinyAPI.do?method=uploadFile&account=admin&folderOID=1O8W6k020Fc&fileName=보수지침_20190101.pdf
->
[카카오API 이미지 업로드 가이드 참고](https://developers.kakao.com/docs/latest/ko/kakaostory/rest-api#upload-image-request)
기존 응답 ( getFolderList() )
// ModelAndView 객체로부터 생성된 JSON 응답값 - 약 400라인
{"message": "",
"status": 200,
"params": {
"status": "open",
"data": {
"OID": "E000",
"fullPathName": "전사 폴더>대상",
... 생략
},
"children": [
{
"object": {
"groupType": "",
...
}
변경 내용 ( /folders/{XFolderOID 등} )
// JacksonMessageConverter 로부터 변환된 JSON 응답값 - 약 1300라인
{
"success": true,
"errorCode": "",
"errorMessage": "",
"data": {
"OID": "E000",
"fullPathName": "전사 폴더>대상",
"parentOID": "C_ROOT",
"title": "대상",
"status": "open",
"children": [
{
"resultMap": {},
"object": {
"opCodes": null,
... 쭉쭉 ...
}
ModelAndView를 이용해 응답 받은 JSON 데이터와 크기가 차이가 나는 점을 발견했다. ( 1.82KB → 23.9KB, 400Line → 1,300Line )
modelAndView.setViewName( “jsonView”); 가 세팅되어 있었는데,
public class JsonView extends AbstractView implements MessageSourceAware {} 를 사용하여
null이나 Collections, Class 등을 설정한 옵션에 따라 StringBuffer에 append 하는 식으로 JSON Stringify 하고 있었다.
→ 즉, 세부적인 권한값 등의 ArrayList가 일부분 빠져있는 JSON이었음.
현재 @RequestMapping을 이용하여 구현한 Controller는 requestMappingHandlerAdapter에 property로 설정된 JacksonMessageConverter가 Java → JSON으로 변환해 준다. 추후에 특정 데이터를 숨겨야 하는등의 필요가 발생하면, JsonView 방식을 적용하기, @JsonIgnore 사용하기,
JacksonMessageConverter를 구현(커스텀) 하는 방법이나, DTO 객체를 생성하여 송/수신 하는 방법(추천) 하여 필요한 정보만 전달하면 되겠다.
public ModelAndView getFolderList(HttpServletRequest request, HttpServletResponse response) throws Throwable{
s_Logger.info("---------- folderList() Begin ----------");
ModelAndView mv = new ModelAndView();
HashMap<String, Object> paramKeyValue = new HashMap<String, Object>();
String OID, account = null;
try {
OID = ServletRequestUtils.getStringParameter(request, WebConstants.PARAM_OID);
account = ServletRequestUtils.getStringParameter(request, "account");
if (StringUtil.isEmpty(OID)) {
throw new DestinyDMException(DestinyCMException.OIDIsNull);
}
s_Logger.info("param[OID] : " + OID);
String systemCode = checkApiKey(request);
createDummySession(request, account);
XTreeNode treeNode = null;
XFolder folder = null;
if( XFolder.CONST_OID_DEPT_FOLDER.equals( OID)) {
treeNode = EDMServiceUtil.getFolderService().getTreeNode(OID, true, null, null, null, ObjectConstants.OBJ_TYPE_XDocument, SortObject.SORT_TYPE_NOT, true);
folder = EDMServiceUtil.getFolderService().get(OID, null);
} else {
treeNode = EDMServiceUtil.getFolderService().getTreeNode(OID, true);
folder = (XFolder) treeNode.getObject();
}
XTreeNode[] children = new XTreeNode[0];
if (treeNode.getChildren() != null && treeNode.getChildren().size() > 0) {
children = (XTreeNode[]) Utils.toArray(treeNode.getChildren());
}
String status = treeNode.isFlagLeaf() ? null : "closed";
if (ObjectUtil.isNotEmpty( treeNode.getChildren())) {
status = "open";
}
HashMap<String, Object> data = new HashMap<String, Object>();
data.put("title", folder.getName());
data.put("OID", folder.getOID());
data.put("parentOID", folder.getParentOID());
data.put("fullPathIndex", folder.getFullPathIndex());
data.put("fullPathName", folderBLO.getFullPathName(OID));
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("status", status);
params.put("data", data);
params.put("children", children);
paramKeyValue.put("params", params);
paramKeyValue.put("status", 200);
paramKeyValue.put("message", "");
mv.addObject( WebConstants.MODEL_RESPONSE_OBJECT, paramKeyValue);
mv.setViewName( RESPONSE_VIEW_NAME);
} catch (DestinyCMException e) {
e_Logger.error("ErrMessage : " + e.getMessage());
e_Logger.error("Cause : " + e.getCause());
e_Logger.error("Codes : " + e.getCodes());
e.printStackTrace();
mv = getResponseMsg( request, response, paramKeyValue, e.getErrCode(), e.getMessage(), RESPONSE_VIEW_NAME, null, null);
((ResponseMessage) mv.getModel().get(WebConstants.MODEL_RESPONSE_MESSAGE)).setStatus(500);
((ResponseMessage) mv.getModel().get(WebConstants.MODEL_RESPONSE_MESSAGE)).setMessage(e.getMessage());
} catch (DestinyException e) {
e_Logger.error("ErrCode : " + e.getErrCode());
e_Logger.error("Message : " + e.getMessage());
e.printStackTrace();
mv = getResponseMsg( request, response, paramKeyValue, e.getErrCode(), e.getMessage(), RESPONSE_VIEW_NAME, null, null);
((ResponseMessage) mv.getModel().get(WebConstants.MODEL_RESPONSE_MESSAGE)).setStatus(500);
((ResponseMessage) mv.getModel().get(WebConstants.MODEL_RESPONSE_MESSAGE)).setMessage(e.getMessage());
} catch (Exception e) {
e_Logger.error("ErrMessage : " + e.getMessage());
e_Logger.error("Cause : " + e.getCause());
e.printStackTrace();
mv = getResponseMsg( request, response, paramKeyValue, "", e.getMessage(), RESPONSE_VIEW_NAME, null, null);
((ResponseMessage) mv.getModel().get(WebConstants.MODEL_RESPONSE_MESSAGE)).setStatus(500);
((ResponseMessage) mv.getModel().get(WebConstants.MODEL_RESPONSE_MESSAGE)).setMessage(e.getMessage());
}finally{
s_Logger.info("---------- folderList() End ----------");
logout(request, response);
}
mv.addObject("status", true);
return mv;
}
@RequestMapping(value="/folders/{OID}", method=RequestMethod.GET)
@ResponseBody
public ResponseEntity<?> getFolderList(HttpServletRequest request, HttpServletResponse response, @PathVariable("OID") String OID) {
s_Logger.info("---------- folderList() Begin ----------");
RestResult<HashMap> responseResult = new RestResult<HashMap>();
String account = null;
try {
account = ServletRequestUtils.getStringParameter(request, "account");
if (StringUtil.isEmpty(OID)) {
throw new DestinyDMException(DestinyCMException.OIDIsNull);
}
s_Logger.info("param[OID] : " + OID);
String systemCode = checkApiKey(request);
createDummySession(request, account);
XTreeNode treeNode = null;
XFolder folder = null;
if( XFolder.CONST_OID_DEPT_FOLDER.equals( OID)) {
treeNode = EDMServiceUtil.getFolderService().getTreeNode(OID, true, null, null, null, ObjectConstants.OBJ_TYPE_XDocument, SortObject.SORT_TYPE_NOT, true);
folder = EDMServiceUtil.getFolderService().get(OID, null);
} else {
treeNode = EDMServiceUtil.getFolderService().getTreeNode(OID, true);
folder = (XFolder) treeNode.getObject();
}
XTreeNode[] children = new XTreeNode[0];
if (treeNode.getChildren() != null && treeNode.getChildren().size() > 0) {
children = (XTreeNode[]) Utils.toArray(treeNode.getChildren());
}
String status = treeNode.isFlagLeaf() ? null : "closed";
if (ObjectUtil.isNotEmpty( treeNode.getChildren())) {
status = "open";
}
HashMap<String, Object> data = new HashMap<String, Object>();
data.put("title", folder.getName());
data.put("OID", folder.getOID());
data.put("parentOID", folder.getParentOID());
data.put("fullPathIndex", folder.getFullPathIndex());
data.put("fullPathName", folderBLO.getFullPathName(OID));
data.put("children", children);
data.put("status", status);
responseResult.setData(data);
responseResult.setSuccess(true);
} catch (Exception e) {
return restExceptionHandler(e);
}finally{
logout(request, response);
}
return new ResponseEntity<Object>(responseResult, HttpStatus.OK);
}
package destiny.custom.DAESANG.web.api;
import java.io.Serializable;
import java.util.HashMap;
public class RestResult <T extends Serializable> implements Serializable {
private static final long serialVersionUID = 1L;
private boolean success = true;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
private String errorCode;
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
private String errorMessage;
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public RestResult(boolean success) {
this(success, "");
}
public RestResult(boolean success, String errorCode) {
this(success, errorCode, "");
}
public RestResult(boolean success, String errorCode, String errorMessage) {
this(success, errorCode, errorMessage, null);
}
public RestResult(boolean success, String errorCode, String errorMessage, T data) {
this.success = success;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.data = data;
}
public RestResult() {
this(true, "", "");
}
}
<servlet-mapping>
<servlet-name>Web</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
REST API 에서 사용할 Url도 DispatcherServlet에 매핑
webConfig_SITE.properties해당 파일에,
webContext-controllers.properties에 append 되도록
/api/**=destinyRESTController를 추가한다.
PropertiesFactoryBean의 property로 webContext-controllers.properties가 들어가 결국
SimpleUrlHandlerMapping Bean에 등록됨
여기까지 하면, “/api/*” 로 들어오는 HTTP Request가 DefaultController로 등록되어 있는```java
@ExceptionHandler(ServletRequestBindingException.class)
@ResponseBody
private ResponseEntity<?> RestExceptionHandler(HttpServletRequest request, Exception e) {
logout(request, null);
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
if( e instanceof DestinyException) {
httpStatus = HttpStatus.BAD_REQUEST;
}
e_Logger.error("getServerStatus Cause : " + e.getCause());
e_Logger.error("getServerStatus Message : " + e.getMessage());
RestResult<HashMap> responseResult = new RestResult();
responseResult.setSuccess(false);
responseResult.setErrorCode("CUSTOM");
responseResult.setErrorMessage(e.getMessage());
responseResult.setData(new HashMap());
return new ResponseEntity<Object>(responseResult, httpStatus);
}
/* ------------------------------------------------------------------------ */
<bean id="exceptionHandlerExceptionResolver" class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver" autowire="byName" customaction="$[add]"/>
```
@ExceptionHandler(ServletRequestBindingException.class) 에서 지정한, “ServletRequestBindingException” 이 발생시에 해당 메서드가 잘 호출되지만, 솔루션 코어내에 등록된 예외 이벤트 관련 부분에서 결국 Exception Page를 보내게 되어, ResponseEntity를 응답 해주지 않는다.
BaseRestApiServerController
코어에 “외부 연동REST API Controller”에 쓰일 목적으로, BaseRestApiServerController 가 구현되어 있다. Base- 객체는 Interface에 공통/필수적인 기능 등을 1차적으로 구현해 둔 것이라 Service, DAO 같은 레이어별로 BaseService, BaseDAO… 등이 있으며 대부분 상속하고 사용하는게 좋다.
BaseRestApiServerController는,