- 프로젝트에 종속성 추가 , 엔드포인트
- 액추에이터 기능 살펴보기
- 액추에이터 커스텀 기능 만들기
애플리케이션을
개발하는 단계를 지나,
운영 단계에 접어들면
애플리케이션이 정상적으로 동작하는지
모니터링하는 환경을 구축하는 것이
매우 중요해진다.
스프링 부트 액추에이터는
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와 호환되지 않는다는 빅 이슈가 있엇다
swagger2를 지우고 springdoc 을 추가해주어서 해결하였다.
springdoc도 swagger처럼 많은 설정을 해줄 수 있으나, 해주지 않아도 사용 가능!
- swagger2 종속성 제거 후 springdoc 종속성 추가
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
- 기존에 작성했던 swagger2 관련 파일 제거
- application 설정파일의 pathmatch 설정 제거
spring: mvc: pathmatch: matching-strategy: ant_path_matcher
- 실행 후 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를 통한 노출이 있어
설정이 구분된다.
management:
endpoints:
web:
exposure:
include: "*"
exclude: "threaddump, heapdump"
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.5버전까진 그랬음
이후 버전에서는 /info 에 대한 정보를 노출시키지 않는다고 공식문서에 나와있다고 한다.
/health
를 이용하면
애플리케이션의 상태를 확인할 수 있다.
별도의 설정 없이 아래 경로로 접근 가능
http://localhost:8080/actuator/health
접속하면 아래와 같은 결과를 만남
나타날 수 있는 status의 값으로는
가 있다.
이 결과는 네트워크 계층 중 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 상태 코드도 변경된다.
스프링 컨테이너에 등록된 스프링 빈의 전체 목록을 표시할 수 있다.
JSON 형식으로 빈의 정보를 반환한다.
다만,
스프링은 워낙 많은 빈이 자동으로 등록되어 운영되기 때문에
관련 정보를 출력해서 육안으로 내용을 파악하기는 어렵다.
http://localhost:8080/act/beans
스크롤이 아주 조그마해졌다.ㅎㄷㄷ
스프링 부트의 자동설정 조건 내역 확인
http://localhost:8080/act/conditions
스크롤 크기가 beans 조회할 때와 맞먹는다. ㅎㄷㄷ
여기서 크롬 라이브러리를 이용해 예쁘게 출력해본 뒤 축소시켜 보면
이렇게 볼 수 있는데,
자동설정의 @Conditional 에 따라 평가되어
positiveMatches 와
negativeMatches 속성으로 구분된다고 한다.
스프링의 환경변수 정보를 확인하는 데 사용된다.
http://localhost:8080/act/env
기본적으로
application.propeties 파일의 변수들이 표시되며,
OS, JVM의 환경변수도 함께 표시된다.
애플리케이션의 로깅 레벨 수준이 어떻게 되어있는지 확인
역시 출력 결과가 매우 많다 ㅎㄷㄷ
http://localhost:8080/act/loggers
post 형식으로 호출하면
로깅 레벨을 변경하는 것도 가능하다.
_____________________🐢🎈____________________
기본 제공 외에
개발자의 요구사항에 맞춘 커스텀 기능 설정도 할 수 있다.
커스텀 기능을 개발하는 방식에는 크게 두 가지가 있다.
첫 번째는
기존 기능에 내용을 추가하는 방식이고,
두 번째는
새로운 엔드포인트를 개발하는 방식이다.
application 설정파일에 내용을 추가하는 방법은
스프링 부트 2.6 버전부터는 먹히지 않는다.
다른 방법으로,
커스텀 기능을 설정할 때
별도의 구현체 클래스를 작성해서
내용을 추가하는 방법을 사용할 수 있다.
액추에이터에서는
InfoContributor 인터페이스를 제공하고 있는데,
이 인터페이스를 구현하는 클래스를 생성하면 된다.
파일 생성 경로 : 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 어노테이션을 사용하면 된다.
지금은 간단하게
애플리케이션에
메모 기록을 남길 수 있는 기능을
엔드포인트로 생성하는 실습을 한다.
파일 생성 경로 : 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 어노테이션을 붙여주면
액추에이터에 엔드포인트로 자동으로 등록된다.
추가할 수 있는 속성값으로는 id
와 enableByDefault
가 있는데,
id
는 해당 엔드포인트의 경로를 지정하는 속성이다.
enableByDefault
는 해당 엔드포인트의 기본 활성화 여부를 설정하는 속성이다.
기본값이 true이다.
엔드포인트를 설정하는 클래스에는
@ReadOperation
@WriteOperation
@DeleteOperation 어노테이션을 사용해
각 동작 메서드를 생성할 수 있다.
@ReadOperation
은
HTTP의 GET
메서드에 반응하는 어노테이션이고,
@WriteOperation
은
HTTP의 POST
메서드에 반응하는 어노테이션이며,
@DeleteOperation
은
HTTP의 DELETE
메서드에 반응하는 어노테이션이다.
어플리케이션 재가동 후
Talend API Tester를 통해 테스트!
http://localhost:8080/act/note
json 데이터 형식으로 보내며,
메서드의 파라미터로 key, value 변수를 정의했기 때문에
네이밍을 지켜준다.
GET 요청을 보내,
ReadOperation을 확인해보면 Write로 보낸 값이 잘 나타난다.
Query 파라미터에
key = 입력했던값 형태로 값을 넣어 호출한다.
실행 결과로
key가 삭제되어 빈 객체만 보여진다.
_____________________🐢🎈____________________
- RestTemplate
- WebClient
일단 지난번에 정리해둔 링크 첨부../_\
RestTemplate
RestTemplate는 다음과 같은 특징을 가진다.
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`;
RestTemplate을 선언하고,
URI와 HTTP 메서드, Body 등을 설정 후
RequestEntity 와 같은 Request 메서드를 통해 요청 로직을 작성한다.
RestTemplate 에서
HttpMessageConverter를 통해
RequestEntity 를 요청 메세지로 변환한다.
RestTemplate에서는
변환된 요청 메시지를
ClientHttpRequestFactory를 통해
CLientHttpRequest로 가져온 후 외부 API로 요청을 보낸다.
외부에서 요청에 대한 응답을 받으면
RestTemplate은 ResponseErrorHandler로 오류를 확인하고,
오류가 있다면 ClientHttpResponse에서
응답 데이터를 처리한다.
받은 응답 데이터가 정상적이라면
다시 한번 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 | 요청과 응답에 대한 콜백을 수정 |
_____________________🐢🎈____________________
응답을 보낼
서버 용도로
별도의 프로젝트를 하나 생성하고
다른 프로젝트에서
RestTemplate을 통해 요청을 보내고 응답을 받아보는 방식으로 실습을 진행한다.
나는 최근에 멀티 모듈 구동하는 방법을 학습해서
멀티 모듈로 실습 !
spring boot 3.1.1로 생성하였다.
의존성은 web
, lombok
두 개면 충분하다.
그리고 나의 경우
최근에 멀티 모듈 구동하는 방법을 학습하여
멀티 모듈 형태로 실습해보고자 한다!
우선
spring boot 프로젝트 이름은 restTemplate
로 하고,
하위 모듈로 springBox
와 rest
.
그리고 위의 두 모듈에서 공통으로 사용될 파일을 관리할 common
모듈을 추가해주어
프로젝트 구성을 다음과 같이 만들었다.
restTemplate
는
모듈들을 관리할 Spring boot 프로젝트 이고,
serverBox
와 rest
는
rest template 테스트시 서버와 클라이언트로 사용할 하위 모듈이다.
common
에는
serverBox 와 rest 에서 공유할 dto 파일을 작성할 것이다.
멀티 모듈 생성 및 실행 방법은 아래 링크 참고!
-> Spring boot 멀티 모듈 프로젝트 만들기
serverBox 모듈과 rest 모듈에서 공통으로 사용할 dto 파일을 미리 작성!
@Getter @Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDto {
private String name;
private String email;
private String organization;
}
하나의 컴퓨터에서 서버와 클라이언트.
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);
}
}
일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나
서비스 또는 비즈니스 계층에 구현된다.
앞서 생성한 서버 프로젝트에 요청을 날리기 위해
서버의 역할을 수행하면서
다른 서버로 요청을 보내는
클라이언트의 역할도 수행하는
rest 모듈을 작성한다.
@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();
}
}
@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)
을 설정해준다.
루트 프로젝트인 restTemplate
의 build.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 호출 !
_____________________🐢🎈____________________
일반적으로
실제 운영 환경에 적용되는 애플리케이션은
정식 버전으로 출시된 스프링 부트의 버전보다 낮은 경우가 많다.
그렇기 때문에 RestTemplate를 많이 사용하고 있다.
하지만
최신 버전에서는 WebClient를 사용할 것을 권고하고 있다.
이러한 흐름에 맞춰
현재 빈번히 사용되고 있는 RestTemplate와
앞으로 많이 사용될 WebClient를 모두 알고 있는 것이 좋다.
spring WebFlux는
HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다.
WebClient는 리액터 Reactor 기반으로 동작하는 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에서 새롭게 추가된 모듈이다.
_____________________🐢🎈____________________
WebClient를 생성하는 방법은 다음과 같이 크게 두 가지가 있다.
먼저,
RestTemplate 실습시 작성했던
serverBox 모듈의
GET , POST 메서드 컨트롤러에 접근할 수 있는 WebClient를 생성한다.
@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();
}
}
@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();
}
}
_____________________🐢🎈____________________