북스터디 : 스프링 부트 핵심가이드(7)

윤장원·2023년 4월 23일
0

11. 액추에이터 활용하기

스프링 부트 액추에이터는 HTTP 엔드포인트나 JMX를 활용해 애플리케이션을 모니터링하고 관리할 수 있는 기능을 제공한다.

액추에이터 기능을 사용하려면 애플리케이션에 spring-boot-starter-actuator 모듈의 종속성을 추가해야 한다.

pom.xml

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

엔드포인트

액추에이터의 엔드포인트는 애플리케이션의 모니터링을 사용하는 경로이다. 스프링 부트에는 여러 내장 엔드포인트가 포함돼 있으며, 커스텀 엔드포인트를 추가할 수도 있다. 액추에이터를 추가하면 기본적으로 엔드포인트 URL로 /actuator가 추가되며 이 뒤에 경로를 추가해 상세 내역에 접근한다. 만약 /actuator 경로가 아닌 다른 경로를 사용하고 싶다면 applicatoin.properties 파일에 다음과 같이 작성한다.

management.endpoints.web.base-path=/custom-path

엔드포인트는 활성화 여부와 노출 여부를 설정할 수 있다. 활성화는 기능 자체를 활성화할 것인지를 결정하는 것으로, 비활성화된 엔드포인트는 애플리케이션 컨텍스트에서 완전히 제거된다. 엔드포인트를 활성화하려면 application.properties 파일에 속성을 추가하면 된다.

management.endpoint.shutdown.enabled=true
management.endpoint.caches.enabled=false

또한 액추에이터 설정을 통해 기능 활성화/비활성화가 아니라 엔드포인트의 노출 여부만 설정하는 것도 가능하다. 노출 여부는 JMX를 통한 노출과 HTTP를 통한 노출이 있어 다음과 같이 설정이 구분된다.

HTTP 설정

management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=threaddump,heapdump

JMX 설정

management.endpoints.jmx.exposure.include=*
management.endpoints.jmx.exposure.exclude=threaddump,heapdump

위 설정을 해석하면 web과 jmx 환경에서 엔드포인트를 전체적으로 노출하며, 스레드 덤프와 힙 덤프 기능은 제외한다는 의미이다.

액추에이터 기능

  1. 애플리케이션 기본 정보(/info)
    제공하는 정보의 범위는 애플리케이션에서 몇 가지 방법을 거쳐 제공할 수도 있으나 applicatoin.properties 파일에 'info.'로 시작하는 속성 값들을 정의하는 것이 가장 쉬운 방법이다.
info.organization.name=wikibooks
info.contact.email=thinkground.flature@gmail.com
info.contact.phoneNumber=010-1234-5678

브라우저에서 http://localhost:8080/actuator/info URL에 접근하면 다음과 같은 결괏값이 확인된다.

{
	"organization":
    {
    	"name":"wikibooks"
    },
    "contact":
    {
    	"email":"thinkground.flature@gmail.com",
        "phoneNumber":"010-1234-5678"
    }
}
  1. 애플리케이션 상태(/health)
    브라우저에서 http://localhost:8080/actuator/health URL에 접근하면 다음과 같은 결괏값이 확인된다.
{"status":"UP"}

status 속성에서 확인할 수 있는 상태 지표는 UP, DOWN, UNKNOWN, OUT_OF_SERVICE가 있다.

  1. 빈 정보 확인(/beans)
    액추에이터의 /beans 엔드포인트를 사용하면 스프링 컨테이너에 등록도니 스프링 빈의 전체 목록을 표시할 수 있다. 이 엔드포인트는 JSON 형식으로 빈의 정보를 반환한다. 다만 스프링은 워낙 많은 빈이 자동으로 등록되어 운영되기 때문에 실제로 내용을 출력해서 육안으로 내용을 파악하기는 어렵다.

  2. 스프링 부트의 자동설정 내역 확인(/conditions)
    스프링 부트의 자동설정(AutoConfiguration) 조건 내역을 확인하려면 '/conditoins' 엔드포인트를 사용한다. 출력 내용은 크게 positiveMatches와 negativeMatches 속성으로 구분되는데, 자동설정의 @Conditional에 따라 평가된 내용을 표시한다.

  3. 스프링 환경변수 정보(/env)
    /env 엔드포인트는 스프링의 환경변수 정보를 확인하는 데 사용된다. 기본적으로 application.properties 파일의 변수들이 표시되며, OS, JVM의 환경변수도 함께 표시된다. 만약 일부 내용에 포함된 민감한 정보를 가리기 위해서는 management.endpoint.env.keys-to-sanitize 속성을 사용하면 된다. 해당 속성에 넣을 수 있는 값은 단순 문자열이나 정규식을 활용한다.

  4. 로깅 레벨 확인(/loggers)
    애플리케이션의 로깅 레벨 수준이 어떻게 설정돼 있는지 확인하려면 /loggers 엔드포인트를 사용할 수 있다. POST 형식으로 호출하면 로깅 레벨을 변경하는 것도 가능하다.

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

