개발 지식 기록/북스터디

[스프링 부트 핵심 가이드] 12. 서버 간 통신

엉망진창좌충우돌 2023. 10. 8. 05:01

본 게시글은 '스프링 부트 핵심 가이드' 책의 내용을 정리한 것입니다.

저자 : 장정우

출판사 : 위키북스


최근 마이크로서비스 아키텍처(MSA)의 사용이 많아지고 있다. MSA는 애플리케이션이 가지고 있는 기능(서비스)이 하나의 비즈니스 범위만 가지는 형태이다. 각 애플리케이션은 자신이 가진 기능을 API로 외부에 노출하고, 다른 서버가 그러한 API를 호출해서 사용할 수 있게 구성되므로 각 서버가 다른 서버의 클라이언트가 되는 경우도 많다.

RestTemplate과 WebClient는 다른 서버로 웹 요청을 보내고 응답을 받을 수 있게 도와준다.

 

12.1 RestTemplate이란?

 

RestTemplate은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다. 이를 통해 RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있다. RestTemplate은 기본적으로 동기 방식으로 처리되며, 비동기 방식으로 사용하고 싶을 경우 AsyncRestTemplate을 사용하면 된다. RestTemplate은 현업에서 많이 쓰이나 지원 중단된 상태이니 WebClient 방식도 함께 알아야 한다.

 

RestTemplate의 특징

  • HTTP 프로토콜의 메서드에 맞는 여러 메서드를 제공한다.
  • RESTful 형식을 갖춘 템플릿이다.
  • HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답을 받을 수 있다.
  • 블로킹(blocking) I/O 기반의 동기 방식으로 사용한다.
  • 다른 API를 호출할 때 HTTP 헤더에 다양한 값을 설정할 수 있다.

 

 

12.1.1 RestTemplate의 동작 원리

 

애플리케이션 코드 구현부에 RestTemplate을 선언하고 URI와 HTTP 메서드, Body 등을 설정한다. 그리고 외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메시지로 변환한다.

RestTemplate에서 변환된 요청 메시지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보낸다.

외부에서 요청에 대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리한다.

받은 응답 데이터가 정상적이라면 다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환해서 애플리케이션으로 반환한다.

 

12.1.2 RestTemplate의 대표적인 메서드

 

메서드 HTTP 형태 설명
getForObject GET GET 형식으로 요청한 결과를 객체로 반환
getForEntity GET GET 형식으로 요청한 결과를 ResponseEntity 형식으로 반환
postForLocation POST POST 형식으로 요청한 결과를 헤더에 저장된 URI로 반환
postForObject POST POST 형식으로 요청한 결과를 객체로 반환
postForEntity POST POST 형식으로 요청한 결과를 ResponseEntity 형식으로 반환
delete DELETE DELETE 형식으로 요청
put PUT PUT 형식으로 요청
patchForObject PATCH PATCH 형식으로 요청한 결과를 객체로 반환
optionsForAllow OPTIONS 해당 URI에서 지원하는 HTTP 메서드를 조회
exchange any HTTP 헤더를 임의로 추가할 수 있고, 어떤 메서드 형식에서도 사용할 수 있음
execute any 요청과 응답에 대한 콜백을 수정

 

 

12.2 RestTemplate 사용하기

 

12.2.1 서버 프로젝트 생성하기

 

서버 용도의 프로젝트를 생성한다. 동일 컴퓨터에서 두 개의 프로젝트를 가동해야 하기에 톰캣의 포트를 변경한다.

spring-boot-starter-web 모듈만 의존성으로 추가하고 프로젝트를 생성한다.

 

컨트롤러를 작성한다.

@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 MemberDto 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);
    }
}

GET과 POST 메서드 형식의 요청을 받기 위한 코드다.

처음 3개의 메서드는 GET 형식의 요청이 들어오는 상황의 케이스를 구현한 것이다. 첫 번째 메서드는 아무 파라미터가 없는 경우, 두 번째는 PathVariable을 사용하는 경우, 세 번째는 RequestParameter를 사용하는 경우다.

그 이후 메서드는 POST 형식의 요청을 받기 위한 메서드이다. 첫 번째 메서드는 Request Parameter와 Request Body를 함께 받고, 두 번째 메서드는 임의의 HTTP 헤더를 받는 메서드이다.

 

여기서 사용된 MemberDto 클래스는 name, email, organization 필드를 가지고 있다.

 

12.2.2 RestTemplate 구현하기

 

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

 

실습을 위해 구성한 프로젝트 설정은 다음과 같다.

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

+ SwaggerConfiguration 클래스 생성 & pom.xml에 의존성 추가

 

클라이언트로부터 요청을 받는 컨트롤러와, RestTemplate을 활용해 다른 서버에 통신 요청을 하는 서비스 계층으로 구성한다.

 

