스프링 부트

[SpringBoot] RestTemplate - exchange (POST, PUT, DELETE) 로 통신하기!

h__hj 2022. 11. 19. 23:24

# RestTemplate  - exchange (POST, PUT, DELETE)

RestTemplate에서 HttpMethod와 관계없이 전체적으로 사용할 수 있는 exchange 를 사용하여 POST, PUT, DELETE 요청하기!
[통신 프로세스]
View ↔ [ ajax ] ↔ Controller ↔ [ RestTemplate ] ↔ RestController ↔ Service ↔ Mapper ↔ DataBase

1. exchange POST 통신, ContentType: application/x-www-form-urlencoded 

2. exchange PUT 통신

3. exchange DELETE 통신

# 환경

Tool  : STS 4.13.0
Ver   : 2.7.5 [GA]
java  : 11
Repo  : MAVEN
DB    : ORACLE XE (11g)
View  : Thymeleaf
jQuery: 3.6.0
Type  : Client(WEB), Server(API)

# Page

# View

<body>
    <h1>REST TEMPLATE V2 테스트 페이지 입니다.</h1>
    <div>
        <h3>SAMPLE REST CODE</h3>
        <input type="button" onclick="call('get')" value="GET" style="display: inline;"/>
        <input type="button" onclick="call('post-json')" value="POST-JSON" style="display: inline;"/>
        <input type="button" onclick="call('post-form')" value="POST-FROM" style="display: inline;"/>
        <br>
        <input type="button" onclick="call('exchange-post')" value="POST" style="display: inline;"/>
        <input type="button" onclick="call('exchange-put')" value="PUT" style="display: inline;"/>
        <input type="button" onclick="call('exchange-delete')" value="DELETE" style="display: inline;"/>
        <br>
    </div>
</body>
<script type="text/javascript" th:inline="javascript">
    function call(path) {
        const target = `/test/rest-v2/${path}`;
        
        $.ajax(target, 
            {
                dataType: "json",
            }).done(function(output) {
                console.log(`OUTPUT[${path}]:`, JSON.stringify(output.result));
            }).fail(function(jqXHR) {
                console.log("ERROR:", jqXHR);
            }
        );
    }
</script>

# Controller - Client(WEB)

@Controller
@RequestMapping("/test/rest-v2")
public class TestRestV2Controller {

    @Value("${api.url.api}")
    private String apiUrlApi;
    
    @Autowired
    private RestTemplate template;
    
    @RequestMapping("/view")
    public ModelAndView view(ModelAndView mav) {
        mav.setViewName("test/restView2");
        return mav;
    }
    /**
     * RestTemplate exchange( URI, method, requestEntity, responseType, [uriVariables] )
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    @RequestMapping("/exchange-post")
    public ModelAndView exchangePost() {
        // 요청하려는 URL 설정.
        String target = apiUrlApi.concat("/api-v1/test/rest/exchange-post");
        // 요청하려는 헤더 ContentType 설정.
        HttpHeaders header = new HttpHeaders();
        header.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        // 요청 시 본문에 담을 데이터 설정.
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>();
        body.add("data", "테스트");
        body.add("data", "TEST");
        // Body와 Header 결합한 Entity 설정.
        HttpEntity requestEntity = new HttpEntity(body, header);
        // 전송 및 응답 데이터 설정.
        ResponseEntity<RestMessage> responseEntity = template.exchange(target, HttpMethod.POST, requestEntity, RestMessage.class);
        RestMessage message = responseEntity.getBody();
        // 응답 데이터 설정.
        ModelAndView mav = new ModelAndView("jsonView");
        mav.addObject("result", message.getData());
        return mav;
    }
    
    /**
     * RestTemplate exchange( URI, method, requestEntity, responseType, [uriVariables] )
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    @RequestMapping("/exchange-put")
    public ModelAndView exchangePut() {
        // 요청하려는 URL 설정.
        String target = apiUrlApi.concat("/api-v1/test/rest/exchange-put");
        // 요청하려는 헤더 ContentType 설정.
        HttpHeaders header = new HttpHeaders();
        header.setContentType(MediaType.APPLICATION_JSON);
        // 요청 시 본문에 담을 데이터 설정.
        List<String> data = new ArrayList<String>();
        data.add("테스트");
        data.add("TEST");
        Map<String, Object> body = new HashMap<String, Object>();
        body.put("krName", "구글");
        body.put("data", data);
        // Body와 Header 결합한 Entity 설정.
        HttpEntity requestEntity = new HttpEntity(body, header);
        // 전송 및 응답 데이터 설정.
        ResponseEntity<RestMessage> responseEntity = template.exchange(target, HttpMethod.PUT, requestEntity, RestMessage.class);
        RestMessage message = responseEntity.getBody();
        // 응답 데이터 설정.
        ModelAndView mav = new ModelAndView("jsonView");
        mav.addObject("result", message.getData());
        return mav;
    }
    
    /**
     * RestTemplate exchange( URI, method, requestEntity, responseType, [uriVariables] )
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    @RequestMapping("/exchange-delete")
    public ModelAndView exchangeDelete() {
        // 요청하려는 URL 설정.
        String target = apiUrlApi.concat("/api-v1/test/rest/exchange-delete");
        // 요청하려는 헤더 ContentType 설정.
        HttpHeaders header = new HttpHeaders();
        header.setContentType(MediaType.APPLICATION_JSON);
        // 요청 시 본문에 담을 데이터 설정.
        List<String> data = new ArrayList<String>();
        data.add("테스트");
        data.add("TEST");
        Map<String, Object> body = new HashMap<String, Object>();
        body.put("krName", "네이버");
        body.put("data", data);
        // Body와 Header 결합한 Entity 설정.
        HttpEntity requestEntity = new HttpEntity(body, header);
        // 전송 및 응답 데이터 설정.
        ResponseEntity<RestMessage> responseEntity = template.exchange(target, HttpMethod.DELETE, requestEntity, RestMessage.class);
        RestMessage message = responseEntity.getBody();
        // 응답 데이터 설정.
        ModelAndView mav = new ModelAndView("jsonView");
        mav.addObject("result", message.getData());
        return mav;
    }
}

# RestController - Server(API)

@Slf4j
@RestController
@RequestMapping("/api-v1/test/rest")
public class TestRestController {

    @Autowired
    private TestService testService;
    /**
     * application/x-www-form-urlencoded;
     */
    @PostMapping("/exchange-post")
    public RestMessage post(@ModelAttribute TestPVO testPVO) {
        log.debug("[PARAMETER] [exchange-post] TestPVO: {}", testPVO);

        List<TestRVO> listTestRVO = testService.domains(testPVO);
        
        RestMessage message = new RestMessage();
        message.setOk();
        message.setData(listTestRVO);
        return message;
    }
    
