bkdragon's log

[gin] Clean Architecture 본문

golang

[gin] Clean Architecture

bkdragon 2024. 9. 20. 19:20

데이터베이스에 접근하는 respository layer,
비지니스 로직을 수행하는 service layer,
요청을 해석하는 controller layer로 계층을 분리할 것이다.

우선 가장 기본 CRUD 만 있는 인터페이스를 만들고 구현체를 만든다.

type IRepository[M any, ID any] interface {    
    List() ([]*M, error)
    Retrieve(id ID) (*M, error)
    Save(entity *M) (*M, error)
    Update(id ID, updates map[string]interface{}) (*M, error)
    Delete(id ID) error
}

DataBase 구조체를 만들어서 IRepository 인터페이스를 구현한다.

type DataBase[M any, ID any] struct {
    db *gorm.DB
}

func NewRepository[M any, ID any](db *gorm.DB) IRepository[M, ID] {
    return &DataBase[M, ID]{db}
}

func (r *DataBase[M, ID]) List() ([]*M, error) {
    var entities []*M
    err := r.db.Find(&entities).Error

    return entities, err
}

func (r *DataBase[M, ID]) Retrieve(id ID) (*M, error)  {
    var entity M
    err := r.db.First(&entity, id).Error

    return &entity, err
}

func (r *DataBase[M, ID]) Save(entity *M) (*M, error) {
    err := r.db.Create(&entity).Error
    return entity, err
}

func (r *DataBase[M, ID]) Update(id ID, updates map[string]interface{}) (*M, error) {
    var entity M
    err := r.db.Model(&entity).Where("id = ?", id).Updates(updates).Error
    if err != nil {
        return nil, err
    }
    err = r.db.First(&entity, id).Error
    return &entity, err
}

func (r *DataBase[M, ID]) Delete(id ID) ( error) {
    var entity M
    err := r.db.Delete(&entity, id).Error
    return err
}

이제 User 모델을 만들고 UserRepository를 구현한다.

type User struct {
    gorm.Model
    Name  string `gorm:"size:100;not null"`
    Age   int    `gorm:"not null"`
    Email string `gorm:"size:100;unique;not null"`
    Deleted bool `gorm:"default:false"`
    CompanyID uint 
    Company   Company
}
type IUserRepository interface {
    IRepository[models.User, int]
    // List() ([]*models.User, error)
    // Retrieve(id int) (*models.User, error)
    // Save(entity *models.User) (*models.User, error)
    // Update(id int, updates map[string]interface{}) (*models.User, error)
    // Delete(id int) (error)
}

임베드라고 형태인데 주석 해놓은 부분과 같은 효과이다. (상속과 비슷하다.)

type UserRepository struct {
    DataBase[models.User, int]
}

임베드는 구조체, 인터페이스 두 곳다 사용할 수 있다.

서비스 레이어엔 방금 만든 userRepository를 가지고 있으면 된다.

type UserService struct {
    repo repository.IUserRepository
}

func NewUserService(repo repository.IUserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) ListUsers() ([]*models.User ,error)  {
    users, err := s.repo.List()

    if err != nil {
        return nil, errors.New("data Base Error")
    }

    if len(users) == 0 {
        return []*models.User{}, nil
    }

    return users, nil
}

func (s *UserService) GetUser(id int)(*models.User ,error) {
    user, err := s.repo.Retrieve(id)
    if err != nil {
        return nil, err
    }
    return user, nil
}


func (s *UserService) CreateUser(request models.CreateUser)(*models.User ,error)  {
    user := models.User{
        Name : request.Name,
        Email: request.Email,
        Age: request.Age,
    }
    createdUser, err := s.repo.Save(&user)

    if err != nil {
        return nil, err
    }

    return createdUser, nil
}

func (s *UserService) UpdateUser(id int, request models.UpdateUser)(*models.User ,error) {
    updateMap := make(map[string]interface{})

    if request.Name != nil {
        updateMap["name"] = request.Name
    }
    if request.Email != nil {
        updateMap["email"] = request.Email
    }
    if request.Age != nil {
        updateMap["age"] = request.Age
    }

    updatedUser, err := s.repo.Update(id, updateMap)
    if err != nil {
        return nil, err
    }

    return updatedUser, nil
}

func (s *UserService) DeleteUser(id int) error {
    err := s.repo.Delete(id)
    if err != nil {
        return err
    }

    return nil
}

컨트롤러 레이어에선 서비스 클래스를 가지고 있고 필요한 요청 바디나 id 등을 실제 request 객체에서 얻어서 인자로 제공만 해주면 된다.

type UserController struct {
    service service.UserService
}

func NewUserController(service service.UserService) *UserController{
    return &UserController{service: service}
}

func (uc *UserController) ListUsers(c *gin.Context) {
    result, err := uc.service.ListUsers()

    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, result)
}

func (uc *UserController) GetUser(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
        return
    }

    result, err := uc.service.GetUser(id)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, *result)
}

func (uc *UserController) GetUserByEmail(c *gin.Context) {
    email := c.Param("email")

    result, err := uc.service.GetUserByEmail(email)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, *result)
}

func (uc *UserController) CreateUser(c *gin.Context) {
    var request models.CreateUser
    if err := c.ShouldBindJSON(&request); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
        return
    }

    result, err:= uc.service.CreateUser(request)

    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, *result)
}

func (uc *UserController) DeleteUser(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
        return
    }

    if err := uc.service.DeleteUser(id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "사용자가 성공적으로 삭제되었습니다"})
}

func (uc UserController) UpdateUser(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "유효하지 않은 ID"})
        return
    }

    var request models.UpdateUser
    if err := c.ShouldBindJSON(&request); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "유효하지 않은 입력"})
        return
    }

    result, err := uc.service.UpdateUser(id, request)

    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, *result)
}

이렇게 기본 CRUD를 구현했다. 만약 이메일을 기준으로 유저를 찾아오는 기능을 추가하고 싶다면 어떻게 해야할까?

레포지토리 레이어 부터 추가하면 된다. IUserRepository 에 FindByEmail 메서드를 추가하고 구현해준다.

type IUserRepository interface {
    IRepository[models.User, int]
    FindByEmail(email string) (*models.User, error)
}
func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
    var user models.User
    if err := r.db.Where("email = ?", email).First(&user).Error; err != nil {
        return nil, err
    }
    return &user, nil
}

서비스 레이어와 컨트롤러 레이어에도 추가해주면 된다.


// service layer
func (s *UserService) GetUserByEmail(email string)(*models.User ,error) {
    user, err := s.repo.FindByEmail(email)

    if err != nil {
        return nil, err
    }

    return user, nil
}

//controller layer
func (uc *UserController) GetUserByEmail(c *gin.Context) {
    email := c.Param("email")

    result, err := uc.service.GetUserByEmail(email)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, *result)
}

각 계층의 역할을 알기 때문에 디버깅, 유지보수가 편해질 것 같다. 그리고 무엇보다 테스트를 작성하기가 굉장히 편해진다.

레포지토리 계층을 모킹해서 리턴 값을 제어하면 리턴 값에 따라 서비스 레이어에서 그에 맞는 결과가 잘 나오는지만 확인하면 된다. 이부분은 또 다른 글에서 다뤄보겠다.

'golang' 카테고리의 다른 글

[gin] Paging Repository  (0) 2024.09.21
[gorm] 다형성 관계  (0) 2024.09.11
[go] http 통신 과정  (0) 2024.09.10
[gin] JSON  (1) 2024.09.08
[gin] ShouldBindJSON  (0) 2024.09.08