상세 컨텐츠

본문 제목

[스프링 3팀] 12. 서버 간 통신 & 섹션 0. 시큐리티

23-24/Spring 3

by recoday 2023. 12. 29. 10:00

본문

728x90

최근 개발되는 서비스들은 마이크로서비스 아키텍처(MSA)를 주로 채택

MSA란 애플리케이션이 가지고 있는 기능(서비스)이 하나의 비지니스 범위만 가지는 형태

 

12.1 RestTemplate이란?
  • RestTemplate은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿
  • HTTP 서버와의 통신을 단순화한 이 템플릿을 이용하면 RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있다.
  • RestTemplate은 기본적으로 동기 방식으로 처리되며, 비동기 방식으로 사용하고 싶은 경우 AsyncRestTemplate 사용
  • RestTemplate은 많이 사용되지만 지원 중단(deprecated)된 상태이므로 향후 자주 사용될 WebClient 방식도 알아두기를 권장
✅ RestTemplate 특징
▪ HTTP 프로토콜의 메서드에 맞는 여러 메서드를 제공
▪ RESTful 형식을 갖춘 템플릿
▪ HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답 받을 수 있다.
▪ 블로킹(blocking) I/O 기반의 동기 방식을 사용
▪ 다른 API를 호출할 때 HTTP 헤더에 다양한 값을 설정할 수 있다.

 

12.1.1 RestTemplate의 동작 원리

  • 위의 그림에서 애플리케이션은 우리가 직접 작성하는 애플리케이션 구현부를 의미
    애플리케이션에서는 RestTemplate을 선언하고 URL과 HTTP 메서드, Body 등을 설정
  • 외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메세지로 변환
  • RestTemplate에서는 변환된 요청 메세지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보냄
  • 외부에서 요청에 대한 응답을 받으면 RestTemplate ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리
  • 받은 응답 데이터가 정상적이라면 다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환해서 애플리케이션으로 반환
12.1.2 RestTemplate의 대표적인 메서드
  • RestTemplate에서는 더욱 편리하게 외부 API로 요청을 보낼 수 있도록 다양한 메서드를 제공

12.2 RestTemplate 사용하기

 

12.2.1 서버 프로젝트 생성하기

1. RestTemplate의 동작을 확인하기 위한 서버 용도의 프로젝트를 생성

2. 실습을 위해 한 컴퓨터 안에서 두 개의 프로젝트를 가동시켜야 하기 때문에 톰캣의 포트 변경

3. 프로젝트에는 spring-boot-starter-web 모듈만 의존성으로 추가

4. 애플리케이션이 가동되는 톰캣의 포트를 변경하기 위해 'application.properties' 파일에 server.port 속성 추가

server.port = 9090

 

컨트롤러에는 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> 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);
    }
}
  • @GetMapping은 GET 형식의 요청이 들어오는 상황의 케이스를 구현
    첫 번째 메서드는 파라미터가 없는 경우, 두 번째는 PathVariable을 사용하는 경우, 세 번째는 RequestParameter를 상요하는 경우
  • @PostMapping은 POST 형식의 요청을 받기 위한 두 개의 메서드 구현
    첫 번째 메서드는 간소화를 위해 요청 파라미터(Request Parameter)와 요청바디(Request Body)를 함께 받도록 구현했고, 두 번째 메서드는 임의의 HTTP 헤더를 받도록 구현
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 객체는 위와 같음
  • MemberDto 클래스는 name, email, organization이라는 총 3개의 필드를 가짐
12.2.2 RestTemplate 구현하기
  • 일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나 서비스 또는 비지니스 계층에 구현됨
  • 앞서 생성한 서버 프로젝트에 요청을 날리기 위해 서버의 역할을 수행하면서 다른 서버로 요청을 보내는 클라이언트의 역할도 수행하는 새로운 프로젝트 생성

  • 위의 그림에서 클라이언트는 서버를 대상으로 요청을 보내고 응답을 받는 역할을 수행
  • 12.2.1절에서 구현한 서버 프로젝트인 serverBox는 서버 2가 됩니다.

 

1. RestTemplate을 포함하는 프로젝트 생성

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

