본문 바로가기

Spring

[Spring] MSA 프로젝트 만들기 (3)

2023.03.22 - [Spring] - [Spring] MSA 프로젝트 만들기 (2)

 

[Spring] MSA 프로젝트 만들기 (2)

2023.03.19 - [Spring] - [Spring] MSA 프로젝트 만들기 (1) [Spring] MSA 프로젝트 만들기 (1) 2023.02.28 - [Server] - [Spring] 마이크로서비스 아키텍쳐 (Micro Service Architecture, MSA) [Spring] 마이크로서비스 아키텍쳐 (Micro

keylog.tistory.com

이전 글에서 gateway-service에서 Custom Filter를 구현하여 권한이 필요한 요청에 인증된 회원의 요청만 전달되도록 하는 방법과 Spring Cloud Gateway에서 CORS 설정을 하는 방법에 대해 알아보았습니다. 이번 글에서는 마이크로 서비스들 간에 통신하는 방법인 RestTemplate FeignClient에 대해서 알아보겠습니다.

 

RestTemplate

RestTemplate 클래스는 다른 서버의 REST-API를 호출할 수 있도록 URI를 설정하고 GET, POST 같은 HTTP 메서드를 사용할 수 있는 메서드를 제공해줍니다. 또한 요청 메시지의 헤더, 바디를 구성할 수 있는 클래스를 사용할 수 있으며, 응답 메시지의 헤더, 상태 코드, 바디를 조회할 수 있는 클래스를 사용할 수 있습니다.

 

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
    
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
    	return new RestTemplate();
    }

}

  

또한 RestTemplate은 멀티 스레드 환경에 안전한 클래스이므로 스프링 빈으로 객체를 생성하고 필요한 곳에 주입해서 사용할 수 있습니다. UserServiceApplication 클래스에서 RestTemplate 반환 값을 처리할 수 있는 getRestTemplate() 메서드를 빈으로 등록해 줍니다. 

 

여기서 @LoadBalanced 어노테이션은 밑에서 RestTemplate으로 challenge-service API를 호출할때 마이크로 서비스의 이름으로 호출하기 위해서 사용했습니다.

 

/**
 * 현재 user가 참가중인 챌린지 개수 반환
 */
@GetMapping("/challengeInfo/users/{userId}")
public ResponseEntity<UserChallengeInfoResponseDto> userChallengeInfo(@PathVariable Long userId) {
    return ResponseEntity.status(HttpStatus.OK).body(challengeService.myChallengeList(userId));
}

 

그런 다음 위와 같이 현재 user가 참가중인 챌린지 개수를 반환해주는 API가 challenge-service에 작성되어있다고 했을 때 아래와 같이 userChallengeInfo API를 호출하는 RestTemplate 코드를 작성해줍니다.

 

@Override
@Transactional(readOnly = true)
public UserResponseDto getUserInfo(String userId) {
    ...

    // Using as restTemplate
    String challengeUrl = String.format(env.getProperty("challenge_service.url"), userId);
    ResponseEntity<UserChallengeInfoResponseDto> userChallengeInfoResponse =
            restTemplate.exchange(challengeUrl, HttpMethod.GET, null,
                    new ParameterizedTypeReference<UserChallengeInfoResponseDto>() {
            });

    UserChallengeInfoResponseDto userChallengeInfoResponseDto = userChallengeInfoResponse.getBody();
        
    ...
}

 

application.yml

...

challenge_service:
  url: http://challenge_service/challenge_service/%s/orders

 

호출할 API의 url은 위와 같이 application.yml 파일에 작성한 뒤  Environment 객체를 사용해 조회해 왔습니다. 그리고 restTemplate 객체에 url, method, body를 작성해주고 ParameterizedTypeReference 객체의 Generic 타입을 지정하여 challenge-service에서 작성한 API와 리턴 타입을 맞춰줍니다.

 

