본문 바로가기

MSA

개별 마이크로 서비스 간 통신

비동기 요청을 보내는 쪽에서  Fegin 요청이 필요하지만 받는 쪽에서는 Fegin 처리가 필요하지 않는다.

1. 사전 설정 및 이론

  • fegin을 사용하도록 요청을 넣는 입장의 마이크로 서비스 애플리케이션 측에 의존성 부여
  • Eureka를 지나서 fegin 요청을 넣음으로 엔트포인트가 아닌 서비스명으로 넣을 수 있었음
  • 통신용 인터페이스에서 호출 정보를 분리해서 저정하기 때문에 Fegin관련 특정 로직을 실행하는 서비스 클래스 내부에 인터페이스와 구현체를 생성하고 해당 인터페이스 내부의 요청 메서드를 호출하면, 타 마이크로 서비스에 그대로 요청이 전달됨
// 인터페이스 자료형, 인터페이스명, 생성자만 만들면 알아서 구현체를 만들어서 대입함
@Service
public class Service {

    private final Repository repository;
    private final 인터페이스명 인터페이스명;

    public Service(Repository repository,
                       인터페이스명 인터페이스명) {
        this.repository = repository; // 도메인에 있는 DB에서 직접 들고 올  repository 
        this.인터페이스명 = 인터페이스명; // 다른 도메인에서 가져오는 조인처럼 사용하는 것
    }

    public 리턴자료형1 조인데이터얻어오기 (자료형 키값) {
        리턴자료형1 변수명 = Repository.findById(키값)
                .orElseThrow(RuntimeException::new);

        리턴자료형2 자료변수 = 인터페이스명.비동기요청메서드(id);

    }
}

 

2. 구현

  • Gateway-service

-yml 파일

server:
  port: 8000

spring:
  application:
    name: apigateway-server
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            message: Global Filter Default Message Test
            pre: true
            post: true

      routes: # 개별 마이크로서비스 정보를 routes에 기입함
