티스토리 뷰

인프런 이도원님의 'Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)' 듣고 정리한 내용입니다.

 

Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) - 인프런 | 강의

Spring framework의 Spring Cloud 제품군을 이용하여 마이크로서비스 애플리케이션을 개발해 보는 과정입니다. Cloud Native Application으로써의 Spring Cloud를 어떻게 사용하는지, 구성을 어떻게 하는지에 대해

www.inflearn.com

 

1. Communication types

Spring Cloud로 개발된 마이크로서비스 간의 통신 방식에는 동기, 비동기가 있다.

동기 방식은 하나의 요청이 들어왔을 때 요청이 끝날 때까지 다른 작업을 하지 못하는 방식이다.

비동기 방식은 AMOP처럼 순차적으로 데이터를 동기화하는 것이 아니라 연결되어 있는 마이크로서비스에 변동 사항을 먼저 전달한다.

 

마이크로서비스들 간에 데이터를 호출할 수 있는 다른 방법은 Rest Template이다. Rest Template 인스턴스를 생성하여 필요한 메서드, 파라미터, 반환값을 처리한다. 또한, HTTP 프로토콜 방식을 이용하고 서비스를 대신 처리해준다.

 

2. RestTemplate

RestTemplate를 사용하려면 먼저 빈으로 등록해주어야 한다.

 

UserServiceApplication.java

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {

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

...

    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

 

 

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

    UserRepository userRepository;
    BCryptPasswordEncoder passwordEncoder;

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

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

//        List<ResponseOrder> orders = new ArrayList<>();
        String orderURL = "http://127.0.0.1:8000/order-service/%s/orders";
        // URL이 확정이 되었다면 이걸로 RestTemplate 주입 받아옴
        ResponseEntity<List<ResponseOrder>> orderListResponse =
                restTemplate.exchange(orderURL, HttpMethod.GET, null,
                                            new ParameterizedTypeReference<List<ResponseOrder>>() {
                });

        List<ResponseOrder> orderList = orderListResponse.getBody();
        userDto.setOrders(orderList);
        // -> Using as RestTemplate

        return null;
    }
    
    ...
}

 orderURL은 호출하고자 하는 OrderService의 주소를 명시한 것이고 여기서 %s는 userId값이다.

 

 

 

orderURL을 native-file-repo 폴더에 있는 user-service.yml 파일로 옮겨보자.

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

 

UserServiceImpl.java

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

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

        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> orderList = orderListResponse.getBody();
        userDto.setOrders(orderList);
        // -> Using as RestTemplate

        return null;
    }

Environmnet 객체를 이용하여 값을 받아오게 설정하였다.

 

 

주문 등록
주문 조회
사용자 정보랑 주문 내역 같이 조회
주문내역 조회

 

native-file-repo 폴더에 있는 user-service.yml에서 order-service 경로를 수정해보겠다.

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

변경된 내용을 반영시키기 위해  busrefresh를 이용했고 경로를 바꿔도 주문내역 조회가 잘 되었다.

 

3. Feign Client - Log, Exception

다음은 FeignClient를 이용해보겠다.

 

pom.xml - 의존성 추가

<!--Feign Client -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

 

UserServiceApplication - @EnableFeignClients 추가

@EnableFeignClients
public class UserServiceApplication {

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

 

order-servcie에서 호출하고 싶은 부분에 해당하는 인터페이스를 만들어준다.

 

OrderServiceClient.java

@FeignClient(name = "order-service") // 호출하려는 마이크로서비스 이름
public interface OrderServiceClient {

    @GetMapping("/order-service/{userId}/orders") // 호출하려는 부분의 endpoint와 연결
    List<ResponseOrder> getOrders(@PathVariable String userId);
}

 

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

...

    OrderServiceClient orderServiceClient;

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

...

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

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

...

        /* Using a Feign Client */
        List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);

        userDto.setOrders(orderList);

        return userDto;
    }