    /**
     * 클라이언트가 기존 리소스를 완전히 교체해야 하는 경우 PUT을 사용할 수 있습니다.
     * 부분 업데이트를 수행할 때 HTTP PATCH를 사용할 수 있습니다.
     * PutMapping 사용.
     */
    @PutMapping("/exchange-put")
    public RestMessage put(@RequestBody TestPVO testPVO) {
        log.debug("[PARAMETER] [exchange-put] TestPVO: {}", testPVO);
        
        List<TestRVO> listTestRVO = testService.domains(testPVO);
        
        RestMessage message = new RestMessage();
        message.setOk();
        message.setData(listTestRVO);
        return message;
    }
    
    /**
     * DeleteMapping 사용.
     */
    @DeleteMapping("/exchange-delete")
    public RestMessage delete(@RequestBody TestPVO testPVO) {
        log.debug("[PARAMETER] [exchange-delete] TestPVO: {}", testPVO);
        
        List<TestRVO> listTestRVO = testService.domains(testPVO);
        
        RestMessage message = new RestMessage();
        message.setOk();
        message.setData(listTestRVO);
        return message;
    }
}

# Service

@Service
public class TestService {

    @Autowired
    private TestMapper testMapper;
    
    public String time(String pattern) {
        return testMapper.time(pattern);
    }
    
    public List<TestRVO> domains(TestPVO testPVO) {
        return testMapper.domains(testPVO);
    }
}

# Mapper - Interface

@Mapper
public interface TestMapper {

    String time(String pattern);

    List<TestRVO> domains(TestPVO testPVO);
}

# Mapper - xml

<mapper namespace="com.prjt.blog.test.mapper.TestMapper">
    
    <select id="time" parameterType="string" resultType="string">
