상세 컨텐츠

본문 제목

[스프링 1팀] 12장. 서버 간 통신, [인프런] 섹션 0. 스프링 시큐리티 기본

23-24/Spring 1

by 숨니 2023. 12. 29. 10:00

본문

728x90

12. 서버 간 통신

MSA란 애플리케이션이 가지고 있는 기능(서비스)이 하나의 비즈니스 범위만 가지는 형태입니다. 각 애플리케이션은 자신이 가진 기능을 API로 외부에 노출하고, 다른 서버가 그러한 API를 호출해서 사용할 수 있게 구성되므로 각 서버가 다른 서버의 클라이언트가 되는 경우도 많습니다. 이번 장에서는 다른 서버로 웹 요청을 보내고 응답을 받을 수 있게 도와주는 RestTemplate과 WebClient에 대해 살펴보겠습니다.

12.1 RestTemplate이란?

스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿입니다. 

HTTP 서버와의 통신을 단순화한 이 템플릿을 이용하면 RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있습니다.

기본적으로 동기 방식으로 처리되며, 비동기방식으로 처리하고 싶을 경우 AsyncRestTemplate을 사용하면 됩니다.

하지만 RestTemplate은 지원 중단된 상태이므로 WebClient 방식도 함께 알아두어야 합니다.

특징

1. HTTP 프로토콜의 메서드에 맞는 여러 메서드를 제공합니다.

2. RESTful 형식을 갖춘 템플릿입니다.

3. HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답을 받을 수 있습니다.

4. 블로킹 IO 기반의 동기 방식을 사용합니다.

5. 다른 API를 호출 할 때 HTTP 헤더에 다양한 값을 설정할 수 있습니다.

12.1.1. RestTemplate의 동작 원리

1) 애플리케이션에 RestTemplate을 선언하고, URI와 HTTP 메서드, Body 등을 설정합니다.

2) 외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메시지로 변환합니다.

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

4) 외부에서 요청에 대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고,

오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리합니다.

5) 받은 응답 데이터가 정상적이라면 다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환해서

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

12.1.2. RestTemplate의 대표적인 메서드

                메서드                             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 요청과 응답에 대한 콜백을 수정

12.2 RestTemplate 사용하기

12.2.1서버 프로젝트 생성하기

1) serverBox 프로젝트 생성

2) server.port=9090으로 속성 추가하기

3) 컨트롤러에 GET과 POST 메서드 형식의 요청을 받기 위한 코드 작성하기

4) MemberDto 작성해 주기

12.2.2 RestTemplate 구현하기 

앞서 생성한 서버 프로젝트에 요청을 날리기 위해 서버의 역할을 수행하면서 다른 서버로 요청을 보내는 클라이언트의 역할도 수생 하는 새로운 프로젝트를 생성합니다.

 

1) GET형식의 RestTemplate 작성하기

UriComponentsBuilder를 사용해서 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();
    }

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

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

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

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

}

5) 실행하기

 

12.2.3 RestTemplate 커스텀 설정

RestTemplate은 커넥션 풀을 지원하지 않습니다. 따라서 매번 호출할 때마다 포트를 열어 커넥션을 생성하게 되는데, TIME_WAIT 상태가 된 소켓을 다시 사용하려고 접근한다면 재사용하지 못하게 됩니다. 이를 방지하기 위해 커넥션 풀 기능을 활성화해서 재사용할 수 있게 아파치에서 제공하는 HttpClient로 대체하는 방식을 사용해 봅시다.

 

1) 의존성 추가하기

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

 

2) 커스텀 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;

 

12.3 WebClient란?

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

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

리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있습니다.

 

WebClient의 특징

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

최근 프로그래밍 추세에 맞춰 스프링에도 리액티브 프로그래밍이 도입되면서 여러 동시적 기능이 제공되고 있습니다.

 

1) 의존성 추가하기

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

 

12.4 WebClient 사용하기

12.4.1 WebClient 구현

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

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

- 서버 프로젝트의 GET, POST 메서드 컨트롤러에 접근할 수 있는 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();
    }
    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();
    }
}

WebClient는 우선 객체를 생성한 후 요청을 전달하는 방식으로 동작합니다.

