2023. 9. 4. 20:20ㆍBackEnd/Spring
![](https://blog.kakaocdn.net/dn/nxktG/btss91v5vdU/0gdMjp7tY38gD5glX7VVEk/img.jpg)
[버전 정보]
Spring Boot: 2.7.12
Spring Cloud: 2021.0.7
Spring Cloud Starter Openfeign: 3.1.6
Java: Openjdk 11
이번 프로젝트에서 Monolithic Spring Boot 프로젝트 코드를 MSA로 전환하기 위해서
2가지 Pain Point가 있었는데
1. 서비스간 통신 방법: HTTP vs gRPC
2. 통신 로직 구현 방법: Spring Cloud Netflix OpenFeign vs RestTemplate
단순한 요청-응답 모델을 사용하고 주로 간단한 데이터 송수신에 주로 사용되는 API가 많아서
HTTP를 사용하였고
기본적인 에러 핸들링과 API Gateway나 Service Discovery 를 이용해서 서비스간 통신 테스트에 유리하고 Interface 선언만으로도 HTTP Request를 정의할 수 있어 코드 가독성에도 유리한 Spring Cloud OpenFeign을 사용하여 MSA로 전환하였다.
1. 의존성 설치
출처 입력
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.12'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.fiveam'
version = '1.0'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.7")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-quartz'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
// ***********의존성 추가***********
implementation 'org.apache.httpcomponents:httpclient:4.5.9'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.6'
// *********************************
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
2. @EnableFeignClients
애플리케이션 최상위 클래스에 @EnableFeignClients 어노테이션 추가
package com.fiveam.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
3. 서비스 엔드포인트 추가
현재 서비스(Order Service)로 요청 받을 서비스(User Service)에 엔드포인트를 생성한다.
package com.fiveam.userservice.user.controller;
import com.fiveam.userservice.response.UserInfoResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.fiveam.userservice.logout.Logout;
import com.fiveam.userservice.user.dto.UserDto;
import com.fiveam.userservice.user.entity.User;
import com.fiveam.userservice.user.mapper.UserMapper;
import com.fiveam.userservice.user.service.UserService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
@Validated
public class UserController {
private final Logout logout;
private final UserMapper mapper;
private final UserService userService;
@GetMapping
public ResponseEntity getUserInfo(){
User loginUser = userService.getLoginUser();
if(loginUser.getProvider() != null){
}
UserDto.Response userInfo = mapper.userToDto(loginUser, HttpMethod.GET);
return new ResponseEntity<>(userInfo, HttpStatus.ACCEPTED);
}
@GetMapping("/{userId}")
public ResponseEntity findUserById(@PathVariable Long userId) {
return new ResponseEntity<>(
UserInfoResponseDto.fromEntity(userService.findUserById(userId)),
HttpStatus.ACCEPTED
);
}
}
4. OpenFeign Interface 생성
출처 입력
현재 서비스(Order Service)에 OpenFeign Interface를 생성
- application.yml 파일 내 feign.user-service의 주소를 쿠버네티스의 유저 서비스 도메인으로 입력
order-service의 OpenFeign Interface에서 User-Service로 요청 보내기 위한 도메인을 입력하는 과정
해당 도메인 주소를 secret 값으로 숨기기 위해서 application.yml에 입력
# application.yml
feign:
user-service: user-service.app.svc.cluster.local:8080
- UserServiceClient Interface
@FeignClient: 요청을 보낼 서비스의 도메인 혹은 IP 주소 & 포트를 입력
@{Get, Post, Patch, Delete, …}Mapping: 메서드 타입
value: 요청 보낼 endpoint
consumes: 클라이언트가 보내는 데이터의 Content-Type
produces: 서버가 클라이언트에게 반환하는 데이터의 Content-Type
package com.fiveam.orderservice.client;
import com.fiveam.orderservice.response.UserInfoResponseDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
@FeignClient("${feign.user-service}")
public interface UserServiceClient {
@GetMapping(value = "/users", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
UserInfoResponseDto getLoginUser(@RequestHeader("Authorization") String authorization);
@GetMapping("/users/{userId}")
ResponseEntity<UserInfoResponseDto> findUserById(@PathVariable Long userId);
@GetMapping("/users/{userId}")
ResponseEntity<UserInfoResponseDto> findUserById(
@RequestHeader("Authorization") String authorization,
@PathVariable Long userId
);
}
- 리턴 타입 예시
한 가지 더, 주의해야 할 점은 요청을 받는 서비스(User Service)의 메서드 응답 DTO 클래스와
요청을 보내는 서비스(Order Service)의 메서드 리턴 타입을 동일하게 맞춰준다.
package com.fiveam.orderservice.response;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.ZonedDateTime;
@Getter
@Setter
@Builder
@ToString
public class UserInfoResponseDto implements Serializable {
private Long id;
private String email;
private String displayName;
private String address;
private String detailAddress;
private String realName;
private String phone;
private String password;
private Long cartId;
private boolean social;
private String sid;
private ZonedDateTime updatedAt;
}
5. OpenFeign Interface 의존성 주
요청을 보낼 서비스(Order Service) 에서
요청을 받을 서비스(User Service)에 대한 OpenFeign Interface의 의존성을 주입
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserServiceClient userService;
}
6. Interface Method 사용
출처 입력
OpenFeign Interface의 경우 개발자가 REST 요청을 보내는 코드를 직접 작성하지 않고
Interface 메소드를 사용하는 것으로
Interface에서 정의한 메서드의 어노테이션 정보에 따라 RESTful 요청을 보낼 수 있다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserServiceClient userService;
public Page<Order> findOrders(Long userId, int page, boolean subscription) {
// ******************* UserServiceClient Interface 메소드 사용 *******************
UserInfoResponseDto user = userService.findUserById(userId).getBody();
// *******************************************************************************
log.info("Find Orders For User: " + user);
if(subscription) {
Page<Order> findAllOrder = orderRepository.findAllByUserIdAndSubscriptionAndOrderStatusNot(
PageRequest.of(page, 7, Sort.by("orderId").descending()),
user.getId(), true, OrderStatus.ORDER_REQUEST);
return findAllOrder;
}
Page<Order> findAllOrder = orderRepository.findAllByUserIdAndSubscriptionAndOrderStatusNotAndOrderStatusNot(
PageRequest.of(page, 7, Sort.by("orderId").descending()),
user.getId(), false, OrderStatus.ORDER_REQUEST, OrderStatus.ORDER_SUBSCRIBE);
return findAllOrder;
}
}
7. 결론
OpenFeign interface 메서드 정의를 통해 기존에 구현된 코드의 큰 수정 없이
추상화된 메서드 사용으로 RESTful API 요청을 간편하게 사용하여
Micro Services Architecture 에서 서비스간 HTTP 요청을 통해 정보를 주고받을 수 있다.