Day_53 ( MSA - 3)

HD.Y·2024년 1월 14일
0

한화시스템 BEYOND SW

목록 보기
46/57
post-thumbnail

🦁 JWT를 활용한 로그인 기능 구현

  • GateWay MSA
    인증을 위한 커스텀 필터와 JWT 토큰을 발급해주는 클래스를 만든다.

💻 인증 필터

@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    public AuthorizationHeaderFilter() {
        super(Config.class);
    }

    public static class Config {
        // application.yml 파일에서 지정한 filer의 Argument값을 받는 부분
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            String token = exchange.getRequest().getHeaders().get("Authorization").get(0).substring(7);   // 헤더의 토큰 파싱 (Bearer 제거)
            String userId = jwtTokenProvider.getUserId(token);

            addAuthorizationHeaders(exchange.getRequest(), userId);

            return chain.filter(exchange);
        };
    }

    // 성공적으로 검증이 되었기 때문에 인증된 헤더로 요청을 변경해준다. 서비스는 해당 헤더에서 아이디를 가져와 사용한다.
    private void addAuthorizationHeaders(ServerHttpRequest request, String userId) {
        request.mutate()
                .header("X-Authorization-Id", userId)
                .build();
    }

    // 토큰 검증 요청을 실행하는 도중 예외가 발생했을 때 예외처리하는 핸들러
    @Bean
    public ErrorWebExceptionHandler tokenValidation() {
        return new JwtTokenExceptionHandler();
    }
    // 실제 토큰이 null, 만료 등 예외 상황에 따른 예외처리
    public class JwtTokenExceptionHandler implements ErrorWebExceptionHandler {
        private String getErrorCode(int errorCode) {
            return "{\"errorCode\":" + errorCode + "}";
        }

        @Override
        public Mono<Void> handle(
                ServerWebExchange exchange, Throwable ex) {
            int errorCode = 500;
            if (ex.getClass() == NullPointerException.class) {
                errorCode = 100;
            } else if (ex.getClass() == ExpiredJwtException.class) {
                errorCode = 200;
            }

            byte[] bytes = getErrorCode(errorCode).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
            return exchange.getResponse().writeWith(Flux.just(buffer));
        }
    }
}

💻 JWT 토큰 발급 클래스

@Component
public class JwtTokenProvider {
    @Value("${jwt.secret-key}")
    private String secretKey;

    public Key getSignKey(String secretKey) {
        return Keys.hmacShaKeyFor(secretKey.getBytes());
    }

    // 사용자 id 가져오는 메서드
    public String getUserId(String token) {
        return extractAllClaims(token).get("id").toString();
    }

    // 토근에서 정보를 가져오는 코드가 계속 중복되어 사용되기 때문에 별도의 메서드로 만들어서 사용하기 위한 것
    public Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSignKey(secretKey))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

  • Member MSA에서 로그인 요청부터 토큰 발급까지의 과정은 아래와 같다.

1) Web Adapter로 로그인 요청이 들어온다.

// 요청 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LoginMemberReq {

    private String email;
    private String password;
}

// 검증용 Dto
@Getter
@Setter
@Builder
public class LoginMemberCommand {
    @NotNull
    private final String email;
    @NotNull
    private final String password;

    public LoginMemberCommand(String email, String password) {
        this.email = email;
        this.password = password;
    }
}

// Web Adapter
@RestController
@RequiredArgsConstructor
@WebAdapter
public class LoginMemberController {

    private final LoginMemberUseCase loginMemberUseCase;
    @RequestMapping(method = RequestMethod.POST, value = "/member/login")
    public ResponseEntity login(@RequestBody LoginMemberReq loginMemberReq) {

        LoginMemberCommand loginMemberCommand = LoginMemberCommand.builder()
                .email(loginMemberReq.getEmail())
                .password(loginMemberReq.getPassword())
                .build();

        return ResponseEntity.ok().body(loginMemberUseCase.loginMember(loginMemberCommand));
    }
}

2) Input Port(UseCase) 를 통해 서비스를 호출한다.

