Spring Boot + MongoDB ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ์ฒ˜๋ฆฌ: Bulk Insert ์„ฑ๋Šฅ ์ตœ์ ํ™” (ft. bson4jackson)

Spring Data MongoDB๋Š” ๋ณต์žกํ•œ ๋“œ๋ผ์ด๋ฒ„ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ถ”์ƒํ™”๋ฅผ ์ œ๊ณตํ•จ์œผ๋กœ์จ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ํŽธ๋ฆฌํ•จ์„ ์ค€๋‹ค. ํ•˜์ง€๋งŒ ์„œ๋น„์Šค๊ฐ€ ์„ฑ์žฅํ•˜๊ณ  ํŠธ๋ž˜ํ”ฝ์ด ๋ชฐ๋ฆฌ๊ธฐ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜, ํ˜น์€ ์ˆ˜์‹ญ๋งŒ ๊ฑด์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ์ด๋Ÿฌํ•œ ํŽธ๋ฆฌํ•จ์€ ์„ฑ๋Šฅ์ ์ธ ๋ณ‘๋ชฉ์„ ๊ฐ€์ ธ์˜ค๊ธฐ๋„ ํ•œ๋‹ค. ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ๋Œ€๋Ÿ‰์˜ Bulk Insert์‹œ์— bson4jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ RawBsonDocument๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ๋Œ์–ด์˜ฌ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•˜๊ณ ์ž ํ•œ๋‹ค.

Spring Data MongoDB ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋ฐฉ์‹

ํ”ํžˆ๋“ค Spring Data MongoDB๊ฐ€ Java ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค์— DB์— ๋„ฃ๋Š”๋‹ค๊ณ  ์˜คํ•ดํ•˜๊ณค ํ•œ๋‹ค. ๋‚˜๋„ ์–ผ๋งˆ์ „๊นŒ์ง€๊ทธ๋ ‡๊ฒŒ ์ƒ๊ฐํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ํŒฉํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

Spring Data์˜ MappingMongoConverter๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ณผ์ •์„ ๊ฑฐ์นœ๋‹ค.

  1. Java POJO (DTO ๊ฐ์ฒด)
  2. Document (java Map ๊ตฌ์กฐ์˜ org.bson.Document)
  3. BSON (Binary JSON, Driver๊ฐ€ ์ธ์ฝ”๋”ฉ ํ•œ๋‹ค.)

ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์˜ JSON ๋ณ€ํ™˜์€ ์—†์ง€๋งŒ Document๋ผ๋Š” ์ค‘๊ฐ„ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ •์€ ์กด์žฌํ•œ๋‹ค. ๋ฌธ์ œ๋Š” ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ด ์ค‘๊ฐ„ ๊ฐ์ฒด ์ƒ์„ฑ ๋น„์šฉ๊ณผ GC ๋ถ€ํ•˜๊ฐ€ ์ƒ๊ฐ๋ณด๋‹ค ์—„์ฒญ๋‚˜๋‹ค๋Š” ์ ์ด๋‹ค.
Document ํ•˜๋‚˜๋ฅผ ์ €์žฅํ•  ๋•Œ ์ƒ์„ฑ๋˜๋Š” ๊ฐ์ฒด๋ฅผ ๊ฐ„๋‹จํžˆ ๊ณ„์‚ฐํ•ด ๋ณด์ž.

  1. Document ๊ฐ์ฒด 1๊ฐœ
  2. Map ๋‚ด๋ถ€ ํ•„๋“œ ๊ฐ์ฒด๋“ค (ํ•„๋“œ ์ˆ˜๋งŒํผ N๊ฐœ)
  3. Key ๊ฐ์ฒด: ํ•„๋“œ๋ช… “name”, “price” … ๊ณผ ๊ฐ™์€ String ๊ฐ์ฒด (N๊ฐœ)
  4. Value ๊ฐ์ฒด: ๊ฐ’ “product-1”, 100.0 .. ๊ฐ๊ฐ ๊ฐ์ฒด (N๊ฐœ)

