MSA 정리 5

이봐요이상해씨·2022년 1월 5일
0

SpringBoot

목록 보기
6/10

Communication types

  1. UserService → Eureka Discovery Service → orderservice1, 2

마이크로 서비스간 통신하는 방법(rest template, feignclient사용)

RestTemplate 이용

  1. UserService → RestTemplate →Eureka Discovery Service → orderService1,2

User-service와 Order-service간의 통신을 확인해보기

로그인 후 → 주문 넣고 → 주문아이디 반환 받기

User Service controller

package com.example.usersmicroservices1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient //유레카 서버 등록
public class UsersMicroservices1Application {

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

    //초기에 기동되는 클래스가 이곳이다 따라서 이곳에 빈을 등록 시켜놓으면 가장먼저 초기화가 된다!
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //빈으로 resttemplate 등록
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

Service(rest template 사용) → public UserDto getUserById 이부분이 변경됨

package com.example.usersmicroservices1.Service;

import com.example.usersmicroservices1.dto.UserDto;
import com.example.usersmicroservices1.jpa.UserEntity;
import com.example.usersmicroservices1.jpa.UserRepository;
import com.example.usersmicroservices1.vo.ResponseOrder;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.modelmapper.spi.MatchingStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
public class UserServiceImpl implements UserService{

    UserRepository userRepository;
    BCryptPasswordEncoder passwordEncoder;

    //추가로 주입받음
    Environment env;
    RestTemplate restTemplate;

    //userservicedetials 구현
    //username == email
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByEmail(username);

        if (userEntity == null){
            throw new UsernameNotFoundException(username);
        }

        //security user 객체
        //모두 검색이 잘 되었다면 해당 유저를 반환하겠다
        //마지막은 권한값
        return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
                true, true, true, true, new ArrayList<>());
    }

    @Autowired //스프링이 기동되면서 등록할 수 있는 빈을 찾아서 메모리에 생성해주는 것이다.
    public UserServiceImpl(UserRepository userRepository,
                           BCryptPasswordEncoder passwordEncoder,
                           Environment env,
                           RestTemplate restTemplate){ //이것도 초기화가 되어있어야 주입이 되는데 주입이 안되면 에러남
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.env = env;
        this.restTemplate = restTemplate;
    }

    @Override
    public UserDto createUser(UserDto userDto) {
        userDto.setUserId(UUID.randomUUID().toString());

        //model mapper를 통해 request요청을 dto로 변환 , entity로 변환할 수 있다.

        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); //모델 메퍼가  변환시, 얼마나 정확하게 매칭되게끔 할지 정하는 전략환경설정을 지정
        UserEntity userEntity = mapper.map(userDto, UserEntity.class); //mapper의 map메소드를 통해 userdto를 userentity.class로 변환시킬 수 있다.
        userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd()));
        userRepository.save(userEntity);

        UserDto returnUserDto = mapper.map(userEntity, UserDto.class);

        return returnUserDto;
    }

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

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

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

        //List<ResponseOrder> orders = new ArrayList<>();

        //orderservice의 controller에서 getmapping으로 선언된, 오더 정보를 갖고옴
        String orderUrl = "http://127.0.0.1:8000/order-service/%s/orders";

        //주소값, 요청방식, 파라미터전달값, 어떤형식으로 전달 받을 것인지 -> 여기에 써있는 값들은
        //order service의 controller의 getOrder부분의 값을 그대로 반환한 것이다.
        ResponseEntity<List<ResponseOrder>> orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
                new ParameterizedTypeReference<List<ResponseOrder>>() {
        });

        //resposneorder타입으로 바꾸기
        List<ResponseOrder> orderList = orderListResponse.getBody();
        userDto.setOrders(orderList);

        return userDto;
    }

    @Override
    public Iterable<UserEntity> getUserByAll() {
        return userRepository.findAll();
    }

    @Override
    public UserDto getUserDetailsByEmail(String email) {
        UserEntity userEntity =  userRepository.findByEmail(email);

        if(userEntity == null){
            throw new UsernameNotFoundException(email);
        }

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

        return userDto;
    }
}

주문확인

포스트맨으로 가입한후

URL : order-service/user-id(가입후 반환된)

  • body에 주문 내역을 입력하면 orderId를 반환 받는다

Order-service에 들어가서 보면 DB에 들어가 있는걸 확인할 수 있다.

