본 포스팅은 스프링 프로젝트에 MongoDB 적용 과정과 트러블 슈팅에 관한 내용을 담은 포스팅입니다.
틀린 내용은 댓글로 지적해주시면 감사하겠습니다.
필자는 팀 프로젝트를 진행하며 주식 데이터를 저장하게 되었다.
대용량의 데이터이기에 NoSQL 도입이 논의되었고, 우리는 MongoDB를 사용하기로 하였다.
현재 배포되어있는 스프링 애플리케이션도 EC2 상의 Docker로 실행되고 있기 때문에, MongoDB 역시 Docker로 실행한다.
본격적인 적용에 앞서 Replica Set 에 대한 개념을 알고 가야한다.
Replica Set은 말 그대로 '복제 데이터베이스의 집합'이다.
즉, 데이터베이스가 단 하나만 primary-secondary(...)의 구조를 두어 하나의 데이터베이스가 다운되더라도 안정성을 보장한다.
이 Replica Set이 필요한 이유는 트랜잭션 때문이다.
MongoDB는 트랜잭션을 옵션으로 지원한다.
따라서, 트랜잭션을 적용하기 위해서는 Replica Set의 oplog 기술이 필요하다.
📌 oplog
데이터베이스 작업된 내용(log)을 모두 기록하여, primary-secondary 데이터베이스를 동기화하는 기술
Replica Set은 primary - secondary - secondary와 같이 3개의 데이터베이스를 사용한다고 한다. 필자는 해당 방식을 참고하여 3개의 데이터베이스(도커 컨테이너)를 실행하여 Replica Set을 구성한다.
Replica Set 내의 데이터베이스는 Key File을 통하여 각 노드 간 인증이 가능하다.
자세한 설명은 본문 및 포스팅 하단의 참고 링크를 확인하길 바란다.
해당 절은 docker-compose를 통한 MongoDB 설치 및 실행 과정이다.
services:
mongodb1:
image: mongo
hostname: mongodb1 # 컨테이너에서 프로세스를 인식하는 이름. 다른 컨테이너와 통신에서도 사용
container_name: mongodb1
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root # root 사용자 username
MONGO_INITDB_ROOT_PASSWORD: qwer1234 # root 사용자 password
volumes: # 로컬:컨테이너
- ./mongo/mongo1/mongod.conf:/etc/mongod.conf # MongoDB 설정 파일
- ./key/mongodb.key:/etc/mongodb.key # 키 파일
- ./data/mongodb1:/data/db # 데이터베이스 내용
command: mongod --replSet rs0 --port 27017 --keyFile /etc/mongodb.key --bind_ip_all
# mongoDB 실행 명령어
ports:
- 27017:27017
networks:
- mongoCluster # replica set 내 컨테이너들을 동일 네트워크에 할당
mongodb2:
image: mongo
hostname: mongodb2
container_name: mongodb2
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: qwer1234
volumes:
- ./mongo/mongo2/mongod.conf:/etc/mongod.conf
- ./key/mongodb.key:/etc/mongodb.key
- ./data/mongodb2:/data/db
command: mongod --replSet rs0 --port 27018 --keyFile /etc/mongodb.key --bind_ip_all
ports:
- 27018:27018
networks:
- mongoCluster
depends_on:
- mongodb1
mongodb3:
image: mongo
hostname: mongodb3
container_name: mongodb3
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: qwer1234
volumes:
- ./mongo/mongo3/mongod.conf:/etc/mongod.conf
- ./key/mongodb.key:/etc/mongodb.key
- ./data/mongodb3:/data/db
command: mongod --replSet rs0 --port 27019 --keyFile /etc/mongodb.key --bind_ip_all
ports:
- 27019:27019
networks:
- mongoCluster
depends_on:
- mongodb1
networks:
mongoCluster:
external: true
위 설정 파일에서 알 수 있듯이 3개의 컨테이너 mongodb1, mongodb2, mongodb3가 시행된다. 각 27017, 27018, 27019 포트를 사용한다.
volume 마운트를 통해 설정 파일, 키 파일, DB 내용을 저장한다.
해당 부분은 폴더 구조를 유심히 봐야한다. 내 로컬 상 폴더 구조는 다음과 같이 구성되어 있어야 한다.
test/ # docker-compose.yml 파일 위치 디렉토리
└ data/
└ mongodb1/
└ mongodb2/
└ mongodb3/
└ key/
└ mongo/
└ mongodb1/
└ mongodb2/
└ mongodb3/
우선, mongod.conf 파일부터 작성한다.
# mongod.conf
# Where and how to store data.
storage:
dbPath: /var/lib/mongodb
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
# network interfaces
net:
port: 27017 # ❗[주의] mongodb1=27017 / mongodb2=27018 / mongodb3=27019
bindIp: 127.0.0.1
# how the process runs
processManagement:
timeZoneInfo: /usr/share/zoneinfo
# Keyfile 위치 설정
security:
authorization: enabled # username과 password를 통한 mongosh 접속 허용
clusterAuthMode: keyFile
keyFile: /etc/mongodb.key
# Replica Set 이름 설정
replication:
replSetName: rs0
mongod.conf는 MongoDB 설정을 정의한 파일이다.
network interfaces 부분에서 각 db 별 포트번호를 바꿔주는 것을 주의하자.
앞서 설명하였듯이 key 파일은 같은 Replica Set내 MongoDB끼리 인증하는 수단이다.
키 파일 생성 생성 방법은 Windows와 Mac에서 다르다.
$ cd key/
$ openssl rand -base64 756 > mongodb.key
$ chmod 400 mongodb.key
# $ chown 999:999 mongodb.key # ❗️ [12.28 수정] Mac의 경우, chown을 해주지 않아야 제대로 실행이 됨. 따라서, 해당 명령어 실행할 필요 없습니다.
맥의 경우에는 위의 명령어를 입력하여 간단하게 키 파일 생성이 가능하다.
윈도우는 키 파일 권한 변경에 있어 문제가 발생한다.
필자는 WSL을 통한 키 파일 생성 후 파일 위치 이동, 다른 컨테이너에서 키 파일 생성 후 이동 등의 방법을 사용하였는데 제대로 되지 않았다. 그러나, 이 부분은 정확한 내용은 아니며 단순 실수 및 기타 요소들로 인한 오류였을 수도 있다.
우선, 해결한 방법을 기준으로 설명하겠다.
기타 컨테이너를 만들어서 해당 경로 test/key/에 키 파일을 생성하면 된다는 글을 발견하고 해당 포스팅에 나와있는 것과 같이 Nginx 컨테이너를 만들어 키 파일 생성 후 삭제해주었다.
이를 진행하기 위하여 우선 앞서 작성한 docker-compose.yml의 모든 내용을 주석처리한 다음 아래 내용으로 바꾼 뒤 컨테이너를 실행하자.
version: "3"
services:
nginx:
image: nginx
container_name: nginx
ports:
- "3001:80"
volumes:
- ./key:/key
$ docker-compose up -d # nginx 컨테이너 실행
$ docker exec -it nginx bash
Nginx 컨테이너 접속 후 아래 명령어를 입력하여 mongodb.key 파일을 생성해주자.
root# cd /key
root# openssl rand -base64 756 > mongodb.key
root# chmod 400 mongodb.key
root# chown 999:999 mongodb.key