Document ํ•˜๋‚˜๋‹น ์ตœ์†Œ 2N + 1๊ฐœ ์ด์ƒ์˜ ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค. (ex. ํ•„๋“œ๊ฐ€ 10๊ฐœ๋ฉด 20 ~30๊ฐœ์˜ ์ž์ž˜ํ•œ ๊ฐ์ฒด ์ƒ์„ฑ)
Value ๊ฐ์ฒด์˜ ๊ฐ’์ด ๋‹จ์ผ ๊ฐ’์ด ์•„๋‹Œ ํด๋ž˜์Šค๋ผ๊ณ  ํ•˜๋ฉด ๋” ๋งŽ์ด ๋Š˜์–ด๋‚  ๊ฒƒ์ด๋‹ค.
์ด๋Ÿฌํ•œ Document๋ฅผ 10๋งŒ๊ฐœ bulk๋กœ ์ €์žฅํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์ž. ์ƒ์„ฑ๋˜๋Š” ๊ฐ์ฒด์˜ ์ˆ˜๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ์ƒ๊ฐํ–ˆ๋˜ ๊ฒƒ ์ด์ƒ์œผ๋กœ ์ƒ๋‹นํžˆ ๋งŽ์ด ์ƒ์„ฑ๋˜๊ณ  GC๊ฐ€ ์ผ์–ด๋‚  ๋•Œ ๊ทธ๋งŒํผ ๋ถ€ํ•˜๋„ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค. ์ด๋Š” ๊ณง ์„ฑ๋Šฅ ์ €ํ•˜๋กœ ์ด์–ด์ง„๋‹ค.

๋ณ‘๋ชฉ์˜ ์›์ธ: ๋ฌด๊ฑฐ์šด ์ค‘๊ฐ„ ๋‹ค๋ฆฌ

์ผ๋ฐ˜์ ์ธ ํŠธ๋ž˜ํ”ฝ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†๊ฒ ์ง€๋งŒ ์œ„์—์„œ ์‚ดํŽด๋ณธ ๋ฐ”์™€ ๊ฐ™์ด 10๋งŒ๊ฑด, 100๋งŒ๊ฑด์„ Bulk Insert ํ•˜๋Š” ์ƒํ™ฉ์ด๋ผ๋ฉด ์–˜๊ธฐ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค.

  • DOM ๋ฐฉ์‹์˜ ํ•œ๊ณ„: Spring Data์˜ ๋ฐฉ์‹์€ XML์˜ DOM ํŒŒ์„œ์ฒ˜๋Ÿผ ์ „์ฒด ๊ฐ์ฒด ๊ตฌ์กฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— Map ํ˜•ํƒœ๋กœ ํŽผ์ณ ๋†“๋Š”๋‹ค.
  • CPU & ๋ฉ”๋ชจ๋ฆฌ ๋‚ญ๋น„: ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ DB๋กœ ์˜ฎ๊ธฐ๊ณ  ์‹ถ์€๋ฐ ์ค‘๊ฐ„์— ๊ฑฐ๋Œ€ํ•œ ๊ฐ์ฒด๋“ค์„ ํž™ ๋ฉ”๋ชจ๋ฆฌ์— ๋งŒ๋“ค์—ˆ๋‹ค ๋ถ€์ˆ˜๋Š” ์ž‘์—…์„ ๋ฐ˜๋ณตํ•œ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๊ฒƒ์€ POJO๋ฅผ ๋ฐ”๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ(BSON)์œผ๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ DB์— ๋„ฃ๋Š” ๊ฒƒ์ด๋‹ค.

