
💡 MongoDB에 @Transactional을 적용하기 위해 replica-set을 설정해보자!!'



위와 같이 Kafka에 Outbox 패턴을 적용하기 위해서는 하나의 트랜잭션 내부에서 메시지 내용도 ChatMessage에 저장하고, Outbox도 저장해야한다.

위 코드로 진행하다 보니 하나의 트랜잭션 내부에서 chatMessageRepository에서의 save와 outboxRepository의 save를 통해 "다중 도큐먼트 트랜잭션"이 발생하여 위와 같은 오류가 발생하였다.
# 기존 DB를 백업한 dump 생성
$ mongodump --db=snail --out=./dump
기존에는 Local 환경에서 snail 이라는 데이터 베이스를 사용했기 때문에 이를 migration 하기 위해서 해당 데이터를 dump 파일로 백업을 진행한다.

# docker-compose.yml 파일 수정 (해당 파일이 존재하는 경로에서 실행)
$ sudo vim docker-compose.yml
위와 같이 일단 MongoDB replica-set 설정을 위해서 docker-compose.yml 파일에 추가 설정을 해준다.
운영 환경 차원에서 Replica-set은 "홀수개"로 설정하는 것이 바람직 하기에, 총 3개를 선언하였다.
# mongodb 키 생성 ( MAC M1 기준 )
sudo openssl rand -base64 756 | sudo tee /Users/hyeok/.ssh/mongodb.key >
/dev/null
# 권한 설정
sudo chmod 400 ~/.ssh/mongodb.key
replica-set 인증에 필요한 key 권한을 설정해준다.

# 백그라운드 환경에 docker-compose 실헹
$ docker compose up -d
# 현재 docker에 띄워져 있는 컨테이너 확인
$ docker ps -a
docker compose를 통해 mongoDB replica-set을 띄워진 것을 확인할 수 있다.
# docker container 접속
docker exec -it mongo1 /bin/bash
# bockerl 계정 몽고 쉘 접속 (root 계정)
mongosh -u bockerl -p bockerl
# admin 데이터 베이스 사용
use admin
# replication 초기화
rs.initiate()
# mongo2 복제세트 추가
rs.add({_id: 1, host: "mongo2:27017"})
# mongo3 복제세트 추가
rs.add({_id: 2, host: "mongo3:27017"})
위 과정을 따라서 진행하면, 컨테이너 내부에 들어가 replica-set 설정을 위해 mongo2 복제 세트 설정을 추가해준다.
![]() |
![]() |
$ rs.config()
$ rs.status()
설정 후, 위 메소드를 사용해서 상태를 확인해보면, replica-set이 잘 설정되었으면, mongo1은 primary로 mongo2는 secondary로 잘 설정된 것을 확인할 수 있다.
<실패 과정>
# docker-compose.yml 파일이 존재하는 곳에서 실행
docker exec -it mongo1 mongorestore \
-u bockerl -p bockerl \
--authenticationDatabase admin \ # 인증 db
--db snail /dump/snail
위 방식을 사용해서 dump 파일을 옮기려고 시도하였지만.. 오류가 발생했다;;

MongoDB 공식 블로그를 참고해서 해결하였고, 위 공식 문서에 동일한 오류를 처리할 수 있는 방식을 소개해뒀다.
<성공 과정>
# 1. 본인이 dump를 저장해둔 파일을 나의 mongo1 컨테이너에 복사한다.
$ docker cp /Users/hyeok/desktop/dump mongo1:/dump
# 2. Migration 하기 위해 복사한 dump를 컨테이너에 저장 ( 나의 경우: snail)
docker exec -it mongo1 mongorestore \
-u bockerl -p bockerl \
--authenticationDatabase admin \
--db snail /dump/snail
위 방식을 통해서 먼저 dump 파일을 컨테이너에 복사하고, 불러와서 저장하는 과정이 필요하였다

rs0 [direct: primary] test> use snail
switched to db snail
rs0 [direct: primary] snail> show dbs
admin 140.00 KiB
config 160.00 KiB
local 440.00 KiB
snail 120.00 KiB
rs0 [direct: primary] snail> show collections
chatMessage
GroupChatRoom
PersonalChatRoom
rs0 [direct: primary] snail> db.createUser({ user: "bockerl", pwd: "bockerl", roles: [{ role: "readWrite", db: "snail"}] })
오예~~~🎉🎉🎉🎉🎉 데이터 마이그레이션을 성공하였고, 실제로 snail 데이터 베이스의 collections를 보면 잘 옮겨져 있는것을 알 수 있다!!

