반응형

https://github.com/Seungkyu-Han/micro_service_webflux

 

GitHub - Seungkyu-Han/micro_service_webflux: Webflux 환경에서 MSA의 Saga, Outbox, CQRS를 연습해보기 위한 리포지

Webflux 환경에서 MSA의 Saga, Outbox, CQRS를 연습해보기 위한 리포지토리입니다. - Seungkyu-Han/micro_service_webflux

github.com

 

우선 CQRS 패턴을 적용하기 위해 

https://youtu.be/BnS6343GTkY?si=kRkeJSen4kr-3tO9

 

해당 영상을 참고했다.

 

사실 CQRS가 Command Query Responsibility Segregation로 그냥 단순히 서비스 로직과 쿼리를 분리해서 무슨 이점이 있을까? 라는 생각을 가지고 있었다.

 

하지만 우아한 형제들에서 이런 사용 사례들을 보고 나니 사용하는 이유를 좀 알 수 있을 것 같았다.

 

그리고 내가 이해한 것이 맞다면 아래와 같은 이유로 사용할 것이다.

 

이런 MSA 환경에서 주문을 수행하기 위해서는 고객의 정보를 조회해야 한다.

 

이런 상황에서 동기적으로 동작하기 위해서는 Customer 서버에 Http 요청을 보내야 하지만 이 과정에서 큰 오버헤드가 발생하고, Http 응답이 오기까지 Order 서버에서 block된다는 문제가 발생한다.

그렇다고 Order 서버에서 Customer Database에 접근하면 분리의 원칙을 위반하고 데이터베이스를 조회하는 과정에서 병목이 발생할 가능성이 있다.

 

그렇기에

그렇기에 Customer 서버에서 데이터의 변경요청이 오면 이벤트를 발행해 다른 서버들에게 알려주고, 다른 서버들은 그 중 필수적으로 필요한 부분만 조회가 빠른 데이터베이스에 저장해 조회하면서 사용하는 것이다.

 

그렇게되면 고객 정보의 Create, Update, Delete는 Customer 서버에서 일어나고 고객정보의 Read는 Order 서버에서 일어나게 된다.

이런 것을 CQRS라고 말한다고 생각한다.

 

구현은 Saga, Outbox에 비해 별로 어렵지 않았다.

기존의 데이터는 MongoDB에 저장하고 있었고 Order 서버에서도 고객의 정보를 추가적으로 저장해야 하는데, 내가 사용할 수 있는 조회가 가장 빠른 데이터베이스인 Redis를 사용했다.

 

    override fun createCustomer(createCustomerCommand: CreateCustomerCommand): Mono<CreateCustomerResponse> {
        val customer = Customer(
            id = CustomerId(ObjectId.get()),
            username = createCustomerCommand.username,
            email = createCustomerCommand.email
        )

        val customerCreatedEvent = customerDomainService.validateAndCreateCustomer(customer)

        return customerMessagePublisher.publish(customerCreatedEvent)
            .then(
                customerRepository.save(customer)
            ).thenReturn(CreateCustomerResponse(customer.id.toString(), customer.username, customer.email))
    }

 

이렇게 고객의 생성 명령이 실행되면 데이터베이스에 저장하며, 이벤트를 발행한다.

물론 이 과정 내에서도 Transaction 처리와 Outbox 패턴을 적용해야 하지만, 그 부분은 생략하도록 하겠다.

 

@KafkaListener(id = "\${kafka.consumer.customer-consumer-group-id}",
        topics = ["\${kafka.topic.customer-create}"])
    override fun receive(
        @Payload values: List<CustomerCreateAvroModel>,
        @Header(KafkaHeaders.RECEIVED_KEY) keys: List<String>,
        @Header(KafkaHeaders.RECEIVED_PARTITION) partitions: List<Int>,
        @Header(KafkaHeaders.OFFSET) offsets: List<Long>
    ) {
        values.forEach{
            customerCreateMessageListener.createCredit(
                Credit(
                    customerId = CustomerId(id = ObjectId(it.id)),
                    totalCreditAmount = Money.ZERO
                )
            ).subscribe()
        }
    }

 

이벤트를 수신하는 부분은 이렇게 고객의 id를 받아서 결제 정보를 만들게 된다.

 

여기서 이벤트는 고객의 아이디, 이름, 이메일 모두 발행이 되었지만, 수신하는 부분에서는 고객의 id만 사용하게 된다.

 

결제 과정에서는 고객의 이름, 이메일을 사용하지 않기 때문에 굳이 저장할 필요가 없다.

