왜 트랜잭션이 안되지?
몽고디비를 처음 써보면서 평소처럼 아무 생각없이 @Transaction을 사용했다.
테스트 환경을 격리하기 위해 별다른 작업없이 @Transaction 애노테이션을 붙혀줬는데, 다음과 같은 에러가 발생했다.
java.lang.IllegalStateException: Failed to retrieve PlatformTransactionManager for @Transactional test:
에러 메세지를 읽어보면 @Transaction 테스트를 위한 트랜잭션 매니저를 찾을 수 없는 것 같다.
스프링은 트랜잭션을 처리하기 위한 트랜잭션 매니저를 자동으로 등록해주는 걸로 아는데, 왜 찾을 수 없다고 하는걸까?
몽고디비는 트랜잭션이 필수가 아니라 트랜잭션 매니저를 등록하지 않는다고 한다.
따라서 별도로 트랜잭션 매니저를 활성화 시켜줘야 한다.
📁 config/MongoConfig.java
@Configuration
public class MongoConfig {
@Bean
public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}
테스트 코드에서는 몽고 트랜잭션 매니저를 추가해주는 것 만으로도 트랜잭션을 통한 롤백이 가능한 것을 확인했다.
그런데 API 테스트를 하면 다른 에러가 또 발생한다.
com.mongodb.MongoQueryException: Command failed with error 20 (IllegalOperation): 'Transaction numbers are only allowed on a replica set member or mongos' on server localhost:27017.
The full response is {"ok": 0.0, "errmsg": "Transaction numbers are only allowed on a replica set member or mongos", "code": 20, "codeName": "IllegalOperation"}
몽고디비는 리플리카셋 구성을 해줘야 트랜잭션을 사용할 수 있다고 한다.
리플리카셋? 도대체 그게 뭐야! 몽고디비 쓰기 쉽지 않다.....
하지만 나는 해내야한다.
리플리카셋이 무엇이고 몽고디비는 왜 이걸 해야하는건지 부터 알아봤다.
Replica Set이 뭐야?
몽고디비는 속도가 빠른 대신 데이터 손실의 위험이 있다는 단점이 있다.
이 손실을 최대한 막기 위해 리플리카셋을 해주는 것이라 한다.
리플리카 셋을 구축하는 이유는 다음과 같이 정리할 수 있다.
- 데이터를 안전하게 보존하기 위해서
- 24시간 접근 가능한 데이터의 상태를 유지하기 위해서
- Transaction 처리를 위해서 (테스트 환경이나 개발 환경에서는 Standalone mongod 인스턴스를 Replica Set으로 변환하여 사용할 수 있지만, 프로덕션 환경에서는 Replica Set 구축이 필수적이다.)
리플리카 셋은 보통 3개의 서버로 이뤄진다.
- Primary Node(마스터 서버) : 메인 데이터베이스 서버
- Secondary Node(슬레이브 서버) : 서브 형태의 서버, 마스터 서버가 죽으면 마스터 서버를 대신함
- Arbiter Node(아비터 서버) : 감시용 서버, 문제가 생각 서버를 대신해 동작할 다른 서버를 시정하는 등의 역할
도커로 MongoDB 리플리카셋 구축하기
필자는 docker를 사용해 몽고디비를 구동하고 있다.
따라서 docker-compose 파일로 리플리카셋 구축하는 방법을 알아보도록 하겠다.
docker-compose를 구성하기 전 리플리카셋 내부 인증을 위한 키 파일을 생성해줘야 한다.
openssl rand -base64 756 > <path-to-keyfile>
chmod 400 <path-to-keyfile>
나는 몽고디비 관련 파일을 한 곳에서 관리할 수 있도록 /mongo 하위로 경로를 지정해주었다.
📁 docker/mongo/mongdb.key
openssl rand -base64 756 > mongodb.key
chmod 400 mongodb.key
그런 다음, docker-compose 파일로 3개의 몽고디비 컨테이너를 생성한다.
📁 docker/dokcer-compose.yml
version: "3.9"
services:
mongo1:
image: mongo
container_name: mongo1 #컨테니어 이름
restart: always #컨테이너가 종료될때 항상 재시작
ports: #호스트 포트(왼쪽)와 컨테이너 내부 포트(오른쪽)를 매핑
- 27017:27017
environment: #컨테이너 내부에서 실행되는 애플리케이션에 환경을 전달
- MONGO_INITDB_ROOT_USERNAME={username}
- MONGO_INITDB_ROOT_PASSWORD={password}
command: mongod --replSet rs --keyFile /etc/mongodb.key --bind_ip_all #컨테이너가 시작될 때 실행할 명령
networks: #서비스가 사용할 컨테이너 네트워크 지정(여러개의 서비스를 하나의 네트워크로 연결시 서비스간 통신 가능)
- mongoNet
volumes:
- ./mongo/data/data1:/data/db
- ./mongo/mongodb.key:/etc/mongodb.key
- ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js #초기 데이터를 위한 init script
mongo2:
image: mongo
container_name: mongo2
restart: always
ports:
- 27018:27017
networks:
- mongoNet
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=1234
command: mongod --replSet rs --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./mongo/data/data2:/data/db
- ./mongo/mongodb.key:/etc/mongodb.key
mongo3:
image: mongo
container_name: mongo3
restart: always
ports:
- 27019:27017
networks:
- mongoNet
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=1234
command: mongod --replSet rs --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./mongo/data/data3:/data/db
- ./mongo/mongodb.key:/etc/mongodb.key
networks:
mongoNet:
name: mongoNet
- command
- --replSet rs : 리플리카셋 이름 지정, 필자는 rs로 했지만 마음대로 변경 가능하다.
완성된 디렉토리 구성은 다음과 같다.
docker
ㄴ mongo
ㄴ data
ㄴ data1
ㄴ data2
ㄴ data3
ㄴ mongodb.key
ㄴ mongo-init.js
ㄴ docker-compose.yml
3개의 몽고디비 컨테이너를 구동한 뒤 Primary Node로 사용할 컨테이너에 접속한다.
필자는 mongo1을 Primary Node로 두고 리플리카셋 설정을 진행했다.
docker exec -it mongo1 mongosh -u {username} -p {password}
rs.initiate({
_id: 'rs',
members: [
{ _id: 0, host: 'mongo1:27017', priority: 2 },
{ _id: 1, host: 'mongo2:27017', priority: 0.5 },
{ _id: 2, host: 'mongo3:27017', priority: 0.5 }
]
});
- _id: 'rs' : docker-compose에서 --replSet 뒤에 지정해준 리플리카셋 이름이다.
- priority : priority가 높은 순서대로 Primary Node가 된다. 이때 주의할 점은 0으로 설정할 경우 해당 멤버는 Arbiter Node가 되어, 투표권만 갖고 데이터 복제를 진행하지 않는다. 만약 Arbiter Node를 의도적으로 사용하는 것이 아니라면 priority 값은 0보다 큰 값으로 설정해야 한다.
rs.status()
rs.status()로 리플리카셋 상태를 확인할 수 있다.
제대로 설정했다면 mongo1은 PRIMARY로 mongo2,3은 SECONDARY로 설정된 것을 확인할 수 있다.

