SMALL

Spring Cloud Gateway란 

API Gateway이다

    (사용자의 요청을 받고 적절한 MicroService에게 라우팅 해주는 서버)

    (API Gateway는 Reverse Proxy 기능을 향상한 것이다)

        (Reverse Proxy : 클라이언트의 요청을 받고 이 요청을 적절한 Backend 서버로 라우팅 해주는 서버) 

라우팅 이외에도 보안, 모니터링/메트릭 등의 기능을 간단하고 효과적인 방법으로 제공한다.

API Gateway 특성상 모든 요청이 거쳐가는 곳이기 때문에 성능이 매우 중요하다.

- 비동기식 이벤트 기반의 WAS인 Netty를 사용

Spring5, SpringBoot2, ProjectReactor로 구축되었다

 

Spring Cloud Gateway 주요 특징

Route

클라이언트의 요청을 어느 서버로 라우팅 할 것인지를 나타내는 내용

목적지 URI, Predicates, Filter로 이루어져 있다.

 

Predicate

요청이 어떤 Path인지 또는 어떤 헤더를 가지고 있는지에 대한 조건 

 

Filter

Spring WebFilter 인스턴스, Filter를 통해서 요청 또는 응답을 변경할 수 있다

 

사용 예시 - application.yml

spring:
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: http://localhost:8045
          predicates:
            - Path=/product/**
        - id: cartservice
          uri: http://localhost:8050
          predicates:
            - Path=/cart/**

 

Microservice를 구축할 경우

다양한 서비스들이 존재할 텐데 사용자가 각각의 서비스들의 주소를 모두 알 수도 없고 알아서도 안된다.

이런 경우에는 사용자의 요청을 API Gateway에서 받고

해당 요청을 받은 API Gateway가 각각의 요청을 적당한 microservice로 라우팅 해주어야 한다

 

위의 시나리오를 구현한 설정값들을 알아보도록 하자

 

 

API Gateway 설정

 

spring:
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: http://localhost:8045
          predicates:
            - Path=/product/**
        - id: cartservice
          uri: http://localhost:8050
          predicates:
            - Path=/cart/**

 

Dependency 추가 ( gradle )

dependencies {
	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
}
LIST
SMALL

ActiveMQ란

간단하게 ActiveMQ는 메시지 브로커이다.

JMS를 지원한다( JMS: Java Message Service) ( JMS는 스펙이고, 그것을 구현해 좋은 제품이 ActiveMQ)

ActiveMQ를 사용하는 이유 

대부분의 동기식 통신 방식은 사용자로부터 받은 요청을 전부 처리할 때까지 Blocking 상태에 빠진다

요청을 모두 처리해야 사용자에게 응답을 줄 수 있다

하지만 메시지 큐 사용 시 요청을 큐에 넣고 Block상태에 빠지지 않고 응답을 줄 수 있다

후에 다른 서비스에서 큐에 쌓인 요청을 Consume 하여 요청을 처리할 수 있다

 

ActiveMQ 설치

 

https://activemq.apache.org/components/classic/download/ 

 

ActiveMQ

 

activemq.apache.org

다운로드한 파일을 unzip후에 bin폴더에 activemq 실행

 

 

ActiveMQ TCP prot : 61616

ActiveMQ WebConsole : http://127.0.0.1:8161 ( default user : admin / admin )

 

 

이제부터 실전 예제를 보자

예제로는 Product-Service와 Cart-Service가 존재한다

두 서비스 사이에 ActiveMQ를 두고

Product에서 ActiveMQ에 메시지를 produce 하고

Cart에서 ActiveMQ에 쌓인 메시지를 consume 한다.

ActiveMQ - Producer 

 

@RestController
@RequestMapping("/product")
@RequiredArgsConstructor
public class ProductController {

    private final ProductRepository productRepository;
    private final JmsTemplate jmsTemplate;
    private final ObjectMapper mapper;

    // get value from yaml file
    @Value("${product.jms.destination}")
    private String jmsQueue;

    @GetMapping("/sendToCart/{id}")
    public ResponseEntity<Product> sendToCart(@PathVariable long id) {
        Optional<Product> product = productRepository.findById(id);

        if (!product.isPresent()) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }

        try {
            String jsonInString = mapper.writeValueAsString(product.get());

            jmsTemplate.convertAndSend(jmsQueue, jsonInString);

            return new ResponseEntity<>(product.get(), HttpStatus.OK);
        } catch (Exception e) {
            e.printStackTrace();
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

 

application.yml

server:
  port: 8045
spring:
  activemq:
    user: admin
    password: admin
    broker-url: tcp://localhost:61616
  application:
    name: product-service
product:
  jms:
    destination: product

ActiveMQ 설정 정보는 간단하다

  • ActiveMQ의 위치정보 (IP, Port)
  • ActiveMQ에 접근하기 위한 권한 정보

이외에 product.jms.destination은 특정값을 설정값으로 분리하여 관리하기 위해 임의로 추가한 값이다.

(코드에 해당 값을 String으로 선언해도 되지만, yml 설정으로 분리) 

 

 

ActiveMQ - Consumer

@Component
@RequiredArgsConstructor
@Slf4j
public class JmsConsumer {

    private final ProductRepository productRepository;
    private final ObjectMapper mapper;

    @JmsListener(destination = "${product.jms.destination}")
    public void consumeMessage(String data) {
        try {
            Product product = mapper.readValue(data, Product.class);

            log.info("data: {}", data);
            productRepository.save(product);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

application.yml

server:
  port: 8050
spring:
  application:
    name: cartservice
  activemq:
    user: admin
    password: admin
    broker-url: tcp://localhost:61616
product:
  jms:
    destination: product

 

참고&git

 

 

LIST
SMALL

Spring에서 트랜잭션을 관리하는 방법은 크게 2가지 방법으로 나눌 수 있다.

 

Programmatic Transaction Management

PlatformTransactionManager, TransactionTemplate 등을 이용하여 

사용자가 직접 트랜잭션 개시, 커밋, 롤백 등을 수행하는 방법

 

Declarative Transaction Management

@Transaction 어노테이션을 통해서

트랜잭션에 관한 코드를 비즈니스 로직으로부터 분리해서 사용하는 방법

 

대부분의 경우에는 Declarative(선언적) Transaction Management를 사용하여 트랜젝션을 관리한다.

하지만, 특수한 경우에는 사용자가 직접 Transaction을 관리해야 하는 상황이 생긴다.

해당 경우에 어떠한 방식으로 트랜잭션을 관리하는지 알아보도록 하자

 

Declarative Transaction Management 예시

우선 일반적인 상황에서의 Transaction Management

 

@RequiredArgsConstructor
@Service
@Slf4j
public class ProductService {
    private final ProductRepository productRepository;
    @Transactional
    public String save(ProductSaveRequestDto requestDto) throws DuplicateException {
    	
    	if (productRepository.existsByCode(requestDto.getCode())) {
    		throw new DuplicateException("Code is duplicated. code = "+ requestDto.getCode());
    	}
        return productRepository.save(requestDto.toEntity()).getId().toString();
    }
}

 

Programmatic Transaction Management 예시

우선 TransactionManager를 @Autowired나 생성자를 통해 주입받는다 (1)

( Spring Boot를 사용한다면 자동적으로 bean이 등록되어, 사용만 하면 되지만, 그렇지 않는다면 직접 등록해주어야 한다) 

후에 Transaction을 개시한다 (transactionManager.getTransaction() ) (2)

DB 관련 작업을 진행한 후에  (3)

원하는 시점에 Transaction을 커밋 또는 롤백한다 (4)

@RequiredArgsConstructor
@Service
@Slf4j
public class TestService {
    
    private final TestRepository testRepositroy;
    private final PlatformTransactionManager transactionManager ;  // (1)
    
    public ResponseDto save(RequestDto requestDto) throws Exception {
        
        List<TestDto> list = requestDto.getParams();
        
         // (2)
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        
        try {
            testRepositroy.save(requestDto);  // (3)
        } catch (Exception e) {
            transactionManager.rollback(status);  // (4)
            throw new Exception();
        }
        
        transactionManager.commit(status);  // (4)
        
        ResponseDto responseDto = new ResponseDto();
        return responseDto;
    }
LIST
SMALL

위상 정렬(Topological Sorting)

 

순서가 정해져 있는 작업을 차례로 수행해야 할 때 그 순서를 결정해주는 데 사용되는 알고리즘

(일을 하는 순서를 찾는 알고리즘)

DAG( Directed Acyclic Graph ) -방향이 존재하고, 사이클이 존재하지 않는 그래프에서 사용 가능 

 

출처:https://jackpot53.tistory.com/85

 

방향이 존재하는 그래프에서 사용하므로 

진입 차수를 사용하여 알고리즘을 구현한다.

 

진입 차수(indegree)

진입 차수(indegree) : 특정한 노드로 들어오는 간선의 개수

위의 그림에서 7 -> 5에서

5의 진입 차수는 1이다

7의 집입 차수는 0이다

 

 

위상 정렬 동작 과정

각 노드의 진입 차수를 구한다. 

진입 차수가 0 인 노드를 큐에 넣는다.

큐에 있는 노드의 간선을 제거 

 - ( 해당 간선을 진입 차수로 둔 노드의 진입 차수도 제거 )

 - 제거 과정에 진입 차수가 0이 된 노드는 큐에 추가 

 

    public boolean topologicalSorting(int numCourses, int[][] prerequisites) {

        List<Integer>[] graph = new ArrayList[numCourses];
        int[] inDegree = new int[numCourses];
        Queue<Integer> queue = new LinkedList<>();

        for (int i = 0; i < numCourses; i++) {
            graph[i] = new ArrayList<>();
        }
        for (int[] pre : prerequisites) { // 노드별 진입차수 계산
            inDegree[pre[0]]++;
            graph[pre[1]].add(pre[0]);
        }
        for (int i = 0; i < inDegree.length; i++) { // 진입차수가 0인 노드 선별
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        while (!queue.isEmpty()) { // 큐의 노드 간선 제거 및 진입차수 제거 
            int idx = queue.poll();

            for (int tmp : graph[idx]) {
                inDegree[tmp]--;
                if (inDegree[tmp] == 0) {
                    queue.offer(tmp);
                }
            }
        }

        return Arrays.stream(inDegree).sum() == 0;
    }

 

관련 문제

https://leetcode.com/problems/course-schedule/

LIST

+ Recent posts