[Spring Cloud] 마이크로서비스간 통신 방법

jsieon97·2023년 3월 17일
0

동기와 비동기

  • 동기 : 동기 방식은 서버에서 요청을 보냈을 때 응답이 돌아와야 다음 동작을 수행할 수 있다. 즉 A작업이 모두 진행 될때까지 B작업은 대기해야한다.
  • 비동기 : 비동기 방식은 반대로 요청을 보냈을 때 응답 상태와 상관없이 다음 동작을 수행 할 수 있다. 즉 A작업이 시작하면 동시에 B작업이 실행된다. A작업은 결과값이 나오는대로 출력된다.

Rest Template 사용

Rest Template

  • Spring 3.0 부터 지원하는 Spring의 HTTP 통신 템플릿
  • HTTP 요청 후 JSON, XML, String 과 같은 응답을 받을 수 있는 템플릿
  • Blocking I/O 기반의 동기방식을 사용하는 템플릿
  • RESTful 형식에 맞추어진 템플릿
  • Header, Content-Tpye등을 설정하여 외부 API 호출
  • Server to Server 통신에 사용

UserService 세팅

  • Rest Template 세팅
// UserServiceApplication.java

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {

    ...
	
    // Bean에 RestTemplate 등록
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
  • UserId로 Order정보 받아오기
// UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private Environment env;
    private RestTemplate restTemplate;

    public UserServiceImpl(UserRepository userRepository,
                           BCryptPasswordEncoder bCryptPasswordEncoder,
                           Environment env,
                           RestTemplate restTemplate) {
        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.env = env;
        this.restTemplate = restTemplate;
    }
	
    ...

	@Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
        
		/* Using as Rest Template */
		// property파일에 userId를 기준으로 order정보를 받아오는 API주소를 등록한다.
        String orderUrl = String.format(env.getProperty("order_service.url"), userId); // property파일에 userId를 기준으로 order정보를 받아오는 API주소
        ResponseEntity<List<ResponseOrder>> orderListResponse =
                restTemplate.exchange(orderUrl, HttpMethod.GET, null,
                        new ParameterizedTypeReference<List<ResponseOrder>>() {
                        });
        List<ResponseOrder> ordersList = orderListResponse.getBody();

        userDto.setOrders(ordersList);

        return userDto;
    }
}

property(yml 파일)설정

order-service:
  url: http://127.0.0.1:8000/order-service/%s/orders/

테스트

UserService, API Gateway, OrderService, ConfigService 실행

  1. 계정 생성
  2. 로그인 후 userId 추출
  3. POST {ContextPath}/order-service/{userId}/orders 로 주문 정보를 담아 요청
  4. Orders 테이블에 주문 정보 저장 확인
  5. GET {ContextPath}/order-service/{userId}/orders 요청을 통해 주문 정보 응답받기

@LoadBalanced 적용

// UserServiceApplication.java

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {

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

    ...

    @Bean
    @LoadBalanced // @LoadBalanced 추가
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
// property 설정 변경

order-service:
  url: http://ORDER-SERVICE/order-service/%s/orders/

FeignClient 사용

FeignClient 란?

  • FeignCliend -> HTTP Client
    • REST Call을 추상화 한 Spring Cloud Netflix 라이브러리
  • 사용방법
    • 호출하려는 HTTP Endpoint에 대한 Interface를 생성
    • @FeignClient 선언
  • Load balanced 지원
  • 선언적 방식으로 Rest 기반 호출을 추상화하여 제공
  • Interface와 Annotation만으로 간단하게 HTTP API 클라이언트를 구현

적용

  • Spring Cloud Netflix 의존성 추가
  • @FeignClient Interface 생성

UserService

UserServiceApplication.java에 Annotation적용

// UserServiceApplication.java

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // FeignClient 적용 어노테이션
public class UserServiceApplication {

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

	...
    
}

UserService에 OrderServiceClient생성

// OrderServiceClient.java

@FeignClient(name = "order-service")
public interface OrderServiceClient {

    @GetMapping("/order-service/{userId}/orders")
    List<ResponseOrder> getOrders(@PathVariable("userId") String userId);

}

UserServiceImpl 수정

// UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private Environment env;
    private RestTemplate restTemplate;
    private OrderServiceClient orderServiceClient;

    public UserServiceImpl(UserRepository userRepository,
                           BCryptPasswordEncoder bCryptPasswordEncoder,
                           Environment env,
                           RestTemplate restTemplate,
                           OrderServiceClient orderServiceClient) {
        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.env = env;
        this.restTemplate = restTemplate;
        this.orderServiceClient = orderServiceClient;
    }

    ...

    @Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

		/* Using as Rest Template */
//        List<ResponseOrder> orders = new ArrayList<>();
//        String orderUrl = String.format(env.getProperty("order-service.url"), userId);
//        ResponseEntity<List<ResponseOrder>> orderListResponse =
//                restTemplate.exchange(orderUrl, HttpMethod.GET, null,
//                        new ParameterizedTypeReference<List<ResponseOrder>>() { // 반환 타입 설정
//                        });
//        List<ResponseOrder> ordersList = orderListResponse.getBody();

        /* Using as feign client */
        List<ResponseOrder> ordersList = orderServiceClient.getOrders(userId);

        userDto.setOrders(ordersList);

        return userDto;
    }

    ...
    
}

