go-clean-arch를 사용하여 Go에서 클린 아키텍처 구현
Daniel Hayes
Full-Stack Engineer · Leapcell

당신의 Go 프로젝트의 코드 아키텍처는 무엇입니까? 헥사고날 아키텍처인가요? 어니언 아키텍처인가요? 아니면 DDD인가요? 어떤 아키텍처를 프로젝트에 채택하든, 핵심 목표는 항상 동일해야 합니다. 코드를 이해하고 테스트하고 유지 관리하기 쉽게 만드는 것입니다.
이 기사에서는 Uncle Bob의 클린 아키텍처에서 시작하여 핵심 아이디어를 간략하게 분석하고 go-clean-arch 리포지토리와 결합하여 Go 프로젝트에서 이러한 아키텍처 개념을 구현하는 방법을 자세히 알아보겠습니다.
클린 아키텍처
클린 아키텍처는 Uncle Bob이 제안한 소프트웨어 아키텍처 설계 철학입니다. 목표는 계층 구조와 명확한 종속성 규칙을 통해 소프트웨어 시스템을 더 쉽게 이해하고 테스트하고 유지 관리할 수 있도록 하는 것입니다. 핵심 아이디어는 관심사를 분리하고 시스템의 핵심 비즈니스 로직(Use Cases)이 구현 세부 사항(예: 프레임워크, 데이터베이스 등)에 의존하지 않도록 하는 것입니다.
클린 아키텍처의 핵심 아이디어는 독립성입니다.
- 프레임워크 독립성: 특정 프레임워크(예: Gin, GRPC 등)에 의존하지 않습니다. 프레임워크는 아키텍처의 핵심이 아닌 도구로 취급해야 합니다.
- UI 독립성: 사용자 인터페이스는 시스템의 다른 부분에 영향을 주지 않고 쉽게 변경할 수 있습니다. 예를 들어 웹 UI는 비즈니스 규칙을 수정하지 않고 콘솔 UI로 대체할 수 있습니다.
- 데이터베이스 독립성: 핵심 비즈니스 로직에 영향을 주지 않고 데이터베이스를 전환할 수 있습니다(예: MySQL에서 MongoDB로).
- 외부 도구 독립성: 외부 종속성(예: 타사 라이브러리)은 시스템 코어에 직접적인 영향을 미치지 않도록 격리해야 합니다.
구조 다이어그램
다이어그램에서 볼 수 있듯이 클린 아키텍처는 일련의 동심원으로 설명되며 각 레이어는 서로 다른 시스템 책임을 나타냅니다.
-
코어 엔티티
- 위치: 가장 안쪽 레이어
- 책임: 시스템의 비즈니스 규칙을 정의합니다. 엔티티는 애플리케이션의 핵심 객체이며 독립적인 라이프사이클을 가집니다.
- 독립성: 비즈니스 규칙과 완전히 독립적이며 비즈니스 규칙이 변경될 때만 변경됩니다.
-
유스 케이스 / 서비스
- 위치: 엔티티 바로 옆 레이어
- 책임: 애플리케이션의 비즈니스 로직을 구현합니다. 시스템의 다양한 작업(유스 케이스) 흐름을 정의하여 사용자 요구 사항이 충족되도록 합니다.
- 역할: 유스 케이스 레이어는 엔티티 레이어를 호출하고 데이터 흐름을 조정하며 응답을 결정합니다.
-
인터페이스 어댑터
- 위치: 다음 외부 레이어
- 책임: UI, 데이터베이스 등 외부 시스템의 데이터를 내부 레이어가 이해할 수 있는 형식으로 변환하고 핵심 비즈니스 로직을 외부 시스템에서 사용할 수 있는 형식으로 변환하는 역할을 합니다.
- 예시: HTTP 요청 데이터를 내부 모델(예: 클래스 또는 구조체)로 변환하거나 유스 케이스 출력 데이터를 사용자에게 표시합니다.
- 구성 요소: 컨트롤러, 게이트웨이, 프레젠터 등이 포함됩니다.
-
프레임워크 및 드라이버
- 위치: 가장 바깥쪽 레이어
- 책임: 데이터베이스, UI, 메시지 큐 등 외부 세계와의 상호 작용을 구현합니다.
- 기능: 이 레이어는 내부 레이어에 의존하지만 그 반대는 아닙니다. 이는 시스템에서 가장 쉽게 교체할 수 있는 부분입니다.
go-clean-arch 프로젝트
go-clean-arch는 클린 아키텍처를 구현하는 샘플 Go 프로젝트입니다. 이 프로젝트는 네 가지 도메인 레이어로 나뉩니다.
모델 레이어
목적: 도메인의 핵심 데이터 구조를 정의하고 Article 및 Author와 같은 프로젝트의 비즈니스 엔티티를 설명합니다.
해당 이론 레이어: 엔티티 레이어.
예시:
package domain import ( "time" ) type Article struct { ID int64 `json:"id"` Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` Author Author `json:"author"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` }
리포지토리 레이어
목적: 데이터 소스(예: 데이터베이스 및 캐시)와 상호 작용하고 유스 케이스 레이어가 데이터에 액세스할 수 있는 통합 인터페이스를 제공하는 역할을 합니다.
해당 이론 레이어: 프레임워크 및 드라이버.
예시:
package mysql import ( "context" "database/sql" "fmt" "github.com/sirupsen/logrus" "github.com/bxcodec/go-clean-arch/domain" "github.com/bxcodec/go-clean-arch/internal/repository" ) type ArticleRepository struct { Conn *sql.DB } // NewArticleRepository는 article.Repository 인터페이스를 나타내는 객체를 만듭니다. func NewArticleRepository(conn *sql.DB) *ArticleRepository { return &ArticleRepository{conn} } func (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) { rows, err := m.Conn.QueryContext(ctx, query, args...) if err != nil { logrus.Error(err) return nil, err } defer func() { errRow := rows.Close() if errRow != nil { logrus.Error(errRow) } }() result = make([]domain.Article, 0) for rows.Next() { t := domain.Article{} authorID := int64(0) err = rows.Scan( &t.ID, &t.Title, &t.Content, &authorID, &t.UpdatedAt, &t.CreatedAt, ) if err != nil { logrus.Error(err) return nil, err } t.Author = domain.Author{ ID: authorID, } result = append(result, t) } return result, nil } func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = ?` list, err := m.fetch(ctx, query, id) if err != nil { return domain.Article{}, err } if len(list) > 0 { res = list[0] } else { return res, domain.ErrNotFound } return }
유스케이스/서비스 레이어
목적: 시스템의 핵심 애플리케이션 로직을 정의하고 도메인 모델과 외부 상호 작용 간의 브리지 역할을 합니다.
해당 이론 레이어: 유스 케이스 / 서비스.
예시:
package article import ( "context" "time" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" "github.com/bxcodec/go-clean-arch/domain" ) type ArticleRepository interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } type AuthorRepository interface { GetByID(ctx context.Context, id int64) (domain.Author, error) } type Service struct { articleRepo ArticleRepository authorRepo AuthorRepository } func NewService(a ArticleRepository, ar AuthorRepository) *Service { return &Service{ articleRepo: a, authorRepo: ar, } } func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { res, err = a.articleRepo.GetByID(ctx, id) if err != nil { return } resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID) if err != nil { return domain.Article{}, err } res.Author = resAuthor return }
전달 레이어
목적: 외부 요청을 수신하고, 유스 케이스 레이어를 호출하고, 결과를 외부(예: HTTP 클라이언트 또는 CLI 사용자)에 반환하는 역할을 합니다.
해당 이론 레이어: 인터페이스 어댑터.
예시:
package rest import ( "context" "net/http" "strconv" "github.com/bxcodec/go-clean-arch/domain" ) type ResponseError struct { Message string `json:"message"` } type ArticleService interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } // ArticleHandler는 기사에 대한 HTTP 핸들러를 나타냅니다. type ArticleHandler struct { Service ArticleService } func NewArticleHandler(e *echo.Echo, svc ArticleService) { handler := &ArticleHandler{ Service: svc, } e.GET("/articles/:id", handler.GetByID) } func (a *ArticleHandler) GetByID(c echo.Context) error { idP, err := strconv.Atoi(c.Param("id")) if err != nil { return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error()) } id := int64(idP) ctx := c.Request().Context() art, err := a.Service.GetByID(ctx, id) if err != nil { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } return c.JSON(http.StatusOK, art) }
go-clean-arch 프로젝트의 기본 코드 아키텍처는 다음과 같습니다.
go-clean-arch/
├── internal/
│ ├── rest/
│ │ └── article.go # 전달 레이어
│ ├── repository/
│ │ ├── mysql/
│ │ │ └── article.go # 리포지토리 레이어
├── article/
│ └── service.go # 유스케이스/서비스 레이어
├── domain/
│ └── article.go # 모델 레이어
go-clean-arch 프로젝트에서 각 레이어 간의 종속성은 다음과 같습니다.
- 유스케이스/서비스 레이어는 리포지토리 인터페이스에 의존하지만 인터페이스의 구현 세부 사항은 알지 못합니다.
- 리포지토리 레이어는 인터페이스를 구현하지만 도메인 레이어의 엔티티에 의존하는 외부 구성 요소입니다.
- 전달 레이어(예: REST 핸들러)는 유스케이스/서비스 레이어를 호출하고 외부 요청을 비즈니스 로직 호출로 변환하는 역할을 합니다.
이 디자인은 종속성 역전 원칙을 따르므로 핵심 비즈니스 로직이 외부 구현 세부 사항과 독립되어 테스트 가능성과 유연성이 향상됩니다.
요약
이 기사에서는 Uncle Bob의 클린 아키텍처와 go-clean-arch 샘플 프로젝트를 결합하여 Go 프로젝트에서 클린 아키텍처를 구현하는 방법을 소개했습니다. 시스템을 핵심 엔티티, 유스 케이스, 인터페이스 어댑터 및 외부 프레임워크와 같은 레이어로 나누어 관심사를 명확하게 분리하고 핵심 비즈니스 로직(Use Cases)이 프레임워크 및 데이터베이스와 같은 외부 구현 세부 사항과 분리됩니다.
go-clean-arch 프로젝트 아키텍처는 코드를 레이어 방식으로 구성하고 각 레이어에 대한 명확한 책임을 부여합니다.
- 모델 레이어(도메인 레이어): 핵심 비즈니스 엔티티를 정의하고 외부 구현과 독립적입니다.
- 유스케이스 레이어: 애플리케이션 로직을 구현하여 엔티티와 외부 상호 작용을 조정합니다.
- 리포지토리 레이어: 데이터 스토리지의 특정 세부 사항을 구현합니다.
- 전달 레이어: 외부 요청을 처리하고 결과를 반환합니다.
이것은 단지 샘플 프로젝트일 뿐입니다. 실제 프로젝트의 아키텍처 설계는 실제 요구 사항, 팀 개발 습관 및 표준에 따라 유연하게 조정되어야 합니다. 핵심 목표는 레이어링 원칙을 유지하고 코드를 이해하고 테스트하고 유지 관리하기 쉽게 만들고 시스템의 장기적인 확장성과 발전을 지원하는 것입니다.
저희는 Go 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무료로 무제한 프로젝트 배포
- 사용량에 대해서만 비용을 지불하십시오. 요청도, 요금도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 전혀 없습니다. 빌드에만 집중하십시오.
설명서에서 자세히 알아보십시오!
X에서 팔로우하십시오: @LeapcellHQ