# spring:
#   datasource:
#     driver-class-name: org.h2.Driver
#     url: jdbc:h2:mem:testdb
#     username: sa
#     password: '{cipher}AQC+kErwNCz8WFWUa4HRa8A0ARenASPqOcuhaKNJwplhjgUWRQ0+7JD8FGVp1mznYswEN2TW/Z4Zu4ui2G/Zap6rWaUeICw1jiGOiSKWA4I1lO3FD/rLfHGmcLyRbFqAxvNJ0vt/iLNfylxe1l3ajrwsKmGkKvE7NbcVG16bnaMny3HYgOqL3xK45D+Mssjk56Tpl0LDYMTCWHBpwfzLqxY5e8YoUfYmiS+gqYw5DeIyQizEF+R1Ntos4+mlMFL6naspnBPcmcUkDXk0ZPemEvp6/mA9gjhgkkfA2bvaUaIS5WhmsYRPATvLERBjpPkKA8yo/a5mUK980uZ2Lbyz3BxqWC0omTTy9XARcgxHPvfWwUhEkrc3KXR2V+jzfn9fqeI='

token:
  expiration_time: 86400000
    secret:"123123"

gateway:
  ip: 127.0.0.1

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

띄어쓰기 오류 secret → nested exception!!

유레카 서버에 등록된 이름으로 url등록하기 → 포트 따로 지정안해도됨

USER-SERVICE

//빈으로 resttemplate 등록
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

//ORDER-SERVICE는 Eureka등록된 서비스 이름이다

Feign Client → HTTP client //rest template 대신 사용

  • REST call을 추상화 한 spring cloud netfilx라이브러리

사용방법

  • 호출하려는 http endpoint에 대한 interface를 생성
  • @FeignClient 선언
  • loadbalanced 지원

User service

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

Application

package com.example.usersmicroservices1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient //유레카 서버 등록
@EnableFeignClients //feign 클라이언트 사용
public class UsersMicroservices1Application {

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

    //초기에 기동되는 클래스가 이곳이다 따라서 이곳에 빈을 등록 시켜놓으면 가장먼저 초기화가 된다!
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //Feign client 사용으로 주석 처리

    //빈으로 resttemplate 등록
//    @Bean
//    @LoadBalanced
//    public RestTemplate getRestTemplate(){
//        return new RestTemplate();
//    }
//}

InterFace 생성

package com.example.usersmicroservices1.client;

import com.example.usersmicroservices1.vo.ResponseOrder;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

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

    //order-service controller의 getmapping으로 된 getordASER의 반환값을 그대로 반환
    @GetMapping("/order-service/{userId}/orders") //실질적인 endpoint를 지정
    List<ResponseOrder> getOrders(@PathVariable String userId);

}

User Service-impl

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

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

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

        /*
        Rest Template 코드는 더이상 사용되지 않을것 ->@feign client가 대체 주석처리
         */
//        //List<ResponseOrder> orders = new ArrayList<>();
//        //orderservice의 controller에서 getmapping으로 선언된, 오더 정보를 갖고옴
//        //user-service.yml파일의 값을 갖고옴
//        String orderUrl = String.format(env.getProperty("order_service.url"),userId);
//        //주소값, 요청방식, 파라미터전달값, 어떤형식으로 전달 받을 것인지 -> 여기에 써있는 값들은
//        //order service의 controller의 getOrder부분의 값을 그대로 반환한 것이다.
//        ResponseEntity<List<ResponseOrder>> orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
//                new ParameterizedTypeReference<List<ResponseOrder>>() {
//        });
//        //resposneorder타입으로 바꾸기
//        List<ResponseOrder> orderList = orderListResponse.getBody();

        /**
         * Fegin client 용 코드
         **/
        List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);
        userDto.setOrders(orderList);

        return userDto;
    }

feign client는 resttemplate과 똑같이 엔드포인트를 기준으로 해당 요청을 갖고온다.

rest template과 비교했을시 코드가 간결하다는 점에서 이점이 있다. 하지만 interface에서 해당 서비스의 엔드포인트를 기준으로 해당 값을 요청해서 갖고 오는 것 이기 때문에 order -service와user-service의 구조를 잘 알고 있는 사람의 경우 보기가 쉽지만, 제 3자가 보기에는 직관성이 떨어진다는 단점이 있다

Feign Client 예외처리

User service, application.yml

logging:
  level:
    com.example.userservice.client: DEBUG

Application.java

package com.example.usersmicroservices1;

import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient //유레카 서버 등록
@EnableFeignClients //feign 클라이언트 사용
public class UsersMicroservices1Application {

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

    //초기에 기동되는 클래스가 이곳이다 따라서 이곳에 빈을 등록 시켜놓으면 가장먼저 초기화가 된다!
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //Feign client 사용으로 주석 처리