// Input Port
public interface LoginMemberUseCase {
    JwtToken loginMember(LoginMemberCommand loginMemberCommand);
}

// Jwt 도메인
@Getter
@AllArgsConstructor
public class JwtToken {
    private Long Id;
    private String accessToken;
    private String refreshToken;

    public static JwtToken generateJetToken(Long id,String accessToken, String refreshToken){
        return new JwtToken(id, accessToken, refreshToken);
    }
}

// 로그인 서비스
@Service
@RequiredArgsConstructor
public class LoginMemberService implements LoginMemberUseCase {

    private final EmailPasswordCheckPort emailPasswordCheckPort;
    private final CreateJwtPort createJwtPort;

    @Override
    public JwtToken loginMember(LoginMemberCommand loginMemberCommand) {
        Member member = Member.builder()
                .email(loginMemberCommand.getEmail())
                .password(loginMemberCommand.getPassword())
                .build();
                
        // DB에 저장된 회원의 패스워드와 일치하는지 확인
        MemberJpaEntity memberJpaEntity = emailPasswordCheckPort.emailPasswordCheck(member);

        // 만약 일치한다면, 아래에서 jwt 토큰 발급 후 반환
        if(memberJpaEntity != null) {
            Member loginMember = Member.builder()
					.id(memberJpaEntity.getId())
					.email(memberJpaEntity.getEmail())
					.nickname(memberJpaEntity.getNickname())
					.build();

			String accessToken = createJwtPort.generateAccessToken(loginMember);
			String refreshToken = createJwtPort.generateRefreshToken(loginMember);

			return JwtToken.generateJetToken(loginMember.getId(), accessToken, refreshToken);
        }
        return null;
    }
}

3) 사용자의 아이디와 패스워드가 일치하는지 DB에서 확인하기 위해 OutPut Port를 통해
  영속성 어댑터를 호출한다.

// OutPut Port
public interface EmailPasswordCheckPort {
       MemberJpaEntity emailPasswordCheck(Member member);
}

// 영속성 어댑터
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class MemberPersistenceAdapter implements RegisterMemberPort, ModifyMemberPort, ModifyMemberStatusPort, EmailPasswordCheckPort {
   private final MemberJpaRepository memberJpaRepository;

   @Override
   public MemberJpaEntity createMember(Member member) {
       MemberJpaEntity memberJpaEntity = MemberJpaEntity.builder()
               .email(member.getEmail())
               .nickname(member.getNickname())
               .password(member.getPassword())
               .status(member.getStatus())
               .build();
       memberJpaRepository.save(memberJpaEntity);
       System.out.println(memberJpaEntity);

       return memberJpaEntity;
   }

       @Override
   public MemberJpaEntity modifyMember(Member member) {
       Optional<MemberJpaEntity> result = memberJpaRepository.findById(member.getId());
       if(result.isPresent()) {

           MemberJpaEntity memberJpaEntity = result.get();

           memberJpaEntity.update(member.getEmail(), member.getPassword(), member.getNickname(), member.getStatus());
           memberJpaRepository.save(memberJpaEntity);
           return memberJpaEntity;
       } else {
           return null;
       }
   }

   @Override
   public Boolean modifyMemberStatus(Member member) {
       Optional<MemberJpaEntity> result = memberJpaRepository.findByEmail(member.getEmail());

       MemberJpaEntity memberJpaEntity = result.get();
       memberJpaEntity.setStatus(true);
       memberJpaRepository.save(memberJpaEntity);

       return true;
   }

   // 여기부터 추가한 이메일, 패스워드 확인 메서드
   @Override
   public MemberJpaEntity emailPasswordCheck(Member member) {
       Optional<MemberJpaEntity> result = memberJpaRepository.findByEmail(member.getEmail());

       if(result.isPresent()) {
           MemberJpaEntity memberJpaEntity = result.get();

           if(memberJpaEntity.getPassword().equals(member.getPassword()) && memberJpaEntity.getStatus()) {
               return memberJpaEntity;
           }
       }
       return null;
   }
}