snail 데이터베이스를 사용할 수 있도록 bockerl 라는 user를 생성하였다.
이제 모든 설정이 완료 되었고, 먼저 MongoDB compass 연결과 프로젝트 연결만 남았다.

mongodb://bockerl:bockerl@localhost:27017,localhost:27018,localhost:27019/snail?authSource=snail&replicaSet=rs0
실제로 Docker 설정한 설정과 로컬에서 연결할 포트번호 및 사용 데이터베이스를 잘 입력해줘서 replica-set 등록을 진행한다.

보이는 것과 같이 MongoDB의 replica-set 등록이 잘되는 것을 확인할 수 있고, local을 통해 Docker에 띄워진 MongoDB를 성공적으로 연결하였다.

실제 프로젝트에서는 위와 동일하게 설정하여 진행하엿으며, 이는 위의 compass의 등록과 유사하다.

최종적으로 기존에 단일노드에서의 @Transactional로 인한 다중 도큐먼트 트랜잭션으로 발생한 오류가 발생하지 않고, 실제로 Replica-set 설정으로 인해 Docker에 떠있는 MongoDB에 잘 저장된 것을 확인수 있었다~~~🥲🥲🥲🥲🥲🥲ㅍ

실제로 삽질을 할때, 이 로컬DB가 실행되고 있었고, 27017포트를 할당하고 있어서 계속 연결이 안되고 timeout이 발생하엿다.. 꼭꼭 확인해보도록 하자
mongodb1:
image: mongo
container_name: mongo1
hostname: mongo1
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: bockerl
MONGO_INITDB_ROOT_PASSWORD: bockerl
command: mongod --replSet rs0 --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./db1:/data/db
- ~/.ssh/mongodb.key:/etc/mongodb.key
mongodb2:
image: mongo
container_name: mongo2
hostname: mongo2
restart: always
ports:
- "27018:27018"
environment:
MONGO_INITDB_ROOT_USERNAME: bockerl
MONGO_INITDB_ROOT_PASSWORD: bockerl
command: mongod --replSet rs0 --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./db2:/data/db
- ~/.ssh/mongodb.key:/etc/mongodb.key
mongodb3:
image: mongo
container_name: mongo3
hostname: mongo3
restart: always
ports:
- "27019:27019"
environment:
MONGO_INITDB_ROOT_USERNAME: bockerl
MONGO_INITDB_ROOT_PASSWORD: bockerl
command: mongod --replSet rs0 --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./db3:/data/db
- ~/.ssh/mongodb.key:/etc/mongodb.key
networks:
infra:
driver: bridge
이것이 나의 docker-compose.yml파일인데 기존에는 컨테이너 내부 연결을 전부 27017로 통일했었다. (ex. 27018:27017)
이렇게 진행하다보니 외부(local)에서는 계속 27018,27019로 접근하여도, Docker 내부에서는 27017로 되어있기 때문에 매치되지 않아서 연결이 실패하는 것이였다.
추가적으로 MongoDB replica-set을 설정하면서, 조금 더 failover에 적합한 설정을 변경해주었다.

- heartbeatIntervalMillis
2000 -> 1000 (헬스 체크 간격 2초 -> 1초)- electionTimeoutMillis
10000 -> 5000 (Primary 장애 감지 후 선출 타임아웃 10초 > 5초)
heartbeat와 electionTimeout 설정은 failover 시 장애 감지 및 새로운 Primary 선출을 보다 빠르고 효율적으로 수행하여, 데이터 정합성을 유지하고 서비스 중단 없이 원활한 전환을 도모하는 데 중요한 역할을 한다.

- catchUpTimeoutMillis
-1 -> 2000 (새 Primary가 Oplog 따라잡도록 부여되는 시간 즉시 -> 2초)- catchUpTakeoverDelayMillis
30000 -> 1000 (새 Primary 승격 후 요청 처리전 대기 30초 -> 1초)
catchUp 설정은 기존 Primary 장애시 새로운 Primary가 선출되면서 데이터 동기화를 완료하고 정상적인 처리로 전환하기 위한 설정이다.
또한, failover 시 데이터 정합성을 유지하고, 서비스 중단 없이 원활한 전환을 도모하는 데 도움을 줍니다.
참고블로그
1. 전체 설정: https://jh2021.tistory.com/24
2. replica 운영 세부 설정( 관련 내용 업데이트 예정) : https://oliveyoung.tech/2024-12-17/catalog-mongo-transaction-2/