/** 현재시간 **/ SELECT TO_CHAR(SYSDATE, #{pattern, jdbcType=VARCHAR}) as time FROM DUAL
    </select>
    
    <select id="domains" parameterType="com.prjt.blog.test.model.TestPVO" resultType="com.prjt.blog.test.model.TestRVO">
/** 도메인 **/
    WITH domain_table AS (
        SELECT null AS kr_name, null AS en_name, null AS addr_url, null AS delegator FROM dual
      UNION ALL 
        SELECT '네이버', 'NAVER', 'https://www.naver.com', '최수연' FROM dual
      UNION ALL 
        SELECT '구글', 'GOOGLE', 'https://www.google.com', '선다 피차이' FROM dual
      UNION ALL 
        SELECT '카카오', 'KAKAO', 'https://www.kakocorp.com', '홍은택' FROM dual
    )
    SELECT * 
      FROM domain_table
  <where>
       AND kr_name is not null
    <if test="krName != null and krName != ''">
       AND kr_name = #{krName, jdbcType=VARCHAR}
    </if>
  </where>
    </select>

</mapper>

# TestPVO

@Data
public class TestPVO {
    private String krName;
    private List<String> data;
}

# 테스트 로그

/* exchange(): POST 시간 순으로 나열 */
Client - [WEB] [REQ]: null
Client - [WEB] [URL]: (GET) http://localhost:8888/test/rest-v2/exchange-post
Client - [REST] [CALL]: data=%ED%85%8C%EC%8A%A4%ED%8A%B8&data=TEST
Client - [REST] [EXEC]: (POST) http://localhost:9999/api-v1/test/rest/exchange-post

Server - [WEB] [REQ]: [{"data":["테스트","TEST"]}]
Server - [WEB] [URL]: (POST) http://localhost:9999/api-v1/test/rest/exchange-post
Server - [PARAMETER] [exchange-post] TestPVO: TestPVO(krName=null, data=[테스트, TEST])

DB SQL - ==>  Preparing: /** 도메인 **/ WITH domain_table AS ( SELECT null AS kr_name, null AS en_name, null AS addr_url, null AS delegator FROM dual UNION ALL SELECT '네이버', 'NAVER', 'https://www.naver.com', '최수연' FROM dual UNION ALL SELECT '구글', 'GOOGLE', 'https://www.google.com', '선다 피차이' FROM dual UNION ALL SELECT '카카오', 'KAKAO', 'https://www.kakocorp.com', '홍은택' FROM dual ) SELECT * FROM domain_table WHERE kr_name is not null
DB SQL - ==> Parameters: 
DB SQL - <==      Total: 3

Server - [WEB] [RES]: {"code":"0000","data":[{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"},{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"},{"krName":"카카오","enName":"KAKAO","addrUrl":"https://www.kakocorp.com","delegator":"홍은택"}]}

Client - [REST] [BACK]: (200) {"code":"0000","message":"정상적으로 처리 되었습니다.","data":[{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"},{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"},{"krName":"카카오","enName":"KAKAO","addrUrl":"https://www.kakocorp.com","delegator":"홍은택"}],"args":null,"success":true}
Client - [WEB] [RES]: {"view":"jsonView","model":{"result":[{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"},{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"},{"krName":"카카오","enName":"KAKAO","addrUrl":"https://www.kakocorp.com","delegator":"홍은택"}]},"cleared":false}

View   - OUTPUT[exchange-post]: [{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"},{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"},{"krName":"카카오","enName":"KAKAO","addrUrl":"https://www.kakocorp.com","delegator":"홍은택"}]
/* exchange(): PUT 시간 순으로 나열 */
Client - [WEB] [REQ]: null
Client - [WEB] [URL]: (GET) http://localhost:8888/test/rest-v2/exchange-put
Client - [REST] [CALL]: {krName=구글, data=[테스트, TEST]}
Client - [REST] [EXEC]: (PUT) http://localhost:9999/api-v1/test/rest/exchange-put

Server - [WEB] [REQ]: [{"krName":"구글","data":["테스트","TEST"]}]
Server - [WEB] [URL]: (PUT) http://localhost:9999/api-v1/test/rest/exchange-put
Server - [PARAMETER] [exchange-put] TestPVO: TestPVO(krName=구글, data=[테스트, TEST])

DB SQL - ==>  Preparing: /** 도메인 **/ WITH domain_table AS ( SELECT null AS kr_name, null AS en_name, null AS addr_url, null AS delegator FROM dual UNION ALL SELECT '네이버', 'NAVER', 'https://www.naver.com', '최수연' FROM dual UNION ALL SELECT '구글', 'GOOGLE', 'https://www.google.com', '선다 피차이' FROM dual UNION ALL SELECT '카카오', 'KAKAO', 'https://www.kakocorp.com', '홍은택' FROM dual ) SELECT * FROM domain_table WHERE kr_name is not null AND kr_name = ?
DB SQL - ==> Parameters: 구글(String)
DB SQL - <==      Total: 1

Server - [WEB] [RES]: {"code":"0000","data":[{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"}]}

Client - [REST] [BACK]: (200) {"code":"0000","message":"정상적으로 처리 되었습니다.","data":[{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"}],"args":null,"success":true}
Client - [WEB] [RES]: {"view":"jsonView","model":{"result":[{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"}]},"cleared":false}

View   - OUTPUT[exchange-put]: [{"krName":"구글","enName":"GOOGLE","addrUrl":"https://www.google.com","delegator":"선다 피차이"}]
/* exchange(): DELETE 시간 순으로 나열 */
Client - [WEB] [REQ]: null
Client - [WEB] [URL]: (GET) http://localhost:8888/test/rest-v2/exchange-delete
Client - [REST] [CALL]: {krName=네이버, data=[테스트, TEST]}
Client - [REST] [EXEC]: (DELETE) http://localhost:9999/api-v1/test/rest/exchange-delete

Server - [WEB] [REQ]: [{"krName":"네이버","data":["테스트","TEST"]}]                                                                                                                                                                                                                                                                                                                                                                                                      
Server - [WEB] [URL]: (DELETE) http://localhost:9999/api-v1/test/rest/exchange-delete                                                                                                                                                                                                                                                                                                                                                                                
Server - [PARAMETER] [exchange-delete] TestPVO: TestPVO(krName=네이버, data=[테스트, TEST])                                                                                                                                                                                                                                                                                                                                                                         
 
DB SQL - ==>  Preparing: /** 도메인 **/ WITH domain_table AS ( SELECT null AS kr_name, null AS en_name, null AS addr_url, null AS delegator FROM dual UNION ALL SELECT '네이버', 'NAVER', 'https://www.naver.com', '최수연' FROM dual UNION ALL SELECT '구글', 'GOOGLE', 'https://www.google.com', '선다 피차이' FROM dual UNION ALL SELECT '카카오', 'KAKAO', 'https://www.kakocorp.com', '홍은택' FROM dual ) SELECT * FROM domain_table WHERE kr_name is not null AND kerver - r_name = ?
DB SQL - ==> Parameters: 네이버(String)                                                                                                                                                                                                                                                                                                                                                                                                                                  
DB SQL - <==      Total: 1                                                                                                                                                                                                                                                                                                                                                                                                                                              
 
Server - [WEB] [RES]: {"code":"0000","data":[{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"}]}                                                                                                                                                                                                                                                                                                                             

Client - [REST] [BACK]: (200) {"code":"0000","message":"정상적으로 처리 되었습니다.","data":[{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"}],"args":null,"success":true}
Client - [WEB] [RES]: {"view":"jsonView","model":{"result":[{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"}]},"cleared":false}

View   - OUTPUT[exchange-delete]: [{"krName":"네이버","enName":"NAVER","addrUrl":"https://www.naver.com","delegator":"최수연"}]

# 내용

3가지 방식으로 나누어 테스트 해보았고, 설정된 부분을 보면 조금 씩 다르다는 걸 알 수 있다.
1. exchange - POST

  • HttpMethod: POST
  • ContentType: application/x-www-form-urlencoded;charset=UTF-8
  • URI: /api-v1/test/rest/exchange-post
  • 파라미터 맵핑: @ModelAttribute
  • 파라미터 타입: TestPVO

2. exchange - PUT

  • HttpMethod: PUT
  • ContentType: application/json
  • URI: /api-v1/test/rest/exchange-put
  • 파라미터 맵핑: @RequestBody
  • 파라미터 타입: TestPVO

3. exchange - DELETE

  • HttpMethod: DELETE
  • ContentType: application/json
  • URI: /api-v1/test/rest/exchange-delete
  • 파라미터 맵핑: @RequestBody
  • 파라미터 타입: TestPVO


exchange의 가장 큰 특징은 HttpMethod를 자유롭게 설정할 수 있다는 것이다. exchange를 사용한다면, 공통 함수를 만들어 공통적으로 들어가는 헤더나 설정을 조작 하기에 좋을 것 같다.

Controller 코드에선 header를 직접 설정하여 ContentType을 설정했지만, 설정하지 않아도 기본적으로 json으로 설정이 되고, 전송 데이터 타입이 MultiValueMap이라면 application/x-www-form-urlencoded 으로 자동 설정 된다.

json 방식으로 요청을 받는다면, @RequestBody를 사용하여 데이터를 맵핑하고,
form 방식으로 요청을 받는다면, @RequestParam를 사용하여 원시형타입이나 MultiValueMap으로 맵핑하고, @ModelAttribute를 사용하여 VO로 맵핑할 수 있다.

# RestTemplate 시리즈

https://hjho95.tistory.com/35 getForObject, postForObject
https://hjho95.tistory.com/36 exchange (POST, PUT, DELETE)
https://hjho95.tistory.com/37 Configuration 
https://hjho95.tistory.com/38 ClientHttpRequestInterceptor, ResponseErrorHandler 

# RestTemplate 시리즈 참조 페이지

RestTemplate: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html
RestTemplate: https://www.baeldung.com/rest-template
Interceptor: https://www.baeldung.com/spring-rest-template-interceptor
ErrorHandling: https://www.baeldung.com/spring-rest-template-error-handling