개발/Go

[Gin] GORM에서 CQRS 아키텍처 구현해보기

devhooney 2025. 6. 18. 08:21
728x90

읽기와 쓰기를 분리하여 유지보수성과 성능을 챙기자!


✅ CQRS란?

CQRS(Command Query Responsibility Segregation)는 **쓰기(Command)**와 읽기(Query) 책임을 서로 다른 모델 또는 레이어로 분리하는 아키텍처 패턴입니다.

  • 기존 CRUD는 하나의 모델이 모든 작업을 처리
  • CQRS는 복잡한 시스템에서 명확하고 유연한 구조 제공

 


 

📦 GORM에서 CQRS를 구현하는 이유

이점 설명

책임 분리 읽기/쓰기 로직 분리로 유지보수 용이
성능 최적화 읽기에서 복잡한 JOIN 최소화 가능
테스트 용이 Command/Query 각각 테스트 가능
확장성 Read-Replica, Kafka 등 확장 용이

 


 

📐 디렉토리 구조 예시

/internal
  /user
    - command_repository.go   # 쓰기
    - query_repository.go     # 읽기
    - user_model.go

 

1️⃣ 모델 정의

// user_model.go
package user

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Name     string
    Email    string
    Password string
}

 

2️⃣ CommandRepository (쓰기 전용)

// command_repository.go
package user

import "gorm.io/gorm"

type UserCommandRepository struct {
    db *gorm.DB
}

func NewUserCommandRepository(db *gorm.DB) *UserCommandRepository {
    return &UserCommandRepository{db}
}

func (r *UserCommandRepository) Create(user *User) error {
    return r.db.Create(user).Error
}

func (r *UserCommandRepository) UpdateName(userID uint, newName string) error {
    return r.db.Model(&User{}).Where("id = ?", userID).Update("name", newName).Error
}

func (r *UserCommandRepository) Delete(userID uint) error {
    return r.db.Delete(&User{}, userID).Error
}

 

3️⃣ QueryRepository (읽기 전용)

// query_repository.go
package user

import "gorm.io/gorm"

type UserQueryRepository struct {
    db *gorm.DB
}

func NewUserQueryRepository(db *gorm.DB) *UserQueryRepository {
    return &UserQueryRepository{db}
}

func (r *UserQueryRepository) FindByID(id uint) (*User, error) {
    var user User
    err := r.db.First(&user, id).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *UserQueryRepository) SearchByEmail(email string) ([]User, error) {
    var users []User
    err := r.db.Where("email LIKE ?", "%"+email+"%").Find(&users).Error
    return users, err
}

 

4️⃣ 사용 예시

cmdRepo := user.NewUserCommandRepository(db)
queryRepo := user.NewUserQueryRepository(db)

newUser := &user.User{Name: "홍길동", Email: "gil@example.com", Password: "hashed_pw"}
_ = cmdRepo.Create(newUser)

_ = cmdRepo.UpdateName(newUser.ID, "김길동")

foundUser, _ := queryRepo.FindByID(newUser.ID)
fmt.Println(foundUser.Name)

_ = cmdRepo.Delete(newUser.ID)

 


 

🧠 실전 팁

상황 전략

조회 최적화 Select("id, name") 등 최소 필드만 조회
읽기 모델 분리 ViewModel 또는 DTO 설계로 성능 최적화
트랜잭션 처리 Command에서 db.Transaction(...) 적극 활용
추상화 인터페이스로 분리하여 테스트/Mock 편의성 확보

 

 


 

🧪 CQRS + 테스트 예시

type UserCommand interface {
    Create(user *User) error
    UpdateName(id uint, name string) error
}

type UserQuery interface {
    FindByID(id uint) (*User, error)
    SearchByEmail(email string) ([]User, error)
}

Mock 객체를 만들어 유닛 테스트 가능


 

✅ 마무리 정리

항목 설명

전략 Command / Query 레이어를 GORM 레벨에서 분리
구현 UserCommandRepository, UserQueryRepository 구성
장점 유지보수성, 성능, 테스트 용이성 확보
추천 시점 API가 커지거나 조회가 복잡해질 때 적용

 

 

 

 

 

 

 

실제 프로젝트에 적용해보며 GORM에서 CQRS가 어떻게 구조적으로 강력한지 체감해보자!

 

 

 

 

728x90