ํ•ด๊ฒฐ์ฑ…: Jackson๊ณผ RawBsonDocument์˜ ๋งŒ๋‚จ (ft. bson4jackson)

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ŠคํŠธ๋ฆฌ๋ฐ ์ง๋ ฌํ™”์™€ MongoDB ๋“œ๋ผ์ด๋ฒ„์˜ RawBsonDocument๋ฅผ ์กฐํ•ฉํ•˜๋Š” ์ „๋žต์„ ์‚ฌ์šฉํ•˜์—ฌ ์ค‘๊ฐ„์— Document๋กœ ๋ณ€ํ™˜ ํ•˜๋Š” ๊ณผ์ •์„ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๋‹ค.

  • Spring Data Converter ์šฐํšŒ: ๋ฌด๊ฑฐ์šด Spring Converter ๋Œ€์‹  bson4jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ•จ๊ป˜ ์†๋„๊ฐ€ ๋น ๋ฅธ Jackson์„ ์‚ฌ์šฉํ•œ๋‹ค.
  • RawBsonDocument: ์ด๋ฏธ ๋ฐ”์ดํŠธ๋กœ ๋ณ€ํ™˜๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์‹ธ๋Š” ๋ž˜ํผ ํด๋ž˜์Šค๋‹ค. ๋“œ๋ผ์ด๋ฒ„๋Š” ์ด ๊ฐ์ฒด๋ฅผ ๋ฐ›์œผ๋ฉด ์žฌํ•ด์„(Parsing) ์—†์ด ๊ทธ๋Œ€๋กœ ์ „์†กํ•œ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ์ €์žฅํ•˜๊ณ ์ž ํ•˜๋Š” POJO ๊ฐ์ฒด RawBsonDocument๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

public RawBsonDocument toRawBsonDocument( Product product ) throws IOException {
  ObjectMapper bsonObjectMapper = new ObjectMapper( new BsonFactory() );
  byte[] bytes = bsonObjectMapper.writeValueAsBytes( product );
  return new RawBsonDocument( bytes );
}
Java

ObjectMapper ์ƒ์„ฑ์ž์— BsonFactory ์ธ์Šคํ„ด์Šค๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์ผ๋ฐ˜์ ์ธ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์˜ JSON์ด ์•„๋‹Œ BSON(Binary JSON) ํ˜•์‹์œผ๋กœ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ํ•œ๋‹ค. Jackson์€ ์ถ”์ƒํ™”๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์–ด์„œ ๋‚ด๋ถ€์˜ JsonFactory๋ฅผ ๊ต์ฒดํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋ฐ์ดํ„ฐ ํฌ๋ฉง์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. BsonFactory๋ฅผ ์ฃผ์ž…ํ•จ์œผ๋กœ์จ ์ด Mapper๋Š” ์ด์ œ ํ…์ŠคํŠธ๊ฐ€ ์•„๋‹Œ BSON ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ฝ์–ด๋“ค์ด๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

์œ„ ์ฝ”๋“œ๋Š” Product POJO ๊ฐ์ฒด๋ฅผ BSON bytes๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ ๋ฐ”๋กœ RawBsonDocument์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ native ํ•˜๊ฒŒ ๋ณ€ํ™˜์„ ์ˆ˜ํ–‰ํ•˜๋ฉด Spring Data MongoDB ์ฒ˜๋ฆฌ ํ”Œ๋กœ์šฐ๊ฐ€ ์ค‘๊ฐ„์—์„œ Document๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ฒ˜๋ฆฌ ์—†์ด ๋ฐ”๋กœ bson ๋ฐ์ดํ„ฐ๋กœ ์ง๋ ฌํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.
BsonFactory ํด๋ž˜์Šค๋Š” ๋‹ค์Œ์˜ ์˜์กด์„ฑ์ด ํ•„์š”ํ•˜๋‹ค.

implementation 'de.undercouch:bson4jackson:2.18.0'
Groovy