2. SwaggerConfiguration 클래스 파일과 의존성 추가

✔️ RestTemplate은 이미 spring-boot-starter-web 모듈에 포함된 기능 -> pom.xml에 별도로 의존성을 추가 X
✔️ 프로젝트 구조로는 클라이언트로부터 요청을 받는 컨트롤러와 RestTemplate을 활용해 다른 서버에 통신 요청을 하는 서비스 계층으로 작성

 

✅ GET 형식의 RestTemplate 작성하기

@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 형식으로 만드는 기능을 수행
  • getName() 메서드는 PathVariable이나 파라미터를 사용하지 않는 호출 방법
  • UricomponentsBuilder는 빌더 형식으로 객체를 생성
  • fromUriString() 메서드에서는 호출부의 URL을 입력하고, 이어서 path() 메서드에 세부 경로를 입력
  • encode() 메서드는 인코딩 문자셋을 설정할 수 있고, 인자를 전달하지 않으면 기본적으로 UTF-8로 아래와 같은 코드 실행
public final UriComponentsBuilder encode() {
  return encode(StandardCharsets.UTF-8);
 }
  • 이후 build() 메서드를 통해 빌더 생성을 종료하고 UriComponents 타입이 리턴됨
  • 위의 예제에서는 toUri() 메서드를 통해 URI 타입으로 리턴받음
    만약 URI 객체를 사용하지 않고 String 타입의 URI를 사용한다면 toUriString() 메서드로 대체해서 사용하면 됨
  • 이렇게 생성된 uri는 restTemplate이 외부 API를 요청하는데 사용되며, getForEntity()에 파라미터로 전달됨
  • getForEntity()는 URI와 응답받는 타입을 매개변수로 사용

  • getNameWithPathVariable() 메서드에서는 path() 메서드 내에 입력한 세부 URI 중 중괄호({ }) 부분을 사용해 개발 단계에서 쉽게 이해할 수 있는 변수명을 입력하고 expand() 메서드에서는 순서대로 값을 입력

  • getNameWithParameter() 메서드는 파라미터로 전달하는 예제
  • 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;
}
  • postWithParamAndBody()는 POST 형식으로 외부 API에 요청할 때 Body 값과 파라미터 값을 담는 방법 두 가지를 보여줌
  • URI uri = UriComponentsBuilder 줄에서는 파라미터에 값을 추가하는 작업이 수행
  • MemberDto memberDto = new MemberDto()부터는 RequestBody에 값을 담는 작업이 수행됨
  • RequestBody에 값을 담기 위해서는 MemberDto memberDto = new MemberDto()와 같이 데이터 객체를 생성
  • postForEntity() 메서드를 사용할 경우에는 파라미터로 데이터 객체를 넣으면 됨
  • postForEntity() 메서드로 서버 프로젝트의 API를 호출하면 서버 프로젝트의 콘솔 로그에는 RequestBody 값이 출력되고 파라미터 값은 결과값으로 리턴됨

✅ 프로젝트에서 쉽게 API를 호출할 수 있도록 Swagger 설정

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            //.apis(RequestHandlerSelectors.any())
            .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();
    }
}

 

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

@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를 호출하면 아래 결과 출력

       * 앞서 생성한 2개의 프로젝트가 모두 가동돼 있는 상태에서 진행되어야 함!

 

 

flature!!
flature@gmail.com
Around Hub Studio
  • 위의 출력 결과는 서버 프로젝트가 파라미터의 값과 Body 값을 정상적으로 전달받았다는 것을 의미

 

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;
}
  • 위의 메서드는 헤더를 추가하는 예제
  • 대부분의 외부 API는 토큰키를 받아 서비스 접근을 인증하는 방식으로 작동
  • 이때 토큰값을 헤더에 담아 전달하는 방식이 가장 많이 사용됨
  • 헤더를 설정하기 위해서는 RequestEntity를 정의해서 사용하는 방법이 가장 편한 방법
  • RequestEntity<MemberDto> requestEntity = ReqeustEntity 코드는 RequestEntity를 생성하고 post() 메서드로 URI를 설정한 후 header() 메서드에서 헤더의 키 이름과 값을 설정하는 코드
  • 대체로 서버 프로젝트의 API 명세에는 헤더에 필요한 키 값을 요구하면서 키 이름을 함께 제시하기 때문에 그에 맞춰 헤더 값을 설정하면 됨
  •  ResponseEntity<MemberDto> responseEntity = restTemplate.exchange(requestEntity, MemberDto.class);에서는 RestTemplate의 exchange() 메서드를 사용
  • exchange() 메서드는 모든 형식의 HTTP 요청을 생성할 수 있음
  • RequestEntity의 설정에서 post() 메서드 대신 다른 형식의 메서드로 정의만 하면 exchange() 메서드로 쉽게 사용할 수 있기 때문에 대부분exchange() 메서드를 사용

 

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

