마이크로서비스 아키텍처 (MSA)
: 애플리케이션이 가지고 있는 기능(서비스)이 하나의 비즈니스 범위만 가지는 형태
📌 애플리케이션 : 자신의 기능을 API로 외부에 노출하고, 다른 서버가 API를 호출해서 사용할 수 있게 구성됨
⇒ 각 서버가 다른 서버의 클라이언트가 되는 경우가 많음
: 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿으로 RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있음
→ 주로 동기 방식으로 처리 / AsyncRestTemplate를 통해 비동기 방식 사용 가능 / 현재 지원 중단 상태
특징
메서드 | HTTP 형태 | 설명 |
getForObject | GET | GET 형식으로 요펑한 결과를 객체로 반환 |
getForEntity | GET | GET 형식으로 요청한 결괄를 ResponseEntity 결과를 반환 |
postForLocation | POST | POST 형식으로 요청한 결과를 헤더에 저장된 URI로 반환 |
postForObject | POST | POST 형식으로 요청한 결과를 객체로 반환 |
postForEntity | POST | POST 형식으로 요청한 결과를 ResponseEntity 형식으로 반환 |
delete | DELETE | DELETE 형식으로 요청 |
put | PUT | PUT 형식으로 요청 |
patchForObject | PATCH | PATCH 형식으로 요청한 결과를 객체로 반환 |
optionsForAllow | OPTIONS | 해당 URI에서 지원하는 HTTP 메서드를 조회 |
exchange |
any |
HTTP 헤더를 임의로 추가할 수 있고, 어떤 메서드 형식에서도 사용할 수 있음 |
execute | any | 요청과 응답에 대한 콜백을 수정 |
한 컴퓨터 안에서 두 개의 프로젝트를 가동하기 위해 톰캣의 포트 변경 필요
컨트롤러 클래스
→ GET, POST 메서드 형식의 요청을 받기 위한 코드
( + PUT, DELETE 메서드 : GET과 POST 형식과 각 구성 방식이 비슷하여 생략)
@RestController
@RequestMapping("/api/v1/crud-api")
public class CrudController {
// 파라미터 없는 경우
@GetMapping
public String getName(){
return "Flature";
}
// PathVariable 파라미터 사용하는 경우
@GetMapping(value = "/{variable}")
public String getVariable(@PathVariable String variable){
return variable;
}
// RequestParam 파라미터 사용하는 경우
@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);
}
// 임의의 HTTP 헤더를 받도록 구현
@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);
}
}
MemeberDto 클래스
: name, email, organization으로 총 3개의 필드
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 + '\'' +
", orgaization='" + organization + '\'' +
'}';
}
}
RestTemplate : 별도의 유틸리티 클래스로 생성하거나 서비스 or 비즈니스 계층에 구현됨
⇒ 앞에서 생성한 서버 프로젝트에 요청 날리기 위해 클라이언트 역할도 수행할 새로운 서버 프로젝트가 필요
서버 프로젝트 생성
의존성
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
GET, POST 형식의 RestTemplate 작성하기
RestTemplate 생성 및 사용 방법 ⇒ UriComponentsBuilder 사용
📌UriComponentsBuilder : 스프링 프레임워크에서 제공하는 클래스
@Service
public class RestTemplateService {
// GET 부분
// PathVariable or 파라미터를 사용하지 않는 호출 및 getForEntity로 파라미터 전달
public String getName(){
// uri-> restTemplate이 외부 API 요청하는데 사용
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090") // 호출부의 URL 입력
.path("/api/v1/crud-api") // 세부 경로 입력
.encode() // 인코딩 문자셋 설정 -> 기본값(StandardCharsets.UTF_8)
.build() // 빌더 생성 종류 및 UriComponents 타입 리턴
.toUri(); // URI타입으로 리턴, String 타입 리턴=> toUriString() 사용
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class); // URI와 응답받는 타입을 매개변수로 사용
return responseEntity.getBody();
}
public String getNameWithPathVariable(){
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/v1/crud-api/{name}") // 중괄호에 변수명 입력
.encode()
.build()
.expand("Flature") // path의 변수명에 해당하는 값을 순서대로 입력, 복수의 값을 넣어야할 경우 ',' 추가하여 구분
.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();
}
// POST 부분 -> 오부 API 요청할 떄 Body 값과 파라미터 값을 담는 방법
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();
// RequestBody에 값을 담는 방법 -> 데이터 객체 생성 후 값을 담음
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); // 파라미터로 데이터 객처 넣기
// postForEntity() 메서드로 API 호출 -> 서버 프로젝트 콘솔 로그에 RequsetBody 값 출력 및 파라미터 값이 결괏값으로 리턴
return responseEntity;
}
// 헤더 추가 -> 대부분 외부 API의 토큰값을 헤더에 담아 전달하는 방식
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@gmail.com");
memberDto.setOrganization("Around Hub Studio");
// RequestEntity -> 헤더 설정하기에 가장 편한 방법
RequestEntity<MemberDto> requestEntity = RequestEntity
.post(uri) // uri 설정
.header("my-header", "Wikibooks API") // 헤더의 키 이름과 값을 설정하는 코드 -> API 명세서에 헤더에 필요한 키 값 요구하면 제시함
.body(memberDto);
RestTemplate restTemplate = new RestTemplate();
// 모든 형식의 HTTP 요청 생성 가능 => RequestEntity 설정을 post가 아닌 다른 형식의 메서드 정의만으로도 쉽게 사용 가능
ResponseEntity<MemberDto> responseEntity = restTemplate.exchange(requestEntity, MemberDto.class);
return responseEntity;
}
}
Swagger 설정 코드
: 프로젝트에서 쉽게 API를 호출할 수 있게 Swagger 설정
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.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();
}
@GetMapping
public ResponseEntity<MemberDto> postDto(){
return restTemplateService.postWithParamAndBody();
}
@GetMapping("/header")
public ResponseEntity<MemberDto> postWithHeader(){
return restTemplateService.postWithHeader();
}
}
⇒ 애플리케이션 실행 후 POST API 호출에 따른 출력 결과
flature!!
flature@gamil.com
Around Hub Studio
RestTemplate : HTTPClient를 추상화하고 있음
📌 HTTPClient : 종류에 따라 기능 차이가 존재 → 가장 큰 차이 : 커넥션 풀(Connection Pool)
📌 RestTemplate는 커넥션 풀 지원x → 매번 호출할 때마다 포트를 열어 커넥션 생성
의존성 추가
→ HttpClient 추가하면 RestTemplate 설정 쉽게 추가, 변경 가능
<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();
// HttpClient 생성하면 setHttpClient()통해 인자 전달해 설정 가능
factory.setHttpClient(httpClient);
factory.setConnectTimeout(2000);
factory.setReadTimeout(5000);
RestTemplate restTemplate = new RestTemplate(factory);
return restTemplate;
}
📌 ClientHttpRequestFactory : 함수형 인터페이스로 대표적인 구현체
⇒ SimpleClientHttpRequestFactory와 HttpComponentsClientHttpRequestFactory가 있음
: 리액트 기반으로 동작하는 API로 스레드와 동시성 문제 벗어나 비동기 형식으로 사용 가능
→ RestTemplate 지원 중단으로 WebClient 사용을 권고하며 Spring WebFlux가 HTTP 요청을 수행하는 클라이언트로 WebClient 제공
특징
의존성 추가
WebClient 사용 가능한 환경 구성을 위해선 WebFlux 모듈에 대한 의존성 추가가 필요
📌WebFlux : 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 스프링 프레임워크5에 새롭게 추가된 모듈
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
생성 방법 → create() 메서드를 이용한 생성 or builder()를 이용한 생성
@Service
public class WebClientService {
public String getName() {
// WebClient -> 객체 생성 후 요청 전달하는 방식으로 동작 => WebClient 객체 이용 시 객체 생성 후 재사용하는 방식으로 구현
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9090") // 기본 URL 설정
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // 헤더의 값 설정
.build();
return webClient.get() // HTTP 메서드를 get(), post(), put(), delete() 등 네이밍이 명확한 메서드 설정 가능
.uri("/api/v1/crud-api") // URI확장 방법
.retrieve() // 요청에 대한 응답을 받았을 떄 그 값을 추출하는 방법
// Mono : Flux와 비교되는 개념 -> 리액티브 스트림에서 데이터를 제공하는 발행자 역할 수행하는 Publicsher의 구현체
.bodyToMono(String.class) // 리턴 타입을 설정해 문자열 객체 받아옴
.block(); // 블로킹 형식으로 동작하게 설정 -> WebClient가 논블로킹 방식으로 동작하기 떄문
}
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")) // parthVariable 추가하는 방법
.retrieve().toEntity(String.class).block();
// 간략하게 작성하는 방법
ResponseEntity<String> responseEntity1 = webClient.get()
.uri("/api/v1/crud-api/{name}", "Flature")
.retrieve()
.toEntity(String.class) // ResponseEntity 타입으로 응답 전달받음
.block();
return responseEntity.getBody();
}
public String getNameWithParameter() {
WebClient webClient = WebClient.create("http://localhost:9090");
// 쿼리 파라미터를 요청에 담기위해 uriBuilder 사용
return webClient.get().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
.queryParam("name", "Flature") // queryParam() 메서드로 전달하려는 값 설정
.build())
// exchange 지원 중단으로 exchangeToMono나 exchangeTFlux 사용 => 응답 결과 코드에 따라 다르게 응답 설정
.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");
// post() 메서드로 POST 메서드 통신 정의, uri()는 uriBuilder로 path와 parameter 설정
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) // HTTP바디 값을 설정 => 일반적으로 데이터 객체를(DTO, VO) 파라미터로 전달
.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") // 헤더에 값 추가 -> 외부 API사용을 위해 인증된 토큰값 담아 전달함
.retrieve()
.toEntity(MemberDto.class)
.block();
}
// 빌드된 WebClient는 변경 x 복사 후 사용o
public void cloneWebClient() {
WebClient webClient = WebClient.create("http://localhost:9090");
WebClient clone = webClient.mutate().build();
}
}
public String getNameWithPathVariable(){
URI uri = // 빈칸
.fromUriString("http://localhost:9090")
// 빈칸
.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("Test")
.toUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return responseEntity.getBody();
}
2. WebClient를 이용하여 PathVariable을 사용하는 GET 방식의 코드를 작성하세요. (필요한 값은 위 문제와 동일)
public String getNameWithPathVariable() {
WebClient webClient = // 빈칸
ResponseEntity<String> responseEntity1 = webClient.get()
// 빈칸
.retrieve()
.toEntity(String.class)
.block();
return responseEntity.getBody();
}
정답:
public String getNameWithPathVariable() {
WebClient webClient = WebClient.create("http://localhost:9090");
ResponseEntity<String> responseEntity1 = webClient.get()
.uri("/api/v1/crud-api/{name}", "Test")
.retrieve()
.toEntity(String.class)
.block();
return responseEntity.getBody();
}
[출처] 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p.349~372
Corner Spring 1
Editor: Luna
[스프링 1팀] 13장. 서비스 인증과 권한 부여 (0) | 2025.01.31 |
---|---|
[스프링 1팀] 10-11장. 유효성 검사와 예외처리 및 액츄에이터 (0) | 2025.01.17 |
[스프링 1팀] 9장.연관관계 매핑 (0) | 2025.01.10 |
[스프링 1팀] 8장. Spring Data JPA 활용 (0) | 2025.01.03 |
[스프링 1팀] 7장. 테스트 코드 작성하기 (0) | 2024.12.27 |