#        유저 서비스 등록
        - id: user-service
          uri: lb://USER-SERVICE  #직접적인 주소를 등록하지 않고 로드밸런서를 설정해서 디스커버리 유레카를 통한다.
          predicates:
            - Path=/user-service/**

        - id: order-service
          uri: lb://ORDER-SERVICE
          predicates:
            - Path=/order-service/**

        - id: item-service
          uri: lb://ITEM-SERVICE
          predicates:
            - Path=/item-service/**
 
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      default-zone: http://127.0.0.1:8761/eureka
  instance:
    instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}

 

user-service

 

- build.gradle 

ext {
    set('springCloudVersion', "2022.0.4")
}

dependencies {
	// openfeign
	implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

 

- Application.java

Fegin은 Eureka를 기반으로 작동함으로 개별 Discovery-client에서 어노테이션을 붙여서 Fegin 클라이언트임을 등록하여 비동기 요청임을 명시합니다.

package com.example.userservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // Eureka를 지나서 feign 요청을 넣는다 특정한 엔드포인트가 아닌 서비스명으로 넣을 수 있음
public class UserServiceApplication {

    public static void main(String[] args) {
       SpringApplication.run(UserServiceApplication.class, args);
    }

}

 

-User

package com.example.userservice.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter // @Setter은 엔티티 수정을 막기 위해서, 캡슐화 + 은닉화
@AllArgsConstructor @NoArgsConstructor @Builder
@Table(name="users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private String name;

    @Column(unique = true)
    private String userId;

    private String encPw;

    private String uuid;

    private LocalDateTime createAt;

}

// 도메인 레이어는 다른 레이어를 참조하면 안 되지만 다른 레이어에서 도메인 레이어를 참조해도 된다.

 

- UsertoOrderFeginClient.java

name 지정 시 spring.application.name이 아닌 Eureka 사이트 Application name으로 해주세요.

요청 하는 쪽: user-service , 요청 받는 쪽: order-service 

package com.example.userservice.feignclient;

import com.example.userservice.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

// name: 유레카에 등록된 서비스명, path:게이트웨이 서버에서 붙는 서비스 앞단에 붙는 접두어
@FeignClient(name="ORDER-SERVICE",path = "order-service") // order-service로 유레카에 등록된 서비스의
public interface UsertoOrderFeignClient {

    @GetMapping("/orders/{userId}/users") // orders/{userId}로 호출을 넣으면
    public List<Order> getOrderListByUserId(@PathVariable String userId); // 요청을 처리한다


}

 

-ResponseFindUserDto

package com.example.userservice.dto;

import com.example.userservice.domain.Order;
import com.example.userservice.domain.User;
import lombok.*;

import java.util.List;

@Getter @Setter
@AllArgsConstructor @NoArgsConstructor @Builder @ToString
public class ResponseFindUserDto {

    private Long id;

    private String uuid;

    private String email;

    private String name;

    private String userId;

    // 필요하다면 구매 내역을 같이 가져올 수 있도록 처리합니다.
    private List<Order> orderList;

    public ResponseFindUserDto(User user){
        this.id = user.getId();
        this.email =user.getEmail();
        this.name =user.getName();
        this.uuid =user.getUuid();
        this.userId=user.getUserId();
    }

}

 

-UserRepository

package com.example.userservice.repoaitory;

import com.example.userservice.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Long> {

    User findUserByUserId(String userId);
}

 

- UserController

package com.example.userservice.controller;

import com.example.userservice.dto.RequestCreateUserDto;
import com.example.userservice.dto.ResponseFindUserDto;
import com.example.userservice.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@RestController
// MVC 패턴에서 @RequestMapping("user-service")가 없어도 빌드 오류가 발생하지는 않지만
// 게이트웨이서  predicates: - Path=/user-service/** 로 등록되었기 때문에 @RequestMapping("user-service")가 필요함!!
@RequestMapping("user-service")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
    
    @PostMapping("users")
    public ResponseEntity<?> createUser(@Valid @RequestBody RequestCreateUserDto userDto){
        userService.creatUser(userDto);
        return new ResponseEntity(HttpStatus.CREATED); // 상태코드 2001
    }
    
    @GetMapping("users/{userId}/orders")
    public ResponseEntity<?> findOrdersByUserId(@PathVariable String userId){
        ResponseFindUserDto userDto = userService.findUserOrderList(userId);
        return ResponseEntity.ok(userDto);
    }
    
}

 

 

Order-service

-Order

Entity 목적으로 만들지 않았기 때문에, @Table 어노테이션이 없습니다.

ResponseFindUserDto에서 Order 클래스 리스트값을 위해 생성되었습니다.

package com.example.userservice.domain;

// ResponseFindUserDto에서  import 에러를 막기 위해서 생성

import lombok.*;

import java.time.LocalDateTime;

// Entity 목적으로 만들지 않음
@Getter @Setter @AllArgsConstructor @NoArgsConstructor
@Builder @ToString
public class Order {
    private Long id;
    private String orderId;
    private Long count;
    private LocalDateTime createAt;
    private String userId;
    private String productId;
}

- OrderRepository

package com.example.orderservice.repository;

import com.example.orderservice.domain.Order;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface OrderRepository extends JpaRepository<Order,Long> {

    // feign을 위한 쿼리메서드로 userId를 이용해 해당 유저의 전체 order 목록을 가져오는 메서드
    Optional<List<Order>> findOrderByUserIdOrderByCreateAtDesc (String userId);
}

 

-OrderServcice

package com.example.orderservice.service;

import com.example.orderservice.domain.Order;
import com.example.orderservice.dto.RequestCreateOrderDto;
import com.example.orderservice.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    public void createOrder(RequestCreateOrderDto orderDto){
        orderRepository.save(orderDto.toEntity());
    }

    // userId를 입력하면 Order리스크를 넘겨주는 메서드 호출을 통해 리턴
    public Optional<List<Order>> getListOrder(String userId){
        Optional<List<Order>> orderUserList = orderRepository.findOrderByUserIdOrderByCreateAtDesc(userId);
//                .orElseThrow(()->new RuntimeException( "해당 유저가 없습니다"));
        return orderUserList;
     }
 }

 

-OrderController

package com.example.orderservice.controller;

import com.example.orderservice.domain.Order;
import com.example.orderservice.dto.RequestCreateOrderDto;
import com.example.orderservice.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("order-service")
public class OrderController {

    private final OrderService orderService;

    @GetMapping("health-check")
    public String healthCheck(){
        return "item-service server is available";
    }

    @PostMapping("orders")
    public ResponseEntity<?> createOrder(@RequestBody RequestCreateOrderDto requestCreateOrderDto){
        orderService.createOrder(requestCreateOrderDto);
        return new ResponseEntity(HttpStatus.CREATED);
    }

    @GetMapping("orders/{userId}/users")
    public ResponseEntity<?> getOrderListByUserId(@PathVariable String userId){
         List<Order> orderList = orderService.getListOrder(userId)
                .orElseThrow(()-> new RuntimeException("없는 유저 아이디로 조회하였습니다"))
                .stream().toList();  // .stream().toList()는 Optional에서 유저 아이디로 조회한 주문 목록을 가져와서, 해당 주문 목록을 리스트로 변환하는 과정을 나타냅니다.
        return ResponseEntity.ok(orderList);
    }
 }