4) 이메일, 패스워드 확인이 정상적으로 끝나면 JWT 토큰 발급을 위해 OutPut Port를 통해서
  토큰 발급 어댑터로 이동한다.

// OutPut Port
public interface CreateJwtPort {
    String generateAccessToken(Member member);
    String generateRefreshToken(Member member);
    boolean validateToken(String token);
    String parseMemberIdFromToken(String token);
}

// 토큰 발급 어댑터
@WebAdapter
@RequiredArgsConstructor
public class createJwtAdapter implements CreateJwtPort {

    @Value("${jwt.secret-key}")
    private String secretKey;

    @Value("${jwt.token.access-expired-time-ms}")
    private Long accessTokenExpiredTimeMs;

    @Value("${jwt.token.refresh-expired-time-ms}")
    private Long refreshTokenExpiredTimeMs;

    @Override
    public String generateAccessToken(Member member) {

        Claims claims = Jwts.claims();
        claims.put("id", member.getId());
        claims.put("email", member.getEmail());
        claims.put("nickname", member.getNickname());

        byte[] secretBytes = secretKey.getBytes();

        String accessToken = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiredTimeMs))
                .signWith(Keys.hmacShaKeyFor(secretBytes), SignatureAlgorithm.HS256)
                .compact();
        return accessToken;
    }

    @Override
    public String generateRefreshToken(Member member) {

        Claims claims = Jwts.claims();
        claims.put("id", member.getId());
        claims.put("email", member.getEmail());
        claims.put("nickname", member.getNickname());

        byte[] secretBytes = secretKey.getBytes();

        String refreshToken = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpiredTimeMs))
                .signWith(Keys.hmacShaKeyFor(secretBytes), SignatureAlgorithm.HS256)
                .compact();
        return refreshToken;
    }
}

  • 여기까지 하면 회원이 로그인할 때 토큰 발급이 완료된다.

  • 다음으로는 상품을 등록할 때 상품의 이미지를 AWS에 업로드 하는 내용이다. MSA는
    "상품 MSA" 와 "상품 이미지 MSA" 가 있다.

1) 상품 등록 요청이 상품 MSA의 Web Adapter로 들어온다.

// 요청 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CreateProductReq {
    private String name;
    private Integer price;
}

// 검증용 Dto
@Getter
@Builder
public class CreateProductCommand {
    @NotNull
    private final String name;

    @NotNull
    private final Integer price;

    @NotNull
    private final Long brandId;

    @NotNull
    private final MultipartFile[] files;

    public CreateProductCommand(String name, Integer price, Long brandId, @NotNull MultipartFile[] files) {
        this.name = name;
        this.price = price;
        this.brandId = brandId;
        this.files = files;
    }
}

// Web Adapter
@RestController
@WebAdapter
@RequiredArgsConstructor
public class CreateProductController {

    private final CreateProductUseCase createProductUseCase;

    @RequestMapping(method = RequestMethod.POST, value = "/product/create")
    public ResponseEntity create(
            @RequestHeader(value = "X-Authorization-Id", required = true) Long id,
            @RequestPart(value = "product") CreateProductReq createProductReq,
            @RequestPart(value = "files") MultipartFile[] files) {
        CreateProductCommand createProductCommand = CreateProductCommand.builder()
                .brandId(id)
                .name(createProductReq.getName())
                .price(createProductReq.getPrice())
                .files(files)
                .build();

        return ResponseEntity.ok().body(createProductUseCase.createProduct(createProductCommand));

    }
}

@RequestPart 어노테이션은 "value" 의 해당하는 이름으로 데이터를 보내면 해당
  value 값에 요청이 매핑되도록 해준다.

➡ 위에서는 X-Authorization-id 는 발급 받은 토큰에 있는 정보이고, 이것은 만들어 놓은
  커스텀 필터를 통과하면서 자연스럽게 값이 들어가게 된다.

➡ 다음으로 "product" 이름으로 Dto 객체의 내용을 JSON 형식으로 보내주고,
  "files" 이름으로 상품 사진 파일들을 넣어서 요청을 보내면 된다.