쉘 스크립트로 리플리카셋 구성까지 한번에 처리하기
📁 docker/run-mongo.sh
#!/bin/bash
# mongodb.key 파일이 없는 경우에만 생성
if [ ! -f "./mongo/mongodb.key" ]; then
echo "create keyfile"
openssl rand -base64 756 > ./mongo/mongodb.key
chmod 400 ./mongo/mongodb.key
fi
docker-compose up -d
sleep 10
echo "init replicaset"
docker exec mongo1 /scripts/rs-init.sh
📁 docker/mongo/rs-init.sh
#!/bin/bash
mongosh -u "root" -p "1234"<<EOF
var config = {
_id: 'rs',
members: [
{ _id: 0, host: 'mongo1:27017', priority: 2 },
{ _id: 1, host: 'mongo2:27017', priority: 0.5 },
{ _id: 2, host: 'mongo3:27017', priority: 0.5 }
]
}
rs.initiate(config);
rs.status();
EOF
고된 삽질 ... ⛏️
사실 도커를 공부하지 않고 작성하다보니 정말 많은 시간 삽질을 했다...
여러 문서들을 정말 많이 찾아보며 도커 컴포즈 파일을 작성했는데 mongo1, mongo2, mongo3 컨테이너가 도통 돌아가지가 않았다.
왜 안되는건줄도 모르겠고, 도커 컴포즈 파일을 잘못 작성했구나 싶어 그에 관련된 것만 계속해서 찾아보며 쩔쩔맸다.
그러다 문득 도커 로그를 차근차근 살펴보게 되었는데..... 역시 로그에 실마리가 있었다..
(계속 가장 마지막 에러 로그만 확인한 죄지...)
수많은 로그 에러 중 실마리를 찾은 로그는 아래와 같다.
{"t":{"$date":"2024-02-02T03:30:44.253+00:00"},"s":"I", "c":"ACCESS", "id":20254, "ctx":"main","msg":"Read security file failed","attr":{"error":{"code":30,"codeName":"InvalidPath","errmsg":"permissions on /etc/mongodb.key are too open"}}}
리플리카셋 구성을 위해 이전에 키 파일을 만들어 준 것이 기억날거다.
나는 계속 이 키를 도커로 연결하는 volumns 경로가 잘못된 것 같다는 생각으로 docker-compose 파일만 계속 수정했었다.
하지만, 에러 메세지를 보면 알 수 있 듯 키 파일의 권한 부여 문제임을 알 수 있다.
이 에러를 눈치 챈 후, 아...설마..... 하며 gpt한테 물어봤다.

