Golang MongoDB Driver 가이드

beluga000·2025년 11월 6일

Go MongoDB Driver 가이드

Golang에서 MongoDB를 제어하거나 연결할 때 사용할 수 있는 드라이버에는 여러 종류가 있습니다.

공식적이며 가장 권장되는 표준 드라이버인 mongo-drvier(go.mongodb.org/mongo-driver), 과거에 표준 처럼 사용 되었던 mgo(github.com/globalsign/mgo), 공식 드라이버를 기반으로 한 ORM 스타일 래퍼 mgm(github.com/kamva/mgm/v3) 등이 있습니다.

mgo의 경우에는 문법이 간단하고 사용하기 편했지만 유지보수가 종료됨에 따라 최신 MongoDB 버전과 호환성이 맞지 않아 이번 내용에서 제외하고, 이 가이드는 Go에서 공식 MongoDB 드라이버(go.mongodb.org/mongo-driver)를 사용하여 MongoDB에 접근하고 쿼리하는 방법을 설명합니다.

설치 및 설정

1. MongoDB Driver 설치

go mod init your-project
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/bson

2. 필요한 패키지 import

import (
    "context"
    "log"
    "time"
    
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

연결 설정

MongoDB 연결 초기화

package inits

import (
    "context"
    "fmt"
    "time"
    
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

var MongoClient *mongo.Client
var MongoDb *mongo.Database

func MongoInit(uri string, dbName string) error {
    // 컨텍스트 설정 (타임아웃 10초)
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    // 클라이언트 옵션 설정
    clientOptions := options.Client().ApplyURI(uri)
    
    // MongoDB에 연결
    client, err := mongo.Connect(ctx, clientOptions)
    if err != nil {
        return fmt.Errorf("MongoDB 연결 실패: %v", err)
    }
    
    // 연결 테스트
    err = client.Ping(ctx, nil)
    if err != nil {
        return fmt.Errorf("MongoDB Ping 실패: %v", err)
    }
    
    MongoClient = client
    MongoDb = client.Database(dbName)
    
    fmt.Println("MongoDB 연결 성공")
    return nil
}

func MongoClose() error {
    if MongoClient != nil {
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        return MongoClient.Disconnect(ctx)
    }
    return nil
}

연결 문자열 예시

// 로컬 MongoDB
uri := "mongodb://localhost:27017"

// MongoDB Atlas (클라우드)
uri := "mongodb+srv://username:password@cluster.mongodb.net/"

// 인증이 필요한 로컬 MongoDB
uri := "mongodb://username:password@localhost:27017/database"

기본 CRUD 작업

BaseModel 구조체 정의

package models

import (
    "time"
    "go.mongodb.org/mongo-driver/bson/primitive"
)


type BaseModel struct {
	// ObjectId
    ID          primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"`
    // soft/hard delete 처리
    Able        bool               `json:"able" bson:"able"`
    // 데이터 생성시간
    CreatedTime time.Time          `json:"createdTime" bson:"createdTime"`
    // 데이터 수정시간
    UpdatedTime time.Time          `json:"updatedTime" bson:"updatedTime"`
}

// MongoModel 인터페이스
type MongoModel interface {
    CollectionName() string
}

// 예시: User 모델
type User struct {
    BaseModel `bson:",inline"`
    Name      string `json:"name" bson:"name"`
    Email     string `json:"email" bson:"email"`
    Age       int    `json:"age" bson:"age"`
}

func (u User) CollectionName() string {
    return "users"
}

Create (생성)

func Create(model MongoModel, doc interface{}) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection(model.CollectionName())
    
    // BaseModel 필드 자동 설정
    if baseDoc, ok := doc.(*BaseModel); ok {
        now := time.Now()
        baseDoc.CreatedTime = now
        baseDoc.UpdatedTime = now
        baseDoc.Able = true
    }
    
    result, err := collection.InsertOne(ctx, doc)
    if err != nil {
        return fmt.Errorf("문서 생성 실패: %v", err)
    }
    
    fmt.Printf("문서 생성 성공. ID: %v\n", result.InsertedID)
    return nil
}

// 사용 예시
func CreateUser() {
    user := User{
        Name:  "홍길동",
        Email: "hong@example.com",
        Age:   30,
    }
    
    err := Create(&user, &user)
    if err != nil {
        log.Printf("사용자 생성 실패: %v", err)
    }
}

Read (조회)

단일 문서 조회 -> FindOne()

func FindById(model MongoModel, id string, result interface{}) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    objectId, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return fmt.Errorf("잘못된 ObjectID: %v", err)
    }
    
    collection := MongoDb.Collection(model.CollectionName())
    err = collection.FindOne(ctx, bson.M{"_id": objectId}).Decode(result)
    if err != nil {
        if err == mongo.ErrNoDocuments {
            return fmt.Errorf("문서를 찾을 수 없음")
        }
        return fmt.Errorf("문서 조회 실패: %v", err)
    }
    
    return nil
}