bson4jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ณ  ํฌ์ŠคํŒ…์„ ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋Š” ํ˜„์žฌ ์‹œ์ ์˜ ์ตœ์‹  ๋ฒ„์ „์€ 2.18.0์ด๋‹ค.
Bulk๋กœ ๋Œ€๋Ÿ‰์˜ POJO ๊ฐ์ฒด๋ฅผ ํ•œ๋ฒˆ์— insertํ•  ๋•Œ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

public void bulkInsert( List<Product> products ) {
    ObjectMapper bsonObjectMapper = new ObjectMapper( new BsonFactory() );
    
    // POJO -> RawBsonDocument ๋ณ€ํ™˜
    List<RawBsonDocument> rawDocs = products.stream()
            .map( product -> {
                byte[] bytes;
                try {
                    bytes = bsonObjectMapper.writeValueAsBytes( product );
                }
                catch ( JsonProcessingException e ) {
                    throw new RuntimeException( e );
                }
                return new RawBsonDocument( bytes );
            } )
            .toList();

    // Native Collection ๊บผ๋‚ด์„œ InsertMany
    MongoCollection<RawBsonDocument> mongoCollection =
            mongoTemplate.getCollection( mongoTemplate.getCollectionName( Product.class ) )
                    .withDocumentClass( RawBsonDocument.class );

    mongoCollection.insertMany( rawDocs );
}
Java

Spring Data MongoDB์˜ Repository์—๋„ Custom Repository๋ฅผ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

public interface ProductRepositoryCustom {
  void bulkInsert( List<Product> products );
}
Java
@RequiredArgsConstructor
public class ProductRepositoryCustomImpl implements ProductRepositoryCustom {
  private final MongoTemplate mongoTemplate;
  // configuration ํด๋ž˜์Šค์—์„œ ObjectMapper bsonObjectMapper = new ObjectMapper(new BsonFactory())
  // ๋กœ ์ƒ์„ฑ๋œ Bean ์ด๋‹ค.
  private final ObjectMapper bsonObjectMapper;

  @Override
  public void bulkInsert( List<Product> products ) {
      // POJO -> RawBsonDocument ๋ณ€ํ™˜
      List<RawBsonDocument> rawDocs = products.stream()
              .map( product -> {
                  byte[] bytes;
                  try {
                      bytes = bsonObjectMapper.writeValueAsBytes( product );
                  }
                  catch ( JsonProcessingException e ) {
                      throw new RuntimeException( e );
                  }
                  return new RawBsonDocument( bytes );
              } )
              .toList();

      // Native Collection ๊บผ๋‚ด์„œ InsertMany
      MongoCollection<RawBsonDocument> mongoCollection =
              mongoTemplate.getCollection( mongoTemplate.getCollectionName( Product.class ) )
                      .withDocumentClass( RawBsonDocument.class );

      mongoCollection.insertMany( rawDocs );
  }
}
Java
// productRepository.bulkInsert(...) ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
public interface ProductRepository extends MongoRepository<Product, String>, ProductRepositoryCustom {
}
Java

์„ฑ๋Šฅ ๋น„๊ต

bson4jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ RawBsonDocument๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ค‘๊ฐ„ ๋ณ€ํ™˜ ๊ณผ์ •์—†์ด ๋ฐ”๋กœ BSON์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์€ ์„ฑ๋Šฅ์ ์ธ ์ด์ ๋„ ์žˆ์ง€๋งŒ ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ, GC ๋ถ€ํ•˜ ๊ฐ์†Œ์™€ ๊ฐ™์€ ํšจ๊ณผ๋“ค์ด ์žˆ๋‹ค.
์ •๋ง๋กœ ์„ฑ๋Šฅ์ ์ธ ์ด์ ์ด ์žˆ๋Š”์ง€ 10๋งŒ๊ฑด์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” 4๊ฐ€์ง€ ์ผ€์ด์Šค์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ ํ™•์ธํ•ด ๋ณด์ž.

  1. 10๋งŒ๋ฒˆ Looping ํ•˜๋ฉด์„œ Repository save ํ˜ธ์ถœ
  2. Repository saveAll ํ˜ธ์ถœ (Spring Data ๊ธฐ๋ณธ Bulk ์ €์žฅ ๋ชจ๋“œ)
  3. MongoTemplate์˜ bulkOps ํ˜ธ์ถœ (Spring Data ์ตœ์ ํ™” ๊ธฐ๋Šฅ)
  4. bson4jackson + RawBsonDocument ์กฐํ•ฉ (Native ํ˜ธ์ถœ)

