장정우님이 지음, [스프링부트 핵심가이드 :: 스프링 부트를 활용한 애플리케이션 개발 실무] 책을 읽고 정리한 필기입니다.📢

RestTemplate 사용하기

이제 RestTemplate을 사용해보겠다. 요청을 보낼 서버 용도로 별도의 프로젝트를 하나 생성하고 다른 프로젝트에서 RestTemplate을 통해 요청을 보내는 방식으로 실습을 진행할 예정이다.

서버 프로젝트 생성하기

먼저 RestTemplate의 동작을 확인하기 위해 서버 용도의 프로젝트를 생성하겠다. 실습 환경에서는 한 컴퓨터 안에서 두 개의 프로젝트를 가동시켜야 하기 때문에 톰캣의 포트를 변경해야 한다. 프로젝트에는 spring-boot-starter-web 모듈만 의존성으로 추가하며, 이 책에서는 serverBox라는 이름으로 프로젝트를 생성했다. 이 프로젝트의 구조는 아래와 같다.


image


1
server.port=9090


컨트롤러에서는 GET과 POST 메서드 형식의 요청을 받기 위한 코드를 구성하겠다. 컨트롤러 클래스는 아래와 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@RestController
@RequestMapping("api/v1/crud-api")
public class CrudController {

    @GetMapping
    public String getName(){
        return "Flature";
    }

    @GetMapping(value = "/{variable}")
    public String getVariable(@PathVariable String variable) {
        return variable;
    }

    @GetMapping("/param")
    public String getNameWithParam(@RequestParam String name){
        return "Hello. " + name + "!";
    }

    @PostMapping
    public ResponseEntity<MemberDto> getMember(
            @RequestBody MemerDto request,
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam String organization)
    {
        System.out.println(request.getName());
        System.out.println(request.getEmail());
        System.out.println(request.getOrganization());

        MemberDto memberDto = new MemberDto();
        memberDto.setName(name);
        memberDto.setEmail(email);
        memberDto.setOrganization(organization);

        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }

    @PostMapping(value = "/add-header")
    public ResponseEntity<MemberDto> addHeader(@RequestHeader("my-header") String header,
                                               @RequestBody MemberDto memberDto){

        System.out.println(header);

        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }
}


PUT, DELETE 메서드는 GET과 POST 형식과 각 구성 방식이 거의 비슷하기 때문에 생략했다. 위 코드의 5~18번 줄의 코드는 GET 형식의 요청이 들어오는 상황의 케이스를 구현한다. 첫 번째 메서드는 아무 파라미터가 없는 경우, 두 번째는 PathVariable을 사용하는 경우, 세 번째는 RequestParameter를 사용하는 경우이다.

20~46번 줄에는 POST 형식의 요청을 받기 위한 두 개의 메서드가 구현돼 있다. 첫번째 메서드는 예쩨의 간소화를 위해 요청 파라미터(Request Parameter)와 요청 바디(Request Body)를 함께 받도록 구현했고, 두 번째 메서드는 임의의 HTTP 헤더를 받도록 구현했다.

여기서 사용된 MemeberDto객체는 아래와 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class MemberDto {

    private String name;
    private String email;
    private String organization;

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getEmail(){
        return email;
    }

    public void setEmail(String email){
        this.email = email;
    }

    public String getOrganization(){
        return organization;
    }

    public void setOrganization(String organization){
        this.organization = organization;
    }

    @Override
    public String toString(){
        return "MemberDto{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", organization='" + organization + '\'' +
                '}';
    }
}


MemberDto클래스는 name, email, organization이라는 총 3개의 필드를 가지고 있다.

RestTemplate구현하기

일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나 서비스 또는 비즈니스 계층에 구현된다. 앞서 생성한 서버 프로젝트에 요청을 날리기 위해 서버의 역할을 수행하면서 다른 서버로 요청을 보내는 클라이언트의 역할도 수행하는 새로운 프로젝트를 생성한다. 간단하게 도식화하면 아래와 같다.


image


위 그림에서 클라이언트는 서버를 대상으로 요청을 보내고 응답을 받는 역할을 하고, 앞에서 구현한 서버 프로젝트는 서버2가 된다.