// 조건으로 단일 문서 조회
func FindOne(model MongoModel, filter bson.M, result interface{}) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection(model.CollectionName())
    err := collection.FindOne(ctx, filter).Decode(result)
    if err != nil {
        return fmt.Errorf("문서 조회 실패: %v", err)
    }
    
    return nil
}

다중 문서 조회 -> Find()

func FindMany(model MongoModel, filter bson.M, results interface{}) error {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection(model.CollectionName())
    cursor, err := collection.Find(ctx, filter)
    if err != nil {
        return fmt.Errorf("문서 조회 실패: %v", err)
    }
    defer cursor.Close(ctx)
    
    err = cursor.All(ctx, results)
    if err != nil {
        return fmt.Errorf("문서 디코딩 실패: %v", err)
    }
    
    return nil
}

// 페이지네이션과 정렬
func FindWithOptions(model MongoModel, filter bson.M, opts *options.FindOptions, results interface{}) error {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection(model.CollectionName())
    cursor, err := collection.Find(ctx, filter, opts)
    if err != nil {
        return fmt.Errorf("문서 조회 실패: %v", err)
    }
    defer cursor.Close(ctx)
    
    return cursor.All(ctx, results)
}

// 사용 예시
func GetUsersPaginated(page, limit int64) ([]User, error) {
    var users []User
    
    // 정렬 및 페이지네이션 옵션
    opts := options.Find().
        SetSort(bson.D{{"createdTime", -1}}).  // 생성시간 내림차순
        SetLimit(limit).
        SetSkip((page - 1) * limit)
    
    filter := bson.M{"able": true}  // 활성화된 사용자만
    
    err := FindWithOptions(&User{}, filter, opts, &users)
    return users, err
}

Update (수정)

func UpdateById(model MongoModel, id primitive.ObjectID, update bson.M) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    // 업데이트 시간 자동 설정
    update["updatedTime"] = time.Now()
    
    collection := MongoDb.Collection(model.CollectionName())
    result, err := collection.UpdateOne(
        ctx,
        bson.M{"_id": id},
        bson.M{"$set": update},
    )
    
    if err != nil {
        return fmt.Errorf("문서 업데이트 실패: %v", err)
    }
    
    if result.MatchedCount == 0 {
        return fmt.Errorf("업데이트할 문서를 찾을 수 없음")
    }
    
    fmt.Printf("문서 업데이트 성공. 수정된 문서 수: %d\n", result.ModifiedCount)
    return nil
}

// 조건으로 여러 문서 업데이트
func UpdateMany(model MongoModel, filter bson.M, update bson.M) error {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    update["updatedTime"] = time.Now()
    
    collection := MongoDb.Collection(model.CollectionName())
    result, err := collection.UpdateMany(
        ctx,
        filter,
        bson.M{"$set": update},
    )
    
    if err != nil {
        return fmt.Errorf("문서 업데이트 실패: %v", err)
    }
    
    fmt.Printf("업데이트 완료. 매칭: %d, 수정: %d\n", result.MatchedCount, result.ModifiedCount)
    return nil
}

