-
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 *Serverthiss.Cfgthis.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)반응형 - 코드에서