Skip to content

Commit

Permalink
section62 정리
Browse files Browse the repository at this point in the history
  • Loading branch information
myeunee committed Nov 3, 2024
1 parent d80f60a commit 80cf6ee
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 0 deletions.
2 changes: 2 additions & 0 deletions chapter16/section62/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.git
.DS_Store
36 changes: 36 additions & 0 deletions chapter16/section62/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Build stage
# 최신 버전으로 수정 ******
FROM golang:1.23.1 AS deploy-builder

WORKDIR /app

# go.mod, go.sum 파일 복사 및 의존성 다운로드
COPY go.mod go.sum ./
RUN go mod download

COPY . .
# 바이너리 빌드
RUN go build -trimpath -ldflags "-w -s" -o app

#--------------------------------
# Deployment stage
# 최신 GLIBC 버전을 사용하기 위해 우분투로 교체

FROM ubuntu:latest as deploy

RUN apt-get update && apt-get install -y libgcc-s1

# 빌드된 바이너리를 복사
COPY --from=deploy-builder /app/app .

# 실행 명령
CMD ["./app"]

#--------------------------------
# Development stage with Air for live reloading

# ******** 최신 버전으로 수정 ***********
FROM golang:1.23.1 as dev
WORKDIR /app
RUN go install github.com/air-verse/air@latest
CMD ["air"]
28 changes: 28 additions & 0 deletions chapter16/section62/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.PHONY: help build build-local up down logs ps test
.DEFAULT_GOAL := help

DOCKER_TAG := latest
build: ## 배포용 도커 이미지 빌드 + 내 모듈 이름에 맞게 수정 **********
docker build -t myeunee/golangstudy-chapter16-section62:${DOCKER_TAG} --target deploy ./

build-local: ## 로컬 환경용 도커 이미지 빌드
docker compose build --no-cache

up: ## 자동 새로고침을 사용한 도커 컴포즈 실행
docker compose up -d

down: ## 도커 컴포즈 종료
docker compose down

logs: ## 도커 컴포즈 로그 출력
docker compose logs -f

ps: ## 컨테이너 상태 확인
docker compose ps

test: ## 테스트 실행
go test -race -shuffle=on ./...

help: ## 옵션 보기
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
33 changes: 33 additions & 0 deletions chapter16/section62/air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
root = "."
tmp_dir = "tmp"

[build]
cmd = "go build -o ./tmp/main ."
# 'cmd'에서 바이너리 파일 지정
bin = "tmp/main"

# 80번 포트를 사용하도록 실행 시 인수를 지정
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main 80"

include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["assets", "tmp", "vendor"]
exclude_regex = ["_test.go"]
exclude_unchanged = true
follow_symlink = true
log = "air.log"
delay = 1000
stop_on_error = true
send_interrupt = false
kill_delay = 500

[log]
time = false

[color]
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
clean_on_exit = true
18 changes: 18 additions & 0 deletions chapter16/section62/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package config

import (
"github.com/caarlos0/env/v6"
)

type Config struct {
Env string `env:"TODO_ENV" envDefault:"dev"`
Port int `env:"PORT" envDefault:"80"`
}

func New() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return nil, err
}
return cfg, nil
}
23 changes: 23 additions & 0 deletions chapter16/section62/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import (
"fmt"
"testing"
)

func TestNew(t *testing.T) {
wantPort := 3333
t.Setenv("PORT", fmt.Sprint(wantPort))

got, err := New()
if err != nil {
t.Fatalf("cannot create config: %v", err)
}
if got.Port != wantPort {
t.Errorf("want %d, but %d", wantPort, got.Port)
}
wantEnv := "dev"
if got.Env != wantEnv {
t.Errorf("want %s, but %s", wantEnv, got.Env)
}
}
14 changes: 14 additions & 0 deletions chapter16/section62/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: "3.9"
services:
app:
image: gotodo
build:
args:
- target=dev
environment:
TODO_ENV: dev
PORT: 8080
volumes:
- .:/app
ports:
- "18000:8080"
8 changes: 8 additions & 0 deletions chapter16/section62/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/myeunee/GolangStudy/chapter16/section62

