개발 시간을 줄이고 특히 새로운 기술 프로젝트에 너혹 싶다면, 클린 아키텍처(Clean Architecture)를 생각해보자.
근데 Go에서는 생각보다 직관적이지가 않다. Go는 OOP 도구가 부족해서 좀 변행해서 구현해야 한다. 이번엔는 기본적인 것만 써서 간단히 백엔드 서비스만 만들도록 하겠다. 먼저 폴더 구조를 알아야 한다.
Go에는 클래스가 없고 대신 구조체와 receiver 함수가 있다. 이걸 클래스 매서드 처럼 사용하면된다.
또한 Go 구조체는 생성자가 없어서 ㅠㅠ 사용자 인스턴스를 만들 때 생성자 함수도 직접 만들어야 한다. 그리고 구조체 메서드를 정의 하면 되겠다.
- pkg > entities> spot: 여기에는 도메인을 포함한 모델이 들어간다. 이는 비즈니스 규칙을 표현한다.
- pkg > spot > service: 비즈니스 로직을 담당하는 계층인데, 여기서는 엔티티를 조적한다. 이부분을 구조체와 메서드로 구분한다.
- pkg > spot > repository: 여기에서는 데이터베이스와 소통한다. 어떻게 하냐면 Service 구조체에 Repository 타입 속성을 추가하면된다. 이게 말이 처음엔 어려운데 코드를 보면 이해가 될거다.
- api> : 여기서는 외부 계층인데 프리젠테이션(보여지는 부분), 컨트롤러(HTTP 핸들러) 등 외부와 관련된 모든것을 다룬다.
1. Creating the Entities: Spot
package entities
import (
"time"
)
type Spot struct {
Id uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Location string `json:"location"`
Author string `json:"author"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type DeleteRequest struct {
Id string `json:"id"`
}
2. Creating the Repository: SpotRepository
Repository는 저장소가 어떻게 동작하는지 가이드한다. Go에는 인터페이스가 있어서 다행인데, Go의 인터페이스는 좀 특이하게 작동한다. Go에서는 "이 구조체가 이 인터페이스를 구현한다"라고 명시적으로 말할 필요 없이 인터페이스에 있는 메서드만 구조체에 넣으면, 그 구조체가 자동으로 인터페이스의 멤버가 된다. 이를 덕타이핑이라고 하는데 생각보다 너무 편한다.
package spot
import (
"camping-backend-with-go/api/presenter"
"camping-backend-with-go/pkg/entities"
"gorm.io/gorm"
"time"
)
type Repository interface {
CreateSpot(spot *entities.Spot) (*entities.Spot, error)
ReadSpot() (*[]presenter.Spot, error)
GetSpot(id int) (*entities.Spot, error)
UpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error)
GetFindById(id int) (*entities.Spot, error)
PartialUpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error)
DeleteSpot(id int) error
}
type repository struct {
DBConn *gorm.DB
}
func NewRepo(dbconn *gorm.DB) Repository {
return &repository{
DBConn: dbconn,
}
}
func (r *repository) GetSpot(id int) (*entities.Spot, error) {
var spot entities.Spot
result := r.DBConn.First(&spot, "id = ?", id)
if result.Error != nil {
return nil, result.Error
}
return &spot, nil
}
func (r *repository) GetFindById(id int) (*entities.Spot, error) {
var spot entities.Spot
result := r.DBConn.First(&spot, "id = ?", id)
if result.Error != nil {
return nil, result.Error
}
return &spot, nil
}
func (r *repository) CreateSpot(spot *entities.Spot) (*entities.Spot, error) {
result := r.DBConn.Create(spot)
if result.Error != nil {
return nil, result.Error
}
return spot, nil
}
func (r *repository) ReadSpot() (*[]presenter.Spot, error) {
var spots []presenter.Spot
result := r.DBConn.Find(&spots)
if result.Error != nil {
return nil, result.Error
}
return &spots, nil
}
func (r *repository) UpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error) {
fetched, err := r.GetFindById(id)
if err != nil {
return nil, err
}
spot.UpdatedAt = time.Now()
result := r.DBConn.Model(fetched).Updates(&spot)
if result.Error != nil {
return nil, result.Error
}
return spot, nil
}
func (r *repository) PartialUpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error) {
fetched, err := r.GetFindById(id)
if err != nil {
return nil, err
}
spot.UpdatedAt = time.Now()
result := r.DBConn.Model(fetched).Updates(&spot)
if result.Error != nil {
return nil, result.Error
}
return fetched, nil
}
func (r *repository) DeleteSpot(id int) error {
spot, err := r.GetFindById(id)
if err != nil {
return err
}
result := r.DBConn.Delete(spot)
if result.Error != nil {
return result.Error
}
return nil
}
package spot
import (
"camping-backend-with-go/api/presenter"
"camping-backend-with-go/pkg/entities"
)
type Service interface {
InsertSpot(spot *entities.Spot) (*entities.Spot, error)
FetchSpots() (*[]presenter.Spot, error)
UpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error)
PartialUpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error)
GetSpot(id int) (*entities.Spot, error)
RemoveSpot(id int) error
}
type service struct {
repository Repository
}
func NewService(r Repository) Service {
return &service{
repository: r,
}
}
func (s *service) PartialUpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error) {
return s.repository.PartialUpdateSpot(spot, id)
}
// InsertSpot is a service layer that helps insert Spot in Camping
func (s *service) InsertSpot(spot *entities.Spot) (*entities.Spot, error) {
return s.repository.CreateSpot(spot)
}
// FetchSpots is a service layer that helps fetch all Spots in Camping
func (s *service) FetchSpots() (*[]presenter.Spot, error) {
return s.repository.ReadSpot()
}
// UpdateSpot is a service layer that helps update Spots in Camping
func (s *service) UpdateSpot(spot *entities.Spot, id int) (*entities.Spot, error) {
return s.repository.UpdateSpot(spot, id)
}
// GetSpot is a service layer that helps update Spots in Camping
func (s *service) GetSpot(id int) (*entities.Spot, error) {
return s.repository.GetSpot(id)
}
func (s *service) RemoveSpot(id int) error {
return s.repository.DeleteSpot(id)
}
3. Creating the Handler: spot_handler
이제 우리의 서비스를 핸들러에 노출시키고, 이 핸들러를 공개한다. net/http나 chi, gin 같은 것을 이용해도되지만 나는 fiber를 이용하겠다. 다른 구현에서 했던 것처럼 비슷하게 할 건데, 이번엔 Handler 구조체가 Service를 필요로 한다. 왜냐하면 핸들러는 서비스와 소통해야 하니까.
package handlers
import (
"camping-backend-with-go/api/presenter"
"camping-backend-with-go/pkg/entities"
"camping-backend-with-go/pkg/spot"
"errors"
"github.com/gofiber/fiber/v2"
"net/http"
)
func AddSpot(service spot.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.Spot
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.SpotErrorResponse(err))
}
if requestBody.Author == "" || requestBody.Title == "" {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.SpotErrorResponse(errors.New(
"Please specify title and author",
)))
}
result, err := service.InsertSpot(&requestBody)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.SpotErrorResponse(err))
}
return c.JSON(presenter.SpotSuccessResponse(result))
}
}
func GetSpots(service spot.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
fetched, err := service.FetchSpots()
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.SpotErrorResponse(err))
}
return c.JSON(presenter.SpotsSuccessResponse(fetched))
}
}
// UpdateSpot is handler/controller which updates data of Spots in the Camping
func UpdateSpot(service spot.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.Spot
err := c.BodyParser(&requestBody)
id, _ := c.ParamsInt("id")
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.SpotErrorResponse(err))
}
result, err := service.UpdateSpot(&requestBody, id)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.SpotErrorResponse(err))
}
return c.JSON(presenter.SpotSuccessResponse(result))
}
}
// GetSpot is handler/controller which updates data of Spots in the Camping
func GetSpot(service spot.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id")
fetched, err := service.GetSpot(id)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.SpotErrorResponse(err))
}
return c.JSON(presenter.SpotSuccessResponse(fetched))
}
}
// PartialUpdateSpot is handler/controller which updates data of Spots in the Camping
func PartialUpdateSpot(service spot.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.Spot
err := c.BodyParser(&requestBody)
id, _ := c.ParamsInt("id")
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.SpotErrorResponse(err))
}
result, err := service.PartialUpdateSpot(&requestBody, id)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.SpotErrorResponse(err))
}
return c.JSON(presenter.SpotSuccessResponse(result))
}
}
// RemoveSpot is handler/controller which removes Books from the BookShop
func RemoveSpot(service spot.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id")
err := service.RemoveSpot(id)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.SpotErrorResponse(err))
}
return c.JSON(&fiber.Map{
"status": true,
"data": "delete successfully",
"err": nil,
})
}
}
4. Creating the Router: Fiber Router
이제 이 핸들러를 Router와 연결하겠다.
package routes
import (
"camping-backend-with-go/api/handlers"
"camping-backend-with-go/pkg/spot"
"github.com/gofiber/fiber/v2"
)
func SpotRouter(app fiber.Router, service spot.Service) {
app.Get("/spots", handlers.GetSpots(service))
app.Get("/spots/:id", handlers.GetSpot(service))
app.Put("/spots/:id", handlers.UpdateSpot(service))
app.Patch("/spots/:id", handlers.PartialUpdateSpot(service))
app.Post("/spots", handlers.AddSpot(service))
app.Delete("/spots/:id", handlers.RemoveSpot(service))
}
5. 이제 마법을 부려볼 때다 이 모두를 하나로 main에다가 엮는다.
package main
import (
"camping-backend-with-go/api/routes"
"camping-backend-with-go/pkg/entities"
"camping-backend-with-go/pkg/healthcheck"
"camping-backend-with-go/pkg/spot"
"github.com/gofiber/fiber/v2/middleware/cors"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"log"
"os"
"github.com/gofiber/fiber/v2"
)
func main() {
db := databaseConnection()
spotRepo := spot.NewRepo(db)
spotService := spot.NewService(spotRepo)
app := fiber.New()
app.Use(cors.New())
v1 := app.Group("/v1")
routes.SpotRouter(v1, spotService)
log.Fatal(app.Listen(":3000"))
}
func databaseConnection() *gorm.DB {
//dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
//db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database. \n", err)
os.Exit(2)
}
log.Println("connected")
err = db.AutoMigrate(&entities.Spot{})
if err != nil {
log.Println(err.Error())
}
return db
}
이렇게하면 클린아키텍쳐로 간단한 CRUD를 만들었다. 여기에 이제 로그인 붙이고 하면 될거 같다.
'devops > go' 카테고리의 다른 글
go fiber에 swagger를 붙여보자. (0) | 2024.11.27 |
---|---|
Json Web Token (0) | 2024.11.26 |
API(Get List) (0) | 2024.11.24 |
clean-architecture 구성 (30) | 2024.11.24 |
go fiber를 배워보자. (30) | 2024.11.22 |