...

}

Rest template 역할을 인터페이스의 메서드로 처리가 가능하다.

 

사용자 정보와 주문내역 조회 성공

 

Feign client는 인터페이스 안에 또다른 마이크로서비스의 메서드를 호출하는 것이기 때문에 알기 어렵다.

 

Feign Client 로그를 사용해보자.

 

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;
    }
}

Fiegn Client가 호출이 되면 관련된 정보를 로그를 통해 확인이 가능하다.

 

다음은 Fiegn Client 예외처리를 해보자.

인터페이스의 endpoint 경로가 틀리다면?

FeignException 발생
Feign Client Log로도 확인 가능

 

UserServiceImpl.java

@Slf4j
@Service
public class UserServiceImpl implements UserService {

...

    OrderServiceClient orderServiceClient;

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

...

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

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

...

        /* Feign exception handling */
        List<ResponseOrder> orderList = null;
        try {
            orderList = orderServiceClient.getOrders(userId);
        } catch (FeignException e) {
            log.error(e.getMessage());
        }

        userDto.setOrders(orderList);

        return userDto;
    }

...

}

 

사용자 정보는 잘 뜨는데 주문내역은 안 뜸
로그로 에러 출력 볼 수 있음!

 

4. ErrorDecoder

ErrorDecoder의 decode라는 메서드는 Feign Client에서 발생했던 에러의 상태코드 값을 가지고 분기되어 있는 코드를 적절하게 나눠서  작업할 수 있게 해준다.

 

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 a orders is empty.");
                }
                break;
            default:
                return new Exception(response.reason());
        }

        return null;
    }

}

 

UserServiceApplication.java - 빈 등록

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {

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

...

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

 

UserServcieImpl.java

@Slf4j
@Service
public class UserServiceImpl implements UserService {

...

    OrderServiceClient orderServiceClient;

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

...

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

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

...

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

        return userDto;
    }

...

}

 

설정했던 error message로 확인 가능

 

사용자가 입력했던 메세지를 설정파일에 등록하여 사용해보자.

 

FeignErrorDecoder.java - 컴포넌트로 등록

@Component
public class FeignErrorDecoder implements ErrorDecoder {

    Environment env;

    @Autowired
    public FeignErrorDecoder(Environment env) {
        this.env = env;
    }

    @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()),
                            env.getProperty("order-service.exception.orders_is_empty"));
                }
                break;
            default:
                return new Exception(response.reason());
        }

        return null;
    }

}

 

native-file-repo 폴더에 있는 user-service.yml에 메시지를 등록한다.

order-service:
  url: http://127.0.0.1:8000/order-service/%s/orders
  exception:
    orders_is_empty: User's a orders is empty.

FeignErrorDecoder를 컴포넌트로 등록해두었기 때문에 UserServiceApplication에서 빈으로 등록해두었던 것은 주석처리해준다.

 

message 확인 가능

 

5. Multiple Orders service

하나의 마이크로서비스에서 2개의 인스턴스를 생성하여 데이터를 분산시켜 저장하면 동기화의 문제가 발생한다.

해결 방법

1. 하나의 데이터베이스를 사용한다.

2. 각각의 데이터베이스를 사용하면서 Messaging Queuing Sever를 이용하여 한쪽에서 발생한 데이터를 전달해주면서 데이터베이스 간의 동기화 시켜준다.

 

Messaging Queuing Sever를 도입해서 데이터베이스를 단일화시키고 저장하는 방식으로 kafka를 이용해보자.

 

order-service 2개 기동

임의의 5개의 주문을 등록한다.

orderservice 2개가 번갈아가며 저장이 된다. (5번은.. 흠)

 

3. 위의 2가지 방법을 혼합해서 사용한다. (Kafka Connector + DB)

데이터베이스 1개를 사용하되 특정하게 데이터 모아서 한번에 처리하는 방식이다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함