GET 형식의 RestTemplate 작성하기

 

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 형식으로 만드는 기능을 수행한다.

첫 메서드는 PathVariable이나 파라미터를 사용하지 않는 호출 방법이다. UriComponentsBuilder는 빌더 형식으로 객체를 생성한다. fromUriString() 메서드에서는 호출부의 URL을 입력하고, 이어서 path() 메서드에 세부 경로를 입력한다. encode() 메서드는 인코딩 문자셋을 설정할 수 있는데, 인자를 전달하지 않으면 기본적으로 UTF-8로 설정된다.

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

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

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

파라미터로 전달하는 경우 queryParam() 메서드를 사용해 (키, 값) 형식으로 파라미터를 추가할 수 있다.

 

POST 형식의 RestTemplate 작성

 

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("organization", "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/add-header")
        .encode()
        .build()
        .toUri();

    MemberDto memberDTO = new MemberDto();
    memberDTO.setName("flature");
    memberDTO.setEmail("flature@wikibooks.co.kr");
    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;
}

첫 메서드는 Body 값과 파라미터 값을 사용하는 경우이다. RequestBody에 값을 담기 위해서는 데이터 객체를 생성하고, postForEntity() 메서드를 사용할 경우 파라미터로 데이터 객체를 넣으면 된다.

 

서비스 코드를 연결하는 컨트롤러 코드를 다음과 같이 작성한다.

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

    @GetMapping("/parameter")
    public String getNameWithParameter(){
        return restTemplateService.getNameWithParameter();
    }

    @PostMapping
    public ResponseEntity<MemberDto> postDto(){
        return restTemplateService.postWithParamAndBody();
    }

    @PostMapping("/header")
    public ResponseEntity<MemberDto> postWithHeader(){
        return restTemplateService.postWithHeader();
    }

}

postDto() 메서드에 해당하는 POST API를 호출하면 서버 프로젝트가 파라미터 값과 Body 값을 받은 것을 확인할 수 있다.

서버 프로젝트에 찍힌 파라미터 값

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

RestTemplate의 exchange() 메서드는 모든 형식의 HTTP 요청을 생성할 수 있다. RequestEntity 설정에서 post() 메서드 대신 다른 형식의 메서드로 정의만 하면 exchange() 메서드로 쉽게 사용할 수 있기 때문에 대부분 exchange() 메서드를 사용한다.

 

12.2.3 RestTemplate 커스텀 설정

 

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

 

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

 

HttpClinet 사용을 위해 다음과 같이 의존성을 추가한다.

<dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

    </dependencies>

 

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를 매개변수로 받는 생성자가 존재한다. ClientHttpRequestFactory는 함수형 인터페이스로, SimpleClientHttpRequestFactory와 HttpComponentsClientHttpRequestFactory가 대표적인 구현체이다. 별도의 구현체를 설정해서 전달하지 않으면 HttpAccessor에 구현돼 있는 내용에 의해 SimpleClientHttpRequestFactory를 사용한다.

HttpComponentsClientHttpRequestFactory 객체를 생성해서 ClientHttpRequestFactory를 사용하면 Timeout 설정을 할 수 있다.

또한 HttpComponentsClientHttpRequestFactory는 커넥션 풀을 설정하기 위해 HttpClient를 HttpComponentsClientHttpRequestFactory에 설정할 수 있다. HttpClient를 생성하는 방법은 HttpClientBuilder.create() 메서드를 사용하거나 HttpClients.custom() 메서드를 사용하는 방법이 있다.

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

 

 

12.3 WebClient란?

 

RestTemplate은 지원 중단되어 WebClient를 알고 있는 것이 좋다. Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다. WebClient는 리액터(Reactor) 기반으로 동작하는 API다. 리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있다.

WebClient 특징

  • 논블로킹(Non-Blocking) I/O를 지원한다.
  • 리액티브 스트림(Reactive Streams)의 백 프레셔(Back Pressure)를 지원한다.
  • 적은 하드웨어 리소스로 동시성을 지원한다.
  • 함수형 API를 지원한다.
  • 동기, 비동기 상호작용을 지원한다.
  • 스트리밍을 지원한다.

 

 

12.3.1 WebClient 구성

 

WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 추가해야 한다.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
</dependencies>

WebFlux는 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 스프링 프레임워크 5에서 새롭게 추가된 모듈이다. 

 

 

12.4 WebClient 사용하기

 

12.4.1 WebClient 구현

 

WebClient를 생성하는 방법은 create() 메서드를 이용한 방법과 builder()를 이용한 방법이 있다.

 

다음 예제 코드를 통해 학습한다.

@Service
public class WebClientService {
    public String getName() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