결과적으로 key/ 디렉토리에 키 파일이 생성된 것을 확인할 수 있다.
이제 Nginx 컨테이너 삭제 후, docker-compose.yml을 내용을 원래 내용으로 바꾼다.
$ docker-compose down
앞서, hostname을 지정해주었다.
이는 로컬에서도 인식할 수 있도록 따로 호스트 설정 파일을 수정해주어야 한다.
/etc/hosts 파일 내에 아래 내용을 추가해주자.
127.0.0.1 mongodb1
127.0.0.1 mongodb2
127.0.0.1 mongodb3
윈도우의 경우에는 아래와 같은 절차를 따라 수정이 가능하다.
C:\Windows\System32\drivers\etc\hosts 열기파일이 보이지 않을 경우, 파일 옵션 txt → '모든파일'로 변경
127.0.0.1 mongodb1
127.0.0.1 mongodb2
127.0.0.1 mongodb3
$ docker-compose up -d

위와 같은 결과가 출력되고, STATUS가 Restart가 아닌 Up 상태라면 정상적으로 실행이 완료된 것이다.
$ docekr exec -it mongodb1 bash
root# mongosh -u root -p qwer1234
Mongosh(Mongo Shell)을 통하여 MongoDB에 접속한다.
test> rs.initiate({_id:"rs0", members:[{_id: 0, host:"mongodb1:27017"},{_id: 1, host: "mongodb2:27018"},{_id: 2, host: "mongodb3:27019"}]})
# 결과
rs0 [direct: secondary] test>
Replica Set 적용 결과, MongoDB1이 secondary로 나오는 현상이 발생하는데 10초 정도의 시간이 지나면 Primary로 전환된다.
시간이 지난 뒤 rs.status() 명령어를 통해 Replica Set 상태를 확인해보면 primary로 바뀐 것을 확인할 수 있다.