지금부터 RestTemplate을 포함하는 프로젝트를 생성하겠다. 다음과 같이 설정해서 프로젝트를 생성한다. 스프링 부트 버전은 이전과 같은 2.5.6 버전으로 진행하며, 다음과 같은 내용을 설정한다.

  • groupId : com.springboot
  • artifactId : rest
  • name : rest
  • Developer Tools : Lombok, Spring Configuration Processor
  • Web : Spring Web


그리고 클라이언트에서 요청하는 것처럼 실습하기 위해 SwaggerConfiguration 클래스 파일과 의존성 추가를 해야 한다. RestTemplate은 이미 spring-boot-starter-web 모듈에 포함돼 있는 기능이므로 pom.xml에 별도로 의존성을 추가할 필요는 없다. 프로젝트의 구조로는 클라이언트로부터 요청을 받는 컨트롤러와, RestTemplater을 활용해 다른 서버에 통신 요청을 하는 서비스 계층으로 작성하겠다.


GET 형식의 RestTemplate 작성하기

먼저 GET 형식의 RestTemplate 예제를 살펴보겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Service
public class RestTemplateService {

    public String getName() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api")
                .encode()
                .build()
                .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }

    public String getNameWithPathVariable() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api/{name}")
                .encode()
                .build()
                .expand("Flature") // 복수의 값을 넣어야할 경우 , 를 추가하여 구분
                .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }

    public String getNameWithParameter() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api/param")
                .queryParam("name", "Flature")
                .encode()
                .build()
                .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }
}


RestTemplate을 생성하고 사용하는 방법은 아주 다양하다. 그중 가장 보편적인 방법은 UriComponentsBuilder를 사용하는 방법이다. UriComponentsBuilder는 스프링 프레임워크에서 제공하는 클래스로서 여러 파라미터를 연결해서 URI 형식으로 만드는 기능을 수행한다.

각 메서드는 정의된 컨트롤러 메서드와 비교해 코드를 확인할 수 있다. 4~16번 줄의 메서드는 PathVariable이나 파라미터를 사용하지 않는 호출 방법이다. UriComponentsBuilder는 빌더 형식으로 객체를 생성한다. fromUriString()메서드에서는 호출부의 URL을 입력하고, 이어서 path() 메서드에 세부 경로를 입력한다. encode() 메서드는 인코딩 문자셋을 설정할 수 있는데, 인자를 전달하지 않으면 기본적으로 UTF-9로 다음과 같은 코드가 실행된다.


1
2
3
public final UriComponentsBuilder encode() {
    return encode(StandardCharsets.UTF-8);
}


이후 builder()메서드를 통해 빌더 생성을 종료하고 UriComponents타입이 리턴된다. 예에에서는 toUri() 메서드를 통해 URI 타입으로 리턴받았따. 만약 URI 객체를 사용하지 않고 String 타입의 URI를 사용한다면 toUriString() 메서드로 대체해서 사용하면 된다.

이렇게 생성된 uri는 restTemplate이 외부 API를 요청하는데 사용되며 ,13번 줄의 getForEntity()에 파라미터로 전달된다. getForEntity()는 URI와 응답받는 타입을 매개변수로 사용한다.

18~31번 줄의 코드에서 눈여겨볼 내용은 path() 메서드와 expand() 메서드 내에 입력한 세부 URI 중 중괄호({}) 부분을 사용해 개발 단계에서 쉽게 이해할 수 있는 변수명을 입력하고 expand() 메서드에서는 순서대로 값을 입력하면 된다. 값을 여러 개 넣어야 하는 경우에는 콤마(,) 로 구분해서 나열한다.

33~46번 줄은 파라미터로 전달하는 예제이다. 예제에서 볼 수 있듯이 queryParam() 메서드를 사용해 (키, 값) 형식의 파라미터를 추가할 수 있다.

POST 형식의 RestTemplate 작성

POST 형식의 RestTemplate 사용법은 아래와 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public ResponseEntity<MemberDto> postWithParamAndBody() {
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api")
            .queryParam("name", "Flature")
            .queryParam("email", "flature@wikibooks.co.kr")
            .queryParam("name", "Wikibooks")
            .encode()
            .build()
            .toUri();

    MemberDto memberDto = new MemberDto();
    memberDto.setName("flature!!");
    memberDto.setEmail("flature@gmail.com");
    memberDto.setOrganization("Around Hub Studio");

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<MemberDto> responseEntity = restTemplate.postForEntity(uri, memberDto, MemberDto.class);

    return responseEntity;
}