    //빈으로 resttemplate 등록
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

		//Fegin client 예외처리 로깅을 위한 빈등록
    @Bean
    public Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}

User-service impl

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

잘못 호출되었지만, 해당 유저의 정보는 불러와지고, order정보는 불러와 지지 않는 것을 볼 수 있다.

FeignError Decoder

try catch문장을 대체함

FeignErrorDecoder.class

package com.example.usersmicroservices1.error;

import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

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")) { //getorders라는 함수명에서만 이 예외처리가 작동
                    return new ResponseStatusException(HttpStatus.valueOf(response.status()),
                            "User's orders is empty");
                }
                break;
            default:
                return new Exception(response.reason());
        }
        return null;
    }
}

Application.class → 빈주입

package com.example.usersmicroservices1;

import com.example.usersmicroservices1.error.FeignErrorDecoder;
import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient //유레카 서버 등록
@EnableFeignClients //feign 클라이언트 사용
public class UsersMicroservices1Application {

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

    //초기에 기동되는 클래스가 이곳이다 따라서 이곳에 빈을 등록 시켜놓으면 가장먼저 초기화가 된다!
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //Feign client 사용으로 주석 처리

    //빈으로 resttemplate 등록
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

    //Fegin client 예외처리 로깅을 위한 빈등록
    @Bean
    public Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
	  //feign 예외 디코더 클래스 빈 주입
    @Bean
    public FeignErrorDecoder getFeignErrorDecoder() {
        return new FeignErrorDecoder();
    }
}

Userservice impl. class

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

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

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

        /*
        Rest Template 코드는 더이상 사용되지 않을것 ->@feign client가 대체 주석처리
         */
//        //List<ResponseOrder> orders = new ArrayList<>();
//        //orderservice의 controller에서 getmapping으로 선언된, 오더 정보를 갖고옴
//        //user-service.yml파일의 값을 갖고옴
//        String orderUrl = String.format(env.getProperty("order_service.url"),userId);
//        //주소값, 요청방식, 파라미터전달값, 어떤형식으로 전달 받을 것인지 -> 여기에 써있는 값들은
//        //order service의 controller의 getOrder부분의 값을 그대로 반환한 것이다.
//        ResponseEntity<List<ResponseOrder>> orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
//                new ParameterizedTypeReference<List<ResponseOrder>>() {
//        });
//        //resposneorder타입으로 바꾸기
//        List<ResponseOrder> orderList = orderListResponse.getBody();

        /**
         * Fegin client 용 코드
         **/

        /*Feign Exception Handling*/
//        List<ResponseOrder> orderList = null;
//        try {
//            orderServiceClient.getOrders(userId);
//        } catch (FeignException ex){
//            log.error(ex.getMessage());
//        }
        
        /*feign error decoder로 예외처리*/
        List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);
        userDto.setOrders(orderList);

        return userDto;
    }

이렇게 지정한 메시지가 호출된것을 알 수 있다.

에러 메시지를 config.yml파일에 등록해서 사용하기

@Component로 해당 클래스 등록

생성자 주입으로 Environment 등록, 해당 서비스 등록

Application부분 빈 등록된거 삭제(왜냐하면 @Component로 등록 시켜놓았기 때문)

Multiple Orders Service(데이터동기화 문제)

Client → user-service → feign, resttemplate → order service, 1, 2 → db 1,2

문제점

하나의 데이터 요청(주문 요청) 정보에 대해 분산으로 저장이된다. → 즉 데이터 동기화 문제가 발생!!

해결방안

  1. 두 서비스(order service 1,2)가 하나의 DB를 사용

2. DB간 동기화

MQ서버를 이용(kafka)를 이용해서 변경된데이터가 있을경우 구독 시켜서, 변경된 데이터를 알려줘서 업데이트가 안된 데이터베이스를 업데이트 (즉 DB가 kafka를 구독하는 상태를 구성)

즉 order service 1,2 → db 1,2 → kafka → db 1,2(카프카가 db뒤에 있다.

3. MQ서버가 orderservice가운데 있다.(kafak connetor + DB)

Order service 1,2 → kafka → db(MQ, DB도 1개)

kafka가가 가운데 있음으로 두 서비스가 요청이 있을시 바로바로 데이터 요청처리 가능하고, 동시성 문제도 해결된다.

1. 두서비스 각각 db사용하기

order service 2개실행,(mvn spring-boot:run)

라운드 로빈 방식으로 각각 데이터가 저장된다.

이렇게 데이터가 각각 갖고와지는 것을 볼 수 있다.

0개의 댓글