Go에서 MongoDB를 사용하는 방법 | 다미


Go와 MongoDB 연동 완벽 가이드

1. 개요

이 문서는 Go 언어에서 MongoDB를 활용한 백엔드 개발 방법을 다룹니다. NoSQL 데이터베이스인 MongoDB와 Go의 공식 드라이버를 사용하여 데이터 저장, 조회, 업데이트, 삭제 기능을 구현하며, 최종적으로 Gin 웹 프레임워크를 활용한 블로그 애플리케이션을 완성합니다.

2. 사전 준비사항

필수 요구사항

  • Go 언어 기본 지식 및 개념 이해
  • Go 1.24 이상 버전 설치
  • MongoDB 설치 (로컬 27017 포트에서 실행)
  • NoSQL 데이터베이스 기본 지식

주요 패키지

go.mongodb.org/mongo-driver/mongo
go.mongodb.org/mongo-driver/bson
github.com/gin-gonic/gin

3. 프로젝트 초기 설정

새 Go 프로젝트 생성

mkdir go-mongodb-integration
cd go-mongodb-integration
go mod init go-mongodb

MongoDB 드라이버 설치

go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/bson

중요한 점: Go 표준 라이브러리의 database/sql 패키지는 SQL 데이터베이스만 지원하므로, MongoDB 작업을 위해서는 반드시 공식 MongoDB 드라이버를 사용해야 합니다.

4. MongoDB 기본 작업

4.1 데이터베이스 및 컬렉션 생성 원리

MongoDB의 “lazy creation” 접근 방식:

  • 데이터베이스는 첫 번째 문서 삽입 시 자동 생성
  • 컬렉션도 첫 번째 데이터 삽입 시 자동 생성
  • client.Database()db.Collection() 함수는 참조만 생성하며, 실제 데이터베이스/컬렉션은 데이터 삽입 시 생성

4.2 문서 삽입 (Create)

type User struct {
    ID   primitive.ObjectID `bson:"_id,omitempty"`
    Name string             `bson:"name"`
    Age  int                `bson:"age"`
}

func main() {
    clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
    
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    client, err := mongo.Connect(ctx, clientOptions)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Disconnect(ctx)

    err = client.Ping(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }

    db := client.Database("test_db")
    usersCollection := db.Collection("users")

    newUser := User{
        Name: "John Doe",
        Age:  30,
    }

    result, err := usersCollection.InsertOne(ctx, newUser)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Inserted user with ID: %v\n", result.InsertedID)
}

핵심 포인트:

  • InsertOne 메서드를 사용하여 단일 문서 삽입
  • 컨텍스트와 타임아웃을 통한 안전한 연결 관리
  • MongoDB 연결 상태 확인을 위한 Ping 메서드 활용

4.3 문서 조회 (Read)

cursor, err := usersCollection.Find(ctx, bson.M{})
if err != nil {
    log.Fatal(err)
}
defer cursor.Close(ctx)

var users []User
if err = cursor.All(ctx, &users); err != nil {
    log.Fatal(err)
}

for _, user := range users {
    fmt.Printf("User: %s, Age: %d, ID: %s\n", user.Name, user.Age, user.ID.Hex())
}

주요 특징:

  • Find 메서드에 빈 필터(bson.M{}) 사용으로 모든 문서 조회
  • cursor.All을 통해 결과를 User 구조체 슬라이스로 디코딩
  • 커서 리소스 해제를 위한 defer cursor.Close(ctx) 필수

4.4 문서 업데이트 (Update)

var userToUpdate User
err = usersCollection.FindOne(ctx, bson.M{"name": "John Doe"}).Decode(&userToUpdate)
if err != nil {
    log.Println("No user found to update")
} else {
    update := bson.M{
        "$set": bson.M{
            "name": "Jane Doe",
            "age":  25,
        },
    }
    result, err := usersCollection.UpdateOne(
        ctx,
        bson.M{"_id": userToUpdate.ID},
        update,
    )
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Updated %v document(s)\n", result.ModifiedCount)
}

중요 사항:

  • $set 연산자로 특정 필드만 업데이트하여 나머지 문서는 변경되지 않음
  • FindOne으로 업데이트할 문서를 먼저 검색
  • UpdateOne의 결과로 수정된 문서 수 확인 가능

4.5 문서 삭제 (Delete)

result, err := usersCollection.DeleteOne(ctx, bson.M{"name": "Jane Doe"})
if err != nil {
    log.Fatal(err)
}
log.Printf("Deleted %v document(s)\n", result.DeletedCount)

특징:

  • DeleteOne은 필터와 일치하는 첫 번째 문서만 삭제
  • 삭제된 문서 수를 result.DeletedCount로 확인 가능

5. Gin 프레임워크를 활용한 블로그 애플리케이션 구축

5.1 프로젝트 구조

go-blog/  
├── main.go  
├── handlers/  
│   └── main.go  
└── templates/  
    ├── index.html  
    ├── post.html  
    ├── create.html  
    └── edit.html

5.2 애플리케이션 초기화

필요한 의존성 설치:

go get github.com/gin-gonic/gin
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/bson