Delete (삭제)

// 소프트 삭제 (able 필드를 false로 변경)
func SoftDeleteById(model MongoModel, id primitive.ObjectID) error {
    return UpdateById(model, id, bson.M{"able": false})
}

// 하드 삭제 (실제 문서 삭제)
func DeleteById(model MongoModel, id primitive.ObjectID) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection(model.CollectionName())
    result, err := collection.DeleteOne(ctx, bson.M{"_id": id})
    if err != nil {
        return fmt.Errorf("문서 삭제 실패: %v", err)
    }
    
    if result.DeletedCount == 0 {
        return fmt.Errorf("삭제할 문서를 찾을 수 없음")
    }
    
    return nil
}

// 조건으로 여러 문서 삭제
func DeleteMany(model MongoModel, filter bson.M) error {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection(model.CollectionName())
    result, err := collection.DeleteMany(ctx, filter)
    if err != nil {
        return fmt.Errorf("문서 삭제 실패: %v", err)
    }
    
    fmt.Printf("삭제 완료. 삭제된 문서 수: %d\n", result.DeletedCount)
    return nil
}

고급 쿼리

복잡한 필터링

func AdvancedQuery() {
    // 나이가 18~65 사이이고 이름이 "김"으로 시작하는 사용자
    filter := bson.M{
        "age": bson.M{
            "$gte": 18,
            "$lte": 65,
        },
        "name": bson.M{
            "$regex": "^김",
        },
        "able": true,
    }
    
    var users []User
    err := FindMany(&User{}, filter, &users)
    if err != nil {
        log.Printf("쿼리 실행 실패: %v", err)
    }
}

// OR 조건
func OrQuery() {
    filter := bson.M{
        "$or": []bson.M{
            {"age": bson.M{"$lt": 20}},
            {"age": bson.M{"$gt": 60}},
        },
    }
    
    var users []User
    FindMany(&User{}, filter, &users)
}

// 배열 필드 쿼리
func ArrayQuery() {
    // tags 배열에 "golang"이 포함된 문서
    filter := bson.M{
        "tags": bson.M{"$in": []string{"golang"}},
    }
    
    // tags 배열의 모든 요소가 조건을 만족하는 문서
    filter2 := bson.M{
        "tags": bson.M{"$all": []string{"golang", "mongodb"}},
    }
}

텍스트 검색

func TextSearch(searchTerm string) ([]User, error) {
    var users []User
    
    // 텍스트 인덱스가 설정되어 있어야 함
    filter := bson.M{
        "$text": bson.M{
            "$search": searchTerm,
        },
    }
    
    // 관련도 점수로 정렬
    opts := options.Find().SetSort(bson.D{{"score", bson.D{{"$meta", "textScore"}}}})
    
    err := FindWithOptions(&User{}, filter, opts, &users)
    return users, err
}

집계(Aggregation)

기본 집계 파이프라인

func AggregateUsers() error {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection("users")
    
    // 집계 파이프라인
    pipeline := []bson.M{
        {
            "$match": bson.M{
                "able": true,
            },
        },
        {
            "$group": bson.M{
                "_id": "$age",
                "count": bson.M{"$sum": 1},
                "avgAge": bson.M{"$avg": "$age"},
            },
        },
        {
            "$sort": bson.M{
                "count": -1,
            },
        },
    }
    
    cursor, err := collection.Aggregate(ctx, pipeline)
    if err != nil {
        return fmt.Errorf("집계 실행 실패: %v", err)
    }
    defer cursor.Close(ctx)
    
    var results []bson.M
    err = cursor.All(ctx, &results)
    if err != nil {
        return fmt.Errorf("집계 결과 디코딩 실패: %v", err)
    }
    
    for _, result := range results {
        fmt.Printf("나이: %v, 개수: %v, 평균: %v\n", 
            result["_id"], result["count"], result["avgAge"])
    }
    
    return nil
}

복잡한 집계 예시

