ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Go + Fiber로 서버 초기 세팅하며 배운 것들 정리
    Development/golang 2026. 3. 22. 18:50
    반응형

    Go + Fiber로 서버 만들기: Java 개발자가 Go를 시작하며 배운 것들

    Go와 Fiber 프레임워크를 사용하여 고가용성 분산 시스템 서버의 초기 구조를 설계하면서 학습한 내용을 정리한다.
    https://github.com/icarus8050/peacock

    프로젝트 구조

    peacock/
    ├── main.go             # 진입점 (config → server → handler 연결)
    ├── config/
    │   └── config.go       # 환경변수 기반 설정 관리
    ├── server/
    │   └── server.go       # Fiber 앱 생성 + Graceful Shutdown
    └── handler/
        ├── handler.go      # 라우트 등록 중앙 관리
        └── health.go       # 헬스체크 엔드포인트 (/health, /ready)
    func main() {
        cfg := config.Load()
        srv := server.New(cfg)
        handler.Register(srv.App)
        if err := srv.Start(); err != nil {
            log.Fatal(err)
        }
    }

    go mod tidy

    go mod tidy는 Go 모듈 의존성을 정리하는 명령어다.

    • 코드에서 import한 외부 패키지를 go.mod에 추가하고 다운로드한다.
    • 더 이상 사용하지 않는 패키지는 go.mod에서 제거한다.
    • go.sum 파일을 생성하여 의존성의 체크섬(해시값)을 기록한다.

    go.sum은 다른 환경에서 동일한 의존성이 다운로드되었는지 검증하는 역할을 하므로, go.mod과 함께 버전 관리에 포함하는 것이 좋다.

    구조체 (struct)

    Go에는 class 키워드가 없다. 대신 구조체(struct) + 메서드로 동일한 역할을 한다.

    type Config struct {
        Port            string
        ReadTimeout     time.Duration
        WriteTimeout    time.Duration
        ShutdownTimeout time.Duration
    }

    Java와 비교하면 다음과 같다:

    public class Config {
        public String port;
        public Duration readTimeout;
        public Duration writeTimeout;
        public Duration shutdownTimeout;
    }

    *포인터를 의미하며, 값 자체를 복사하는 게 아니라 원본 객체의 메모리 주소를 참조한다. Java에서 객체 변수가 참조(reference)를 갖는 것과 같은 개념이다.

    type Server struct {
        App *fiber.App      // Fiber 앱에 대한 포인터
        Cfg *config.Config  // Config에 대한 포인터
    }

    생성자

    Go에는 생성자 문법이 없다. 관례적으로 New() 함수가 생성자 역할을 한다.

    func New(cfg *config.Config) *Server {
        app := fiber.New(fiber.Config{
            ReadTimeout:  cfg.ReadTimeout,
            WriteTimeout: cfg.WriteTimeout,
        })
        return &Server{App: app, Cfg: cfg}
    }

    &Server{...}는 Server 구조체를 만들고 그 포인터를 반환한다는 뜻이다.

    gofmt

    Go에는 gofmt라는 공식 포매터가 있어서, 코드 저장 시 자동으로 포맷을 정리해 준다. 구조체 필드의 타입 정렬도 gofmt가 자동으로 처리하므로 작성 시 포맷을 신경 쓸 필요가 없다.

    패키지와 파일명

    Go에서 파일명과 패키지명은 관계가 없다. 규칙은 하나뿐이다: 같은 디렉토리의 모든 .go 파일은 같은 패키지명을 사용해야 한다.

    handler/
    ├── handler.go    → package handler
    └── health.go     → package handler

    파일명이 무엇이든, 같은 디렉토리이므로 둘 다 package handler다.

    리시버 (Receiver)

    메서드가 어떤 타입에 속하는지를 지정하는 문법이다. Java의 this와 비슷한 역할을 한다.

    func (s *Server) Start() error {
        s.Cfg.Port       // Java의 this.cfg.port 와 동일
        s.App.Listen(...)
    }
    Go Java
    s *Server this
    s.Cfg this.cfg

    *Server에서 *는 포인터 리시버로, 원본 인스턴스를 참조하여 내부 필드를 수정할 수 있다. * 없이 Server로 쓰면 값이 복사되어 원본에 영향을 줄 수 없다.

    리시버의 제약

    리시버는 같은 패키지 안에서 정의된 구조체만 사용할 수 있다. 다른 패키지의 타입에 메서드를 추가하려면 래핑(wrapping)이나 임베딩으로 해결한다.

    임베딩 (Embedding)

    구조체 안에 다른 구조체를 필드 이름 없이 넣는 것이다.

    type ServerConfig struct {
        *config.Config  // 임베딩
    }
    
    sc := &ServerConfig{Config: config.Load()}
    sc.Port  // Config의 필드에 바로 접근 가능

    Java의 상속과 비슷하지만, Go에서는 조합(composition)이라는 다른 개념이다.

    Java 상속 Go 임베딩
    관계 "is-a" (ServerConfig Config이다) "has-a" (ServerConfig Config을 갖고 있다)
    다형성 부모 타입으로 사용 가능 불가능
    다중 단일 상속만 여러 구조체 임베딩 가능

    에러 처리: if 초기화문

    Go에는 try/catch가 없다. 함수가 에러를 반환값으로 돌려주고, if err != nil 패턴으로 처리한다.

    if err := srv.Start(); err != nil {
        log.Fatal(err)
    }

    ; 앞이 초기화문(함수 실행 및 결과 저장), 뒤가 조건문(에러 여부 확인)이다. 위 코드는 아래와 동일하다:

    err := srv.Start()
    if err != nil {
        log.Fatal(err)
    }

    초기화문을 사용하면 err 변수의 스코프가 if 블록 안으로 제한되는 장점이 있다.

    고루틴 (Goroutine)과 채널 (Channel)

    go func()

    go 키워드로 고루틴을 생성하여 함수를 비동기로 실행한다. Java의 스레드와 비슷하지만 훨씬 가볍다.

    go func() {
        s.App.Listen(addr)
    }()

    func() { ... }()는 익명 함수를 즉시 실행하는 문법이며, 마지막 ()가 호출 부분이다.

    chan과 <- 연산자

    chan은 고루틴 간에 데이터를 주고받는 채널이다.

    ch := make(chan string)    // string 채널 생성
    ch := make(chan string, 5) // 버퍼 크기 5인 채널
    
    ch <- "hello"   // 채널에 값 보내기 (채널이 왼쪽)
    msg := <-ch     // 채널에서 값 받기 (채널이 오른쪽, 값 올 때까지 대기)

    select

    여러 채널을 동시에 기다리는 문법이다. 먼저 값이 도착한 채널의 case가 실행된다.

    select {
    case err := <-errCh:    // 서버에 에러가 발생하거나
        return err
    case sig := <-quit:     // 종료 시그널(Ctrl+C)이 오거나
        log.Printf("received signal %s, shutting down...", sig)
    }

    select는 채널에 값이 올 때까지 블로킹된다. server.go에서 고루틴이 서버를 별도로 실행하고, Start() 함수는 select에서 대기 상태에 진입한다. 이 대기가 없으면 main 함수가 바로 끝나면서 프로그램이 종료된다.

    defer

    함수가 끝날 때 실행할 코드를 예약하는 키워드다. Java의 finally와 비슷하다.

    ctx, cancel := context.WithTimeout(context.Background(), s.Cfg.ShutdownTimeout)
    defer cancel()  // 함수 종료 시 cancel() 자동 호출 → 타이머 리소스 해제

    cancel()context.WithTimeout이 반환하는 컨텍스트 취소 함수다. defer로 호출하는 이유는 shutdown이 타임아웃 전에 완료되더라도 컨텍스트 내부의 타이머 리소스를 해제해야 하기 때문이다. 호출하지 않으면 메모리 누수가 발생할 수 있다.

    여러 번의 defer

    defer는 한 함수 내에서 여러 번 호출 가능하며, 역순(LIFO)으로 실행된다.

    func process() error {
        file, _ := os.Open("data.txt")
        defer file.Close()           // 2번째로 실행
    
        conn, _ := db.Connect()
        defer conn.Close()           // 1번째로 실행
    
        // 작업 수행...
    }

    나중에 열린 리소스가 먼저 닫히므로, 의존 관계가 있을 때 안전한 순서가 보장된다.

    fmt.Sprintf

    문자열을 포맷팅하여 반환하는 함수다. Java의 String.format()과 동일하다.

    addr := fmt.Sprintf(":%s", s.Cfg.Port)  // ":3000"

    자주 쓰는 포맷 지정자:

    지정자 의미 예시 결과
    %s 문자열 fmt.Sprintf("hello %s", "world")"hello world"
    %d 정수 fmt.Sprintf("port: %d", 3000)"port: 3000"
    %v 값의 기본 형식 fmt.Sprintf("%v", true)"true"
    %w 에러 래핑 (Errorf에서만) fmt.Errorf("failed: %w", err)
    반응형

    댓글

Designed by Tistory.