커스텀 기능을 개발하는 방식에는 크게 두 가지가 있다. 첫 번째는 기존 기능에 내용을 추가하는 방식이고, 두 번째는 새로운 엔드포인트를 개발하는 방식이다.

  1. 정보 제공 인터페이스의 구현체 생성
    커스텀 기능을 설정할 때는 별도의 구현체 클래스를 작성해서 내용을 추가하는 방법이 많이 활용된다. 액추에이터에서는 InfoContributor 인터페이스를 제공하고 있는데, 이 인터페이스를 구현하는 클래스를 생성하면 된다. 다음과 같이 InfoContributor 인터페이스에 대한 구현 클래스를 생성한다.
@Component
public class CustomInfoContributor implements InfoContributor {

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

contribute 메서드에서 파라미터로 받는 Builder 객체는 액추에이터 패키지의 Info 클래스 안에 정의돼 있는 클래스로서 Info 엔드포인트에서 보여줄 내용을 담는 역할을 수행한다.

  1. 커스텀 엔드포인트 생성
    @Endpoint 어노테이션으로 빈에 추가된 객체들은 @ReadOperation, @WriteOperation, @DeleteOperation 어노테이션을 사용해 JMX나 HTTP를 통해 커스텀 엔드포인트를 노출시킬 수 있다. 만약 JMX에서만 사용하거나 HTTP에서만 사용하는 것으로 제한하고 싶다면, @JmxEndpoint, @WebEndpoint 어노테이션을 사용하면 된다.
@Component
@Endpoint(id = "note", enableByDefault = true)
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;
    }

}

GET, POST, DELETE 요청으로 값 가져오기, 추가, 삭제를 할 수 있다.

12. 서버 간 통신

RestTemplate

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

RestTemplate은 다음과 같은 특징을 가지고 있다.

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

RestTemplate의 동작 원리

애플리케이션은 우리가 직접 작성하는 애플리케이션 코드 구현부를 의미한다. 애플리케이션에서는 RestTemplate을 선언하고 URI와 HTTP 메서드, Body 등을 설정한다.
그리고 외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메시지로 변환한다.
RestTemplate에서는 변환된 요청 메시지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보낸다.
외부에서 요청에 대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리한다.
받은 응답 데이터가 정상적이라면 다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환해서 애플리케이션으로 반환한다.

RestTemplate의 대표적인 메서드

RestTemplate 구현

일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나 서비스 또는 비즈니스 계층에 구현된다. RestTemplate은 spring-boot-starter-web 모듈에 포함돼 있는 기능이므로 pom.xml에 별도로 의존성을 추가할 필요는 없다.
RestTemplate을 생성하고 사용하는 방법은 다양하다. 그중 가장 보편적인 방법은 UriComponentsBuilder를 사용하는 방법이다. UriComponentsBuilder는 스프링 프레임워크에서 제공하는 클래스로서 여러 파라미터를 연결해서 URI 형식으로 만드는 기능을 수행한다.

RestTemplateService의 GET 예제

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

fromUriString() 메서드에서는 호출부의 URL을 입력하고, 이어서 path() 메서드에 세부 경로를 입력한다. encode() 메서드는 인코딩 문자셋을 설정할 수 있는데, 인자를 전달하지 않으면 기본적으로 UTF-8로 실행된다.
이후 build() 메서드를 통해 빌더 생성을 종료하고 UriComponents 타입이 리턴된다. 예제에서는 toUri() 메서드를 통해 URI 타입으로 리턴받았다. 만약 URI 객체를 사용하지 않고 String 타입의 URI를 사용한다면 toUriString() 메서드로 대체해서 사용하면 된다.
이렇게 생성된 uri는 restTemplate이 외부 API를 요청하는 데 사용되며, getForEntity()에 파라미터로 전달된다.

RestTemplateService의 POST 예제

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;
}

RestTemplate 커스텀 설정

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

httpClient 의존성 추가

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

커스텀 RestTemplate 객체 생성 메서드

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;
}

WebClient

Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다. WebClient는 리액터 기반으로 동작하는 API이다. 리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있다.

WebClient 특징

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

WebClient 구성

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

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

WebClient 구현

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

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

WebClient를 활용한 GET 요청 예제

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

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

        return responseEntity.getBody();
    }
}

WebClient는 HTTP 메서드를 get(), post(), put(), delete() 등의 네이밍이 명확한 메서드로 설정할 수 있다. 그리고 URI를 확장하는 방법으로 uri() 메서드를 사용할 수 있다.
retrieve() 메서드는 요청에 대한 응답을 받았을 때 그 값을 추출하는 방법 중 하나이다. bodyToMono() 메서드를 통해 리턴 타입을 설정해서 문자열 객체를 받아오게 돼 있다.
WebClient는 기본적으로 논블로킹 방식으로 동작하기 때문에 기존에 사용하던 코드의 구조를 블로킹 구조로 바꿔줄 필요가 있다. 예제에서는 block()이라는 메서들르 추가해서 블로킹 형식으로 동작하게끔 설정했다.

WebClient를 활용한 Post 요청 예제

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

0개의 댓글