📌 기타 MongoDB 접근 방법
- mongodb1 컨테이너에서 접속
- 해당 mongodb_ 컨테이너 접속
secondary MongoDB에 접속하는 방법은 위 2가지 모두 가능하다.
앞서, 다른 컨테이너 에서도 연결이 가능하도록 hostname을 지정해준 것 을 바탕으로 접속 가능하다.# mongodb2 접속 $ mongosh -u root -p qwer1234 --host mongodb2 --port 27018 # mongodb3 접속 $ mongosh -u root -p qwer1234 --host mongodb3 --port 27019
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
spring:
data:
mongodb:
host: ${MONGODB_HOST:localhost}
port: ${MONGODB_PORT:27017}
authentication-database: admin
database: mydatabase
username: root
password: qwer1234
spring:
data:
mongodb:
uri: mongodb://root:qwer1234@localhost:27017/mydatabase?authSource=admin
위와 같은 2가지 방식으로 auto configuration이 진행된다고 한다.
차후 다루겠지만 username/password 설정 시 auto figuraiton이 제대로 진행되지 않는 문제가 발생하였다. 트러블 슈팅에 관한 내용은 아래에서 서술한다.
@Configuration
@EnableTransactionManagement // 트랜잭션 적용
@RequiredArgsConstructor
public class MongoConfig extends AbstractMongoClientConfiguration {
/* 초기에 Bean 등록 X - 트러블슈팅 참고 */
@Bean
public MongoClient mongoClient() {
return com.mongodb.client.MongoClients.create("mongodb://root:qwer1234@localhost:27017/mydatabase?authSource=admin");
}
@Bean
public MongoTransactionManager transactionManager() { // 트랜잭션 적용
return new MongoTransactionManager(mongoDbFactory());
}
@Override
protected String getDatabaseName() {
return "muzusi";
}
}
MongoConfig 클래스는 AbstractMongoClientConfiguration을 사용한다.
트랜잭션 적용을 위하여 MongoTransactionManger를 Bean으로 등록해준다.
현재는 편의를 위하여 데이터베이스 주소를 하드코딩하였다.
필자는 초기에 MongoClient를 Bean으로 등록하지 않았다.
초기에는 username/password를 설정하지 않은 상태였고 그래도 동작에 문제가 없었다.
그러나, username과 password 사용 이후 모든 CRUD 과정이 진행되지 않았다.
에러 코드는 다음과 같다.
Command failed with error 13 (Unauthorized):
'Command insert requires authentication' on server 127.0.0.1:27017.
The full response is {"ok": 0.0, "errmsg": "Command insert requires authentication", "code": 13, "codeName": "Unauthorized"
해당 에러코드는 명시되어 있듯이 사용자 정보가 일치하지 않는다는 것이었다.
@Value 어노테이션을 이용하여 username과 password 값을 출력하여도 정상적으로 값이 주입되어 있었다.
혹자는 root 계정이 아니라, 내가 지정한 데이터베이스(mydatabase)에 계정을 생성 후 readWrite 권한을 주어야한다고 하였다. root 사용자는 당연히 모든 권한을 가지고 있는데 해당 방법은 해결책이 아니었다.
그러나, 이는 차후 root 계정이 아닌 데이터베이스 별 사용자를 분리하여 적용시켜 안정성을 높이는 개선 방안으로 떠오르기도 한다.
많은 블로그나 stackoverflow 글을 찾아다녔지만 다들 uri만 제대로 적용시키면 해결이 되었다는 이야기가 많았다.
필자는 계속해서 unauthorized 오류가 발생하였기에, 현재 Spring과 연결된 MongoDB 커넥션을 확인하고 싶었다. 해당 커넥션에서 username과 password가 정상적으로 적용이 되는 지 확인하고 싶었다.
우선, 설정 파일의 내용은 MongoDataAutoConfiguration.class를 거쳐 확인 가능하다.

해당 파일에 MongoProperties.class가 ConfigurationProperties로 적용되고 있다.

MongoProperties.class에는 앞서 보았던 환경 변수들이 정의되어있다.
여기서, 이 MongoProperties를 실제로 받아 MongoDB와 커넥션에 사용되는 인스턴스는 무엇인지 궁금해졌고 필자는 다시 MongoConfig의 부모 클래스인 AbstractMongoClientConfiguration을 찾아보았다.

필자는 AbstractMongoClientConfiguration에서 MongoClient가 실제 커넥션에 참여하고 있다는 것을 확인하였다.
그런데, MongoClient는 Bean으로 등록되어 있지 않다.
application.yml에 적으면 auto configuration이 되는 줄 알고 있었지만, 이 개념이 틀릴 수도 있다고 생각하여 MongoConfig 내에 MongoClient를 Bean으로 등록해주었다.
결과는 성공이었다.
따라서, MongoClient를 Bean으로 등록한 이유는 위와 같다.
그러나, 아직 해결하지 못하거나 궁금한 부분이 남아있다.
MongoClient 이 외에도 MongoClientSettings와 MongoCredentials 등 정확한 환경변수와 매핑 과정을 거치는 경로에 대한 의문이 존재한다.
해당 부분은 추가 포스팅으로 작성할 계획이다.
필자가 위에서 서술한 내용은 단순히 '연결'만을 위해 작성된 코드이다.
적용 과정과 트러블 슈팅을 겪으면서 개선 방안을 생각해보았을 때 MongoDB 적용 시 다음과 같은 내용을 보완할 수 있을 것 같다.
📌 유저 권한 부여 및 관련 명령어
- MongoDB 내 유저 생성
# 유저 생성 $ db.createUser({ user: "test", pwd: "testpw", roles: [{ role: "readWrite", db: "mydatabase"}]})
- MongoDB 내 유저 권한 부여
$ db.grantRolesToUser("test", [{ role: "readWrite", db: "mydatabase" }])
- 유저 비밀번호 변경
# admin 계정 접속 후 $ db.changeUserPassword("test", "newtestpw")
uri에 host, port, credentials를 적용할 수 없다고 설명되어 있다.@Value 어노테이션을 이용하거나, MongoClient 생성 방식을 제대로 탐구하여 환경변수 적용 방식을 개선해나갈 수 있을 것이라 생각된다. 기회가 된다면 해당 부분도 차후 포스팅으로 작성할 예정이다.📌 참고자료
MongoDB Replica Set
MongoDB Transaction
Spring Data MongoDB 공식문서
MongoProperties Github