✅ 아파치의 HttpClient를 사용하기 위해 아래와 같이 의존성 추가

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpClient</artifactId>
  </dependency>
  • 의존성을 추가하면 RestTemplate의 설정을 더욱 쉽게 추가하고 변경할 수 있음

✅ 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;
}
  • ClientHttpRequestFactory는 함수형 인터페이스로, 대표적인 구현체로써 SimpleClientHttpReqeustFactory와 HttpComponentsClientHttpReqeustFactory가 존재
  • 별도의 구현체를 설정해서 전달하지 않으면 HttpAccessor에 구현돼 있는 내용에 의해 SimpleClientHttpRequestFactory를 사용
  • 별도의 HttpComponentsClientHttpReqeustFactory 객체를 생성해서 SimpleClientHttpRequestFactory를 사용하면 위의 예제와 같이 RestTemplate의 Timeout 설정을 할 수 있음
  • HttpComponentsClientHttpReqeustFactory는 커넥션 풀을 설정하기 위해 HttpClient를 HttpComponentsClientHttpRequestFactory에 설정할 수 있음
  • HttpClient를 생성하는 방법은 2가지가 존재하고, 위의 예제 중 앞의 코드처럼 HttpClientBuilder.create() 메서드를 사용하거나, 뒤의 코드처럼 HttpClients.custom() 메서드를 사용
  • 생성한 HttpClient는 factory.setHttpClient(httpClient)와 같이 factory의 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 모듈에 대한 의존성 추가 필요
  • 아래와 같이 pom.xml 파일에 의존성 추가
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
  • WebFlux는 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 스프링 프레임워크 5에서 새롭게 추가된 모듈
  • pom.xml에 위와 같이 WebFlux를 추가하면 WebClient를 사용할 수 있는 환경이 만들어짐

 

12.4 WebClient 사용하기

 

✅ WebClient 구현

  • WebClient를 생성하는 방법은 크게 두 가지 존재
    ▪ create() 메서드를 이용한 생성
    ▪ builder()를 이용한 생성

 

✅ 서버 프로젝트의 GET 메서드 컨트롤러에 접근할 수 있는 WebClient 생성

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

    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();
    }
  • 3개의 메서드 정의
  • 첫 번째 getName() 메서드는 builder()를 활용해 WebClient를 만들고 다른 두 개의 메서드에서는 create()를 활용해 WebClient를 생성
  • WebClient는 우선 객체를 생성한 후 요청을 전달하는 방식으로 동작
  • 이를 위해 WebClient webClient = WebClient.builder()에서 builder()를 통해 baseUrl() 메서드에서 기본 URL을 설정하고 defaultHeader() 메서드로 헤더의 값을 설정
  • 일반적으로 WebClient 객체를 이용할 때는 이처럼 WebClient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋음
예제에서 소개된 메서드 외 builder()를 사용할 경우 확장할 수 있는 메서드
▪ defaultHeader() : WebClient의 기본 헤더 설정
▪ defaultCookie() : WebClient의 기본 쿠키 설정
▪ defaultUriVariable() : WebClient의 기본 URI 확장값 설정
▪ filter() : WebClient에서 발생하는 요청에 대한 필터 설정

 

  • 빌드된 WebClient는 변경할 수 없지만, 복사해서 사용할 수는 있음