builder()를 통해 baseUrl() 메서드에서 기본 URL을 설정하고 defaultHeader() 메서드로 헤더의 값을 설정하는 것처럼 객체 생성 후 재사용하는 방식으로 구현하는 것이 좋습니다.

<buider() 사용 시 확장할 수 있는 메서드>

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

[인프런] 섹션 0. 스프링 시큐리티 기본

1. 환경설정

1) 데이터베이스 security를 만들어 줍니다

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

2) 프로젝트를 생성해 줍니다.

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: update #create update none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true

4) indexContoroller를 작성해 줍니다.

여기에서 머스테치 기본 폴더는 src/main/resources/입니다. mustache 의존성을 받았기 때문에 뷰리볼버 설정을 

생략할 수 있는데 여기서 뷰리졸버 설정은 templates (prefix), mustache (suffix)입니다.

5) index.html을 작성해 줍니다.

6) config 아래 WebMvcConfig파일을 만들어 줍니다.

여기에서 머스테치를 재설정해줍니다. 뷰리졸버가 html을 던지고 UTF-8로 되어있음을 알려주는 형식입니다.

2. 시큐리티 설정

1) indexController에 다양한 함수를 만들어 줍니다.

-> user, login, join 등의 함수

2) SecurityConfig를 만들어서 권한 설정을 해줍니다.

- @EnableWebSecurity를 이용하면 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됩니다.

- 로그인을 하지 않으면 user에 접근하지 못하는 등의 설정을 할 수 있습니다.

-. formLogin(). loginPage("/login")을 통해서 제한된 페이지로 이동했을 때 바로 로그인 페이지로 이동할 수 있게 설정할 수 있습니다.

3.  시큐리티 회원가입

1) 유저테이블을 만들어줍니다.

2) 모델 패키지를 만들고 User 클래스를 만들어줍니다.

 

3) joinForm.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>

4) JpaRepository를 이용한 UserRepository 인터페이스를 생성해 줍니다.

// JpaRepository 를 상속하면 자동 컴포넌트 스캔됨.
public interface UserRepository extends JpaRepository<User, Integer>{

}

5) 이후 indexController의 join부분을 수정합니다.

그전에 비밀번호를 암호화하기 위해서 securityConfig를 수정해서 bean을 등록해 주어야 합니다.

4.  시큐리티 로그인

1) Auth 패키지 아래 PrincipalDetails 클래스를 만들어 줍니다.

로그인 주소가 호출이 되면("/login") 시큐리티가 낚아채서 대신 로그인을 진행해 줍니다.

로그인 진행이 완료가 되면 시큐리티 session을 만들어줍니다.(Security ContextHolder)

- 오브젝트 => Authentication 타입 객체( Authentication 안에 User 정보가 있어야 함)

- User오브젝트타입 => UserDetails 타입 객체

정리하자면 Security  session 안에 Authentication 안에 UserDetails을 가지고 오는 것입니다.

2) PrincipalDetails에

3) Authentication을 작성하기 위해서 Auth에 PrincipalDetailsService를 작성해 줍니다.

@Service를 설정을 해줍니다.

시큐리티 설정에서 loginProcessingUrl("/login");

login 요청이 오면 자동으로 UserDeatilsService 타입으로 IoC 되어 있는 loadByUsername 함수가 실행됩니다.

-> User에 있는 이름을 찾아 리턴해 주는 함수를 작성한다고 생각하면 됩니다.

4) SecurityController에 defaultSuccessUrl("/");을 작성합니다.

그러면 user로 로그인하면 user페이지에서 계속 사용할 수 있습니다.

 

5.  시큐리티 권한처리

1) DB에 manager와 admin의 아이디를 설정해 줍니다.

2) SecurityController에서 @EnableGlobalMethodSecurity(securedEnabled=true, prePostEnabled=true)를 이용하여 secured 어노테이션과 preAuthorize 어노테이션을 활성화합니다.

- @Secured()를 이용하여 특정인만 특정 메서드에 접근할 수 있게 할 수 있습니다. 여기에서는 개인정보 페이지에 설정하였습니다.

- @PreAutheorize()을 이용하면 함수가 실행되기 직전에 @Secured()와 같은 역할을 수행합니다.

 

작성자: 아리

728x90

관련글 더보기