Today
-
Yesterday
-
Total
-
  • 스프링 부트 핵심 가이드 7주차: 11-12
    책 공부/스프링 부트 핵심 가이드 2023. 12. 7. 00:01

    📖 책 정보

    모바일 가이드

    11장 액추에이터 활용하기

    1. 엔드포인트
    2. 액추에이터 기능 살펴보기
    3. 커스텀 기능 만들기

    12장 서버 간 통신

    1. RestTemplate?
    2. RestTemplate 사용하기
    3. WebClient?
    4. WebClient 사용하기



    11장 액추에이터 활용하기

    1. 프로젝트에 종속성 추가 , 엔드포인트
    2. 액추에이터 기능 살펴보기
    3. 액추에이터 커스텀 기능 만들기



    1. 프로젝트에 종속성 추가 , 엔드포인트

    애플리케이션을

    개발하는 단계를 지나,

    운영 단계에 접어들면


    애플리케이션이 정상적으로 동작하는지

    모니터링하는 환경을 구축하는 것이

    매우 중요해진다.


    스프링 부트 액추에이터는

    HTTP 엔드포인트나 JMX를 활용해

    애플리케이션을 모니터링하고 관리할 수 있는 기능을 제공한다.

    👉 JMX 란?
    Java Management Extensions JMX 는 
    
    실행 중인 애플리케이션의 상태를
    
    모니터링하고 설정을 변경할 수 있게 해주는 API 이다.
    
    JMX를 통해 리소스 관리를 하려면 
    
    MBeans ( Managed Beans)를 생성해야 한다.
    

    - 종속성 추가

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-actuator'
    }

    이전에 사용하던 프로젝트에서 계속 실습!


    추가적으로

    swagger2 가 spring-boot-starter-actuator와 호환되지 않는다는 빅 이슈가 있엇다

    https://stackoverflow.com/questions/70036953/spring-boot-2-6-0-spring-fox-3-failed-to-start-bean-documentationpluginsboo


    swagger2를 지우고 springdoc 을 추가해주어서 해결하였다. springdoc도 swagger처럼 많은 설정을 해줄 수 있으나, 해주지 않아도 사용 가능!

    1. swagger2 종속성 제거 후 springdoc 종속성 추가
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
    
    1. 기존에 작성했던 swagger2 관련 파일 제거

    3. application 설정파일의 pathmatch 설정 제거 ```yml spring: mvc: pathmatch: matching-strategy: ant_path_matcher ```
    1. 실행 후 swagger ui 진입 경로 localhost:8080/swagger-ui/index.html

    - 엔드포인트

    액추에이터의 엔드포인트는

    애플리케이션의 모니터링을 사용하는 경로이다.


    스프링 부트에는

    여러 내장 엔드포인트가 포함돼있으며

    커스텀 엔트포인트를 추가할 수도 있다.


    액추에이터를 추가하면

    기본적으로

    엔드포인트 URL로 /actuator 가 추가되며,

    이 뒤에 경로를 추가해

    상세 내역에 접근한다.


    만약,

    /actuator 가 아닌 다른 경로를 사용하고 싶다면

    application.properties , application.yml 파일에

    다음과 같이 작성해준다.

    management:
      endpoints:
        web:
          base-path: /custom-path

    자주 사용되는 액추에이터의 엔드포인트는 다음과 같다.


    ID 설명
    auditevents

    AuditEventRepository 빈이 필요.
    호출된 Audit 이벤트 정보를 표시
    beans 애플리케이션에 있는 모든 스프링 빈 리스트 표시
    caches 사용 가능한 캐시 표시
    conditions 자동 구성 조건 내역 생성
    configprops @ConfiguratopmProperties의 속성 리스트 표시
    env 애플리케이션에서 사용할 수 있는 환경 속성 표시
    health 애플리케이션의 상태 정보 표시
    httptrace

    가장 최근에 이뤄진 100건의 요청 기록 표시
    HttpTraceRepository 빈 필요.
    info 애플리케이션의 정보 표시
    integrationgraph

    스프링 통합 그래프 표시.
    spring-integration-core 모듈에 대한 의존성 추가 필요
    loggers 애플리케이션의 로거 구성을 표시, 수정
    metrics 애플리케이션의 메트릭 정보 표시
    mappings 모든 @RequestMapping의 매핑 정보 표시
    quartz Quartz 스케줄러 작업에 대한 정보 표시
    scheduledtasks 애플리케이션에서 예약된 작업 표시
    sessions



    스프링 세션 저장소에서
    사용자의 세션을 검색하고 삭제 할 수 있다.
    스프링 세션을 사용하는
    서블릿 기반 웹 애플리케이션이 필요하다.
    shutdown

    애플리케이션을 정상적으로 종료할 수 있다.
    디폴트 : 비활성화
    startup


    애플리케이션이 시작될 때 수집된 시작 단계 데이터를 표시한다.
    BufferingApplicationStartup으로 구성된
    스프링 애플리케이션이 필요하다.
    threaddump 스레드 덤프를 수행


    만약 Spring MVC, Spring WebFlux, Jersey를 사용한다면 추가로 다음과 같은 엔드포인트를 사용할 수 있다.

    ID 설명
    heapdump


    힙 덤프 파일을 반환.
    핫스팟 HotSpot VM 상에서 hprof 포맷의 파일이 봔환되며,
    OpenJ9JVM 에서는 PHD 포맷 파일을 반환한다.
    jolokia


    Jolokia가 클래스패스에 있을 때 HTTP를 통해 JMX 빈을 표시한다.
    jolokia-core 모듈에 대한 의존성 추가가 필요.
    WebFlux에서는 사용할 수 없다.
    logfile

    logging.file.name 또는 logging.file.path 속성이 설정돼 있는 경우
    로그 파일의 내용 반환
    Prometheus

    Prometheus 서버에서 스크랩할 수 있는 형식으로 메트릭 표시
    micrometer-registry-prometheus 모듈의 의존성 추가 필요



    엔드포인트는

    활성화 여부와 노출 여부를 설정할 수 있다.


    활성화는

    기능 자체를 활성화할 것인지를 결정하는 것으로,

    비활성화된 엔드포인트는

    애플리케이션 컨텍스트에서 완전히 제거된다.


    엔드포인트를 활성화하려면

    application 설정파일에

    액추에이터 속성을 추가하면 된다.

    management:
      endpoints:
        web:
          base-path: /act
          
      ## End point Active
      endpoint:
        shutdown:
          enabled: true
        caches:
          enabled: false

    이 설정은 엔드포인트 기본 경로를 /act로 변경하고,

    shutdown 기능은 활성화,

    caches 기능은 비활성화하겠다는 의미이다.


    또한,

    엑추에이터 설정을 통해

    기능 활성화 / 비활성화가 아니라

    엔드 포인트의 노출 여부만 설정하는 것도 가능하다.


    노출 여부는

    JMX를 통한 노출과

    HTTP를 통한 노출이 있어

    설정이 구분된다.

    HTTP 노출 설정

    management:
      endpoints:
        web:
          exposure:
            include: "*"
            exclude: "threaddump, heapdump"   

    JMX 노출 설정

    management:
      endpoints:
        jmx:
          exposure:
            include: "*"
            exclude: "threaddump, heapdump"

    위의 두 설정 모두

    엔드포인트를 전체적으로 노출하며,

    스레드 덤프와 힙 덤프 기능은 제외한다는 의미이다.


    엔드포인트는

    애플리케이션에 관한 민감한 정보를 포함하고 있기 때문에

    노출 설정을 신중하게 고려해야 한다.


    노출 설정에 대한 기본값은 다음과 같다.

    ID JMX WEB
    auditevents X
    beans X
    caches X
    conditions X
    configprops X
    env X
    health
    heapdump 해당 없음 X
    httptrace X
    info X
    integrationgraph X
    jolokia 해당 없음 X
    logfile 해당 없음 X
    loggers X
    liquibase X
    metrics X
    mappings X
    Prometheus 해당 없음 해당 없음
    quartz X
    scheduledtasks X
    sessions X
    shutdown X
    startup X
    threaddump X



        🐢🎈


    2. 액추에이터 기능 살펴보기

    액추에이터를 활성화하고

    노출 지점도 설정하고 나면

    애플리케이션에서 해당 기능을 사용할 수 있다.


    모든 기능을 살펴보기 위해서는

    다른 의존성을 추가하거나, 설정을 추가해야 하기 때문에

    이번 실습에서는 기본 제공 기능 위주로 살펴본다.


    - 앱 기본 정보 /info

    가동 중인 애플리케이션의 정보를 볼 수 있다. 스프링 부트 2.5버전까진 그랬음

    이후 버전에서는 /info 에 대한 정보를 노출시키지 않는다고 공식문서에 나와있다고 한다.

    참고한 블로그


    - 앱 상태 /health

    /health를 이용하면

    애플리케이션의 상태를 확인할 수 있다.

    별도의 설정 없이 아래 경로로 접근 가능

    http://localhost:8080/actuator/health

    접속하면 아래와 같은 결과를 만남

    나타날 수 있는 status의 값으로는

    • UP
    • DOWN
    • UNKNOWN
    • OUT_OF_SERVICE

    가 있다.


    이 결과는 네트워크 계층 중 L4 Loadbalancing 레벨에서

    애플리케이션의 상태를 확인하기 위해 사용된다.


    만약 상세 정보를 확인하고 싶다면

    아래와 같이 설정할 수 있다.

    management:
      endpoint:
        health:
          show-details: always

    설정 후 실행 화면

    이때 ,

    설정값으로 입력 할수 있는 것으로

    • never (default) : 세부사항 표시 안함

    • when-authorized : 승인된 사용자에게만 세부상태 표시. 확인 권한은 application 설정파일에 추가한 management.endpoint.health.roles 속성으로 부여할 수 있다.

    • always : 모든 사용자에게 세부 정보 표시


    그리고 status 정보에 대해서 확인할 것은

    모든 status의 값이 UP 상태 일 때만

    어플리케이션의 상태가 UP으로 표시된다.


    하나라도 DOWN 상태인 항목이 있다면

    어플리케이션의 상태도 DOWN 으로 표기되며

    HTTP 상태 코드도 변경된다.


    - 빈 정보 확인 /beans

    스프링 컨테이너에 등록된 스프링 빈의 전체 목록을 표시할 수 있다.

    JSON 형식으로 빈의 정보를 반환한다.


    다만,

    스프링은 워낙 많은 빈이 자동으로 등록되어 운영되기 때문에

    관련 정보를 출력해서 육안으로 내용을 파악하기는 어렵다.

    http://localhost:8080/act/beans
    

    스크롤이 아주 조그마해졌다.ㅎㄷㄷ


    - 스프링 부트 자동 설정 내역 확인 /conditions

    스프링 부트의 자동설정 조건 내역 확인

    http://localhost:8080/act/conditions
    

    스크롤 크기가 beans 조회할 때와 맞먹는다. ㅎㄷㄷ


    여기서 크롬 라이브러리를 이용해 예쁘게 출력해본 뒤 축소시켜 보면

    이렇게 볼 수 있는데,

    자동설정의 @Conditional 에 따라 평가되어

    positiveMatches 와

    negativeMatches 속성으로 구분된다고 한다.


    - 스프링 환경변수 정보 /env

    스프링의 환경변수 정보를 확인하는 데 사용된다.

    http://localhost:8080/act/env
    

    기본적으로

    application.propeties 파일의 변수들이 표시되며,

    OS, JVM의 환경변수도 함께 표시된다.


    - 로깅 레벨 확인 /loggers

    애플리케이션의 로깅 레벨 수준이 어떻게 되어있는지 확인

    역시 출력 결과가 매우 많다 ㅎㄷㄷ

    http://localhost:8080/act/loggers
    

    post 형식으로 호출하면

    로깅 레벨을 변경하는 것도 가능하다.



        🐢🎈


    3. 액추에이터 커스텀 기능 만들기

    기본 제공 외에

    개발자의 요구사항에 맞춘 커스텀 기능 설정도 할 수 있다.


    커스텀 기능을 개발하는 방식에는 크게 두 가지가 있다.


    첫 번째는

    기존 기능에 내용을 추가하는 방식이고,


    두 번째는

    새로운 엔드포인트를 개발하는 방식이다.


    - 정보 제공 인터페이스 구현체 생성

    application 설정파일에 내용을 추가하는 방법은

    스프링 부트 2.6 버전부터는 먹히지 않는다.


    다른 방법으로,

    커스텀 기능을 설정할 때

    별도의 구현체 클래스를 작성해서

    내용을 추가하는 방법을 사용할 수 있다.


    액추에이터에서는

    InfoContributor 인터페이스를 제공하고 있는데,

    이 인터페이스를 구현하는 클래스를 생성하면 된다.


    CustomInfoContributor.java

    파일 생성 경로 : config/actuator/

    @Component
    public class CustomInfoContributor implements InfoContributor {
    
        @Override
        public void contribute(Info.Builder builder) {
            Map<String, Object> content = new HashMap<>();
            content.put("code-info", "InfoContributor 구현체에서 정의한 정보입니다.");
            builder.withDetail("custom-info-contributor", content);
        }
    }

    새로 생성한 CustomInfoContributor 클래스를

    InfoContributor 인터페이스의 구현체로 설정하면

    contribute() 메서드를 오버라이드 할 수 있다.


    이 메서드에서 파라미터로 받는 Builder 객체

    액추에이터 패키지의 Info 클래스 안에 정의돼 있는 클래스로서

    Info 엔드포인트에서 보여줄 내용을 담는 역할 을 수행한다.


    이제 어플리케이션을 재가동 하고

    http://localhost:8080/actuator/info

    경로로 들어가보면

    위에서 생성한 클래스의 정보가 출력되는 것을 확인할 수 있다.


    - 커스텀 엔드포인트 생성

    @Endpoint 어노테이션으로 빈에 추가된 객체들은

    @ReadOperation , @WriteOperation , @DeleteOperation 어노테이션을 사용 ,

    JMX나 HTTP를 통해

    커스텀 엔드포인트를 노출시킬 수 있다.


    만약, JMX에서만 사용하거나 HTTP에서만 사용하는 것으로 제한하고 싶다면

    @JmxEndpoint , @WebEndpoint 어노테이션을 사용하면 된다.


    지금은 간단하게

    애플리케이션에

    메모 기록을 남길 수 있는 기능을

    엔드포인트로 생성하는 실습을 한다.

    NoteEndpoint

    파일 생성 경로 : config/actuator/

    @Component
    @Endpoint(id = "note") 
    public class NoteEndpoint {
        private Map<String, Object> noteContent = new HashMap<>();
    
        @ReadOperation
        public Map<String, Object> getNote() {
            return noteContent;
        }
    
        @WriteOperation
        public Map<String, Object> writeNote(String key, Object value) {
            noteContent.put(key, value);
            return noteContent;
        }
    
        @DeleteOperation
        public Map<String, Object> deleteNote(String key) {
            noteContent.remove(key);
            return noteContent;
        }
    }

    @Endpoint 어노테이션을 붙여주면 액추에이터에 엔드포인트로 자동으로 등록된다.

    추가할 수 있는 속성값으로는 idenableByDefault 가 있는데,

    id 는 해당 엔드포인트의 경로를 지정하는 속성이다.

    enableByDefault 는 해당 엔드포인트의 기본 활성화 여부를 설정하는 속성이다. 기본값이 true이다.


    엔드포인트를 설정하는 클래스에는

    @ReadOperation @WriteOperation @DeleteOperation 어노테이션을 사용해

    각 동작 메서드를 생성할 수 있다.


    @ReadOperation 은 HTTP의 GET 메서드에 반응하는 어노테이션이고,

    @WriteOperation 은 HTTP의 POST 메서드에 반응하는 어노테이션이며,

    @DeleteOperation 은 HTTP의 DELETE 메서드에 반응하는 어노테이션이다.


    어플리케이션 재가동 후 Talend API Tester를 통해 테스트!


    1. Read 확인

    http://localhost:8080/act/note


    2. Write 확인

    json 데이터 형식으로 보내며, 메서드의 파라미터로 key, value 변수를 정의했기 때문에 네이밍을 지켜준다.


    3. Read 확인

    GET 요청을 보내, ReadOperation을 확인해보면 Write로 보낸 값이 잘 나타난다.


    4. Delete 확인

    Query 파라미터에 key = 입력했던값 형태로 값을 넣어 호출한다.

    실행 결과로

    key가 삭제되어 빈 객체만 보여진다.



        🐢🎈


    12장 서버 간 통신

    1. RestTemplate
    2. WebClient

    일단 지난번에 정리해둔 링크 첨부../_\ RestTemplate

    1. RestTemplate?

    RestTemplate는 다음과 같은 특징을 가진다.

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

    - 동작 원리

    RestTemplate.class

    package org.springframework.web.client;
    
    import org.springframework.http.converter.`HttpMessageConverter`;
    import org.springframework.http.`RequestEntity`;
    import org.springframework.http.`ResponseEntity`;
    import org.springframework.http.client.`ClientHttpRequestFactory`;
    import org.springframework.http.client.`ClientHttpRequest`;
    import org.springframework.http.client.`ClientHttpResponse`;

    0. 애플리케이션에서 RestTemplate 작성

    RestTemplate을 선언하고,

    URI와 HTTP 메서드, Body 등을 설정 후

    RequestEntity 와 같은 Request 메서드를 통해 요청 로직을 작성한다.


    1~3. 애플리케이션 ---요청---> REST API

    RestTemplate 에서

    HttpMessageConverter를 통해

    RequestEntity 를 요청 메세지로 변환한다.


    4~5. RestTemplate ---요청메세지--> ClientHttpRequest

    RestTemplate에서는

    변환된 요청 메시지를

    ClientHttpRequestFactory를 통해

    CLientHttpRequest로 가져온 후 외부 API로 요청을 보낸다.


    6~8. 외부 API로부터 받은 응답 --RestTemplate--> ResponseErrorHandler

    외부에서 요청에 대한 응답을 받으면

    RestTemplate은 ResponseErrorHandler로 오류를 확인하고,

    오류가 있다면 ClientHttpResponse에서

    응답 데이터를 처리한다.


    8~11. 응답이 정상 --HttpMessageConverter--> 애플리케이션

    받은 응답 데이터가 정상적이라면

    다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환해서

    애플리케이션으로 반환한다.



    - 대표적인 메서드

    RestTemplate에서는 더욱 편리하게 외부 API로 요청을 보낼 수 있도록

    다음과 같은 다양한 메서드를 제공한다.

    메서드 HTTP 형태                      설명                     
    getForObject  GET 응답값을 객체로 반환
    getForEntity  GET 응답값을 ResponseEntity 형식으로 반환

    postForLocation  POST 응답값을 헤더에 저장된 URI로 반환
    postForObject POST 응답값을 객체로 반환
    postForEntity POST 응답값을 ResponseEntity 형식으로 반환

    delete DELETE DELETE 형식으로 요청
    put PUT PUT 형식으로 요청
    patchForObject PATCH PATCH 형식으로 요청한 결과를 객체로 반환

    optionsForAllow OPTIONS 해당 URI에서 지원하는 HTTP 메서드를 조회

     exchange 

    any

    HTTP 헤더를 임의로 추가할 수 있고,
    어떤 메서드 형식에서도 사용할 수 있음
    execute any 요청과 응답에 대한 콜백을 수정



        🐢🎈


    2. RestTemplate 사용하기

    응답을 보낼 서버 용도로 별도의 프로젝트를 하나 생성하고

    다른 프로젝트에서 RestTemplate을 통해 요청을 보내고 응답을 받아보는 방식으로 실습을 진행한다.


    나는 최근에 멀티 모듈 구동하는 방법을 학습해서

    멀티 모듈로 실습 !


    - 테스트할 Spring Boot 프로젝트 생성하기

    spring boot 3.1.1로 생성하였다.

    의존성은 web , lombok 두 개면 충분하다.


    그리고 나의 경우 최근에 멀티 모듈 구동하는 방법을 학습하여 멀티 모듈 형태로 실습해보고자 한다!

    우선 spring boot 프로젝트 이름은 restTemplate로 하고, 하위 모듈로 springBoxrest . 그리고 위의 두 모듈에서 공통으로 사용될 파일을 관리할 common 모듈을 추가해주어

    프로젝트 구성을 다음과 같이 만들었다.

    restTemplate 는 모듈들을 관리할 Spring boot 프로젝트 이고,

    serverBoxrest 는 rest template 테스트시 서버와 클라이언트로 사용할 하위 모듈이다.

    common 에는 serverBox 와 rest 에서 공유할 dto 파일을 작성할 것이다.


    멀티 모듈 생성 및 실행 방법은 아래 링크 참고! -> Spring boot 멀티 모듈 프로젝트 만들기


    - common 모듈에 MemberDto 파일 작성하기

    serverBox 모듈과 rest 모듈에서 공통으로 사용할 dto 파일을 미리 작성!

    @Getter @Setter
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class MemberDto {
    
        private String name;
        private String email;
        private String organization;
    
    }

    - serverBox 모듈 작성하기

    하나의 컴퓨터에서 서버와 클라이언트.

    2개의 톰캣을 구동해야 하기 때문에 포트 번호를 변경해준다.

    포트 번호 변경은 resources 하위에 있는 application 설정파일에 다음과 같이 작성해주면 된다.

    server:
      port: 8888

    그리고 controller 에는

    GET과 POST 메서드 형식의

    요청을 받기 위한 코드를 작성한다.

    @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> getMamber(
                @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());
    
            return ResponseEntity.ok(
                    MemberDto.builder()
                            .name(name)
                            .email(email)
                            .organization(organization).build()
            );
        }
        
        @PostMapping("/add-header")
        public ResponseEntity<MemberDto> addHeader(
                @RequestHeader("my-header") String header, // 임의의 http header 받기
                @RequestBody MemberDto memberDto
        ) {
            System.out.println(header);
            
            return ResponseEntity.ok(memberDto);
        }
    }

    - rest 모듈에서 RestTemplate 구현하기

    일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나

    서비스 또는 비즈니스 계층에 구현된다.


    앞서 생성한 서버 프로젝트에 요청을 날리기 위해

    서버의 역할을 수행하면서

    다른 서버로 요청을 보내는

    클라이언트의 역할도 수행하는

    rest 모듈을 작성한다.

    GET 형식의 RestTemplate 작성하기

    @Service
    public class RestTemplateService {
        private final String BASE_PATH = "http://localhost:8888";
    
        /**
         * GET 매서드 요청
         */
        public String getName() {
            URI uri = UriComponentsBuilder
                    .fromUriString(BASE_PATH)
                    .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(BASE_PATH)
                    .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(BASE_PATH)
                    .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();
        }
    }

    POST 형식의 RestTemplate 작성

    @Service
    public class RestTemplateService {
        private final String BASE_PATH = "http://localhost:8888";
        
        /**
         * POST 매서드 요청
         */
        public ResponseEntity<MemberDto> postWithParamAndBody() {
            URI uri = UriComponentsBuilder
                    .fromUriString(BASE_PATH)
                    .path("/api/v1/crud-api")
                    .queryParam("name", "Flature")
                    .queryParam("email", "flature@wikibooks.co.kr")
                    .queryParam("organization", "Wikibookx")
                    .encode()
                    .build()
                    .toUri();
    
            return new RestTemplate().postForEntity(
                    uri,
                    MemberDto.builder()
                            .name("falture!!")
                            .email("flature@gmail.com")
                            .organization("around hub studio")
                            .build(),
                    MemberDto.class
            );
        }
    
        public ResponseEntity<MemberDto> postWithHeader() {
            URI uri = UriComponentsBuilder
                    .fromUriString(BASE_PATH)
                    .path("/api/v1/crud-api/add-header")
                    .encode()
                    .build()
                    .toUri();
    
            RequestEntity<MemberDto> requestEntity = RequestEntity
                    .post(uri)
                    .header("my-header", "wikibooks api")
                    .body(
                        MemberDto.builder()
                                .name("falture!!")
                                .email("flature@gmail.com")
                                .organization("around hub studio")
                                .build()
                    );
    
            return new RestTemplate().exchange(
                    requestEntity,
                    MemberDto.class
            );
        }
    }

    그리고

    쉽게 api를 호출할 수 있게 Swagger (springdoc) 을 설정해준다.

    루트 프로젝트인 restTemplatebuild.gradle을 열어서

    project(':rest') { } 블럭 안에 의존성 추가후 gradle만 build 시켜준다.

    project(':rest') {
        bootJar { enabled = true }
        jar { enabled = false }
    
        dependencies {
            implementation project(':common')
            
            // springdoc 
            implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
        }
    }

    서비스 코드를 연결하는 컨트롤러 작성

    @RequiredArgsConstructor
    @RestController
    @RequestMapping("/rest-template")
    public class RestTemplateController {
    
        private final RestTemplateService service;
    
        @GetMapping
        public String getName() {
            return service.getName();
        }
    
        @GetMapping("/path-variable")
        public String getNameWithPathVariable() {
            return service.getNameWithPathVariable();
        }
    
        @GetMapping("/parameter")
        public String getNameWithParameter() {
            return service.getNameWithParameter();
        }
    
        @PostMapping
        public ResponseEntity<MemberDto> postDto() {
            return service.postWithParamAndBody();
        }
    
        @PostMapping("/header")
        public ResponseEntity<MemberDto> postWithHeader() {
            return service.postWithHeader();
        }
    }

    이제 rest 모듈과 serverBox를 둘다 실행하고, (처음 실행 시 각각의 실행 Application 파일에서 아래의 녹색 화살표 눌러줘야 됨)

    swagger ui에 접속한다. localhost:8008/swagger-ui/index.html


    postDto() 메서드에 해당하는 POST API 호출 !



        🐢🎈


    3. WebClient?

    일반적으로

    실제 운영 환경에 적용되는 애플리케이션은

    정식 버전으로 출시된 스프링 부트의 버전보다 낮은 경우가 많다.


    그렇기 때문에 RestTemplate를 많이 사용하고 있다.


    하지만

    최신 버전에서는 WebClient를 사용할 것을 권고하고 있다.


    이러한 흐름에 맞춰

    현재 빈번히 사용되고 있는 RestTemplate와

    앞으로 많이 사용될 WebClient를 모두 알고 있는 것이 좋다.


    spring WebFlux는

    HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다.

    WebClient는 리액터 Reactor 기반으로 동작하는 API이다.


    리액터 기반이므로

    스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있다.


    WebClient의 특징을 먼저 살펴본다면 다음과 같다.

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

    최근 프로그래밍 추세에 맞춰

    스프링에도 리액티브 프로그래밍이 도입되면서

    여러 동시적 기능이 제공되고 있다.


    다만, 이 책에서는 리액티브 프로그래밍을 자세히 다루지 않으며,

    WebClient를 사용할 수 있는 환경을 구성하고

    사용하는 방법에 대해서만 다룰 예정이다.


    - WebClient 구성

    WebClient를 사용하려면

    WebFlux 모듈에 대한 의존성을 추가해야 한다.

    implementation 'org.springframework.boot:spring-boot-starter-webflux'

    나의 경우

    루트 프로젝트(restTemplate)의 build.gradle의 rest 모듈 설정 부분에 추가해주었다.

    project(':rest') {
        bootJar { enabled = true }
        jar { enabled = false }
    
        dependencies {
            implementation project(':commons')
    
            // springdoc
            implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
    
            // webflux
            implementation 'org.springframework.boot:spring-boot-starter-webflux'
        }
    }

    WebFlux는

    클라이언트와 서버 간

    리액티브 애플리케이션 개발을 지원하기 위해

    스프링 프레임워크5에서 새롭게 추가된 모듈이다.

        🐢🎈


    4. WebClient 사용하기

    - WebClient 구현

    WebClient를 생성하는 방법은 다음과 같이 크게 두 가지가 있다.

    • create() 메서드를 이용한 생성
    • builder() 를 이용한 생성

    먼저,

    RestTemplate 실습시 작성했던

    serverBox 모듈의

    GET , POST 메서드 컨트롤러에 접근할 수 있는 WebClient를 생성한다.

    WebClientService

    @Service
    public class WebClientService {
        private final String BASE_PATH = "http://localhost:8888";
    
        /**
         * GET 메서드 요청 
         */
        public String getName() {
            WebClient webClient = WebClient.builder()
                    .baseUrl(BASE_PATH)
                    .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(BASE_PATH);
    
            ResponseEntity<String> responseEntity = webClient
                    .get() // get method
                    .uri(uriBuilder ->
                            uriBuilder
                                    .path("/api/v1/crud-api/{name}")
                                    .build("flature"))
                    .retrieve() // 요청에 대한 응답을 받았을 때 그값을 추출하는 방법 중 하나!
                    .toEntity(String.class)
                    .block(); // 블로킹 형식으로 동작하게 하는 설정
    
            return responseEntity.getBody();
        }
    
        public String getNameWithParameter() {
            WebClient webClient = WebClient.create(BASE_PATH);
    
            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();
        }
    
        /**
         * POST 메서드 요청
         */
        public ResponseEntity<MemberDto> postWithParamAndBody() {
            WebClient webClient = WebClient.builder()
                    .baseUrl(BASE_PATH)
                    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                    .build();
    
            MemberDto memberDto = MemberDto.builder()
                    .name("zhyun")
                    .email("zhyun@wlgus.kim")
                    .organization("organization")
                    .build();
    
            return webClient
                    .post()
                    .uri(uriBuilder ->
                            uriBuilder.path("/api/v1/crud-api")
                                    .queryParam("name", "zhyun kim")
                                    .queryParam("email", "gimwlgus@kakao.com")
                                    .queryParam("organization", "wikibooks")
                                    .build())
                    .bodyValue(memberDto) // HTTP Body 에 값 추가
                    .retrieve()
                    .toEntity(MemberDto.class)
                    .block();
        }
    
        public ResponseEntity<MemberDto> postWithHeader() {
            WebClient webClient = WebClient.builder()
                    .baseUrl(BASE_PATH)
                    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                    .build();
    
            MemberDto memberDto = MemberDto.builder()
                    .name("zhyun")
                    .email("zhyun@wlgus.kim")
                    .organization("organization")
                    .build();
    
            return webClient
                    .post()
                    .uri(uriBuilder ->
                            uriBuilder.path(BASE_PATH).build())
                    .bodyValue(memberDto) // HTTP Body 에 값 추가
                    .header("my-header", "🐢💨") // 커스텀 Header 값 추가
                    .retrieve()
                    .toEntity(MemberDto.class)
                    .block();
    
        }
    }

    WebClientController

    @RequiredArgsConstructor
    @RestController
    @RequestMapping("/web-client")
    public class WebClientController {
    
        private final WebClientService service;
    
        @GetMapping
        public String getName() {
            return service.getName();
        }
    
        @GetMapping("/path-variable")
        public String getNameWithPathVariable() {
            return service.getNameWithPathVariable();
        }
    
        @GetMapping("/parameter")
        public String getNameWithParameter() {
            return service.getNameWithParameter();
        }
    
        @PostMapping
        public ResponseEntity<MemberDto> postDto() {
            return service.postWithParamAndBody();
        }
    
        @PostMapping("/header")
        public ResponseEntity<MemberDto> postWithHeader() {
            return service.postWithHeader();
        }
    }

    실행 확인



        🐢🎈


Designed by Tistory / Custom by 얼거스