여기까지 작성해준 뒤 user-service에서 사용자 정보를 조회해오는 API를 호출해주면 아래와 같이 challenge-service에서 조회해 온 정보가 포함되어서 리턴되는 것을 확인할 수 있습니다.

FeignClient

FeignClient는 Rest call을 추상화한 Spring Cloud Netflix에서 제공하는 라이브러리입니다. FeignClient 방식은 호출하고자 하는 마이크로 서비스의 API를 인터페이스로 정의하고 @FeignClient 어노테이션을 적용해주는 것 만으로 쉽게 사용할 수 있습니다. 그렇기 때문에 RestTemplate를 사용하기 위해 작성했던 코드 같이 중복되는 코드를 줄여주며 RestTemplate 보다 간편하게 사용할 수 있습니다.

사용방법

build.gradle

dependencies {
    ...

    // Feign Client
    implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '4.0.1'

    ...
}

 

먼저 build.gradle에서 위와 같이 FeignClient dependency를 추가해줍니다.

그런다음 user-service에서 FeignClient를 사용하겠다는 의미로 UserServiceApplication 클래스에서 @EnableFeignClients 어노테이션을 선언해줍니다.

 

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {

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

}

 

ChallengeServiceClient

@FeignClient(name = "challenge-service")
public interface ChallengeServiceClient {

    @GetMapping("/challenges/challengeInfo/users/{userId}")
    BaseResponseDto<ChallengeResponseDto> getChallengeInfo(@PathVariable Long userId);
}

 

challenge-service에서 호출할 API를 정의하기 위해 인터페이스를 생성해줍니다. 그런다음 @FeignClient 어노테이션을 추가해준 뒤 name 속성에 호출할 마이크로 서비스의 이름을 작성해 줍니다. 저희는 challenge-service에서 getChallengeInfo() 라는 API를 호출할 것이기 때문에 challenge-service로 작성하겠습니다.

 

그런다음 호출하고자 하는 API와 method와 url 그리고 파라미터 등을 맞춰서 메소드를 생성해줍니다.

 

이후 사용방법은 모놀리스식 서비스에서 다른 서비스의 메소드를 호출하는 방법과 같습니다. 작성한 Client 인터페이스를 의존성 주입받아서 challengeServiceClient.getChallengeInfo(userId); 이런식으로 메서드를 호출해서 사용하시면 됩니다.

 

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

    ...

    private final PayServiceClient payServiceClient;

    private final ChallengeServiceClient challengeServiceClient;

	...

    @Override
    @Transactional(readOnly = true)
    public MypageResponseDto getMypageInfo(Long userId) {
        User user = getUser(userId);
        ChallengeResponseDto challengeResponseDto = challengeServiceClient.getChallengeInfo(userId);
        MoneyResponseDto moneyResponseDto = payServiceClient.getMoneyInfo(userId);
        return MypageResponseDto.of(user, challengeResponseDto, moneyResponseDto);
    }

    private User getUser(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new ApiException(ExceptionEnum.MEMBER_NOT_EXIST_EXCEPTION));
    }

}

 

참고로 RestTemplateFeignClient 두가지 방식 모두 마이크로 서비스간 내부에서 통신할때는 gateway-service를 통과하지 않습니다. 물론 API Gateway를 통해 호출해도 되지만, 내부적인 통신을 사용할 때 굳이 외부에서 다시 접속 요청을 할 필요는 없기 때문에 권한이 필요한 요청의 경우 토큰에 userId를 담아서 전달하지 않고 @PathVariable이나 @RequestBody를 사용해서 userId를 전송해주는 방식으로 호출했습니다.

 

지금까지 마이크로 서비스간 내부통신을 하기 위한 방법으로 RestTemplate과 FeignClient에 대해서 알아보았습니다. 다음 글에서는 JenkinsDocker를 사용해서 CI-CD 파이프라인을 구축하고 파이프라인 스크립트 코드를 작성하여 변경된 마이크로 서비스만 재배포 되도록 병렬처리 하는 방법에 대해서 알아보겠습니다.