        return webClient.get()
            .uri("/api/v1/crud-api")
            .retrieve()
            .bodyToMono(String.class)
            .block();
    }

    public String getNameWithPathVariable() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        ResponseEntity<String> responseEntity = webClient.get()
            .uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/{name}")
                .build("Flature"))
            .retrieve().toEntity(String.class).block();
    }

    public String getNameWithParameter() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        return webClient.get().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
                .queryParam("name", "Flature")
                .build())
            .exchangeToMono(clientResponse -> {
                if (clientResponse.statusCode().equals(HttpStatus.OK)) {
                    return clientResponse.bodyToMono(String.class);
                } else {
                    return clientResponse.createException().flatMap(Mono::error);
                }
            })
            .block();
    }
}

getName() 메서드는 builder()를 활용해 WebClient를 생성하고 다른 메서드는 create()를 활용해 WebClient를 생성한다.

 

WebClient는 객체를 생성한 후 요청을 전달하는 방식으로 동작한다. builder()를 이용하는 경우 baseUrl() 메서드에서 기본 URL을 설정하고 defaultHeader() 메서드로 헤더의 값을 설정하는 것을 볼 수 있다. 

builder()를 사용할 경우 확장할 수 있는 메서드는 다음과 같다.

  • defaultHeader() : WebClient의 기본 헤더 설정
  • defaultCookie() : WebClient의 기본 쿠키 설정
  • defaultUriVariable() : WebClient의 기본 URI 확장값 설정
  • filter() : WebClient에서 발생하는 요청에 대한 필터 설정

 

일반적으로 WebClient 객체를 이용할 때는 WebClient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋다.

일단 빌드된 WebClient는 변경할 수 없다. 다만 복사해서 사용할 수 있다.

WebClient webClient = WebClient.create("http://localhost:9090");
WebClient clone = webClientmutate().build();

리턴값을 보면 get() 메서드를 사용하고 있는 것을 볼 수 있다. WebClient는 HTTP 메서드를 get(), post(), put(), delete() 등 메서드로 설정할 수 있다. 그리고 URI를 확장하는 방법으로 uri() 메서드를 사용할 수 있다.

retrieve() 메서드는 요청에 대한 응답을 받았을 때 그 값을 추출하는 방법 중 하나이다. retrieve() 메서드는 bodyToMono() 메서드를 통해 리턴 타입을 설정한다.

WebClinet가 기본적으로 논블로킹 방식이기에 block() 메서드를 통해 블로킹 형식으로 동작하게 할 수 있다.

 

두 번째 메서드는 PathVariable 값을 추가해 요청을 보내는 메서드이다. uri() 메서드 내부에서 uriBuilder를 사용해 path를 설정하고 build() 메서드에 추가할 값을 넣는다. 다음처럼 간략하게 작성할 수도 있다.

ResponseEntity<String> responseEntity1 = webClient.get()
    .uri("/api/v1/crud-api/{name}", "Flature")
    .retrieve()
    .toEntity(String.class)
    .block();

return responseEntity.getBody();

toEntity()를 사용하면 ResponseEntity 타입으로 응답을 전달받을 수 있다.

 

세 번째 메서드는 쿼리 파라미터를 함께 전달하는 메서드이다. uriBuilder를 사용하며, queryParam() 메서드를 사용해 전달하려는 값을 설정한다. exchange() 메서드는 지원 중단 됐기에 exchangeToMono() 또는 exchangeToFlux()를 사용해야 한다. exchange()는 응답 결과 코드에 따라 응답을 다르게 설정할 수 있다.

 

@Service
public class WebClientService {
	public ResponseEntity<MemberDto> postWithParamAndBody() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

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

        return webClient.post().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
                .queryParam("name", "Flature")
                .queryParam("email", "flature@wikibooks.co.kr")
                .queryParam("organization", "Wikibooks")
                .build())
            .bodyValue(memberDTO)
            .retrieve()
            .toEntity(MemberDto.class)
            .block();
    }

    public ResponseEntity<MemberDto> postWithHeader() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

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

        return webClient
            .post()
            .uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/add-header")
                .build())
            .bodyValue(memberDTO)
            .header("my-header", "Wikibooks API")
            .retrieve()
            .toEntity(MemberDto.class)
            .block();
    }

    public void cloneWebClient() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        WebClient clone = webClient.mutate().build();
    }
}

POST 요청 또한 전체적으로 GET 요청과 다르지 않다.

하지만 HTTP 바디 값을 담는 방법과 커스텀 헤더를 추가하는 방법은 학습해야 한다.

두 번째 메서드를 보면 bodyValue() 메서드를 통해 HTTP 바디 값을 설정한다. HTTP 바디에는 일반적으로 데이터 객체(DTO, VO 등)를 파라미터로 전달한다.

세 번째 메서드를 보면 header() 메서드를 통해 헤더에 값을 추가했음을 볼 수 있다. 일반적으로 임의로 추가한 헤더에는 외부 API 사용을 위한 인증된 토큰값을 담아 전달한다.