go 1.23.1

require (
github.com/caarlos0/env/v6 v6.10.1
golang.org/x/sync v0.8.0
)
4 changes: 4 additions & 0 deletions chapter16/section62/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
35 changes: 35 additions & 0 deletions chapter16/section62/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"context"
"fmt"
"log"
"net"
"os"

"github.com/myeunee/GolangStudy/chapter16/section62/config"
)

func main() {
if err := run(context.Background()); err != nil {
log.Printf("failed to terminated server: %v", err)
os.Exit(1)
}
}

// 리팩토링한 run 함수
func run(ctx context.Context) error {
cfg, err := config.New()
if err != nil {
return err
}
l, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port))
if err != nil {
log.Fatalf("failed to listen port %d: %v", cfg.Port, err)
}
url := fmt.Sprintf("http://%s", l.Addr().String())
log.Printf("start with: %v", url)
mux := NewMux()
s := NewServer(l, mux)
return s.Run(ctx)
}
13 changes: 13 additions & 0 deletions chapter16/section62/mux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import "net/http"

func NewMux() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// 정적 분석 오류를 회피하기 위해 명시적으로 반환값을 버린다.
_, _ = w.Write([]byte(`{"status": "ok"}`))
})
return mux
}
28 changes: 28 additions & 0 deletions chapter16/section62/mux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"io"
"net/http"
"net/http/httptest"
"testing"
)

func TestNewMux(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/health", nil)
sut := NewMux()
sut.ServeHTTP(w, r)
resp := w.Result()
t.Cleanup(func() { _ = resp.Body.Close() })
if resp.StatusCode != http.StatusOK {
t.Error("want status code 200, but", resp.StatusCode)
}
got, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read body: %v", err)
}
want := `{"status": "ok"}`
if string(got) != want {
t.Errorf("want %q, but got %q", want, got)
}
}
48 changes: 48 additions & 0 deletions chapter16/section62/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"context"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"

"golang.org/x/sync/errgroup"
)

type Server struct {
srv *http.Server
l net.Listener
}

func NewServer(l net.Listener, mux http.Handler) *Server {
return &Server{
srv: &http.Server{Handler: mux},
l: l,
}
}

func (s *Server) Run(ctx context.Context) error {
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
// http.ErrServerClosed는
// http.Server.Shutdown()이 정상 종료한 것을 보여주는 것으로 아무 문제 없음
if err := s.srv.Serve(s.l); err != nil &&
err != http.ErrServerClosed {
log.Printf("failed to close: %+v", err)
return err
}
return nil
})

<-ctx.Done()
if err := s.srv.Shutdown(context.Background()); err != nil {
log.Printf("failed to shutdown: %+v", err)
}
// 정상 종료를 기다림
return eg.Wait()
}
54 changes: 54 additions & 0 deletions chapter16/section62/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"context"
"fmt"
"io"
"net"
"net/http"
"testing"

"golang.org/x/sync/errgroup"
)

func TestServer_Run(t *testing.T) {
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("failed to listen port %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
eg, ctx := errgroup.WithContext(ctx)
mux := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})

eg.Go(func() error {
s := NewServer(l, mux)
return s.Run(ctx)
})

in := "message"
url := fmt.Sprintf("http://%s/%s", l.Addr().String(), in)
// // 어떤 포트로 리슨하고 있는지 확인
t.Logf("try request to %q", url)
rsp, err := http.Get(url)
if err != nil {
t.Errorf("failed to get: %+v", err)
}
defer rsp.Body.Close()
got, err := io.ReadAll(rsp.Body)
if err != nil {
t.Fatalf("failed to read body: %v", err)
}
// http 서버의 반환값을 검증
want := fmt.Sprintf("Hello, %s!", in)
if string(got) != want {
t.Errorf("want %q, but got %q", want, got)
}
// run함수에 종료 알림을 전송
cancel()
// run함수의 반환값을 검증
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
}
Binary file added chapter16/section62/tmp/main
Binary file not shown.

0 comments on commit 80cf6ee

Please sign in to comment.