Skip to content

Latest commit

 

History

History
419 lines (331 loc) · 12.3 KB

File metadata and controls

419 lines (331 loc) · 12.3 KB

Go Backend Template

English | 한국어

Go로 백엔드 서버 만들 때마다 "이번엔 구조를 좀 더 예쁘게 짜봐야지" 하고 시작해서, 결국 프로젝트 중반에 갈아엎기를 반복하다, 드디어 정착한 구조.

더 이상 리팩토링 지옥에 빠지지 않길 바라며, 그리고 미래의 내가 "그때 어떻게 했더라? 할 때 참고하려고 만든 레포.

레이어드 아키텍처 기반의 Go 백엔드 템플릿입니다.

목차

아키텍처

이 템플릿은 레이어드 아키텍처 패턴을 따릅니다:

Route → Middleware → Handler → Service → Repository

디렉토리 구조

go-backend-template/
├── cmd/
│   └── server/                        # 애플리케이션 진입점
│       ├── main.go
│       └── config.go
├── internal/
│   ├── app/
│   │   └── server/                    # HTTP 서버 구현
│   │       ├── server.go
│   │       ├── handler/               # HTTP 핸들러 (컨트롤러)
│   │       │   ├── base.go            # 공통 에러 처리
│   │       │   ├── context.go         # 컨텍스트 헬퍼
│   │       │   └── user/              # User 도메인 핸들러
│   │       │       ├── handler.go
│   │       │       └── dto.go         # 요청/응답 DTO
│   │       ├── middleware/            # HTTP 미들웨어
│   │       │   └── auth/
│   │       │       └── auth.go
│   │       ├── routes/                # 라우트 정의
│   │       │   ├── routes.go
│   │       │   └── user.go
│   │       └── service/               # 비즈니스 로직 레이어
│   │           └── user/
│   │               ├── service.go
│   │               ├── input.go       # 서비스 입력 타입
│   │               └── dependencies.go # 의존성 인터페이스
│   └── pkg/                           # 공유 내부 패키지
│       ├── auth/                      # 인증 유틸리티
│       │   ├── jwt.go
│       │   └── password.go
│       ├── domain/                    # 도메인 에러
│       │   └── errors.go
│       ├── entity/                    # 도메인 엔티티
│       │   └── user.go
│       └── repository/                # 데이터 접근 레이어
│           ├── errors.go              # 공통 Repository 에러
│           └── postgres/
│               ├── repository.go
│               ├── schema.go          # 테이블 스키마
│               └── user.go
├── build/
│   └── Dockerfile
├── deployments/
│   ├── docker-compose.yml
│   └── .env.example
├── go.mod
├── go.sum
├── Makefile
└── README.md

레이어별 책임

1. Handler 레이어 (handler/)

  • HTTP 요청 파싱 (경로 파라미터, 쿼리, 바디)
  • 요청 형식 유효성 검증
  • 요청 DTO를 서비스 입력으로 변환
  • 서비스 메서드 호출
  • 서비스 출력을 응답 DTO로 변환
  • 에러 처리 및 HTTP 응답 전송

2. Service 레이어 (service/)

  • 비즈니스 로직 구현
  • 레포지토리 호출 조율
  • 도메인 에러 반환
  • HTTP 관련 코드 없음

3. Repository 레이어 (repository/)

  • 데이터베이스 접근
  • SQL 쿼리
  • 데이터베이스 에러 반환

4. Domain 레이어 (domain/, entity/)

  • 도메인 엔티티
  • HTTP 상태 매핑이 포함된 도메인 에러

주요 구현 내용

Service Input

Handler의 DTO와 Service의 Input을 분리. Handler는 HTTP 요청/응답에 집중하고, Service는 비즈니스 로직에 집중.

// handler/user/dto.go - HTTP 요청용
type CreateUserRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
}

// service/user/input.go - 비즈니스 로직용
type CreateUserInput struct {
    Email    string
    Username string
    Password string
}

도메인 에러