최소 데이터 보관 원칙에 의해 꼭 필요한 column들만 저장을 하는 것이 좋다.

 

    override fun createCredit(credit: Credit): Mono<Void> {
        return creditRepository.save(credit).then()
    }
    override fun save(credit: Credit): Mono<Credit>{
        return reactiveRedisTemplate.opsForValue().set(
            "${redisPrefix}${credit.customerId.id}",
            objectMapper.writeValueAsString(creditToCreditEntity(credit))
        ).thenReturn(credit)
    }

 

그리고는 조회가 빠른 Redis에 저장을 해두고 주문이 발생하면, Customer 서버에 데이터를 요청하지 않고 빠르게 주문을 수행할 수 있도록 한다.

반응형

https://github.com/Seungkyu-Han/Toge-do-backend

 

GitHub - Seungkyu-Han/Toge-do-backend: Toge-do 앱의 백엔드 리포지토리입니다

Toge-do 앱의 백엔드 리포지토리입니다. Contribute to Seungkyu-Han/Toge-do-backend development by creating an account on GitHub.

github.com

 

https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture

 

지속 가능한 소프트웨어 설계 패턴: 포트와 어댑터 아키텍처 적용하기

들어가며 헥사고날 아키텍처(Hexagonal Architecture)로 더 잘 알려져 있는 포트와 어댑터 아키텍처(Ports and Adapters Architecture)는 인터페이스나 기반 요소(infrastructure)의 변경에 영향을 받지 않는 핵심 ..

engineering.linecorp.com

해당 글을 참고하였습니다.

 

현재의 시스템 구조이다.

여러개의 서버에서 사용하는 부분을 module로 추출해서 멀티모듈로 개발을 하다 보니, Hexagonal Architecture를 적용하고 싶어졌다.

이런 생각이 빨리 들었으면 더 쉽게 적용할 수 있었을텐데...아쉽니다.

 

우선 Heaxgonal Architecture의 설명이다.

애플리케이션의 핵심 비즈니스 로직과 외부 시스템(데이터베이스, API등)을 분리하여 시스템을 더욱 유연하고 테스트하기 쉽게 만드는 것을 목표로 하는 소프트웨어 설계 패턴이다.

외부에 포트와 어댑터만 노출하기 때문에 Ports and Adapters 패턴으로도 불린다고 한다.

이런 식으로 외부에 포트를 노출하고 있으며, primary 포트(어댑터)와 secondary포트(어댑터)로 나뉜다.

 

primary는 외부에서 요청을 받아야 동작하는 요소들을 말한다.

이런 식으로 Service로 인터페이스를 제공하고 있기에, 해당 인터페이스에 있는 메서드들이 포트가된다.

컨트롤러는 HTTP API의 요청을 받아 Service의 인터페이스를 연결해주고 있기 때문에 어댑터이다.

 

secondary는 애플리케이션이 호출하면 동작하는 요소들을 말한다.

보통 Repository 인터페이스로 개발을 하기에 Repository가 포트이고, 해당 Repository를 상속받는 구현체가 어댑터가 된다.

 

보통 애플리케이션이 핵심 로직으로, 변경이 굉장히 자주 일어나게 된다.

그렇기에 요청을 하고 받는 어댑터와의 결합도를 낮추어야 하는데, 이를 포트를 의존하는 방법으로 해결한다.

 

만약 여기에 빠른 속도를 위한 Redis를 추가하더라도, 기존의 핵심 도메인에는 영향을 주지 않고, 어댑터만 추가하게 되는 것이다.

이 때더 포트가 애플리케이션과 도메인을 보호하게 됩니다.

 

이외에도 더 많은 개념이 있는데, 일단 여기까지만 소개하고 현재 진행중인 프로젝트에 적용하는 방법을 생각해보았다.

 

우선 기존에도 도메인은 model module로 추출하여, 핵심 로직을 작성해 사용하고 있었다.

이 모듈을 그대로 domain으로 사용할 수 있을 것 같았다.

그 모듈을 중심으로 구성해보면

 

이렇게 만들 수 있지 않을까 생각이 든다.

 

사실 이렇게 블로그를 작성하고 다시 설계하면서 생각해보니, 기존의 설계와 크게 다르지 않다는 것을 알게되었다.

이렇게 만들면 각 단계에서 수행할 테스트도 명확해지기 때문에 좋았었다.

 

이 Hexagonal Architecture가 클린코드에서 굉장히 중요한 개념이라고 한다.

 

앞으로도 중복되는 코드들을 제거하고, 더 나은 설계를 적용하기 위해 다양한 방법을 생각해보도록 하자.

+ Recent posts