728x90

sqlc 사용기: Go + PostgreSQL, SQL 기능을 하는 함수 생성

 

 

 

 

1. Intro

sqlc라 최고라는 말을 듣고 적용해보았다!.. 이 도구는 SQL 스키마와 쿼리를 기반으로 타입 안전한 Go 코드를 생성해주어, 안정성과 개발 생산성을 높여준다고 합니다. 이번 글에서는 sqlc를 사용하여 PostgreSQL 데이터베이스와 상호작용하는 방법에 대해 살펴보겠습니다.

2. sqlc 설정

sqlc를 사용하기 위해서는 먼저 sqlc.yaml 설정 파일을 생성해야 합니다. 이 파일에는 데이터베이스 엔진, 쿼리 파일, 스키마 파일, 그리고 생성된 Go 코드의 출력 위치 등을 지정합니다.

version: "2"
cloud:
  project: "xxxxxxxxxxx"
sql:
  - engine: "postgresql"
    queries: "query.sql"
    schema: "schema.sql"
    database:
      uri: "postgres://postgres:xxxxxxxx@xxx-rds.xxxxxxx.ap-xxxxxxxx-2.rds.amazonaws.com:5432/postgres"
    gen:
      go:
        package: "db"
        out: "db"
        sql_package: "pgx/v5"

 

 

3. 스키마 정의

schema.sql 파일에 데이터베이스 테이블 구조를 정의합니다. 예를 들어, authors 테이블을 다음과 같이 정의할 수 있습니다

CREATE TABLE authors (
  id   BIGSERIAL PRIMARY KEY,
  name text      NOT NULL,
  bio  text
);

테이블 정보를 담은 DDL을 실행해 줍니다. 자동으로 마이그레이션을 해주고, 마이그레이션 파일을 저장해주는 라이브러리가 있다고 하는데, 다음에 적용해볼 예정입니다.

 

4. 쿼리 작성

query.sql 파일에 필요한 데이터베이스 쿼리를 작성합니다. sqlc는 이 쿼리들을 분석하여 Go 함수로 변환합니다.

-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1 LIMIT 1;

-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;

-- name: CreateAuthor :one
INSERT INTO authors (
  name, bio
) VALUES (
  $1, $2
)
RETURNING *;

-- name: UpdateAuthor :exec
UPDATE authors
  set name = $2,
  bio = $3
WHERE id = $1;

-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1;

 

 

5. 코드 생성

sqlc 설정이 완료되면, 다음 명령어를 실행하여 Go 코드를 생성합니다

sqlc generate

이 명령은 db/ 폴더 내에 query.sql.go, models.go, db.go 파일을 생성합니다. 이 파일들에는 데이터베이스 SQL 구문을 실행하기 위한  함수들이 포함되어 있습니다.


6. 생성된 코드 사용

생성된 코드를 사용하여 데이터베이스 작업을 수행할 수 있습니다. 예를 들어

// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.26.0
// source: query.sql

package db

import (
	"context"

	"github.com/jackc/pgx/v5/pgtype"
)

const createAuthor = `-- name: CreateAuthor :one
INSERT INTO authors (
  name, bio
) VALUES (
  $1, $2
)
RETURNING id, name, bio
`

type CreateAuthorParams struct {
	Name string
	Bio  pgtype.Text
}

func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) {
	row := q.db.QueryRow(ctx, createAuthor, arg.Name, arg.Bio)
	var i Author
	err := row.Scan(&i.ID, &i.Name, &i.Bio)
	return i, err
}

const deleteAuthor = `-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1
`

func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error {
	_, err := q.db.Exec(ctx, deleteAuthor, id)
	return err
}

const getAuthor = `-- name: GetAuthor :one
SELECT id, name, bio FROM authors
WHERE id = $1 LIMIT 1
`

func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
	row := q.db.QueryRow(ctx, getAuthor, id)
	var i Author
	err := row.Scan(&i.ID, &i.Name, &i.Bio)
	return i, err
}

const listAuthors = `-- name: ListAuthors :many
SELECT id, name, bio FROM authors
ORDER BY name
`