2) Web Adapter가 Input Port(UseCase)를 통해 서비스를 호출한다.

// Input Port
public interface CreateProductUseCase {
    Product createProduct(CreateProductCommand createProductCommand);
}

// 상품 도메인
@Getter
@AllArgsConstructor
@Builder
public class Product {
    private final Long id;
    private final Long brandId;
    private final String name;
    private final Integer price;
}

// 상품 이미지 도메인
@Builder
@Getter
@AllArgsConstructor
public class ProductImages {
    private final Long productId;
    private final MultipartFile[] files;
}

// 상품 서비스
@UseCase
@RequiredArgsConstructor
public class ProductService implements CreateProductUseCase, GetProductUseCase {
    private final CreateProductPort createProductPort;
    private final UploadProductImagePort uploadProductImagePort;
    private final GetProductPort getProductPort;

    @Override
    public Product createProduct(CreateProductCommand createProductCommand) {

        Product product = Product.builder()
                .brandId(createProductCommand.getBrandId())
                .name(createProductCommand.getName())
                .price(createProductCommand.getPrice())
                .build();
        
        // 상품 정보를 DB에 저장하기 위한 작업
        product = createProductPort.createProduct(product);


        ProductImages productImages = ProductImages.builder()
                .productId(product.getId())
                .files(createProductCommand.getFiles())
                .build();
                
        // 상품 이미지 업로드를 위한 작업        
        uploadProductImagePort.uploadProductImagePort(productImages);

        return product;
    }
}

3) 먼저 상품 데이터를 DB에 저장하기 위해 OutPut Port를 통해 영속성 어댑터를 호출한다.

// Output Port
public interface CreateProductPort {
    Product createProduct(Product product);
}

// 상품 엔티티
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class ProductJpaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long brandId;
    private String name;
    private Integer price;
}

// 상품 레포지터리
@Repository
public interface ProductJpaRepository extends JpaRepository<ProductJpaEntity, Long> {
}

// 영속성 어댑터
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class ProductPersistenceAdapter implements CreateProductPort, GetProductPort {
    private final ProductJpaRepository productJpaRepository;

    @Override
    public Product createProduct(Product product) {
        ProductJpaEntity productJpaEntity = ProductJpaEntity.builder()
                .brandId(product.getBrandId())
                .name(product.getName())
                .price(product.getPrice())
                .build();

        productJpaEntity = productJpaRepository.save(productJpaEntity);

        return Product.builder()
                .id(productJpaEntity.getId())
                .brandId(productJpaEntity.getBrandId())
                .name(productJpaEntity.getName())
                .price(productJpaEntity.getPrice())
                .build();
    }
}

4) 다음으로 상품 이미지 업로드를 위해 이미지 업로드 어댑터를 호출한다.

// OutPut Port
public interface UploadProductImagePort {
      void uploadProductImagePort(ProductImages productImages);
}