에러 해결을 위해 mongodb.key 파일을 삭제하고 다시 만들고를 반복하다보니 그 과정에서 권한 설정을 빼먹어 일어난 문제였다.
권한을 제어해주고 나니 잘 동작했다..^^
Reference
[Baeldung] Spring Data MongoDB Transactions
[공식문서] Spring Data MongoDB Transactions
Spring MongoDB Transaction Support
[youtube] 몽고디비 리플리카 셋업부터 테스트까지
MongoDB Replica Set 환경 구축하기 (Feat.Docker)
'💻 Computer Science > 데이터베이스' 카테고리의 다른 글
MySQL 잘 사용하기 (1) (0) | 2023.02.10 |
---|---|
[H2] H2 데이터베이스 사용하기 (1) | 2022.06.02 |
[MariaDB] 마리아디비 사용하기 (0) | 2022.03.24 |
SQL 총 정리 (2) | 2022.02.10 |
왜 트랜잭션이 안되지?
몽고디비를 처음 써보면서 평소처럼 아무 생각없이 @Transaction을 사용했다.
테스트 환경을 격리하기 위해 별다른 작업없이 @Transaction 애노테이션을 붙혀줬는데, 다음과 같은 에러가 발생했다.
java.lang.IllegalStateException: Failed to retrieve PlatformTransactionManager for @Transactional test:
에러 메세지를 읽어보면 @Transaction 테스트를 위한 트랜잭션 매니저를 찾을 수 없는 것 같다.
스프링은 트랜잭션을 처리하기 위한 트랜잭션 매니저를 자동으로 등록해주는 걸로 아는데, 왜 찾을 수 없다고 하는걸까?
몽고디비는 트랜잭션이 필수가 아니라 트랜잭션 매니저를 등록하지 않는다고 한다.
따라서 별도로 트랜잭션 매니저를 활성화 시켜줘야 한다.
📁 config/MongoConfig.java
@Configuration
public class MongoConfig {
@Bean
public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}
테스트 코드에서는 몽고 트랜잭션 매니저를 추가해주는 것 만으로도 트랜잭션을 통한 롤백이 가능한 것을 확인했다.
그런데 API 테스트를 하면 다른 에러가 또 발생한다.
com.mongodb.MongoQueryException: Command failed with error 20 (IllegalOperation): 'Transaction numbers are only allowed on a replica set member or mongos' on server localhost:27017.
The full response is {"ok": 0.0, "errmsg": "Transaction numbers are only allowed on a replica set member or mongos", "code": 20, "codeName": "IllegalOperation"}
몽고디비는 리플리카셋 구성을 해줘야 트랜잭션을 사용할 수 있다고 한다.
리플리카셋? 도대체 그게 뭐야! 몽고디비 쓰기 쉽지 않다.....
하지만 나는 해내야한다.
리플리카셋이 무엇이고 몽고디비는 왜 이걸 해야하는건지 부터 알아봤다.
Replica Set이 뭐야?
몽고디비는 속도가 빠른 대신 데이터 손실의 위험이 있다는 단점이 있다.
이 손실을 최대한 막기 위해 리플리카셋을 해주는 것이라 한다.
리플리카 셋을 구축하는 이유는 다음과 같이 정리할 수 있다.
- 데이터를 안전하게 보존하기 위해서
- 24시간 접근 가능한 데이터의 상태를 유지하기 위해서
- Transaction 처리를 위해서 (테스트 환경이나 개발 환경에서는 Standalone mongod 인스턴스를 Replica Set으로 변환하여 사용할 수 있지만, 프로덕션 환경에서는 Replica Set 구축이 필수적이다.)
리플리카 셋은 보통 3개의 서버로 이뤄진다.
- Primary Node(마스터 서버) : 메인 데이터베이스 서버
- Secondary Node(슬레이브 서버) : 서브 형태의 서버, 마스터 서버가 죽으면 마스터 서버를 대신함
- Arbiter Node(아비터 서버) : 감시용 서버, 문제가 생각 서버를 대신해 동작할 다른 서버를 시정하는 등의 역할
도커로 MongoDB 리플리카셋 구축하기
필자는 docker를 사용해 몽고디비를 구동하고 있다.
따라서 docker-compose 파일로 리플리카셋 구축하는 방법을 알아보도록 하겠다.
docker-compose를 구성하기 전 리플리카셋 내부 인증을 위한 키 파일을 생성해줘야 한다.
openssl rand -base64 756 > <path-to-keyfile>
chmod 400 <path-to-keyfile>
나는 몽고디비 관련 파일을 한 곳에서 관리할 수 있도록 /mongo 하위로 경로를 지정해주었다.
📁 docker/mongo/mongdb.key
openssl rand -base64 756 > mongodb.key
chmod 400 mongodb.key
그런 다음, docker-compose 파일로 3개의 몽고디비 컨테이너를 생성한다.
📁 docker/dokcer-compose.yml
version: "3.9"
services:
mongo1:
image: mongo
container_name: mongo1 #컨테니어 이름
restart: always #컨테이너가 종료될때 항상 재시작
ports: #호스트 포트(왼쪽)와 컨테이너 내부 포트(오른쪽)를 매핑
- 27017:27017
environment: #컨테이너 내부에서 실행되는 애플리케이션에 환경을 전달
- MONGO_INITDB_ROOT_USERNAME={username}
- MONGO_INITDB_ROOT_PASSWORD={password}
command: mongod --replSet rs --keyFile /etc/mongodb.key --bind_ip_all #컨테이너가 시작될 때 실행할 명령
networks: #서비스가 사용할 컨테이너 네트워크 지정(여러개의 서비스를 하나의 네트워크로 연결시 서비스간 통신 가능)
- mongoNet
volumes:
- ./mongo/data/data1:/data/db
- ./mongo/mongodb.key:/etc/mongodb.key
- ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js #초기 데이터를 위한 init script
mongo2:
image: mongo
container_name: mongo2
restart: always
ports:
- 27018:27017
networks:
- mongoNet
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=1234
command: mongod --replSet rs --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./mongo/data/data2:/data/db
- ./mongo/mongodb.key:/etc/mongodb.key
mongo3:
image: mongo
container_name: mongo3
restart: always
ports:
- 27019:27017
networks:
- mongoNet
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=1234
command: mongod --replSet rs --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./mongo/data/data3:/data/db
- ./mongo/mongodb.key:/etc/mongodb.key
networks:
mongoNet:
name: mongoNet
- command
- --replSet rs : 리플리카셋 이름 지정, 필자는 rs로 했지만 마음대로 변경 가능하다.
완성된 디렉토리 구성은 다음과 같다.
docker
ㄴ mongo
ㄴ data
ㄴ data1
ㄴ data2
ㄴ data3
ㄴ mongodb.key
ㄴ mongo-init.js
ㄴ docker-compose.yml
3개의 몽고디비 컨테이너를 구동한 뒤 Primary Node로 사용할 컨테이너에 접속한다.
필자는 mongo1을 Primary Node로 두고 리플리카셋 설정을 진행했다.
docker exec -it mongo1 mongosh -u {username} -p {password}
rs.initiate({
_id: 'rs',
members: [
{ _id: 0, host: 'mongo1:27017', priority: 2 },
{ _id: 1, host: 'mongo2:27017', priority: 0.5 },
{ _id: 2, host: 'mongo3:27017', priority: 0.5 }
]
});
- _id: 'rs' : docker-compose에서 --replSet 뒤에 지정해준 리플리카셋 이름이다.
- priority : priority가 높은 순서대로 Primary Node가 된다. 이때 주의할 점은 0으로 설정할 경우 해당 멤버는 Arbiter Node가 되어, 투표권만 갖고 데이터 복제를 진행하지 않는다. 만약 Arbiter Node를 의도적으로 사용하는 것이 아니라면 priority 값은 0보다 큰 값으로 설정해야 한다.
rs.status()
rs.status()로 리플리카셋 상태를 확인할 수 있다.
제대로 설정했다면 mongo1은 PRIMARY로 mongo2,3은 SECONDARY로 설정된 것을 확인할 수 있다.

