Golang 및 Clean Archeitecture로 API 서버 구축

의 목적


나는 실제 클린 아케텍처 개발 상황에서 어떤 구조를 구축해 쉽게 개발할 수 있는지 설계했다.
나는 실천을 구상하여 구축을 고려하고 있다.API 문서가 아닌 sawagger를 구현했습니다.

사용할 도구

  • docker
  • echo(air)
  • gorm
  • swagger
  • goa
  • 디렉토리 구조


    ├─ docker/
        ├─ api
            └─ environments
                ├─ production/
                ├─ development/
                └─ test/
        ├─ document
        └─ mysql 
    ├─ documents/
        └─ design/
    ├─ server/
        ├─ adapters/
            ├─ controllers/
            ├─ gateways/
            └─ presenter/
        ├─ infrastructure/
            ├─ routes/
            └─ database/
        ├─ usecases/
        ├─ entities/
        └─ main.go
    └─ docker-compose.development.yml
    

    환경 정보


    docker-compose로 컨테이너를 관리합니다.컨테이너의 내용은 다음과 같다.
  • API server
  • Clean Architecture를 사용한 API 서버 구축
  • echo+air+gorm 사용
  • 개발 효율을 높이기 위해 개발자는 에어
  • 를 사용한다.
  • Database
  • 지속 데이터
  • MySQL8.0
  • 사용
  • Swagger UI
  • Swagger UI 구축
  • Swagger Editor
  • openapi.yaml 만들기 및 업데이트
  • goa
  • 자세한 내용은 docker-compose를 참조하십시오.development.yml 및 docker 디렉토리를 확인하십시오.초기 데이터에 사용할 sql 파일도 만들었습니다.
    https://github.com/yantera/clean-architecture-for-go

    Clean Archeitecture 정보


    이번에는 녹색 구조를 따라 어떤 목록 구성에서 어떤 역할을 하고 있는지만 설명했다.청결 구조를 스스로 조사하세요.

    Adapters


    이 디렉터리는 외부 통신과의 처리를 실시했다.
  • Controllers
  • Infrastructure에서 받은 요청을 수락합니다.
  • 요청을 받은 후에도 인터랙터에 투척 처리를 초기화합니다.
  • search.go
    package guest
    
    import (
    	gateway "api/server/adapters/gateways"
    	usecase "api/server/usecases/guest"
    	"github.com/labstack/echo/v4"
    	"strconv"
    )
    
    type GuestController struct {
    	Interactor usecase.GuestInteractor
    }
    
    func InitGuestController(sqlHandler gateway.SQLHandler) *GuestController {
    	return &GuestController{
    		Interactor: usecase.GuestInteractor{
    			GuestPort: &gateway.GuestRepository{
    				SQLHandler: sqlHandler,
    			},
    		},
    	}
    }
    
    func (controller *GuestController) Show(c echo.Context) (err error) {
    	id, _ := strconv.Atoi(c.Param("id"))
    	guest, err := controller.Interactor.GuestByID(id)
    	if err != nil {
    		c.JSON(500, err)
    		return
    	}
    	c.JSON(200, guest)
    	return
    }
    
    func (controller *GuestController) Index(c echo.Context) (err error) {
    	guests, err := controller.Interactor.Guests()
    	if err != nil {
    		c.JSON(500, err)
    		return
    	}
    	c.JSON(200, guests)
    	return
    
  • Gateways
  • 외부 도구를 연결하는 데 사용되는interface입니다.
  • interface의 정의와interface를 통해 외부 방법을 실행하는 처리를 기본적으로 정의했다.
  • guest_repository.go
    package gateways
    
    import (
    	"api/server/entities"
    )
    
    type GuestRepository struct {
    	SQLHandler
    }
    
    func (repo *GuestRepository) FindByID(id int) (guest entities.Guest, err error) {
    	if err = repo.Find(&guest, id).Error; err != nil {
    		return
    	}
    	return
    }
    
    func (repo *GuestRepository) FindAll() (guests entities.Guests, err error) {
    	if err = repo.Find(&guests).Error; err != nil {
    		return
    	}
    	return
    }
    
    sqlhandler.go
    package gateways
    
    import (
    	"gorm.io/gorm"
    )
    
    type SQLHandler interface {
    	First(interface{}, ...interface{}) *gorm.DB
    	Last(interface{}, ...interface{}) *gorm.DB
    	Take(interface{}, ...interface{}) *gorm.DB
    	Find(interface{}, ...interface{}) *gorm.DB
    }
    
  • Presenter
  • 외부로 출력할 때의 데이터를 포맷합니다.
  • 정의했지만 이번에는 사용하지 않았다.
  • Infrastructures


    이 디렉터리는 응용 프로그램 외부로 통신하는 처리에 사용됩니다.
  • Routes
  • 라우팅
  • Routes의 라우팅이 늘어나면 가독성이 떨어지기 때문에 디렉터리를 잘라내어 파일을 분할할 수 있습니다.할 수 있다고 생각하는 사람은 루트다.고만 하면 될 것 같아서요.
    ├─ infrastructure/
        ├─ routes/
            └─ guest.go
        └─ route.go
    
    route.go
    package infrastructure
    
    import (
    	"api/server/infrastructure/routes"
    	"github.com/labstack/echo/v4"
    	"github.com/labstack/echo/v4/middleware"
    )
    
    func Run() {
    	e := echo.New()
    
    	e.Use(middleware.Logger())
    	e.Use(middleware.Recover())
    	e.Use(middleware.CORS())
    
    	routes.InitGuest(e)
    
    	e.Logger.Fatal(e.Start(":1323"))
    }
    
    guest.go
    package routes
    
    import (
    	controller "api/server/adapters/controllers/guest"
    	"api/server/infrastructure/database"
    	"github.com/labstack/echo/v4"
    )
    
    func InitGuest(e *echo.Echo) {
    	guestController := controller.InitGuestController(database.InitSQLHandler())
    
    	e.GET("/guests/:id", func(c echo.Context) error { return guestController.Show(c) })
    	e.GET("/guests", func(c echo.Context) error { return guestController.Index(c) })
    }
    
  • Database
  • 데이터베이스에 접근하는 초기 설정입니다.
  • sql_handler.go
    package database
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"log"
    	"os"
    	"time"
    
    	"api/server/adapters/gateways"
    )
    
    type SQLHandler struct {
    	db *gorm.DB
    }
    
    func InitSQLHandler() gateways.SQLHandler {
    	user := os.Getenv("DB_USER")
    	pass := os.Getenv("DB_PASSWORD")
    	containerName := os.Getenv("DB_CONTAINER_NAME")
    	port := os.Getenv("DB_PORT")
    	dbName := os.Getenv("DB_NAME")
    
    	newLogger := logger.New(
    		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
    		logger.Config{
    			SlowThreshold:             time.Second, // Slow SQL threshold
    			LogLevel:                  logger.Info, // Log level
    			IgnoreRecordNotFoundError: true,        // Ignore ErrRecordNotFound error for logger
    			Colorful:                  false,       // Disable color
    		},
    	)
    
    	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, pass, containerName, port, dbName)
    	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    		Logger: newLogger,
    	})
    	if err != nil {
    		panic(err)
    	}
    	sqlHandler := new(SQLHandler)
    	sqlHandler.db = db
    	return sqlHandler
    }
    
    func (handler *SQLHandler) First(out interface{}, where ...interface{}) *gorm.DB {
    	return handler.db.First(out, where...)
    }
    
    func (handler *SQLHandler) Take(out interface{}, where ...interface{}) *gorm.DB {
    	return handler.db.Take(out, where...)
    }
    
    func (handler *SQLHandler) Last(out interface{}, where ...interface{}) *gorm.DB {
    	return handler.db.Last(out, where...)
    }
    
    func (handler *SQLHandler) Find(out interface{}, where ...interface{}) *gorm.DB {
    	return handler.db.Find(out, where...)
    }
    

    Usecases

  • Enities와 Gateways를 부르는 처리를 실시합니다.
  • 저는 개인적으로 이 역할이 소위 서비스와 비슷하다고 생각합니다.
  • 일부러 Enity 별로 디렉토리를 분리합니다.모든 Usecase를 다 부를 수 있는 것이 아니라 필요한 Usecase만 부르고 싶어서 이렇게 하는 것이다.
    usecases/
        └─ guest
            ├─ interactor.go
            └─ port.go
    
    interactor.go
    package guest
    
    import (
    	"api/server/entities"
    )
    
    type GuestInteractor struct {
    	GuestPort GuestPort
    }
    
    func (interactor *GuestInteractor) GuestByID(id int) (guest entities.Guest, err error) {
    	guest, err = interactor.GuestPort.FindByID(id)
    	return
    }
    
    func (interactor *GuestInteractor) Guests() (guests entities.Guests, err error) {
    	guests, err = interactor.GuestPort.FindAll()
    	return
    }
    
    port.go
    package guest
    
    import (
    	"api/server/entities"
    )
    
    type GuestPort interface {
    	FindByID(id int) (entities.Guest, error)
    	FindAll() (entities.Guests, error)
    }
    

    Entities

  • 도메인 논리 구현.
  • 많은 회사의 도메인 이름이 있어 일률적으로 논할 수는 없지만 굳이 말하자면 어떤 데이터를 제작, 갱신, 읽기, 삭제하는지 고려해 보세요.
  • guest.go
    package entities
    
    type Guests []Guest
    
    type Guest struct {
    	ID            int    `json:"id" param:"id"`
    	FirstName     string `json:"first_name"`
    	FirstNameKana string `json:"first_name_kana"`
    	LastName      string `json:"last_name"`
    	LastNameKana  string `json:"last_name_kana"`
    	Gender        int    `json:"gender"`
    	Birthday      string `json:"birthday"`
    	Tel           int    `json:"tel"`
    	Email         string `json:"email"`
    }
    

    최후


    여기까지 읽어주셔서 감사합니다.
    나는 이번 실시가 단지 하나의 생각일 뿐이라고 생각한다.좋은 생각과 개선점이 있는 사람이 의견을 얻었으면 좋겠어요.
    이번 구성은 이 페이지에 있으니 관심 있는 사람은 반드시 시도해 보세요.
    https://github.com/yantera/clean-architecture-for-go

    좋은 웹페이지 즐겨찾기