// 상품 이미지 업로드 어댑터
@WebAdapter
@RequiredArgsConstructor
public class UploadProductImageServiceAdapter implements UploadProductImagePort {
    private final OpenFeignUploadProductImage openFeignUploadProductImage;
    @Override
    public void uploadProductImagePort(ProductImages productImages) {
        try {
            openFeignUploadProductImage.call(productImages.getProductId(), productImages.getFiles());
            System.gc();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 상품 이미지 MSA로 동기 요청을 보내기 위한 인터페이스
@FeignClient(name = "ProductImage", url = "http://localhost:8084/productimage")
public interface OpenFeignUploadProductImage {
    @PostMapping(value = "/upload/{productId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    void call(@PathVariable Long productId, @RequestPart("files") MultipartFile[] files);
}

5) 상품 이미지 MSA에서 요청을 받기 위해 Web Adapter를 만들어준다.

// 요청 Dto
@Data
@Builder
public class RegisterProductImageRequest {
    private final MultipartFile[] files;
}

// 검증용 Dto
@Builder
@Data
public class RegisterProductCommand {
    private final Long productId;
    private final MultipartFile[] files;
}

// Web Adapter
@WebAdapter
@RestController
@RequiredArgsConstructor
public class RegisterProductController {

    private final ProductImageUseCase productInputPort;

    @RequestMapping(method = RequestMethod.POST, value = "/productimage/upload/{productId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity registerProductImage(@PathVariable Long productId, @RequestPart("files") MultipartFile[] files) {

        RegisterProductCommand command = RegisterProductCommand.builder()
                .productId(productId)
                .files(files)
                .build();
        return ResponseEntity.ok().body(productInputPort.registerProductImage(command));
    }
}

6) InputPort(UseCase)를 통해 서비스를 호출한다.

// InputPort
public interface ProductImageUseCase {
    List<ProductImage> registerProductImage(RegisterProductCommand command);
}

// 상품 이미지 도메인
@Builder
@Getter
@AllArgsConstructor
public class ProductImage {
    private final Long id;
    private final Long productId;
    private final String imagePath;
}

// 상품 이미지 서비스
@Service
@RequiredArgsConstructor
public class ProductImageService implements ProductImageUseCase, GetProductImageUseCase {
    private final ProductImagePort productImagePort;
    private final ProductImageUploadPort productImageUploadPort;
    private final GetProductImagePort getProductImagePort;

    @Override
    public List<ProductImage> registerProductImage(RegisterProductCommand command) {
        List<ProductImage> productImages = new ArrayList<>();
        for (MultipartFile file : command.getFiles()) {
            
            // 상품 이미지 업로드용 어댑터
            String imagePath = productImageUploadPort.uploadProductImage(file);
            ProductImage productImage = ProductImage.builder()
                    .productId(command.getProductId())
                    .imagePath(imagePath)
                    .build();
                    
            // 상품 이미지 DB 저장용 어댑터        
            productImage = productImagePort.registerProductImage(productImage);
            productImages.add(productImage);
        }

        return productImages;
    }
}

7) 먼저 상품 이미지 업로드를 위한 어댑터를 호출한다.

// Output Port
public interface ProductImageUploadPort {
    String uploadProductImage(MultipartFile file);
}

// 어댑터
@ExternalSystemAdapter
@RequiredArgsConstructor
public class ProductImageUploadAdapter implements ProductImageUploadPort {

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    private final AmazonS3 s3;

    @Override
    public String uploadProductImage(MultipartFile file) {
        String imagePath = uplopadFile(file);

        return imagePath;
    }


    public String makeFolder() {
        String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        String folderPath = str.replace("/", File.separator);

        return folderPath;
    }
    public String uplopadFile(MultipartFile file) {
        String originalName = file.getOriginalFilename();
        String folderPath = makeFolder();
        String uuid = UUID.randomUUID().toString();
        String saveFileName = folderPath + File.separator + uuid + "_" + originalName;
        InputStream input = null;
        try {
            input = file.getInputStream();
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentLength(file.getSize());
            metadata.setContentType(file.getContentType());


            s3.putObject(bucket, saveFileName.replace(File.separator, "/"), input, metadata);

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                input.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return s3.getUrl(bucket, saveFileName.replace(File.separator, "/")).toString();
    }
}

8) 다음으로 상품 이미지 정보를 DB에 저장하기 위한 어댑터를 호출한다.

// Output Port
public interface ProductImagePort {
    ProductImage registerProductImage(ProductImage product);
}

// 상품 이미지 엔티티
@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductImageJpaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long productId;
    private String imagePath;
}

// 상품 이미지 레포지터리
public interface ProductImageJpaRepository extends JpaRepository<ProductImageJpaEntity,Long> {
    List<ProductImageJpaEntity> findAllByProductId(Long productId);
}

// 영속성 어댑터
@PersistenceAdapter
@RequiredArgsConstructor
public class ProductImagePersistenceAdapter implements ProductImagePort, GetProductImagePort {
    private final ProductImageJpaRepository productImageJpaRepository;