WebClient webClient = WebClient.create("http://localhost:9090");
WebClient clone = webClient.mutate().build();

 

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

  • 두번째 메서드인 getNameWithPathVariable() 메서드는 pathVariable 값을 추가해 요청을 보내는 예제
  • .uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/{name}")와 같이 uri() 메서드 내부에서 uriBuilder를 사용해 path를 설정하고 build() 메서드에 추가값을 넣는 것으로 pathVariable을 추가할 수 있음
  • 두번째 메서드는 bodyToMono() 메서드가 아닌 toEntity()를 사용하는 예제
  • toEntity()를 사용하면 ResponseEntity 타입으로 응답을 전달받을 수 있음

  • 세번째 메서드인 getNameWithParameter() 메서드는 쿼리 파라미터를 함께 전달하는 방법 제시
  • 쿼리 파라미터를 요청에 담기 위해서는 return webClient.get().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")와 같이 uriBuilder를 사용하며, queryParam() 메서드를 사용해 전달하려는 값을 설정
  • 또한 위의 예제에서는 retrieve() 대신 exchange() 메서드를 사용했는데, exchange() 메서드는 지원 중단되었기 때문에 exchangeToMono() 또는 exchangeToFlux()를 사용해야 함
  • exchange() 메서드는 응답 결과 코드에 따라 다르게 응답을 설정할 수 있음
  • '.exchangeToMono(clientResponse -> { ' 코드에서 보는 것처럼 clientResponse 결과값으로 상태 값에 따라 if문 분기를 만들어 상황에 따라 결과값을 다르게 전달할 수 있음

✅ 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();
}
  • webClient를 생성하고 사용하는 방법은 위의 GET 요청을 만드는 방법과 다르지 않지만, HTTP 바디 값을 담는 방법과 커스텀 헤더를 추가하는 방법이 다름
  • return webClient.post().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")에서부터는 webClient에서 post() 메서드를 통해 POST 메서드 통신을 정의
  • uri()는 uriBuilder로 path와 parameter를 설정
  • 그 후 bodyValue() 메서드를 통해 HTTP 바디 값을 설정
  • HTTP 바디에는 일반적으로 데이터 객체(DTO, VO 등)을 파라미터로 전달
  • 아래의 pustWithHeader() 메서드는 POST 요청을 보낼 때 헤더를 추가해서 보내는 예제
  • 전반적인 내용은 동일하며, header() 메서드를 사용해 헤더에 값을 추가
  • 일반적으로 임의로 추가하 헤더에는 외부 API를 사용하기 위해 인증된 토큰값을 담아 전달

[섹션 0. 시큐리티]

스프링부트 시큐리티 1강- 환경설정

 

1. MySQL DB 및 사용자 생성

create user 'cos'@'%' identified by 'cos1234';
GRANT ALL PRIVILEGES ON *.* TO 'cos'@'%';
create database security;
use security;

 

2. 프로젝트 생성

 ▪ Lombok

 ▪ Spring Boot DevTools

 ▪ Spring Data JPA

 ▪ MySQL Driver

 ▪ Spring Security

 ▪ Mustache 

 ▪ Spring Web

 

3. application.yml 작성

server:
  port: 8080
  servlet:
    context-path: /
    encoding:
      charset: UTF-8
      enabled: true
      force: true
      
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Seoul
    username: cos
    password: cos1234
    
  mvc:
    view:
      prefix: /templates/
      suffix: .mustache

  jpa:
    hibernate:
      ddl-auto: create #create update none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true

 

4. controller 패키지 생성 후 IndexController 생성

@Controller // View를 리턴
public class IndexController {

  // localhost:8080/
  // localhost:8080
  @GetMapping({"", "/"})
  public String index() {
      // 머스테치(Mustache)
      return "index"; // index 파일은 View
  }
}
  • Mustache는 스프링이 권장하고 있어서 기본 폴더가 src/main/resources/로 생성됨
  • View Resolver 설정 : templates (prefix), .mustache (suffix) -> mustahce 의존성 추가 시 설정 생략 가능

5. templates에 index 파일 생성

  • return "index"는 src/main/resources/templates/index.mustache를 찾음
  • config 패키지 생성 시 WebMvcConfig.java 생성