func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
	rows, err := q.db.Query(ctx, listAuthors)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Author
	for rows.Next() {
		var i Author
		if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateAuthor = `-- name: UpdateAuthor :exec
UPDATE authors
  set name = $2,
  bio = $3
WHERE id = $1
`

type UpdateAuthorParams struct {
	ID   int64
	Name string
	Bio  pgtype.Text
}

func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error {
	_, err := q.db.Exec(ctx, updateAuthor, arg.ID, arg.Name, arg.Bio)
	return err
}
import (
    "context"
    "your-project/db"
)

func main() {
    // 데이터베이스 연결 설정
    conn, err := pgx.Connect(context.Background(), "your-database-url")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(context.Background())

    queries := db.New(conn)

    // 새 저자 생성
    author, err := queries.CreateAuthor(context.Background(), db.CreateAuthorParams{
        Name: "John Doe",
        Bio: pgtype.Text{String: "A prolific writer", Valid: true},
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created author: %v\n", author)

    // 저자 목록 조회
    authors, err := queries.ListAuthors(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    for _, a := range authors {
        fmt.Printf("Author: %s\n", a.Name)
    }
}

sqlc를 사용하면 데이터베이스 스키마, SQL 쿼리와 Go 코드 사이의 불일치를 줄이고, type-safe을 보장하며, 개발 생산성을 향상시킬 수 있습니다. 또한 데이터베이스 스키마 변경 시 자동으로 코드를 업데이트할 수 있어 유지보수가 용이하다고 합니다.

결론적으로, sqlc는 Go와 PostgreSQL을 함께 사용하는 프로젝트에서 강력한 도구가 될 수 있습니다. 타입 안전한 데이터베이스 액세스 코드를 자동으로 생성함으로써 개발자는 비즈니스 로직에 더 집중할 수 있게 되며, 데이터베이스 관련 오류를 크게 줄일 수 있는 장점이 있다고 합니다.

읽어주셔서 감사합니다 ☺️

 

 

 

728x90

배포 자동화: Go + Docker + ECS + Fargate + ECR + Github Action

 

 

1. Intro

인프라에 대해 공부해보다가 Go + Docker + ECS + Fargate + ECR + Github Action으로 CI/CD를 구성했다는 블로그 글을 보고, 따라해 보았다.

 

2. Dockerfile 만들기

Dockerfile은 Docker 이미지를 생성하기 위한 설정 파일입니다. Go + gin backend를 위한 Dockerfile을 만들어 보겠습니다.

FROM golang:alpine AS builder

ENV GO111MODULE=on \
    CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64

WORKDIR /build

COPY go.mod go.sum main.go ./

RUN go mod download

RUN go build -o main .

WORKDIR /dist

RUN cp /build/main .

FROM scratch

COPY --from=builder /dist/main .

ENTRYPOINT ["/main"]

 

 

3. ECS 만들기 (Fargate vs EC2)

Amazon ECS(Elastic Container Service)는 컨테이너화된 애플리케이션을 쉽게 실행하고 관리할 수 있는 완전 관리형 컨테이너 오케스트레이션 서비스입니다. ECS를 사용할 때 두 가지 주요 실행 모드가 있습니다 (Fargate, EC2). 생성할 때 두 개 중 하나를 선택할 수 있습니다. 저는 Fargate를 선택했습니다.

Fargate

서버리스 옵션으로, 인프라 관리 없이 컨테이너를 실행할 수 있습니다. 확장성이 뛰어나고 관리가 쉽습니다.

EC2

사용자가 직접 EC2 인스턴스를 관리하며, 더 많은 제어와 커스터마이징이 가능합니다.

예전에는 EC2 + Code Deploy 조합으로 배포 인프라 구성을 하곤 했는데, 이번에는 Fargate를 시도해보고자 합니다.

Fargate는 인프라 관리 부담을 줄이고 싶은 경우에 적합하며, EC2는 더 세밀한 제어가 필요한 경우에 선택한다고 합니다.

클러스터 (Cluster) 개념

ECS의 최상위 리소스 그룹입니다.
여러 개의 EC2 인스턴스 또는 Fargate 작업을 논리적으로 그룹화합니다. (저는 Fargate를 사용했습니다.)
한 클러스터 내에서 여러 서비스와 태스크를 실행할 수 있습니다.

태스크 (Task) 개념

애플리케이션의 최소 배포 단위입니다.
하나 이상의 컨테이너로 구성됩니다.
태스크 정의(Task Definition)를 통해 실행할 컨테이너, 사용할 리소스 등을 정의합니다.



서비스 (Service)

지정된 수의 태스크를 클러스터에서 동시에 실행하고 관리합니다.
태스크가 중지되면 자동으로 새 태스크를 시작하여 지정된 개수를 유지합니다. (저는 1개를 지정해서 최대 1개만 실행됩니다.)
로드 밸런서와 연결하여 트래픽을 분산할 수 있다고 합니다. (요금 주의)



컨테이너 (Container)

Docker 이미지를 기반으로 실행되는 격리된 실행 환경입니다. (ECR에 push하고 태그로 관리됩니다)
go + gin 으로 구성된 프로젝트를 Docker로 빌드했습니다. 
태스크 내에서 하나 이상의 컨테이너가 함께 실행될 수 있습니다.

 

4. 인바운드 규칙 편집

8080 포트 열기 (인바운드 규칙 편집)

ECS 태스크가 실행되는 Fargate 태스크에 연결된 보안 그룹의 인바운드 규칙을 편집해야 합니다.
ECS -> 클러스터 -> 서비스 -> 보안 그룹으로 이동합니다.
해당 보안 그룹을 선택하고 "인바운드 규칙 편집"을 클릭합니다.

유형: 사용자 지정 TCP
포트 범위: 8080
소스: 필요에 따라 특정 IP 범위 또는 모든 트래픽(0.0.0.0/0)을 선택

저는 8080포트를 열었으므로, 8080포트를 외부에서 접근 가능하도록 개방했습니다.



이렇게 설정하면 외부에서 8080 포트를 통해 ECS에서 실행 중인 go backend app에 접근할 수 있게 됩니다. 단, 보안을 위해 가능한 한 접근을 제한적으로 설정하는 것이 좋습니다.
또한, 태스크 정의에서도 컨테이너 포트 매핑을 설정해야 합니다. 태스크 정의의 컨테이너 설정에서 8080 포트를 호스트 포트와 매핑해야 합니다.

 

4. ECR 만들기 (Docker 이미지 저장소)

Amazon ECR(Elastic Container Registry)은 Docker 컨테이너 이미지를 안전하게 저장, 관리 및 배포할 수 있는 완전 관리형 Docker 컨테이너 레지스트리입니다. ECR을 사용하면 프라이빗 레포지토리를 생성하여 Docker 이미지를 저장하고, ECS와 쉽게 통합할 수 있습니다.

 

5. Github Action (develop branch에 머지 시 워크플로우 실행)

develop 브랜치에 머지가 되었을 때 (이벤트 발생), Docker 이미지가 빌드되고 ECR에 push(업로드)된 다음에, 해당 도커 이미지를 ECS에서 실행되게 할 것입니다. (8080포트로 접근 가능하게)

GitHub Actions를 사용하면 이러한 CI/CD 파이프라인을 구축할 수 있습니다. develop 브랜치에 머지될 때 자동으로 빌드, 테스트, 배포 과정을 실행하도록 설정할 수 있습니다.

aws.yml 코드

name: Deploy to Amazon ECS

on:
  push:
    branches: [ "develop" ]

env:
  AWS_REGION: ap-northeast-2                   # set this to your preferred AWS region, e.g. us-west-1
  ECR_REPOSITORY: fye-backend-repository           # set this to your Amazon ECR repository name
  ECS_SERVICE: fye-backend-service                 # set this to your Amazon ECS service name
  ECS_CLUSTER: fye-cluster                 # set this to your Amazon ECS cluster name
  ECS_TASK_DEFINITION: ./fye-task.json # set this to the path to your Amazon ECS task definition
                                               # file, e.g. .aws/task-definition.json
  CONTAINER_NAME: fye-backend-container           # set this to the name of the container in the
                                               # containerDefinitions section of your task definition

permissions:
  contents: read

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build, tag, and push image to Amazon ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        IMAGE_TAG: ${{ github.sha }}
      run: |
        # Build a docker container and
        # push it to ECR so that it can
        # be deployed to ECS.
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
    - name: Fill in the new image ID in the Amazon ECS task definition
      id: task-def
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: ${{ env.ECS_TASK_DEFINITION }}
        container-name: ${{ env.CONTAINER_NAME }}
        image: ${{ steps.build-image.outputs.image }}

    - name: Deploy Amazon ECS task definition
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.task-def.outputs.task-definition }}
        service: ${{ env.ECS_SERVICE }}
        cluster: ${{ env.ECS_CLUSTER }}
        wait-for-service-stability: true

상단에 있는 env: 부분에서 지역과 ecs, ecr 관련 정보들을 넣습니다. json은 태스크 탭에서 다운로드할 수 있습니다. 그리고 iam을 생성해서 github secrets에 access key와 secret key를 등록하여 github action이 aws의 다양한 서비스를 조작할 수 있게 설정합니다. 저는 iam에 아래의 6개 권한을 추가했습니다. 그리고 .csv로 추출하여 github의 secret에 키를 등록했습니다.

6개 권한 추가
인프라 구성 성공!, 수많은 시도들..

 

6. 마무리 및 소감

No Docker + Code Deploy로 했을 때는 1~2분이면 서버 배포가 끝났는데, Docker + ECS로 해보니 생각보다 오래 걸려서 당황했다..

Docker Build 하는데만 48s, Deploy ECS Task Definition 하는 데만 4m 16s가 걸렸다. 총 5분.. 

다수가 협업할 때는 장점이 충분히 있을 것 같지만, 1인이나 소규모로 할 때는 No Docker + Code Deploy를 사용하는 게 속도면에서 좋을 것 같다.

728x90

Golang으로 백엔드 개발 도전기

Go 언어

 

1. Intro

간단한 백엔드를 만들일이 있어서 고민을 하다가, 유튜브 알고리즘에 뜬 아래 두 동영상을 보고 Golang Backend를 공부해야겠다고 다짐했습니다. 약 4년전, 공군 개발병 복무 시절, Golang + go gin 으로 간단한 REST API를 사지방에서 만들어보고 초당 몇번의 요청이 가능한지 테스트해보고 빨라서 놀랐던 기억이 있었는데, 그 시절이 떠오르네요.
[Golang 도입, 그리고 4년 간의 기록 / Golang과 함께 서버 레이턴시를 500배 개선한 후기] 
https://www.youtube.com/watch?v=75X_eBW0mog

https://www.youtube.com/watch?v=NVqVS64qClk

2. 왜 Golang인가?

간단한 백엔드 개발을 위한 언어를 선택하는 과정에서 가장 중요하게 생각한 것은 성능이 빠른지와 단순한지였습니다. Golang은 다음과 같은 매력적인 장점들이 있습니다.

빠른 컴파일 속도
Golang의 빠른 컴파일 속도는 개발 생산성을 크게 향상시킵니다. 공군 시절 Spring 프로젝트를 수정하고 컴파일 하는데 1분 가까이 걸려서 멍때렸던 기억이 있었는데, 아직 경험해보지 못했지만 go는 프로젝트가 커져도 빠른 컴파일이 된다고 합니다.
뛰어난 동시성 처리
Go routine과 채널을 통한 동시성 제어는 Golang의 큰 장점 중 하나입니다. 요즘 Sass(Software as a service)가 많아지면서 Third party app(GPT etc..)과 API 연동할 일이 많은데 이때 강점을 가집니다.
언어의 단순성
간결하고 읽기 쉬운 문법으로 빠르게 학습할 수 있습니다. 사실 다른 빠른 언어들도 있지만, 개인적으로 go 언어가 이해가 잘되었습니다.
성능 최적화
메모리와 CPU 사용을 쉽게 최적화할 수 있다고 합니다. 고성능 애플리케이션 개발에 많이 사용된다고 합니다.

물론, Golang에도 몇 가지 단점이 있습니다..

에러 처리의 복잡성
모든 에러를 명시적으로 처리해야 해서 코드가 길어질 수 있습니다.
GC 관련 이슈
대규모 시스템에서는 가비지 컬렉션으로 인한 성능 저하가 발생할 수 있습니다.
제네릭 지원 부족
제네릭 기능의 부재로 인해 코드 재사용성이 떨어질 수 있습니다.
특정 패턴 강제
에러 처리 등에서 특정 패턴을 따라야 하는 경우가 많습니다.

3. 선택한 기술 스택

Golang 커뮤니티를 탐색하면서 많은 개발자들이 추천하거나 github star가 많은 라이브러리를 찾았습니다. 제가 선택한 기술 스택은 다음과 같습니다:

Go Gin
경량화되고 빠른 웹 프레임워크로, RESTful API 개발에 적합합니다.
SQLC
SQL 쿼리를 Go 코드로 변환해주는 도구로, 타입 안정성과 성능을 모두 잡을 수 있습니다.
PostgreSQL with RDS
요즘 많이 쓰는 관계형 데이터베이스입니다.
Docker
개발 환경의 일관성을 유지할 수 있습니다.
Github Action CI/CD
easy..!

4. 특이했던 점

NestJS에서 사용했었던 schema.prisma처럼 migration 폴더에 데이터베이스 정보(PK, FK, Constraint, relation, type 등)를 기술하는 것. @Controller, @Service 같은 어노테이션이 없는것. pointer star (*), & 등 C 같은 느낌. 특이한 for문 등. 내일은 AWS RDS 데이터베이스 만들고, 연결해봐야겠다. 그리고 메모장같은 간단한 CRUD를 만들어보고 GPT나 Claude API 연동 시도해봐야겠다..! 

 
읽어주셔서 감사합니다 😊
 
 

+ Recent posts