MongoDB๋ฅผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ณ„๋„๋กœ mongodb๋ฅผ ์„ค์น˜ํ•  ํ•„์š” ์—†์ด Spring Boot Docker Compose Support๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽธ๋ฆฌํ•˜๊ฒŒ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ์•„๋ž˜ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•˜๋ฉด ๋„์›€์ด ๋  ๊ฒƒ ๊ฐ™๋‹ค.
spring boot docker compose support ๋กœ์ปฌ์—์„œ ์ธํ”„๋ผ ์˜ฌ๋ ค์„œ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ (spring boot 3.1)

ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉํ•  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

@Document( collection = "products" )
@NoArgsConstructor
@Getter
@Setter
public class Product {
    @Id
    private String id;
    private String name;
    private String category;
    private double price;
    private Map<String, String> attributes;
    private LocalDateTime createdAt;

    // ์ƒ์„ฑ์ž, Getter, Setter ์ƒ๋žต (Lombok @Data ์‚ฌ์šฉ ๊ถŒ์žฅ)
    public Product(String name, double price) {
        this.name = name;
        this.price = price;
        this.category = "Test-Category";
        this.attributes = Map.of("color", "red", "size", "L");
        this.createdAt = LocalDateTime.now();
    }
}
Java

ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰์ „์— ์‹คํ–‰๋˜๋Š” ์ดˆ๊ธฐํ™” ์ฒ˜๋ฆฌ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

private List<Product> testData;
private static final int DATA_SIZE = 100_000; 
    
@BeforeEach
void setup() {
    // DB ์ดˆ๊ธฐํ™”
    mongoTemplate.dropCollection( Product.class );

    // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (๋ฉ”๋ชจ๋ฆฌ์— ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜์—ฌ ์ธก์ • ์˜ค์ฐจ ์ œ๊ฑฐ)
    System.out.println( "Generating " + DATA_SIZE + " objects..." );
    testData = IntStream.range( 0, DATA_SIZE )
            .mapToObj( i -> new Product( "Product-" + i, i * 1.5 ) )
            .collect( Collectors.toList() );
}
Java

๊ฒฐ๊ณผ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ์ฝ”๋“œ๋‹ค.

private void printResult( String method, long millis ) {
    System.out.printf( "--> [%-25s] : %5d ms (%.2f seconds)%n", method, millis, millis / 1000.0 );
}
Java

10๋งŒ๋ฒˆ Looping ํ•˜๋ฉด์„œ Repository save ํ˜ธ์ถœ

@Test
@DisplayName( "1. [Worst] Loop Save: ๋ฐ˜๋ณต๋ฌธ์œผ๋กœ ํ•˜๋‚˜์”ฉ ์ €์žฅ" )
void testLoopSave() {
    StopWatch sw = new StopWatch();
    sw.start();

    for ( Product product : testData ) {
        repository.save( product );
    }

    sw.stop();
    printResult( "Loop Save", sw.getTotalTimeMillis() );
}
Java

๊ฒฐ๊ณผ (์•ฝ 48์ดˆ)

--> [Loop Save                ] : 47959 ms (47.96 seconds)
Plaintext

