IT/MongoDB

MongoDB 실무 사용법 + 자바 연동 예제

어느 개발자의 블로그 2025. 11. 11. 14:55
반응형

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 스키마 설계의 원칙

  1. 애플리케이션 중심 설계: 먼저 어떻게 데이터를 읽을 것인지부터 생각
  2. 접근 패턴 분석: 자주 함께 조회되는 데이터는 임베디드 구조
  3. 중복의 용인과 관리: 성능과 중복을 균형 있게 결정
  4. 문서 크기 관리: 문서가 너무 크면 성능 저하 (최대 16MB)
  5. 스키마 검증: 필요시 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()으로 확인
    }
}
  

인덱싱 체크리스트:

  1. 자주 조회되는 필드부터 인덱싱
  2. 범위 쿼리가 있으면 해당 필드 인덱싱
  3. 정렬이 필요한 필드는 정렬 방향과 인덱스 방향을 맞추기
  4. Write 작업이 많으면 인덱스 수를 과도하게 늘리지 않기
  5. 인덱스 크기와 메모리 사용량 모니터링

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 성능 최적화 체크리스트

  1. CPU, 메모리, I/O 활용률 등 성능 모니터링
  2. 인덱스 구조 리뷰: 불필요한 인덱스 제거, 필요한 인덱스 추가
  3. 쿼리 단순화: Aggregation 단계 최소화, $lookup 남용 금지
  4. 하드웨어 자원(CPU, RAM, 디스크) 점검
  5. 데이터가 커지면 샤딩 등 분산 구조 설계 고려

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는 빠른 프로토타이핑과 유연한 데이터 구조가 필요한 현대적 애플리케이션에 이상적입니다. 하지만 스키마 설계, 인덱싱, 쿼리 최적화를 처음부터 고려해야 안정적인 운영이 가능합니다.

개발 로드맵:

  1. 프로젝트 초기: 데이터 모델링, 스키마 설계
  2. 개발 중: 기본 CRUD, Repository 패턴 구현
  3. 성능 개선: 인덱싱, 쿼리 최적화
  4. 프로덕션: 모니터링, 백업, 트랜잭션 설정
  5. 운영: 정기적인 성능 분석 및 개선

이 접근 방식을 따르면 MongoDB의 장점을 최대한 활용하면서도 안정적인 애플리케이션을 구축할 수 있습니다.

반응형