public ResponseEntity<MemberDto> postWithHeader() {
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api")
            .encode()
            .build()
            .toUri();

    MemberDto memberDto = new MemberDto();
    memberDto.setName("flature!!");
    memberDto.setEmail("flature@gmail.com");
    memberDto.setOrganization("Around Hub Studio");

    RequestEntity<MemberDto> requestEntity = RequestEntity
            .post(uri)
            .header("my-header", "Wikibooks API")
            .body(memberDto);

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<MemberDto> responseEntity = restTemplate.exchange(requestEntity,
            MemberDto.class);

    return responseEntity;
}


예제에서 1~22번 줄은 POST 형식으로 외부 API에 요청할 때 Body 값과 파라미터 값을 담는 방법 두 가지를 모두 보여준다. 2~10번 줄에서는 파라미터에 값을 추가하는 작업이 수행되며, 12~19번 줄에서는 RequestBody 에 값을 담는 작업이 수행된다. RequestBody에 값을 담기 위해서는 12~15번 줄과 같이 데이터 객체를 생성한다. postForEntity() 메서드를 사용할 경우에는 파라미터로 데이터 객체를 넣으면 된다.

postForEntity() 메서드로 서버 프로젝트의 API를 호출하면 서버 프로젝트의 콘솔 로그에는 RequestBody값이 출력되고 파라미터 값은 결괏값으로 리턴된다. 앞에서 프로젝트를 생성하면서 설명했지만 이 프로젝트에서 쉽게 API를 호출할 수 있게 Swagger를 설정하겠다. pom.xml파일에 Swagger 의존성을 추가한 후 아래와 같이 Swagger 설정 코드를 작성한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.springboot.rest"))
            .paths(PathSelectors.any())
            .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("Spring Boot Open API Test with Swagger")
            .description("설명 부분")
            .version("1.0.0")
            .build();
    }
}


그러고 나서 앞에서 작성한 서비스 코드를 연결하는 컨트롤러 코드를 아래와 같이 작성한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RestController
@RequestMapping("/rest-template")
public class RestTemplateController {
    
    private final RestTemplateService restTemplateService;
    
    public RestTemplateController(RestTemplateService restTemplateService){
        this.restTemplateService = restTemplateService;
    }
    
    @GetMapping
    public String getName(){
        return restTemplateService.getName();
    }
    
    @GetMapping("/path-variable")
    public String getNameWithPathVariable(){
        return restTemplateService.getNameWithPathVariable();
    }
    
    @PostMapping
    public ResponseEntity<MemberDto> postDto(){
        return restTemplateService.postWithParamAndBody();
    }
    
    @PostMapping("/header")
    public ResponseEntity<MemberDto> postWithHeader(){
        return restTemplateService.postWithHeader();
    }
}


여기까지 진행했다면 애플리케이션을 실행하고 postDto() 메서드에 해당하는 POST API를 호출하면 아래의 결과가 출력된다. 참고로 이번 장에서 진행하는 실습은 앞서 생성한 2개의 프로젝트가 모두 가동돼 있는 상태에서 진행해야 한다.


image


1
2
3
flature!!
fature@gmail.com
Around Hub Studio


위 출력 결과는 서버 프로젝트가 파라미터의 값과 Body 값을 정상적으로 전달받았다는 것을 의미한다.

위에서 작성한 서비스 코드의 24~47번 줄의 메서드는 헤더를 추가하는 예제이다. 대부분의 외부 API는 토큰키를 받아 서비스 접근을 인증하는 방식으로 작동한다. 이때 토큰값을 헤더에 담아 전달하는 방식이 가장 많이 사용된다. 헤더를 설정하기 위해서는 RequestEntity 를 정의해서 사용하는 방법이 가장 편한 방법이다. 37~40번 줄은 RequestEntity를 생성하고 post() 메서드로 URI를 설정한 후 header() 메서드에서 헤더의 키 이름과 값을 설정하는 코드이다. 대체로 서버 프로젝트의 API 명세에는 헤더에 필요한 키 값을 요구하면서 키 이름을 함께 제시하기 때문에 그에 맞춰 헤더 값을 설정하면 된다.