์‚ฌ์‹ค repository.save()๋ฅผ 10๋งŒ๋ฒˆ ๋ฐ˜๋ณต ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์€ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜๋„ ์žˆ๊ฒ ์ง€๋งŒ mongodb ์„œ๋ฒ„์™€์˜ ํ†ต์‹  ์‹œ๊ฐ„๋„ 10๋งŒ๋ฒˆ์„ ์ˆ˜ํ–‰ํ•˜๋‹ˆ๊นŒ ๊ทธ ์‹œ๊ฐ„์˜ ๋น„์ค‘์ด ์ƒ๋‹นํ•  ๊ฒƒ์ด๋ผ๊ณ  ๋ณด์—ฌ์ง„๋‹ค.

Repository saveAll ํ˜ธ์ถœ (Spring Data ๊ธฐ๋ณธ Bulk ์ €์žฅ ๋ชจ๋“œ)

@Test
@DisplayName( "2. [Normal] Repository SaveAll: Spring Data ๊ธฐ๋ณธ Bulk" )
void testRepositorySaveAll() {
    StopWatch sw = new StopWatch();
    sw.start();

    repository.saveAll( testData );

    sw.stop();
    printResult( "Repository.saveAll()", sw.getTotalTimeMillis() );
}
Java

๊ฒฐ๊ณผ

--> [Repository.saveAll()     ] :  1602 ms (1.60 seconds)
Plaintext

MongoTemplate์˜ bulkOps ํ˜ธ์ถœ (Spring Data ์ตœ์ ํ™” ๊ธฐ๋Šฅ)

@Test
@DisplayName( "3. [Better] BulkOperations: Spring Data ์ตœ์ ํ™” ๊ธฐ๋Šฅ" )
void testBulkOperations() {
    StopWatch sw = new StopWatch();
    sw.start();

    mongoTemplate.bulkOps( BulkOperations.BulkMode.UNORDERED, Product.class )
            .insert( testData )
            .execute();

    sw.stop();
    printResult( "BulkOperations", sw.getTotalTimeMillis() );
}
Java

๊ฒฐ๊ณผ

--> [BulkOperations           ] :  1600 ms (1.60 seconds)
Plaintext

Repository.saveAll() ๊ณผ ๋น„๊ตํ–ˆ์„ ๋•Œ ๊ฑฐ์˜ ์ฐจ์ด๊ฐ€ ์—†๋‹ค.

bson4jackson + RawBsonDocument ์กฐํ•ฉ (Native ํ˜ธ์ถœ)

@Test
@DisplayName( "4. [Best] RawBsonDocument: Jackson ์ง๋ ฌํ™” + Native Driver" )
void testRawBsonDocument() throws Exception {
    StopWatch sw = new StopWatch();
    sw.start();

    // 1. Jackson์„ ์ด์šฉํ•ด POJO -> RawBsonDocument ๋ณ€ํ™˜ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ)
    List<RawBsonDocument> rawDocs = new ArrayList<>( DATA_SIZE );
    for ( Product product : testData ) {
        // ID ์ž๋™ ์ƒ์„ฑ ๋กœ์ง์ด ์—†์œผ๋ฏ€๋กœ ํ•„์š”ํ•˜๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ ํ• ๋‹นํ•˜๊ฑฐ๋‚˜ DB์— ๋งก๊น€
        // ์—ฌ๊ธฐ์„œ๋Š” ์„ฑ๋Šฅ ์ธก์ •์„ ์œ„ํ•ด ์ˆœ์ˆ˜ ๋ณ€ํ™˜ ๋น„์šฉ๋งŒ ํฌํ•จ
        byte[] bytes = bsonObjectMapper.writeValueAsBytes( product );
        rawDocs.add( new RawBsonDocument( bytes ) );
    }

    // 2. Native Collection ๊บผ๋‚ด๊ธฐ (RawBsonDocument ํƒ€์ž…์œผ๋กœ)
    MongoCollection<RawBsonDocument> collection = 
              mongoTemplate.getCollection( mongoTemplate.getCollectionName( Product.class ) )
                          .withDocumentClass( RawBsonDocument.class );

    // 3. Driver ๋ ˆ๋ฒจ insertMany ์‹คํ–‰
    collection.insertMany( rawDocs );

    sw.stop();
    printResult( "RawBsonDocument + Jackson", sw.getTotalTimeMillis() );
}
Java