package com.cos.securityex01.config;

import org.springframework.boot.web.servlet.view.MustacheViewResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer{  

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {
      MustacheViewResolver resolver = new MustacheViewResolver();

      resolver.setCharset("UTF-8");
      resolver.setContentType("text/html;charset=UTF-8");
      resolver.setPrefix("classpath:/templates/");
      resolver.setSuffix(".html");

      registry.viewResolver(resolver);
  }
}

 

스프링부트 시큐리티 2강- 시큐리티 설정
  • controller 패키지의 IndextController.java
@Controller
public class IndexController {

	@Autowired
	private UserRepository userRepository;

	@Autowired
	private BCryptPasswordEncoder bCryptPasswordEncoder;

	@GetMapping({ "", "/" })
	public @ResponseBody String index() {
		return "인덱스 페이지입니다.";
	}

	@GetMapping("/user")
	public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principal) {
		System.out.println("Principal : " + principal);
		// iterator 순차 출력 해보기
		Iterator<? extends GrantedAuthority> iter = principal.getAuthorities().iterator();
		while (iter.hasNext()) {
			GrantedAuthority auth = iter.next();
			System.out.println(auth.getAuthority());
		}

		return "유저 페이지입니다.";
	}

	@GetMapping("/admin")
	public @ResponseBody String admin() {
		return "어드민 페이지입니다.";
	}
	
	//@PostAuthorize("hasRole('ROLE_MANAGER')")
	//@PreAuthorize("hasRole('ROLE_MANAGER')")
	@Secured("ROLE_MANAGER")
	@GetMapping("/manager")
	public @ResponseBody String manager() {
		return "매니저 페이지입니다.";
	}

	@GetMapping("/login")
	public String login() {
		return "login";
	}
    
    	@GetMapping("/joinForm")
	public String login() {
		return "joinForm";
	}
    
    	@GetMapping("/loginForm")
	public String login() {
		return "loginForm";
	}

	@GetMapping("/join")
	public String join() {
		return "join";
	}

	@PostMapping("/joinProc")
	public String joinProc(User user) {
		System.out.println("회원가입 진행 : " + user);
		String rawPassword = user.getPassword();
		String encPassword = bCryptPasswordEncoder.encode(rawPassword);
		user.setPassword(encPassword);
		user.setRole("ROLE_USER");
		userRepository.save(user);
		return "redirect:/";
	}
    
    @Secured("ROLE_ADMIN")
    @GetMapping("/info")
    public @ResponseBody String info() {
        return "개인정보";
    }
    
    @PreAuthorize("hasRole('ROLE_MANAGER' or hasRole('ROLE_ADMIN)")
    @GetMapping("/data")
    public @ResponseBody String data() {
        return "데이터정보";
    
}

 

  • config 패키지의 SecurityConfig.java
@Configuration // IoC 빈(bean)을 등록
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션 (스프링 시큐리티 필터가 스프링 필터체인에 등록됨)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
    // @Bean : 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해줌
	@Bean
	public BCryptPasswordEncoder encodePwd() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		
		http.csrf().disable();
		http.authorizeRequests()
			.antMatchers("/user/**").authenticated()
			//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')")
			//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")
			.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
			.anyRequest().permitAll()
		.and()
			.formLogin()
			.loginPage("/login")
			.loginProcessingUrl("/loginProc")
			.defaultSuccessUrl("/");
	}
}

 

스프링부트 시큐리티 3강 - 시큐리티 회원가입
스프링부트 시큐리티 4강 - 시큐리티 로그인
스프링부트 시큐리티 5강 - 시큐리티 권한처리
  • templates 패키지에 login.html으로 로그인 페이지 생성
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr/>
<!-- 시큐리티는 x-www-form-url-encoded 타입만 인식 -->
<form action="/loginProc" method="post">
	<input type="text" name="username" />
	<input type="password" name="password" />
	<button>로그인</button>
</form>
<a href="/joinForm"> 회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>
  • model 패키지에 User.java 생성