마지막으로 43번 줄에는 RestTemplateexchange() 메서드를 사용했다. exchange() 메서드는 모든 형식의 HTTP 요청을 생성할 수 있다. RequestEntity의 설정에서 post() 메서드 대신 다른 형식의 메서드로 정의만 하면 exchange() 메서드로 쉽게 사용할 수 있기 때문에 대부분 exchange() 메서드를 사용하는 편이다.

지금까지 GET, POST 형식으로 RestTemplater을 사용하는 방법을 알아봤다.

RestTemplate 커스텀 설정

RestTemplateHTTPClient를 추상화하고 있다. HttpClient의 종류에 따라 기능에 차이가 다소 있는데, 가장 큰 차이는 커넥션 풀(Connection Poll)이다.

RestTemplate은 기본적으로 커넥션 풀을 지원하지 않는다. 이 기능을 지원하지 않으면 매번 호출할 때 마다 포트를 열어 커넥션을 생성하게 되는데, TIME_WAIT 상태가 된 소켓을 다시 사용하려고 접근한다면 재사용하지 못하게 된다. 이를 방지하기 위해서는 커넥션 풀 기능을 활성화해서 재사용할 수 있게 하는 것이 좋다. 이 기능을 활성화하는 가장 대표적인 방법은 아파치에서 제공하는 HttpClient로 대체해서 사용하는 방식이다.

먼저 아파치의 HttpClient를 사용하려면 아래와 같이 의존성을 추가해야 한다.


1
2
3
4
5
6
7
8
<dependencies>
    .. 생략 ..
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
    .. 생략 ..
</dependencies>


의존성을 추가하면 RestTemplate의 설정을 더욱 쉽게 추가하고 변경할 수 있다. 아래를 통해 살펴보겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public RestTemplate restTemplate(){
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();

    HttpClient client = HttpClientBuilder.create()
            .setMaxConnTotal(500)
            .setMaxConnPerRoute(500)
            .build();

    CloseableHttpClient httpClient = HttpClients.custom()
            .setMaxConnTotal(500)
            .setMaxConnPerRoute(500)
            .build();

    factory.setHttpClient(httpClient);
    factory.setConnectTimeout(2000);
    factory.setReadTimeout(5000);

    RestTemplate restTemplate = new RestTemplate(factory);
    
    return restTemplate;
}


RestTemplate의 생성자를 보면 다음과 같이 ClientHttpRequestFactory를 매개변수로 받는 생성자가 존재한다


1
2
3
4
public RestTemplate(ClientHttpRequestFactory requestFactory) {
    this();
    this.setRequestFactory(requestFactory);
}


ClientHttpRequestFactory는 함수형 인터페이스(functional interface)로, 대표적인 구현체로서 SimpleClieentHttpRequestFactoryHttpComponentsClientHttpRequestFactory가 있다. 별도의 구현체를 설정해서 전달하지 않으면 HttpAccessor에 구현돼 있는 내용에 의해 SimpleClientHttpRequestFactory를 사용하게 된다.

별도의 HttpComponentsClientHttpRequestFactory 객체를 생성해서 ClientHttpRequestFactory를 사용하면 15~16번 줄과 같이 RestTemplateTimeout 설정을 할 수 있다.

그리고 HttpComponentsClientHttpRequestFactory는 커넥션 풀을 설정하기 위해 HttpClientHttpComponentsClientHttpRequestFactory에 설정할 수 있다. HttpClient를 생성하는 방법은 두 가지가 있는데 4~7번 줄의 HttpClientBuilder.create() 메서드를 사용하거나 9~12번 줄의 HttpClients.custom() 메서드를 사용하는 것이다.

생성한 HttpClient는 14번 줄과 같이 factorysetHttpClient() 메서드를 통해 인자로 전달해서 설정할 수 있다. 이렇게 설정된 factory 객체를 RestTemplate을 초기화하는 과정에서 인자로 전달하면 된다.


4번 줄에서는 HttpClient 객체를 생성했고 9번 줄에서는 CloseableHttpClient를 생성했다. 두 객체는 비슷하면서도 기능 면에서 차이가 있다. 이 두객체의 차이를 비교해보면 커넥션에 대한 기초 지식까지 늘릴 수 있는 기회가 될 것이다.

SpringBoot 카테고리 내 다른 글 보러가기

댓글남기기