쉘 스크립트로 리플리카셋 구성까지 한번에 처리하기
📁 docker/run-mongo.sh
#!/bin/bash
# mongodb.key 파일이 없는 경우에만 생성
if [ ! -f "./mongo/mongodb.key" ]; then
echo "create keyfile"
openssl rand -base64 756 > ./mongo/mongodb.key
chmod 400 ./mongo/mongodb.key
fi
docker-compose up -d
sleep 10
echo "init replicaset"
docker exec mongo1 /scripts/rs-init.sh
📁 docker/mongo/rs-init.sh
#!/bin/bash
mongosh -u "root" -p "1234"<<EOF
var config = {
_id: 'rs',
members: [
{ _id: 0, host: 'mongo1:27017', priority: 2 },
{ _id: 1, host: 'mongo2:27017', priority: 0.5 },
{ _id: 2, host: 'mongo3:27017', priority: 0.5 }
]
}
rs.initiate(config);
rs.status();
EOF
고된 삽질 ... ⛏️
사실 도커를 공부하지 않고 작성하다보니 정말 많은 시간 삽질을 했다...
여러 문서들을 정말 많이 찾아보며 도커 컴포즈 파일을 작성했는데 mongo1, mongo2, mongo3 컨테이너가 도통 돌아가지가 않았다.
왜 안되는건줄도 모르겠고, 도커 컴포즈 파일을 잘못 작성했구나 싶어 그에 관련된 것만 계속해서 찾아보며 쩔쩔맸다.
그러다 문득 도커 로그를 차근차근 살펴보게 되었는데..... 역시 로그에 실마리가 있었다..
(계속 가장 마지막 에러 로그만 확인한 죄지...)
수많은 로그 에러 중 실마리를 찾은 로그는 아래와 같다.
{"t":{"$date":"2024-02-02T03:30:44.253+00:00"},"s":"I", "c":"ACCESS", "id":20254, "ctx":"main","msg":"Read security file failed","attr":{"error":{"code":30,"codeName":"InvalidPath","errmsg":"permissions on /etc/mongodb.key are too open"}}}
리플리카셋 구성을 위해 이전에 키 파일을 만들어 준 것이 기억날거다.
나는 계속 이 키를 도커로 연결하는 volumns 경로가 잘못된 것 같다는 생각으로 docker-compose 파일만 계속 수정했었다.
하지만, 에러 메세지를 보면 알 수 있 듯 키 파일의 권한 부여 문제임을 알 수 있다.
이 에러를 눈치 챈 후, 아...설마..... 하며 gpt한테 물어봤다.

에러 해결을 위해 mongodb.key 파일을 삭제하고 다시 만들고를 반복하다보니 그 과정에서 권한 설정을 빼먹어 일어난 문제였다.
권한을 제어해주고 나니 잘 동작했다..^^
Reference
[Baeldung] Spring Data MongoDB Transactions
[공식문서] Spring Data MongoDB Transactions
Spring MongoDB Transaction Support
[youtube] 몽고디비 리플리카 셋업부터 테스트까지
MongoDB Replica Set 환경 구축하기 (Feat.Docker)
'💻 Computer Science > 데이터베이스' 카테고리의 다른 글
MySQL 잘 사용하기 (1) (0) | 2023.02.10 |
---|---|
[H2] H2 데이터베이스 사용하기 (1) | 2022.06.02 |
[MariaDB] 마리아디비 사용하기 (0) | 2022.03.24 |
SQL 총 정리 (2) | 2022.02.10 |