@Data
@Entity
public class User {
	@Id // primary key
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	private String username;
	private String password;
	private String email;
	private String role; //ROLE_USER, ROLE_ADMIN
	@CreationTimestamp
	private Timestamp createDate;
}
  • templates 패키지에 join.html로 회원가입 페이지 생성
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr/>
<form action="/joinProc" method="post">
	<input type="text" name="username" placeholder="Username"/> <br/>
	<input type="password" name="password" placeholder="Password"/> <br/>
	<input type="email" name="email" placeholder="Email"/> <br/>
	<button>회원가입</button>
</form>
</body>
</html>
  • repository 패키지에 UserRepository.java 생성
// JpaRepository 를 상속하면 자동 컴포넌트 스캔됨.
// CRUD 함수를 JpaRepository가 들고 있음
// @Repository 어노테이션이 없어도 IoC됨 (자동으로 Bean으로 등록됨)
public interface UserRepository extends JpaRepository<User, Integer>{

    // findBy 규칙 -> Username 문법
    // select * from user where username = 1?
	
	// Jpa Naming 전략
	// SELECT * FROM user WHERE username = 1?
	User findByUsername(String username);
	// SELECT * FROM user WHERE username = 1? AND password = 2?
	// User findByUsernameAndPassword(String username, String password);
	
	// @Query(value = "select * from user", nativeQuery = true)
	// User find마음대로();
}

 

  • config 패키지에 auth 폴더 생성 후 PrincipalDetails.java 생성
// 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킴
// 로그인을 진행이 완료가 되면 시큐리티 session을 만들어줌 (Security ContextHolder)
// 오브젝트 => Authentication 타입의 객체이여야 함
// Authentication 안에는 User 정보가 있어야 함
// User 오브젝트의 타입 => UserDetails 타입 객체이여야 함

//Sercurity Session => Authentication => UserDetails

// Authentication 객체에 저장할 수 있는 유일한 타입
@Data
public class PrincipalDetails implements UserDetails{

	private User user;

	public PrincipalDetails(User user) {
		super();
		this.user = user;
	}
	
	@Override
	public String getPassword() {
		return user.getPassword();
	}

	@Override
	public String getUsername() {
		return user.getUsername();
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		Collection<GrantedAuthority> collet = new ArrayList<GrantedAuthority>();
		collet.add(()->{ return user.getRole();});
		return collet;
	}
}
  • auth 패키지에 PrincipalDetailsService.java 생성
@Service
public class PrincipalDetailsService implements UserDetailsService{

	@Autowired
	private UserRepository userRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUsername(username);
		if(user == null) {
			return null;
		}
		return new PrincipalDetails(user);
	}
}

Quiz

 

1. ( RestTemplate )은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다.

2. RestTemplate 동작 원리 중에서 외부 API로 요청을 보내게 되면 RestTemplate에서는 ( HttpMessageConverter )를 통해 ( RequestEntity )를 요청 메세지로 변환한다.

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

4. RestTemplate에서는 기본적으로 커넥션 풀을 제공하지 않지만, 이 기능을 활성화해서 사용하기 위해 가장 대표적으로 아파치에서 제공하는 ( HttpClient )로 대체해서 사용한다.

5. 현업에서는 RestTemplate을 많이 사용하지만, 최근 지원이 중단되어 ( WebClient )를 사용할 것을 권고하고 있다.

6. WebClient를 생성하는 방법은 ( create() ) 메서드를 이용한 생성과 ( builder() )를 이용한 생성으로 크게 두 가지가 존재한다.

7. WebClient의 특징 중 하나로 WebClient는 기본적으로  ( 논블로킹(Non-Blocking ) 방식으로 동작하기 때문에 기존에 사용하던 코드의 구조를 블로킹 구조로 바꿔줄 필요가 있다.

 

Code Quiz

 

1. GET 형식의 RestTemplate 작성해보기

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

   }

 

2. 서버 프로젝트의 GET 메서드 컨트롤러에 접근할 수 있는 WebClient 생성 코드 작성해보기

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

 

 

[출처] 장정우, 『스프링 부트 핵심가이드 스프링 부트를 활용한 애플리케이션 개발 실무』, 위키북스(2022), p349-372

 

ⓒ 다다

728x90

관련글 더보기