예외 처리 (FeignException)

UserService에서 예외처리

// application.yml

logging:
  level:
    com.example.userservice.client: DEBUG
    
// UserServiceApplication.java

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {

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

    ...

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}
// UserServiceImpl.java

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private Environment env;
    private RestTemplate restTemplate;
    private OrderServiceClient orderServiceClient;

    public UserServiceImpl(UserRepository userRepository,
                           BCryptPasswordEncoder bCryptPasswordEncoder,
                           Environment env,
                           RestTemplate restTemplate,
                           OrderServiceClient orderServiceClient) {
        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.env = env;
        this.restTemplate = restTemplate;
        this.orderServiceClient = orderServiceClient;
    }

    ...

    @Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

        /* Using as Rest Template */
//        List<ResponseOrder> orders = new ArrayList<>();
//        String orderUrl = String.format(env.getProperty("order-service.url"), userId);
//        ResponseEntity<List<ResponseOrder>> orderListResponse =
//                restTemplate.exchange(orderUrl, HttpMethod.GET, null,
//                        new ParameterizedTypeReference<List<ResponseOrder>>() { // 반환 타입 설정
//                        });
//        List<ResponseOrder> ordersList = orderListResponse.getBody();

        /* Using as feign client */
        List<ResponseOrder> ordersList = null;
        try{
            ordersList = orderServiceClient.getOrders(userId);
        } catch (FeignException ex) {
            log.error(ex.getMessage());
        }
        userDto.setOrders(ordersList);

        return userDto;
    }

    ...
    
}

예외 처리를 통해 log 추적 가능

ErrorDecoder

UserServiceApplication.java

	// 추가
    
	@Bean
    public FeignErrorDecoder getFeignErrorDecoder() {
        return new FeignErrorDecoder();
    }

FeignErrorDecoder생성

// FeignErrorDecoder.java

public class FeignErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        switch (response.status()) {
            case 400:
                break;
            case 404:
                if (methodKey.contains("getOrders")) {
                    return new ResponseStatusException(HttpStatus.valueOf(response.status()),
                            "User's orders is empty");
                }
                break;
            default:
                return new Exception(response.reason());
        }

        return null;
    }
}

error코드의 status값 마다 message를 다르게 할 수 있다.
message를 env.getProperty()를 이용해 property파일로 수정되게 바꿀 수 있다. FeignErrorDecoder@Component를 적용해 (이 경우 UserServiceApplication에 FeignErrorDecoder를 없애도 정상 실행된다.)

UserServiceImpl에선 실행만 하면 알아서 예외처리된다.

@Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

        /* Using as Rest Template */
//        List<ResponseOrder> orders = new ArrayList<>();
//        String orderUrl = String.format(env.getProperty("order-service.url"), userId);
//        ResponseEntity<List<ResponseOrder>> orderListResponse =
//                restTemplate.exchange(orderUrl, HttpMethod.GET, null,
//                        new ParameterizedTypeReference<List<ResponseOrder>>() { // 반환 타입 설정
//                        });
//        List<ResponseOrder> ordersList = orderListResponse.getBody();

        /* Using as feign client */
//        List<ResponseOrder> ordersList = null;
//        try{
//            ordersList = orderServiceClient.getOrders(userId);
//        } catch (FeignException ex) {
//            log.error(ex.getMessage());
//        }

        /* ErrorDecoder */
        List<ResponseOrder> ordersList = orderServiceClient.getOrders(userId);
        userDto.setOrders(ordersList);

        return userDto;
    }
profile
개발자로써 성장하는 방법

0개의 댓글