도메인 에러에 HTTP 상태 코드 매핑을 포함. Handler에서 일관된 에러 처리 가능.

// domain/errors.go
type DomainError interface {
    error
    HTTPStatus() int
}

type UserNotFoundError struct {
    Id int
}

func (e UserNotFoundError) Error() string {
    return fmt.Sprintf("user not found with id: %d", e.Id)
}

func (e UserNotFoundError) HTTPStatus() int {
    return http.StatusNotFound
}

의존성 주입

Service는 구체적인 구현이 아닌 인터페이스에 의존. 테스트 시 모킹 용이.

// service/user/dependencies.go
type IUserRepository interface {
    GetUserById(id int) (*entity.User, error)
    InsertUser(user *entity.User) (int, error)
    // ...
}

type IPasswordHasher interface {
    Hash(password string) (string, error)
    Compare(hashedPassword, password string) error
}

BaseHandler

공통 에러 처리 로직을 BaseHandler에 구현. 도메인별 Handler에서 임베딩하여 재사용.

// handler/base.go
type BaseHandler struct{}

func (b *BaseHandler) HandleDomainError(c *gin.Context, err error) {
    if domainErr, ok := err.(domain.DomainError); ok {
        c.AbortWithStatusJSON(domainErr.HTTPStatus(), gin.H{"message": domainErr.Error()})
        return
    }
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
}

// handler/user/handler.go
type UserHandler struct {
    handler.BaseHandler  // 임베딩
    userService *user.Service
}

시작하기

사전 요구사항

  • Go 1.21+
  • PostgreSQL 16+
  • Docker & Docker Compose (선택)

빌드

# 바이너리 빌드
make build

# Docker 이미지 빌드
make docker-build

테스트

# 모든 테스트 실행
make test

# 커버리지와 함께 테스트 실행
make test-coverage

# 특정 패키지 테스트 실행
go test -v ./internal/app/server/service/user/...
go test -v ./internal/app/server/handler/user/...
go test -v ./internal/pkg/auth/...
go test -v ./internal/pkg/repository/postgres/...

# 레이스 디텍션과 함께 테스트 실행
go test -race ./...

테스트