๊ฒฐ๊ณผ

--> [RawBsonDocument + Jackson] :   702 ms (0.70 seconds)
Plaintext

saveAll๊ณผ bulkOps ํ˜ธ์ถœ๊ณผ ๋น„๊ตํ–ˆ์„ ๋•Œ 2๋ฐฐ ์ •๋„ ์ฐจ์ด๊ฐ€ ๋‚œ๋‹ค. 700ms ์‹œ๊ฐ„์€ ์ผ์ƒ์—์„œ๋Š” ์ฐฐ๋‚˜์˜ ์‹œ๊ฐ„์ด๊ฒ ์ง€๋งŒ ์ปดํ“จํŒ… ์„ธ๊ณ„์—์„œ๋Š” ์—„์ฒญ๋‚œ ์‹œ๊ฐ„์ด๋‹ค. ์—”ํ‹ฐํ‹ฐ ๊ตฌ์„ฑ์ด ๋ณต์žกํ•˜๋ฉด ๋ณต์žกํ•  ์ˆ˜๋ก ์ด ์‹œ๊ฐ„์˜ ์ฐจ์ด๋Š” ๋” ๋ฒŒ์–ด์งˆ ๊ฒƒ์ด๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ์ƒ์„ฑ๋˜๋Š” ๊ฐ์ฒด์˜ ์ˆ˜๊ฐ€ ํ›จ์”ฌ ์ ์–ด์„œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฉด์—์„œ, GC ์ธก๋ฉด์—์„œ๋„ ํ›จ์”ฌ ์œ ๋ฆฌํ•˜๋‹ค.


๋ชจ๋“  ์ฝ”๋“œ๋ฅผ bson4jackson๊ณผ RawBsonDocument๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ๋กœ ๋ฐ”๊ฟ€ ํ•„์š”๋Š” ์—†๋‹ค. ์ƒํ™ฉ์— ๋งž์ถฐ ์ ์ ˆํ•œ ๋ฐฉ์‹์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.

  1. ์ผ๋ฐ˜์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง (๋‹จ๊ฑด / ์†Œ๋Ÿ‰)
    • ์ƒ์‚ฐ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ๋„ ์ค‘์š”ํ•œ ๋งŒํผ ๊ทธ๋ƒฅ Repository๋ฅผ ์“ฐ๋Š”๊ฒŒ ํŽธํ•˜๋‹ค.
    • Spring Data๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์ข‹์€ ๊ธฐ๋Šฅ์„ ํฌ๊ธฐํ•  ์ด์œ ๊ฐ€ ์—†๋‹ค.
  2. ์ ๋‹นํ•œ ๋ฐฐ์น˜ ์ž‘์—…
    • MongoTemplate์˜ bulkOps๋‚˜ Repository์˜ saveAll์„ ์จ๋„ ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™๋‹ค.
  3. ๊ทนํ•œ์˜ ๋Œ€์šฉ๋Ÿ‰ ์ฒ˜๋ฆฌ / ๋กœ๊ทธ ์ˆ˜์ง‘ / ์ดˆ๋Œ€ํ˜• ๋ฐฐ์น˜
    • ์ด ๋•Œ RawBsonDocument๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์ž.
    • Spring Data ์ถ”์ƒํ™” ๋Œ€์‹ ์— nativeํ•˜๊ฒŒ ๊ฐ€์•ผํ•  ๋•Œ๋‹ค.

์ฐธ๊ณ ๊ธ€

https://www.baeldung.com/java-jackson-mongodb-pojo-mapping