type AgeGroup struct {
    ID       string  `bson:"_id"`
    Count    int     `bson:"count"`
    AvgAge   float64 `bson:"avgAge"`
    MinAge   int     `bson:"minAge"`
    MaxAge   int     `bson:"maxAge"`
}

func GroupUsersByAgeRange() ([]AgeGroup, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection("users")
    
    pipeline := []bson.M{
        {
            "$match": bson.M{"able": true},
        },
        {
            "$addFields": bson.M{
                "ageGroup": bson.M{
                    "$switch": bson.M{
                        "branches": []bson.M{
                            {"case": bson.M{"$lt": []interface{}{"$age", 20}}, "then": "10대"},
                            {"case": bson.M{"$lt": []interface{}{"$age", 30}}, "then": "20대"},
                            {"case": bson.M{"$lt": []interface{}{"$age", 40}}, "then": "30대"},
                            {"case": bson.M{"$lt": []interface{}{"$age", 50}}, "then": "40대"},
                        },
                        "default": "50대 이상",
                    },
                },
            },
        },
        {
            "$group": bson.M{
                "_id":    "$ageGroup",
                "count":  bson.M{"$sum": 1},
                "avgAge": bson.M{"$avg": "$age"},
                "minAge": bson.M{"$min": "$age"},
                "maxAge": bson.M{"$max": "$age"},
            },
        },
        {
            "$sort": bson.M{"avgAge": 1},
        },
    }
    
    cursor, err := collection.Aggregate(ctx, pipeline)
    if err != nil {
        return nil, fmt.Errorf("집계 실행 실패: %v", err)
    }
    defer cursor.Close(ctx)
    
    var results []AgeGroup
    err = cursor.All(ctx, &results)
    return results, err
}

인덱스 관리

인덱스 생성

func CreateIndexes() error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection("users")
    
    // 단일 필드 인덱스
    indexModel1 := mongo.IndexModel{
        Keys: bson.D{{"email", 1}}, // 1: 오름차순, -1: 내림차순
        Options: options.Index().SetUnique(true), // 유니크 인덱스
    }
    
    // 복합 인덱스
    indexModel2 := mongo.IndexModel{
        Keys: bson.D{
            {"name", 1},
            {"age", -1},
        },
    }
    
    // 텍스트 인덱스
    indexModel3 := mongo.IndexModel{
        Keys: bson.D{
            {"name", "text"},
            {"email", "text"},
        },
    }
    
    // TTL 인덱스 (시간 기반 자동 삭제)
    indexModel4 := mongo.IndexModel{
        Keys: bson.D{{"createdTime", 1}},
        Options: options.Index().SetExpireAfterSeconds(30 * 24 * 3600), // 30일 후 삭제
    }


    
    indexes := []mongo.IndexModel{indexModel1, indexModel2, indexModel3, indexModel4}
    
    result, err := collection.Indexes().CreateMany(ctx, indexes)
    if err != nil {
        return fmt.Errorf("인덱스 생성 실패: %v", err)
    }
    
    fmt.Printf("생성된 인덱스: %v\n", result)
    return nil
}

// 인덱스 조회
func ListIndexes() error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    collection := MongoDb.Collection("users")
    cursor, err := collection.Indexes().List(ctx)
    if err != nil {
        return fmt.Errorf("인덱스 조회 실패: %v", err)
    }
    defer cursor.Close(ctx)
    
    var indexes []bson.M
    err = cursor.All(ctx, &indexes)
    if err != nil {
        return fmt.Errorf("인덱스 디코딩 실패: %v", err)
    }
    
    for _, index := range indexes {
        fmt.Printf("인덱스: %+v\n", index)
    }
    
    return nil
}

TTL(수명) 인덱스의 만료 삭제는 MongoDB 서버(mongod)의 백그라운드 TTL 모니터가 수행하기 때문에, Go 프로세스가 꺼져 있어도 mongod가 실행 중이면 만료 문서는 자동으로 삭제됩니다.
[참고문서]
https://www.mongodb.com/ko-kr/docs/manual/core/index-ttl/#std-label-index-feature-ttl

profile
Developer

0개의 댓글