Service 레이어 테스트 (service/*_test.go)

  • 레포지토리 인터페이스 모킹
  • 패스워드 해셔 모킹
  • 비즈니스 로직 격리 테스트
  • 도메인 에러 반환 검증
type MockUserRepository struct {
    mock.Mock
}

func (m *MockUserRepository) GetUserById(id int) (*entity.User, error) {
    args := m.Called(id)
    return args.Get(0).(*entity.User), args.Error(1)
}

Handler 레이어 테스트 (handler/*_test.go)

  • httptest를 사용한 HTTP 테스트
  • 서비스 레이어 모킹
  • 요청 파싱 및 유효성 검증 테스트
  • 응답 포맷팅 테스트
router := gin.New()
router.POST("/users", handler.CreateUser)

req := httptest.NewRequest(http.MethodPost, "/users", body)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusCreated, w.Code)

Repository 레이어 테스트 (repository/*_test.go)

  • 쿼리 빌딩 단위 테스트
  • 테스트 데이터베이스를 사용한 통합 테스트 (기본적으로 스킵)
  • 설정 유효성 검증 테스트

Auth 패키지 테스트 (auth/*_test.go)

  • JWT 토큰 생성 및 검증
  • 패스워드 해싱 및 비교
  • 엣지 케이스 (만료된 토큰, 잘못된 패스워드)

새 도메인 추가하기

  1. internal/pkg/entity/에 엔티티 생성
  2. internal/pkg/domain/errors.go에 도메인 에러 추가
  3. internal/pkg/repository/postgres/에 레포지토리 메서드 생성
  4. internal/app/server/service/<domain>/에 서비스 생성
    • dependencies.go - 레포지토리 인터페이스
    • input.go - 서비스 입력 타입
    • service.go - 비즈니스 로직
  5. internal/app/server/handler/<domain>/에 핸들러 생성
    • dto.go - 요청/응답 DTO
    • handler.go - HTTP 핸들러
  6. internal/app/server/routes/<domain>.go에 라우트 추가
  7. server.go에서 연결

구조 확장 시 고려사항

프로젝트가 커질 때 참고할 수 있는 가이드입니다.

도메인이 많아질 때

도메인별로 완전히 분리하는 구조 고려:

internal/
├── user/           # user 도메인 전체
│   ├── handler/
│   ├── service/
│   ├── repository/
│   └── entity/
├── order/          # order 도메인 전체
│   └── ...

도메인 에러 분리

도메인이 많아지면 에러도 파일별로 분리:

internal/pkg/domain/
├── errors.go           # 공통 에러 (InternalServerError, ValidationError 등)
├── user_errors.go      # User 도메인 에러
├── order_errors.go     # Order 도메인 에러
└── product_errors.go   # Product 도메인 에러

또는 도메인별 완전 분리 시:

internal/user/domain/errors.go
internal/order/domain/errors.go

서비스 간 의존성

순환 의존성이 발생했다면, 먼저 설계를 재검토할 것.

순환 의존성은 책임 분리가 잘못되었거나, 상위 레이어에서 조율해야 할 로직이 서비스에 들어간 경우가 많음. 인터페이스 분리는 기술적 해결책일 뿐, 근본적인 해결이 아닐 수 있음.

설계 재검토 예시:

// Before: UserService가 삭제 전 주문 확인을 직접 함
type UserService struct {
    orderService *OrderService  // 순환 발생
}

// After: 상위 레이어(Handler)에서 조율
func (h *Handler) DeleteUser(userId int) error {
    if hasOrders, _ := h.orderService.HasActiveOrders(userId); hasOrders {
        return errors.New("active orders exist")
    }
    return h.userService.Delete(userId)
}

차선책: 인터페이스로 분리

설계 변경이 어려운 경우, 각 서비스가 필요한 기능만 인터페이스로 정의:

type IUserGetter interface {
    GetUserById(id int) (*entity.User, error)
}

type IOrderChecker interface {
    HasActiveOrders(userId int) (bool, error)
}

type UserService struct {
    orderChecker IOrderChecker  // 인터페이스에 의존
}

type OrderService struct {
    userGetter IUserGetter      // 인터페이스에 의존
}

트랜잭션 관리

여러 Repository를 하나의 트랜잭션으로 묶어야 할 때 고려할 수 있는 Unit of Work 패턴:

type UnitOfWork interface {
    Begin() error
    Commit() error
    Rollback() error
    Users() IUserRepository
    Orders() IOrderRepository
}

ORM / Query Builder 도입

현재 템플릿은 raw SQL을 사용. 쿼리가 복잡해지면 다음 도구들을 고려:

도구 특징
sqlx database/sql 확장. 구조체 매핑, Named Query 지원
sqlc SQL → Go 코드 생성. 타입 안전, 컴파일 타임 검증
squirrel SQL 쿼리 빌더. Fluent API, 동적 쿼리 생성에 강함
goqu 쿼리 빌더. 다양한 DB 방언 지원, 활발한 유지보수
GORM 풀 ORM. 마이그레이션, 관계 매핑, 훅 지원
ent Facebook 개발. 스키마 기반 코드 생성, 그래프 순회
Bun 경량 ORM. PostgreSQL 기능 잘 지원

선택 기준:

  • 단순 CRUD 위주 → sqlx
  • 동적 쿼리 생성 → squirrel, goqu
  • 타입 안전성 중시 → sqlc
  • 복잡한 관계/마이그레이션 필요 → GORM, ent

기타 고려사항

상황 고려할 패턴/도구
조회 성능 이슈 캐싱 레이어 (Redis)
비동기 처리 필요 이벤트 기반 아키텍처
API 하위 호환성 API 버저닝 (/api/v1/, /api/v2/)
서비스 규모 폭발 마이크로서비스 분리 검토
환경별 설정 관리 환경별 config 파일
분산 추적 필요 OpenTelemetry