MSA 서비스 전환 방법 / Spring Cloud OpenFeign

2023. 9. 4. 20:20BackEnd/Spring

 

[버전 정보]
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 요청을 통해 정보를 주고받을 수 있다.