MongoDB 실무 사용법 + 자바 연동 예제
1. MongoDB란 무엇인가?
MongoDB는 문서 지향형 NoSQL 데이터베이스로, JSON과 유사한 BSON(Binary JSON) 형식으로 데이터를 저장합니다. 관계형 데이터베이스와는 달리 고정된 스키마가 없어 유연한 데이터 구조를 가지며, 애플리케이션의 변화에 빠르게 대응할 수 있습니다.
MongoDB의 핵심 특징:
- 문서 기반 저장: 데이터가 컬렉션(테이블)의 문서(행) 형태로 저장
- 유연한 스키마: 같은 컬렉션 내 문서들이 서로 다른 필드 구조 가능
- 고성능: 인덱싱, 샤딩을 통한 확장성
- ACID 지원: MongoDB 4.0+에서 다중 문서 트랜잭션 지원
2. MongoDB 데이터 모델링 전략
MongoDB의 핵심은 “어떻게 데이터를 저장할 것인가” 입니다. 관계형 데이터베이스의 정규화와는 다른 접근이 필요합니다.
2.1 데이터 모델의 두 가지 패턴
패턴 1: 임베디드(Embedded) 구조 - 데이터 중복 허용
한 문서에 연관 데이터를 직접 포함시키는 방식입니다. 주문과 주문 항목이 함께 저장됩니다.
{
"_id": ObjectId("..."),
"orderId": "ORD123",
"customerId": "CUST456",
"orderDate": "2025-11-11",
"items": [
{ "productId": "PROD001", "quantity": 2, "price": 50 },
{ "productId": "PROD002", "quantity": 1, "price": 100 }
],
"total": 200
}
장점: 단일 쿼리로 모든 정보 조회, 빠른 응답
단점: 데이터 중복, 문서 크기 증가
패턴 2: 참조(Reference) 구조 - 정규화
컬렉션을 분리하고 _id로 연결하는 방식입니다.
// 주문 컬렉션
{
"_id": ObjectId("AAA"),
"orderId": "ORD123",
"customerId": ObjectId("BBB"),
"items": [ObjectId("CCC"), ObjectId("DDD")]
}
// 상품 컬렉션
{
"_id": ObjectId("CCC"),
"productId": "PROD001",
"name": "상품1",
"quantity": 100,
"price": 50
}
장점: 데이터 중복 최소화, 유지보수 용이
단점: 조인 필요, 성능 저하 가능성
2.2 스키마 설계의 원칙
- 애플리케이션 중심 설계: 먼저 어떻게 데이터를 읽을 것인지부터 생각
- 접근 패턴 분석: 자주 함께 조회되는 데이터는 임베디드 구조
- 중복의 용인과 관리: 성능과 중복을 균형 있게 결정
- 문서 크기 관리: 문서가 너무 크면 성능 저하 (최대 16MB)
- 스키마 검증: 필요시 JSON Schema로 일관성 보장
실무 팁: 대부분의 프로젝트는 임베디드와 참조를 혼합해서 사용합니다. 자주 접근되고 데이터량이 적다면 임베디드, 독립적인 라이프사이클을 가진다면 참조를 사용하는 것이 좋습니다.
3. MongoDB Java 드라이버 설정
3.1 Spring Boot에서 설정
Maven 의존성 추가:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
application.yml 설정:
spring:
data:
mongodb:
uri: mongodb://localhost:27017/mydb
# 또는 개별 설정
host: localhost
port: 27017
database: mydb
username: admin
password: password
MongoDB Atlas(클라우드) 사용 예:
spring:
data:
mongodb:
uri: mongodb+srv://username:password@cluster.mongodb.net/dbname?retryWrites=true&w=majority
3.2 Connection Pool 설정
MongoDB Java 드라이버는 기본적으로 Connection Pool을 관리하며, 기본 최대 연결 수는 100개입니다.
spring:
data:
mongodb:
uri: mongodb://localhost:27017/mydb?maxPoolSize=50&maxIdleTimeMS=60000&connectTimeoutMS=5000
주요 Connection Pool 옵션:
- maxPoolSize: 최대 연결 수 (기본값: 100)
- maxIdleTimeMS: 유휴 연결 유지 시간
- connectTimeoutMS: 연결 타임아웃
- socketTimeoutMS: 소켓 타임아웃
4. Spring Data MongoDB를 활용한 CRUD 구현
4.1 도메인 모델 정의
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.time.LocalDateTime;
import java.util.List;
@Document(collection = "users")
public class User {
@Id
private String id;
@Field("name")
private String name;
private int age;
private String email;
private List<Address> addresses;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 생성자, getter, setter
public User() {}
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// getter, setter 생략
}
@Document(collection = "addresses")
class Address {
private String street;
private String city;
private String country;
private String zipCode;
// 생성자, getter, setter
}
4.2 Repository 인터페이스
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends MongoRepository<User, String> {
// 자동 생성되는 메서드
List<User> findByName(String name);
List<User> findByAgeGreaterThan(int age);
List<User> findByNameAndAge(String name, int age);
// @Query를 사용한 커스텀 쿼리
@Query("{ 'age': { $gt: ?0 } }")
List<User> findYoungerUsers(int age);
@Query("{ 'email': ?0 }")
Optional<User> findByEmail(String email);
// 정렬과 함께
@Query("{ 'age': { $gte: ?0 } }")
List<User> findAdultUsers(int minAge);
}
4.3 Service 계층 구현
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// Create
public User createUser(User user) {
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
}
// Read
public List<User> getAllUsers() {
return userRepository.findAll();
}
public Optional<User> getUserById(String id) {
return userRepository.findById(id);
}
public List<User> getUsersByName(String name) {
return userRepository.findByName(name);
}
public List<User> getUsersOlderThan(int age) {
return userRepository.findByAgeGreaterThan(age);
}
// Update
public User updateUser(String id, User userDetails) {
Optional<User> user = userRepository.findById(id);
if (user.isPresent()) {
User existingUser = user.get();
existingUser.setName(userDetails.getName());
existingUser.setAge(userDetails.getAge());
existingUser.setEmail(userDetails.getEmail());
existingUser.setUpdatedAt(LocalDateTime.now());
return userRepository.save(existingUser);
}
return null;
}
// Delete
public void deleteUser(String id) {
userRepository.deleteById(id);
}
public void deleteAllUsers() {
userRepository.deleteAll();
}
}
4.4 REST Controller 작성
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.createUser(user);
return new ResponseEntity<>(savedUser, HttpStatus.CREATED);
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return new ResponseEntity<>(users, HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable String id) {
Optional<User> user = userService.getUserById(id);
return user
.map(value -> new ResponseEntity<>(value, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
@GetMapping("/search/byName")
public ResponseEntity<List<User>> getUsersByName(@RequestParam String name) {
List<User> users = userService.getUsersByName(name);
return new ResponseEntity<>(users, HttpStatus.OK);
}
@GetMapping("/search/older")
public ResponseEntity<List<User>> getUsersOlderThan(@RequestParam int age) {
List<User> users = userService.getUsersOlderThan(age);
return new ResponseEntity<>(users, HttpStatus.OK);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable String id, @RequestBody User userDetails) {
User updatedUser = userService.updateUser(id, userDetails);
if (updatedUser != null) {
return new ResponseEntity<>(updatedUser, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable String id) {
userService.deleteUser(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
5. MongoDB 고급 쿼리 - Aggregation Framework
단순한 find() 쿼리를 넘어서, 복잡한 데이터 분석이 필요할 때 Aggregation Framework를 사용합니다.
Aggregation은 파이프라인 개념으로 데이터가 여러 단계를 거치며 변환됩니다.
5.1 기본 Aggregation 구조
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserAggregationService {
@Autowired
private MongoTemplate mongoTemplate;
// 연령대별 사용자 그룹화 및 집계
public void groupUsersByAge() {
Aggregation aggregation = Aggregation.newAggregation(
// $match: 조건에 맞는 문서 필터링
Aggregation.match(Criteria.where("age").gte(20)),
// $group: 연령대별로 그룹화
Aggregation.group("age")
.count().as("count")
.avg("age").as("avgAge")
.min("age").as("minAge")
.max("age").as("maxAge"),
// $sort: 결과 정렬
Aggregation.sort(Sort.Direction.DESC, "_id")
);
AggregationResults<AgeGroupResult> results =
mongoTemplate.aggregate(aggregation, "users", AgeGroupResult.class);
results.getMappedResults().forEach(System.out::println);
}
// 이름별 사용자 수 집계
public void countUsersByName() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.group("name")
.count().as("count")
.first("email").as("sampleEmail"),
Aggregation.sort(Sort.Direction.DESC, "count")
);
AggregationResults<Map> results =
mongoTemplate.aggregate(aggregation, "users", Map.class);
results.getMappedResults().forEach(System.out::println);
}
// $lookup을 사용한 Join
public void joinWithOrders() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.lookup("orders", "_id", "userId", "userOrders"),
Aggregation.project("name", "email", "userOrders"),
Aggregation.match(Criteria.where("userOrders").ne(new ArrayList<>()))
);
AggregationResults<Map> results =
mongoTemplate.aggregate(aggregation, "users", Map.class);
results.getMappedResults().forEach(result -> {
System.out.println("사용자: " + result.get("name"));
System.out.println("주문 수: " +
((List<?>) result.get("userOrders")).size());
});
}
}
class AgeGroupResult {
private Integer _id;
private Integer count;
private Double avgAge;
private Integer minAge;
private Integer maxAge;
// getter, setter
}
주요 Aggregation 단계:
- $match: SQL의 WHERE 절과 같은 필터링
- $group: 데이터 그룹화 및 집계 연산
- $sort: 정렬
- $project: 필드 선택/제외
- $lookup: 컬렉션 간 조인
- $unwind: 배열 요소 분해
6. MongoDB 인덱싱과 성능 최적화
6.1 인덱스의 종류와 생성
인덱스는 MongoDB 성능의 핵심입니다. 기본적으로 모든 컬렉션은 _id 필드에 인덱스가 자동 생성됩니다.
단일 필드 인덱스:
@Document(collection = "users")
@CompoundIndex(name = "email_unique", def = "{ 'email' : 1 }", unique = true)
public class User {
@Id
private String id;
@Indexed
private String email;
private String name;
// ...
}
// 또는 MongoDB CLI
// db.users.createIndex({ email: 1 })
복합 인덱스(Compound Index):
@CompoundIndex(name = "name_age_idx", def = "{ 'name' : 1, 'age': 1 }")
public class User {
// ...
}
// 또는 MongoDB CLI
// db.users.createIndex({ name: 1, age: 1 })
문자열 검색(Text Index):
db.users.createIndex({ name: "text", email: "text" });
// 쿼리
db.users.find({ $text: { $search: "john" } });
6.2 인덱스 최적화 전략
@Service
public class IndexOptimizationService {
@Autowired
private MongoTemplate mongoTemplate;
// 인덱스 생성
public void createIndices() {
// 자주 조회되는 필드
mongoTemplate.getCollection("users")
.createIndex(new Document("name", 1));
// 범위 쿼리가 많은 필드
mongoTemplate.getCollection("users")
.createIndex(new Document("age", 1));
// 정렬이 필요한 필드
mongoTemplate.getCollection("users")
.createIndex(new Document("createdAt", -1));
}
// 쿼리 실행 계획 분석 (예시)
public void analyzeQuery() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("age").gte(20))
);
// 실제 실행 계획은 MongoDB shell에서 explain()으로 확인
}
}
인덱싱 체크리스트:
- 자주 조회되는 필드부터 인덱싱
- 범위 쿼리가 있으면 해당 필드 인덱싱
- 정렬이 필요한 필드는 정렬 방향과 인덱스 방향을 맞추기
- Write 작업이 많으면 인덱스 수를 과도하게 늘리지 않기
- 인덱스 크기와 메모리 사용량 모니터링
7. MongoDB 트랜잭션 (ACID 지원)
MongoDB 4.0부터는 다중 문서 트랜잭션을 지원하여 ACID 특성을 보장합니다.
7.1 트랜잭션 개념
ACID란:
- Atomicity(원자성): 모든 작업이 성공하거나 모두 실패
- Consistency(일관성): 데이터 무결성 유지
- Isolation(고립성): 동시성 제어
- Durability(지속성): 커밋된 데이터 영구 보존
실무 예제 - 계좌이체:
@Service
public class TransactionService {
@Autowired
private MongoTemplate mongoTemplate;
@Transactional
public void transferMoney(String fromAccountId, String toAccountId,
double amount) throws Exception {
// 1. fromAccount의 잔액 확인 및 차감
Account fromAccount = mongoTemplate.findById(fromAccountId, Account.class);
if (fromAccount.getBalance() < amount) {
throw new Exception("잔액 부족");
}
fromAccount.setBalance(fromAccount.getBalance() - amount);
mongoTemplate.save(fromAccount);
// 2. toAccount 잔액 증가
Account toAccount = mongoTemplate.findById(toAccountId, Account.class);
toAccount.setBalance(toAccount.getBalance() + amount);
mongoTemplate.save(toAccount);
// 중간에 에러 발생 시 → 두 작업 모두 롤백
// 성공하면 → 두 작업 모두 커밋
}
}
@Document(collection = "accounts")
public class Account {
@Id
private String id;
private String accountNumber;
private double balance;
private LocalDateTime lastModified;
// getter, setter
}
8. 데이터 Import/Export 및 백업
8.1 JSON 형식 Export/Import
Export:
mongoexport -d mydb -c users -o users.json --jsonArray --port 27017
Import:
mongoimport -d mydb -c users --file users.json --jsonArray --port 27017
인증이 필요한 경우:
mongoexport -u username -p 'password' -d mydb -c users \
-o users.json --authenticationDatabase admin
mongoimport -u username -p 'password' -d mydb -c users \
--file users.json --authenticationDatabase admin
8.2 Binary(BSON) 형식 - 대용량 권장
Dump:
mongodump --db mydb --collection users --out ./backup/
Restore:
mongorestore --db mydb --collection users ./backup/mydb/users.bson
장점: 용량이 작고 타입 정보가 보존되며, 속도가 빠릅니다.
9. MongoDB 모니터링 및 성능 진단
9.1 기본 모니터링 명령어
# 서버 상태 확인
db.serverStatus()
# 데이터베이스 통계
db.dbStats()
# 현재 실행 중인 연산
db.currentOp()
# Collection별 read/write 속도
mongotop --host localhost --port 27017
# 쿼리 실행 통계
mongostat --host localhost --port 27017
9.2 프로파일링 설정
# 느린 쿼리 감지 (100ms 이상)
db.setProfilingLevel(1, { slowms: 100 })
# 모든 쿼리 기록
db.setProfilingLevel(2)
# 프로필 끄기
db.setProfilingLevel(0)
# 기록된 쿼리 확인
db.system.profile.find({}).limit(1).sort({ ts: -1 }).pretty()
9.3 성능 최적화 체크리스트
- CPU, 메모리, I/O 활용률 등 성능 모니터링
- 인덱스 구조 리뷰: 불필요한 인덱스 제거, 필요한 인덱스 추가
- 쿼리 단순화: Aggregation 단계 최소화,
$lookup남용 금지 - 하드웨어 자원(CPU, RAM, 디스크) 점검
- 데이터가 커지면 샤딩 등 분산 구조 설계 고려
10. 실무에서 자주 하는 실수와 해결책
실수 1: 스키마 설계 없이 시작
문제: 나중에 데이터 구조 변경이 어려워짐
해결책:
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email"],
properties: {
name: { bsonType: "string" },
email: { bsonType: "string", pattern: ".*@.*" }
}
}
}
})
실수 2: 인덱스 없이 운영
문제: 데이터 증가에 따른 성능 급락
해결책: 개발 초기부터 인덱스 계획 세우기
@CompoundIndex(def = "{ 'email': 1 }")
@CompoundIndex(def = "{ 'createdAt': -1 }")
public class User {
// ...
}
실수 3: N+1 쿼리 문제
문제 예시:
List<User> users = userRepository.findAll(); // 쿼리 1
for (User user : users) {
List<Order> orders = orderRepository.findByUserId(user.getId()); // N개 쿼리
}
해결책: Aggregation $lookup 사용
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.lookup("orders", "_id", "userId", "userOrders")
);
실수 4: 무분별한 데이터 중복
문제: 데이터 불일치 시 동기화 어려움
해결책: 변경 빈도가 높은 데이터는 참조 구조 사용, 중복 시 업데이트 전략 명확히 수립
결론
MongoDB는 빠른 프로토타이핑과 유연한 데이터 구조가 필요한 현대적 애플리케이션에 이상적입니다. 하지만 스키마 설계, 인덱싱, 쿼리 최적화를 처음부터 고려해야 안정적인 운영이 가능합니다.
개발 로드맵:
- 프로젝트 초기: 데이터 모델링, 스키마 설계
- 개발 중: 기본 CRUD, Repository 패턴 구현
- 성능 개선: 인덱싱, 쿼리 최적화
- 프로덕션: 모니터링, 백업, 트랜잭션 설정
- 운영: 정기적인 성능 분석 및 개선
이 접근 방식을 따르면 MongoDB의 장점을 최대한 활용하면서도 안정적인 애플리케이션을 구축할 수 있습니다.