    @Override
    public ProductImage registerProductImage(ProductImage product) {
        ProductImageJpaEntity productImageJpaEntity = ProductImageJpaEntity.builder()
                .productId(product.getProductId())
                .imagePath(product.getImagePath())
                .build();
        productImageJpaEntity = productImageJpaRepository.save(productImageJpaEntity);
        return ProductImage.builder()
                .id(productImageJpaEntity.getId())
                .productId(productImageJpaEntity.getProductId())
                .imagePath(productImageJpaEntity.getImagePath())
                .build();
    }
}

  • 여기까지 하면 상품의 등록 절차가 끝이난다.

서킷 브레이커 란❓

  • MSA를 도입하면서 단일 서비스 컴포넌트는 여러개로 쪼개져 서로 호출하는/호출당하는 관계를 가진다. 이런 경우 대두되는 문제중 하나가 서비스 간 장애 전파 이다.

    하나의 서비스 컴포넌트에 장애가 발생하면 그걸 호출하는 또다른 컴포넌트까지 장애를 전파받는다.

    Service 1 -- 호출 --> Service 2

    위와 같은 관계에 놓였을 때 Service 2 의 응답속도가 매우 느려졌다고 가정해보겠다.
    이때 Service 1 의 모든 쓰레드가 2 의 응답을 기다리고만 있다면 다른 요청을 처리할 수 없게 되니 상태는 더 악화된다.

    이런 식으로 Service 2 의 상태가 Service 1 에 영향을 주는 경우, "서비스 간 장애가 전파되었다" 고 표현한다.

    Service 1 과 같이, 2 를 호출하는 또다른 서비스 컴포넌트가 존재한다면 해당 서비스도 장애 전파가 불가피하다. 이런 상황은 자칫 전체 시스템에 영향을 줄 수도 있다.


  • 🐶 Circuit Breaker 패턴
    이런 문제를 해결하는 디자인 패턴 중 하나이다.

    Service 1 --> |Circuit Breaker| --> Service 2

    기본적으로 Service 1 -> Service 2 호출 사이에 Circuit Breaker 를 설치하는 개념이다.
    Service 2 로의 모든 호출은 이 Circuit Breaker 를 통하게되고,
    Service 2 가 정상적인 상황에서는 트래픽이 문제없이 통과한다.

    하지만 Circuit Breaker 측에서 Service 2 에 문제가 생김을 감지했다면 무슨 일이 발생할까?

    Service 2 로의 호출을 강제로 끊어서 Service 1 의 쓰레드들이 더이상 요청에 대한 응답을 기다리지 않도록, 장애가 전파되는걸 방지한다.


🐼 프로젝트에 서킷 브레이커 적용하기

  • pom.xml 파일에 라이브러리 추가
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
            <version>2.1.7</version>
        </dependency>
  • 서킷 브레이커 적용하기
@WebAdapter
@RequiredArgsConstructor
public class GetProductServiceAdapter implements GetProductPort {
    private final OpenFeignGetProduct openFeignGetProduct;
    
    // ✅ 서킷 브레이커 적용을 위한 서킷브레이커 팩토리 의존성 주입 
    private final CircuitBreakerFactory circuitBreakerFactory;
    
    // ✅ 서킷 브레이커 적용
    @Override
    public Product getProductPort(Long id) {

        CircuitBreaker circuitBreaker = circuitBreakerFactory.create("getProduct");

        Product product = circuitBreaker.run(() ->
                openFeignGetProduct.call(id)
                , throwable -> null
        );
        return product;
    }
}

  • 이렇게 하면, 상품 이미지 MSA가 에러가 발생했을 때, 상품의 정보는 그대로 웹페이지에 출력되도록 되고, 상품의 이미지만 안나오게 된다.

  • 만약, 서킷 브레이커 적용을 안해준다면, 상품 이미지 MSA 가 에러 터지면, 상품 MSA도 영향을 받게 되어 모든 서비스가 작동을 안하게 되는 것이다.

  • 따라서, MSA로 구현을 할 때 서비스와 서비스 간 동기 방식으로 HTTP 요청/응답을 보내는 부분이라면, 서킷 브레이커를 적용시켜주는 것이 서비스를 운영하는데 효율적일 것이다.

profile
개발자가 되기까지의 과정

0개의 댓글