메인 애플리케이션 설정:

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
    client, err := mongo.Connect(ctx, clientOptions)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Disconnect(ctx)

    db := client.Database("blog_db")
    h := handlers.NewHandler(db)

    router := gin.Default()
    router.LoadHTMLGlob("templates/*")

    // 라우트 설정
    router.GET("/", h.HomePage)
    router.GET("/post/:id", h.ViewPost)
    router.GET("/create", h.CreatePost)
    router.GET("/edit/:id", h.EditPost)
    router.POST("/save", h.SavePost)
    router.GET("/delete/:id", h.DeletePost)

    router.Run(":8080")
}

5.3 핸들러 구조체 및 Post 모델

type Post struct {
    ID        primitive.ObjectID `bson:"_id,omitempty" json:"id"`
    Title     string             `bson:"title" json:"title"`
    Content   string             `bson:"content" json:"content"`
    CreatedAt time.Time          `bson:"created_at" json:"created_at"`
    UpdatedAt time.Time          `bson:"updated_at" json:"updated_at"`
}

type Handler struct {
    db         *mongo.Database
    collection *mongo.Collection
}

func NewHandler(db *mongo.Database) *Handler {
    return &Handler{
        db:         db,
        collection: db.Collection("posts"),
    }
}

구조체 설계 포인트:

  • bson 태그로 MongoDB 저장 방식 지정
  • json 태그로 JSON 직렬화 설정
  • 생성 및 수정 시간 자동 관리

5.4 주요 핸들러 함수들

홈페이지 (전체 포스트 목록)

func (h *Handler) HomePage(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    cursor, err := h.collection.Find(ctx, bson.M{})
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    defer cursor.Close(ctx)

    var posts []Post
    if err = cursor.All(ctx, &posts); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.HTML(http.StatusOK, "index.html", posts)
}

포스트 저장 (생성/수정)

func (h *Handler) SavePost(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    id := c.PostForm("id")
    title := c.PostForm("title")
    content := c.PostForm("content")
    now := time.Now()

    if id == "" {
        // 새 포스트 생성
        post := Post{
            Title:     title,
            Content:   content,
            CreatedAt: now,
            UpdatedAt: now,
        }
        result, err := h.collection.InsertOne(ctx, post)
        // 에러 처리 생략
    } else {
        // 기존 포스트 업데이트
        objID, err := primitive.ObjectIDFromHex(id)
        // ID 변환 에러 처리
        
        update := bson.M{
            "$set": bson.M{
                "title":      title,
                "content":    content,
                "updated_at": now,
            },
        }
        result, err := h.collection.UpdateOne(ctx, bson.M{"_id": objID}, update)
        // 에러 처리 생략
    }

    c.Redirect(http.StatusSeeOther, "/")
}

핵심 로직:

  • ID 유무에 따른 생성/수정 로직 분기
  • primitive.ObjectIDFromHex를 통한 MongoDB ObjectID 변환
  • 타임스탬프 자동 관리

5.5 HTML 템플릿 구성

메인 페이지 (index.html)

  • Tailwind CSS를 활용한 반응형 그리드 레이아웃
  • 포스트 카드 형태로 제목, 내용 미리보기, 생성일 표시
  • 편집/삭제 버튼 제공
  • 150자 초과 시 내용 자동 자르기 기능

포스트 상세 페이지 (post.html)

  • 전체 포스트 내용 표시
  • 생성일과 수정일 구분 표시
  • 편집/삭제 기능 제공

생성/편집 페이지 (create.html, edit.html)

  • 제목과 내용 입력 폼
  • 편집 시 기존 데이터 자동 로드
  • 유효성 검사 포함

6. 실용적인 팁과 주의사항

보안 관련

  • 연결 타임아웃 설정으로 무한 대기 방지
  • 에러 핸들링을 통한 안전한 애플리케이션 운영
  • ObjectID 검증을 통한 유효하지 않은 요청 차단

성능 최적화

  • 컨텍스트 기반 타임아웃으로 리소스 관리
  • 커서 리소스 해제를 위한 defer 패턴 활용
  • 데이터베이스 연결 재사용

개발 편의성

  • 구조체 태그를 활용한 자동 매핑
  • Gin의 HTML 템플릿 로딩 기능 활용
  • RESTful API 패턴 준수

7. 확장 가능한 기능들

문서에서 제안하는 향후 개선 방향:

  • 사용자 인증 시스템
  • 태그 및 카테고리 기능
  • 댓글 시스템
  • 페이지네이션
  • 검색 기능

8. 결론

MongoDB의 문서 기반 구조는 데이터 모델이 시간에 따라 변화해야 하는 애플리케이션에 매우 적합합니다. Go의 강력한 타입 시스템과 MongoDB의 유연성을 결합하여 빠른 반복 개발과 확장성을 동시에 확보할 수 있습니다.

이 튜토리얼을 통해 Go와 MongoDB를 활용한 완전한 웹 애플리케이션 개발 과정을 학습했으며, 실제 프로덕션 환경에서 활용할 수 있는 견고한 기반을 마련했습니다.

1개의 좋아요