MongoDB 트랜잭션 적용하기 (feat. replica-set)

의혁·2025년 4월 3일
0
post-thumbnail

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

1. 문제상황

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

위 코드로 진행하다 보니 하나의 트랜잭션 내부에서 chatMessageRepository에서의 save와 outboxRepository의 save를 통해 "다중 도큐먼트 트랜잭션"이 발생하여 위와 같은 오류가 발생하였다.

💡 단일 노드의 MongoDB에서는 다중 도큐먼트 트랜잭션을 처리할 수 없다.(replica-set 설정 필요)


2. 해결방법

1) 기존 DB 데이터 백업 (for migration)

# 기존 DB를 백업한 dump 생성
$ mongodump --db=snail --out=./dump

기존에는 Local 환경에서 snail 이라는 데이터 베이스를 사용했기 때문에 이를 migration 하기 위해서 해당 데이터를 dump 파일로 백업을 진행한다.

2) docker-compose를 통한 replica-set 환경 구성

docker-compose.yml 파일 생성

# docker-compose.yml 파일 수정 (해당 파일이 존재하는 경로에서 실행)
$ sudo vim docker-compose.yml 

위와 같이 일단 MongoDB replica-set 설정을 위해서 docker-compose.yml 파일에 추가 설정을 해준다.
운영 환경 차원에서 Replica-set은 "홀수개"로 설정하는 것이 바람직 하기에, 총 3개를 선언하였다.

replica-set 인증에 사용될 mongodb.key 권한 설정

# 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 실헹
$ docker compose up -d

# 현재 docker에 띄워져 있는 컨테이너 확인
$ docker ps -a

docker compose를 통해 mongoDB replica-set을 띄워진 것을 확인할 수 있다.


3) Replica-set 설정

docker 환경 접속 후 replica 설정 지정

# 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 복제 세트 설정을 추가해준다.

replica-set 설정 확인

$ rs.config()
$ rs.status()

설정 후, 위 메소드를 사용해서 상태를 확인해보면, replica-set이 잘 설정되었으면, mongo1은 primary로 mongo2는 secondary로 잘 설정된 것을 확인할 수 있다.

4) 데이터베이스 마이그레이션 적용

local에 저장되어 있는 dump 파일을 컨테이너에 옮기기

<실패 과정>

# 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를 보면 잘 옮겨져 있는것을 알 수 있다!!

5) user 생성

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


3. 결과

1) 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를 성공적으로 연결하였다.

2) 실제 프로젝트 테스트

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

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


4. 주의사항 (Trouble Shooting)

1) 꼭.. 정말 꼭 현재 본인 로컬 mongoDB가 연결 포트를 잡아먹고 있는지 확인하자

실제로 삽질을 할때, 이 로컬DB가 실행되고 있었고, 27017포트를 할당하고 있어서 계속 연결이 안되고 timeout이 발생하엿다.. 꼭꼭 확인해보도록 하자

2) docker-compose.yml 파일에서 외부에서 접근할 포트를 잘 확인하자

  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로 되어있기 때문에 매치되지 않아서 연결이 실패하는 것이였다.

사실 제대로 작성된 글이 많지는 않았고, 정말 뒤죽박죽이었다... 2일을 밤을 새서 결국은 해결해내니까 정말 뿌듯.. 내가 설정을 진행하면서 했던 삽질과 문제원인을 소개하고자 한다. 꼭 다른 사람들은 나와 같은 경험을 하지 않았으면 한다.


5. Failover를 위한 추가 설정 ( + 2025.04.08)

추가적으로 MongoDB replica-set을 설정하면서, 조금 더 failover에 적합한 설정을 변경해주었다.

1) Election Timeout & Heartbeat Interval 설정

  1. heartbeatIntervalMillis
    2000 -> 1000 (헬스 체크 간격 2초 -> 1초)
  2. electionTimeoutMillis
    10000 -> 5000 (Primary 장애 감지 후 선출 타임아웃 10초 > 5초)

heartbeat와 electionTimeout 설정은 failover 시 장애 감지 및 새로운 Primary 선출을 보다 빠르고 효율적으로 수행하여, 데이터 정합성을 유지하고 서비스 중단 없이 원활한 전환을 도모하는 데 중요한 역할을 한다.

2) Catch-up 설정

  1. catchUpTimeoutMillis
    -1 -> 2000 (새 Primary가 Oplog 따라잡도록 부여되는 시간 즉시 -> 2초)
  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/

profile
매일매일 차근차근 나아가보는 개발일기

0개의 댓글