[Spring] 결제시스템, Spring Boot + MSA 로 리팩토링 [구현]
- -
이전 글에서 기획과 설계에 대한 부분을 다뤘고, 이제는 이것을 구현하는 과정을 담는 글을 기록,
[Spring] 결제시스템, Spring Boot + MSA 로 리팩토링 [기획 & 설계]
실무에서 결제 프로세스를 다루며 많은 문제점들이 발견했다. 레거시한 코드와 절차지향의 프로세스를 통해 처리를 하다 보니 성능이 떨어졌으며, 약간의 과부하만 발생해도 지연 문제가 빈번
seung-seok.tistory.com
🟢 결제시스템, Spring Boot + MSA 로 리팩토링
아키텍처와 플로우를 설명했고 이제는 이것을 직접 구현해보자. 각각의 서비스에 대한 로직과 코드에 대해 깊이 설명하지 않고, 이를 구성하는 것에 초점을 두고 글을 작성한다.
Docker 를 사용한 서비스 구성
각각의 서비들을 Docker image 로 생성해서 이를 Docker Compose 를 통해서 여러 개의 서비스들을 컨테이너로 구성하고 이를 하나로 통합하여 관리하는 방식으로 서비스를 구현할 것이다.
💡 docker-compose란?
여러개의 컨테이너로부터 이루어진 서비스를 구축, 실행하는 순서를 자동으로 하여 관리를 간단하게 하는 툴.
하나의 docker-compose.yaml 파일에 컨테이너에 사용 될 이미지, 옵션 등 여러개의 컨테이너 설정 내용을 모아서 사용한다. compose 파일을 준비해서 커맨드를 1번 실행하는 것만으로 그 파일로부터 설정을 읽어들여 모든 컨테이너 서비스를 실행시킬 수 있기 때문에 더 간편하고 직관적으로 여러 컨테이너를 관리할 수 있다.
먼저 간단하게 순서를 정리하면 아래와 같다.
1. 각 서비스들을 image 로 구성
2. 이를 docker-compose 를 통해 서비스화
우선 intellij 에서 Dockerfile 을 통해서 image 를 build 하는 방법을 알아보자.
Dockerfile 생성 및 코드 작성
Dockerfile은 docker 의 이미지를 직접 생성하기 위한 용도로 작성하는 파일이다. 이에 대해 실행한 코드를 첨부하고 하나하나 설명해 보겠다!
Dockerfile
# Base, ubuntu 이미지
FROM ubuntu:18.04
# jdk 설치
RUN apt-get update && \
apt-get install -y openjdk-17-jdk && \
apt-get clean;
# work directory 지정
WORKDIR /app
COPY build/libs/OrderService.jar /app/app.jar
# 수행될 Command
CMD ["java", "-jar", "app.jar"]
FROM ubuntu:18.04 : Ubuntu 18.04 버전의 공식 Docker 이미지를 기반 이미지로 사용하겠다고 선언, Docker 이미지를 구축할 때 이 기반 이미지에서 시작
RUN : 이미지 빌드 과정 중 명령어를 실행한다. 패키지 목록을 업데이트하고 OpenJDK 17 버전을 설치하며, apt-get clean 을 통해 생성된 캐시를 정리하여 Docker 이미지의 크기를 줄인다.
WORKDIR : 해당 지시어는 Docker 컨테이너 내에서 명령어가 실행될 기본 디렉토리를 생성한다. 이 지시어 다음에 실행되는 모든 RUN, CMD, ENTRYPOINT, COPY, ADD 지시어는 /app 디렉토리에서 실행된다.
COPY : 빌드 컨텍스트의 파일이나 디렉토리를 Docker 이미지 내의 지정된 경로로 복사한다. 여기서는 호스트의 build/libs/OrderService.jar 파일을 컨테이너의 /app/app.jar 로 복사한다는 의미다.
CMD : 컨테이너가 시작될 때 실행될 기본 명령어를 정의한다. 이 경우 컨테이너가 실행되면 자바 애플리케이션인 app.jar 파일을 실행한다. java -jar app.jar 명령이 실행되며, 이는 /app 디렉토리 내에 위치한 app.jar 을 실행시킨다.
2. Docker build 전초 작업
이렇게 Dockerfile 을 모두 작성했으면 이제 Docker build 를 해야 한다. intellij 의 Edit Configurations 를 누르고 Add New Configuration > Docker > Docker Image 를 선택한다.
image-tag 는 해당 이미지의 이름이 될 것이다. 또한 이미지 생성만 할 것이기 대문에 다른 옵션은 선택하지 않았다.
3. application.yml 파일 수정
다중 컨테이너 환경에서, Docker Compose 를 사용하면 서비스(컨테이너) 간 네트워킹을 용이하게 만들어 준다. 서비스 각각이 Docker Compose 파일에 정의 된 이름으로 서로를 찾을 수 있게 된다. 만약 mysql 을 Docker Compose 에서 mysql-service 로 지칭했다면 url 을 jdbc:mysql://mysql-service:3306/{DB_NAME} 과 같이 표기해서 데이터베이스에 접근할 수 있다.
4. bootJar, Jar 파일로 생성
Dockerfile 에서 COPY 할 jar 파일을 생성해준다.
5. Docker build
이러한 과정을 통해서 개발한 Spring Boot 서비스를 Docker 의 이미지로 만들었다.
⭐️ Event 를 이용한 리팩토링
베타 버전에선 모두 동기적으로 처리하며 외부 서비스를 사용할 땐, feign Client 를 사용해서 처리하는 방식으로 진행했다. 이번 알파 버전에선 Kafka 를 통해 비동기적인 방식으로 재구축을 진행한다. feign Client 에 대한 사용법은 따로 포스팅한 내용이 있어서 이를 첨부한다!
[Spring] feign 에 대하여
프로젝트를 MSA 구조로 진행하며, 서비스 로직에서 다른 서비스의 API 를 호출해야 하는 경우가 빈번하게 생겼다. 이를 RestTemplate 을 통해 해결할 수도 있지만 보다 더 나은, 간편한 기능을 통해 풀
seung-seok.tistory.com
1. kafka 설정
Kafka 를 Spring Boot 에서 사용하기 위해 몇가지 설정을 해주어야 한다. 카프카 연결에 대한 대략적인 내용도 따로 포스팅한 기록이 있다. 이를 참조하면 도움이 될 것 같아 첨부!
[Docker] Docker Compose 로 Kafka Cluster를 !
프로젝트를 진행하며 이를 비동기로 처리하기 위한 방안을 모색하던 중, 메세지 브로커인 Kafka 를 사용하기로 결정했다. 처음 접해보는 기술인 만큼 직접 실습해보며, kafka 를 선택한 이유와 그
seung-seok.tistory.com
build.gradle
// kafka
implementation 'org.springframework.kafka:spring-kafka'
우선 의존성 설정, build.gradle 에 카프카 의존성을 추가해 주어야한다.
application.yml
spring:
kafka:
bootstrap-servers:
# kafka INTERNAL HOST
- kafka1:9092
- kafka2:9092
- kafka3:9092
consumer:
group-id: testgroup
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.ByteArraySerializer
docker-compose.yml
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_SERVER_ID: 1
ZOOKEEPER_CLIENT_PORT : 2181
ports:
- "22181:2181"
kafka1:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- "19092:19092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:9092,EXTERNAL://localhost:19092
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
kafka2:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- "19093:19093"
environment:
KAFKA_BROKER_ID: 2
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka2:9092,EXTERNAL://localhost:19093
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
kafka3:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- "19094:19094"
environment:
KAFKA_BROKER_ID: 3
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka3:9092,EXTERNAL://localhost:19094
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
kafka-ui:
image: provectuslabs/kafka-ui:latest
depends_on:
- kafka1
- kafka2
- kafka3
ports:
# 호스트 포트 : 컨테이너 내부 포트
- 8090:8080
environment:
KAFKA_CLUSTERS_0_NAME: MyKafkaCluster
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka1:9092,kafka2:9092,kafka3:9092
KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
application.yml 파일이다. 동일하게 같은 Docker Compose 안에 서비스를 구성할 것이기 때문에 직접적인 URL 이 아닌 서비스 명을 기재할 것인데, docker-compose Kafka 의 KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:9092 를 지정해뒀다. Kafka 서비스의 내부 포트를 그대로 사용해주면 된다.
💡 옵션 설명
group-id : Kafka Consumer Group 의 ID 를 지정. 같은 group-id를 가진 컨슈머들은 메세지를 공유하여 처리한다.
auto-offset-reset : Kafka 에서 해당 컨슈머 그룹의 이전 오프셋이 없거나 유효하지 않을 경우 사용할 초기 오프셋 위치를 지정한다. ealiest 는 가장 초기의 메세지부터 처리하겠다는 것을 의미한다.
key-deserializer : Kafka 의 메세지의 키를 역직렬화하는데 사용할 클래스다. StringDeserializer는 Kafka 메세지의 키를 문자열로 변환한다.
value-deserializer : 메세지 본문을 역직렬화하는데 사용할 클래스다. ByteArrayDeserializer 는 메세지의 값을 바이트 배열로 반환한다.
2. Protobuf
Protobuf(Protocol Buffers) 는 구글에서 개발한 이진 직렬화 포맷이다. Protobuf 는 플랫폼 및 언어에 독립적이며, 다양한 언어와 시스템에서 데이터를 효율적으로 전송하기 위해 사용된다. 주요 장점은 작은 메세지 크기, 빠른 직렬화/역직렬화 속도 및 강렬한 타입 안정성을 제공한다는 것이다.
⭐️ Kafka Event 전달에 쓰이는 데이터 구조의 일관성을 보장하며 손쉽게 정의할 수 있다. 또한 빠르게 데이터를 직렬화/역직렬화 하기 위해 이 기술을 채택했다.
build.gradle
plugins {
id 'com.google.protobuf' version '0.9.4'
}
dependencies {
implementation 'com.google.protobuf:protobuf-java:3.25.2'
}
// compile 을 위한 task
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.25.2'
}
}
위와 같이 플러그인과 라이브러리 의존성을 추가해 주었다.
프로토콜 버퍼 스키마 작성, eda_message.proto 파일
// version
syntax = 'proto3';
// 경로
package payment.protobuf;
message ProductTag {
int64 product_id = 1;
repeated string tags = 2;
}
프로젝트 내에 원하는 데이터 구조를 정의한다. 주의할 점은 main/proto 경로에 해당 파일이 존재해야 한다는 것이다.
프로젝트를 빌드하면 .proto 파일에 대한 Java 코드가 자동으로 생성된다. 이렇게 생성된 코드를 사용하여 데이터를 직렬화/역직렬화 할 수 있다.
2. 기존의 프로세스, Kafka 를 통한 EDA 적용
@Autowired
private KafkaTemplate<String, byte[]> kafkaTemplate;
서비스 코드에 kafkaTemplate 을 선언해준다. byte[] 로 지정한 이유는 특정 Object 를 직렬화해서 보내기 위함이다.
AS-IS
// Open feign
var dto = new ProductTagsDto();
dto.tags = tags;
dto.productId = product.id;
searchClient.addTagCache(dto);
기존의 로직은 외부 서비스의 메서드를 feign Client 를 사용하여 직접 호출했다. 이는 동기적인 프로세스이며 효율적이지 않다고 판단하여 수정하면 ..
TO-BE
var message = EdaMessage.ProductTags.newBuilder()
.setProductId(product.id)
.addAllTags(tags)
.build();
kafkaTemplate.send("product_tags_added", message.toByteArray());
메세지를 생성해서 보내주는 형식으로 수정했다. 이제 메세지를 발행했으니, 소비하는 케이스를 정의해보자.
EventListener
// Bean 등록
@Component
public class EventListener {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
public SearchService searchService;
@KafkaListener(topics = "product_tags_added")
public void consumeTagAdded(byte[] message) throws InvalidProtocolBufferException {
var object = EdaMessage.ProductTags.parseFrom(message);
logger.info("[product_tags_added] consumer : {}", object);
searchService.addTagCache(object.getProductId(), object.getTagsList());
}
}
@KafkaListener 어노테이션에 지정한 토픽, "product_tags_added" 이 생성되면 자동으로 consumeTagAdded 메서드가 실행된다. feign Client 를 통해서 사용했던 searchService.addTagCache 를 그대로 메세지 기반으로 동작하기만 하면 되는 방식이다.
⭐️ 프로젝트 고도화
정리하면 이렇다! 각각의 서비스를 Docker image 로 생성, 그리고 그것들을 docker-compose 를 사용해 다중 컨테이너의 환경을 하나로 통합하여 관리한다. 기존의 동기적인 서비스들은 Kafka 를 통해 Event 기반의 비동기적 프로세스로 수정하여 다른 서비스들과의 느슨한 결합, 변경 영향도도 최소화를 할 수 있었다.
실무에서 아쉬웠던 결제 프로세스에 대한 점들을 이번 프로젝트를 통해 개선해보며 많은 것들을 느꼈다. 이제는 코드 레벨 뿐만 아니라 인프라, 아키텍처에 대한 이해도와 설계 능력도 중요해지는 시기인 만큼 어느 하나라도 놓을 수 없으며 이를 발전시키기 위해 많은 공부를 해야 할 것 같다. 이렇게 이번 프로젝트는 마무리 🖌️
'Java' 카테고리의 다른 글
[Spring] MSA 기반 SNS 서비스 with AWS EKS [설계 / AWS 설정] (0) | 2024.08.18 |
---|---|
[Spring] 결제시스템, Spring Boot + MSA 로 리팩토링 [기획 & 설계] (0) | 2024.07.28 |
[Spring] feign 에 대하여 (0) | 2024.07.17 |
[Spring] 성능 테스트 ? Locust로! with Docker (0) | 2024.05.23 |
[Spring] Spring Boot 에서 멀티 모듈 구현 (0) | 2024.05.22 |
소중한 공감 감사합니다