[Go 2주 완성 #03] Day 5~6: 클래스 없는 객체지향 - 상속을 버리고 합성을 취하다
이 글의 핵심
[Go 2주 완성 #03] Day 5~6: 클래스 없는 객체지향 - 상속을 버리고 합성을 취하다. 시리즈 안내·클래스 없는 객체지향.
시리즈 안내
📚 Go 2주 완성 시리즈 #03 | 전체 목차 보기
이 글은 C++ 개발자를 위한 2주 완성 Go 언어 커리큘럼의 Day 5~6 내용입니다.
이전: #02 자료구조 ← | → 다음: #04 인터페이스
💡 초보자를 위한 한 줄: Go에는
class키워드가 없습니다.struct+ 메서드로 객체를 만듭니다. 상속도 없습니다. 대신 구조체 임베딩(작은 struct를 큰 struct에 끼워 넣기)으로 코드를 재사용합니다. 메서드의(p *Person)을 리시버라고 하며, 포인터 리시버는 필드 수정 가능, 값 리시버는 복사본으로 동작합니다.
들어가며: “클래스가 없는데 어떻게 객체지향을 하죠?”
C++에서는 class로 데이터와 메서드를 묶고, 상속으로 코드를 재사용했습니다:
class Animal {
public:
virtual void Speak() { cout << "..."; }
};
class Dog : public Animal { // ← 상속
public:
void Speak() override { cout << "Woof!"; }
};
Go는 완전히 다릅니다. 클래스도 없고 상속도 없습니다:
type Animal struct {
name string
}
type Dog struct {
Animal // ← 임베딩 (상속 아님!)
breed string
}
대신 struct와 합성(Composition)(상속 대신 작은 타입을 조립·끼워 넣어 기능을 확장하는 방식)으로 더 심플하고 유연한 객체지향을 구현합니다. “상속보다 합성을 선호하라”는 디자인 원칙이 언어 차원에서 강제되는 셈입니다.
이 글에서 배울 내용:
struct로 데이터 정의하기- 메서드와 리시버의 개념
- 포인터 리시버 vs 값 리시버
- 구조체 임베딩으로 코드 재사용
C++ 개발자 관점: C++ 백그라운드에서 Go로 전환하며 겪은 차이점과 함정을 중심으로 설명합니다. 포인터, 동시성, 메모리 관리 등 핵심 개념을 비교하며 정리했습니다.
실무에서의 체감
C++ 위주로 서버를 다루던 환경에서 Go를 도입할 때 흔히 드는 인상은 문법과 툴체인이 단순해 보인다는 점입니다. 프로덕션에서는 그 단순함이 빌드·배포·동시성 코드 가독성으로 이어지는 경우가 많습니다. 자주 언급되는 장점:
- 개발 속도: 팀·도메인에 따라 다르지만, 네트워크·CLI 코드를 빠르게 완성하기 쉬운 편입니다.
- 안정성: GC가 있어 수동 할당 해제 부담이 줄어듭니다.
- 배포: 단일 바이너리로 옮기기 쉬운 구조입니다.
1. 구조체: 클래스의 대체재
C++ vs Go: 구조체 정의
// C++: 클래스
class Person {
private:
std::string name;
int age;
public:
Person(const std::string& n, int a) : name(n), age(a) {}
void SetName(const std::string& n) { name = n; }
std::string GetName() const { return name; }
void SetAge(int a) { age = a; }
int GetAge() const { return age; }
void Introduce() const {
std::cout << "I'm " << name << ", " << age << " years old\n";
}
};
// 사용
Person p("Alice", 30);
p.Introduce();
// Go: 구조체 + 메서드
// 패키지 선언
package main
import "fmt"
type Person struct {
Name string // 대문자 = public (패키지 외부 접근 가능)
age int // 소문자 = private (패키지 내부만)
}
// 생성자 관례 (NewXxx 함수)
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
age: age,
}
}
// Getter (관례: Get 접두사 생략)
func (p *Person) Age() int {
return p.age
}
// Setter
func (p *Person) SetAge(age int) {
p.age = age
}
// 메서드
func (p *Person) Introduce() {
fmt.Printf("I'm %s, %d years old\n", p.Name, p.age)
}
// 사용
func main() {
p := NewPerson("Alice", 30)
p.Introduce()
}
핵심 차이점:
- 접근 제어: 대문자 시작 = public, 소문자 = private (패키지 단위)
- 생성자:
NewXxx함수가 관례 (강제 아님) - 메서드: 구조체 외부에 정의 (리시버 사용)
구조체 초기화 방법
// Go: 다양한 초기화 방법
package main
type Point struct {
X, Y int
}
func main() {
// 1. 필드 순서대로
p1 := Point{10, 20}
// 2. 필드명 지정 (권장)
p2 := Point{X: 10, Y: 20}
// 3. 일부만 초기화 (나머지는 제로 값)
p3 := Point{X: 10} // Y는 0
// 4. 제로 값으로 초기화
p4 := Point{} // X=0, Y=0
// 5. 포인터로 생성
p5 := &Point{X: 10, Y: 20}
// 6. new (제로 값으로 초기화)
p6 := new(Point) // &Point{} 와 동일
}
2. 메서드와 리시버
Go의 메서드는 리시버(Receiver)를 가진 함수입니다. C++의 this 포인터와 유사하지만 명시적입니다.
C++ vs Go: 메서드 정의
// C++: 클래스 내부에 메서드 정의
class Counter {
private:
int count;
public:
Counter() : count(0) {}
void Increment() {
count++; // this->count++와 동일
}
int GetCount() const {
return count;
}
};
// Go: 구조체 외부에 메서드 정의
package main
type Counter struct {
count int
}
// 생성자
func NewCounter() *Counter {
return &Counter{count: 0}
}
// 메서드 (포인터 리시버)
func (c *Counter) Increment() {
c.count++ // (*c).count++ 와 동일 (자동 역참조)
}
// 메서드 (포인터 리시버, 읽기 전용이지만 일관성 위해)
func (c *Counter) GetCount() int {
return c.count
}
리시버 문법: func (receiver Type) MethodName() { }
3. 포인터 리시버 vs 값 리시버
값 리시버 (Value Receiver)
// Go: 값 리시버 - 복사본에서 동작
type Point struct {
X, Y int
}
func (p Point) Distance() float64 {
return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
func (p Point) Move(dx, dy int) {
p.X += dx // ❌ 원본 변경 안 됨 (복사본 수정)
p.Y += dy
}
func main() {
p := Point{3, 4}
fmt.Println(p.Distance()) // 5
p.Move(10, 10)
fmt.Println(p) // {3 4} - 변경 안 됨!
}
포인터 리시버 (Pointer Receiver)
// Go: 포인터 리시버 - 원본 수정
type Point struct {
X, Y int
}
func (p *Point) Move(dx, dy int) {
p.X += dx // ✅ 원본 변경됨
p.Y += dy
}
func (p *Point) Reset() {
p.X = 0
p.Y = 0
}
func main() {
p := Point{3, 4}
p.Move(10, 10) // Go가 자동으로 &p로 변환
fmt.Println(p) // {13 14} - 변경됨!
// 포인터로 생성해도 동일
p2 := &Point{1, 2}
p2.Move(5, 5) // Go가 자동 처리
fmt.Println(*p2) // {6 7}
}
리시버 선택 가이드
flowchart TD
A[메서드 정의] --> B{필드 수정?}
B -->|Yes| C[포인터 리시버 *T]
B -->|No| D{구조체 크기}
D -->|큰 구조체\n64바이트 이상| C
D -->|작은 구조체| E{일관성}
E -->|다른 메서드가\n포인터 리시버| C
E -->|모두 값 리시버| F[값 리시버 T]
권장 사항:
- 포인터 리시버 사용 시기:
- 메서드가 필드를 수정할 때
- 구조체가 클 때 (복사 비용 절감)
- 일관성 유지 (한 타입의 메서드는 모두 같은 리시버 타입)
- 값 리시버 사용 시기:
- 읽기 전용 메서드
- 작은 구조체 (int, bool, 작은 struct)
- 불변성(Immutability)이 중요할 때
4. 구조체 임베딩: 상속 없는 재사용
C++ vs Go: 상속 vs 합성
// C++: 상속으로 코드 재사용
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}
virtual void Speak() {
std::cout << "Some sound\n";
}
void Eat() {
std::cout << name << " is eating\n";
}
};
class Dog : public Animal {
public:
Dog(const std::string& n) : Animal(n) {}
void Speak() override {
std::cout << "Woof!\n";
}
void Fetch() {
std::cout << name << " is fetching\n";
}
};
// 사용
Dog d("Buddy");
d.Speak(); // "Woof!"
d.Eat(); // "Buddy is eating"
d.Fetch();
// Go: 임베딩으로 코드 재사용
// 패키지 선언
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Speak() {
fmt.Println("Some sound")
}
func (a Animal) Eat() {
fmt.Printf("%s is eating\n", a.Name)
}
type Dog struct {
Animal // 임베딩 - Animal의 필드와 메서드가 Dog에 포함됨
Breed string
}
// Dog의 고유 메서드
func (d Dog) Fetch() {
fmt.Printf("%s is fetching\n", d.Name) // Animal.Name 직접 접근
}
// Animal.Speak 오버라이드
func (d Dog) Speak() {
fmt.Println("Woof!")
}
// 사용
func main() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
d.Speak() // "Woof!" (오버라이드됨)
d.Eat() // "Buddy is eating" (Animal의 메서드)
d.Fetch() // "Buddy is fetching"
}
핵심 차이점:
- Go는 상속이 없습니다
- 임베딩: 다른 구조체를 필드로 포함하면, 그 구조체의 메서드가 자동으로 “승격”됩니다
- 오버라이드: 같은 이름의 메서드를 정의하면 임베딩된 메서드를 가립니다
임베딩의 동작 원리
// Go: 임베딩 상세
package main
import "fmt"
type Base struct {
Value int
}
func (b Base) Print() {
fmt.Println("Base:", b.Value)
}
type Derived struct {
Base // 임베딩
Extra string
}
func main() {
d := Derived{
Base: Base{Value: 42},
Extra: "additional",
}
// Base의 메서드 직접 호출
d.Print() // "Base: 42"
// Base 필드 직접 접근
fmt.Println(d.Value) // 42 (d.Base.Value와 동일)
// 명시적 접근도 가능
d.Base.Print()
fmt.Println(d.Base.Value)
}
다중 임베딩
// Go: 여러 구조체 임베딩
package main
import "fmt"
type Logger struct{}
func (l Logger) Log(msg string) {
fmt.Println("[LOG]", msg)
}
type Validator struct{}
func (v Validator) Validate(data string) bool {
return data != ""
}
type Service struct {
Logger // 임베딩 1
Validator // 임베딩 2
name string
}
func (s *Service) Process(data string) {
s.Log("Processing started") // Logger의 메서드
if !s.Validate(data) { // Validator의 메서드
s.Log("Invalid data")
return
}
s.Log("Processing completed")
}
func main() {
svc := &Service{name: "MyService"}
svc.Process("test data")
}
5. 생성자 패턴
Go에는 생성자가 없지만, NewXxx 함수를 만드는 것이 관례입니다.
C++ vs Go: 생성자
// C++: 생성자
class Database {
private:
std::string host;
int port;
bool connected;
public:
// 생성자
Database(const std::string& h, int p)
: host(h), port(p), connected(false) {}
// 기본 생성자
Database() : Database("localhost", 5432) {}
// 소멸자
~Database() {
if (connected) {
disconnect();
}
}
};
// Go: NewXxx 생성자 패턴
package main
type Database struct {
host string
port int
connected bool
}
// 생성자 (포인터 반환이 관례)
func NewDatabase(host string, port int) *Database {
return &Database{
host: host,
port: port,
connected: false,
}
}
// 기본값 생성자
func NewDefaultDatabase() *Database {
return NewDatabase("localhost", 5432)
}
// 소멸자 없음 - defer로 정리
func (db *Database) Close() error {
if db.connected {
return db.disconnect()
}
return nil
}
// 사용
func main() {
db := NewDatabase("localhost", 5432)
defer db.Close() // RAII 대용
// 작업...
}
Functional Options 패턴
많은 옵션이 필요할 때 사용하는 고급 패턴입니다.
// Go: Functional Options 패턴
package main
import "time"
type Server struct {
host string
port int
timeout time.Duration
maxConn int
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func NewServer(opts ...Option) *Server {
// 기본값
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConn: 100,
}
// 옵션 적용
for _, opt := range opts {
opt(s)
}
return s
}
// 사용
func main() {
// 기본값 사용
srv1 := NewServer()
// 일부 옵션만 지정
srv2 := NewServer(
WithHost("0.0.0.0"),
WithPort(9000),
)
// 모든 옵션 지정
srv3 := NewServer(
WithHost("192.168.1.1"),
WithPort(3000),
WithTimeout(60 * time.Second),
)
}
6. 실습 과제
과제 1: Rectangle 구조체
// Go: Rectangle 구조체와 메서드
package main
import "fmt"
type Rectangle struct {
Width, Height float64
}
func NewRectangle(w, h float64) *Rectangle {
return &Rectangle{Width: w, Height: h}
}
func (r *Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r *Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := NewRectangle(10, 5)
fmt.Printf("Area: %.2f\n", rect.Area())
fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
rect.Scale(2)
fmt.Printf("After scaling - Area: %.2f\n", rect.Area())
}
과제 2: Stack 구현
// Go: Stack 자료구조 구현
package main
import (
"errors"
"fmt"
)
type Stack struct {
items []int
}
func NewStack() *Stack {
return &Stack{items: make([]int, 0)}
}
func (s *Stack) Push(item int) {
s.items = append(s.items, item)
}
func (s *Stack) Pop() (int, error) {
if len(s.items) == 0 {
return 0, errors.New("stack is empty")
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, nil
}
func (s *Stack) Peek() (int, error) {
if len(s.items) == 0 {
return 0, errors.New("stack is empty")
}
return s.items[len(s.items)-1], nil
}
func (s *Stack) IsEmpty() bool {
return len(s.items) == 0
}
func (s *Stack) Size() int {
return len(s.items)
}
func main() {
stack := NewStack()
stack.Push(1)
stack.Push(2)
stack.Push(3)
fmt.Println("Size:", stack.Size()) // 3
if top, err := stack.Peek(); err == nil {
fmt.Println("Top:", top) // 3
}
for !stack.IsEmpty() {
if item, err := stack.Pop(); err == nil {
fmt.Println("Popped:", item)
}
}
}
과제 3: 임베딩 활용
// Go: 임베딩으로 기능 확장
package main
import (
"fmt"
"time"
)
// 기본 타이머
type Timer struct {
start time.Time
}
func (t *Timer) Start() {
t.start = time.Now()
}
func (t *Timer) Elapsed() time.Duration {
return time.Since(t.start)
}
// 로깅 기능 추가
type Logger struct {
prefix string
}
func (l Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
// Timer + Logger 합성
type LoggedTimer struct {
Timer // 임베딩
Logger // 임베딩
}
func NewLoggedTimer(prefix string) *LoggedTimer {
return &LoggedTimer{
Logger: Logger{prefix: prefix},
}
}
func (lt *LoggedTimer) Start() {
lt.Timer.Start()
lt.Log("Timer started")
}
func (lt *LoggedTimer) Stop() time.Duration {
elapsed := lt.Elapsed()
lt.Log(fmt.Sprintf("Timer stopped: %v", elapsed))
return elapsed
}
func main() {
timer := NewLoggedTimer("BENCHMARK")
timer.Start()
time.Sleep(100 * time.Millisecond)
timer.Stop()
}
정리: Day 5~6 학습 체크리스트
완료해야 할 항목
-
struct로 데이터 타입 정의 - 대문자/소문자로 접근 제어
- 메서드와 리시버 문법 이해
- 포인터 리시버 vs 값 리시버 차이 숙지
-
NewXxx생성자 패턴 사용 - 구조체 임베딩으로 코드 재사용
- 실습 과제 3개 완료
C++에서 Go로 전환 포인트
| C++ | Go | 비고 |
|---|---|---|
class | struct + 메서드 | 더 심플 |
| 생성자/소멸자 | NewXxx / defer | 관례 기반 |
this 포인터 | 리시버 (명시적) | 더 명확 |
| 상속 | 임베딩 | 합성 우선 |
virtual | 인터페이스 (다음 글) | 암시적 |
다음 단계 예고
Day 5~6에서는 구조체와 메서드를 배웠습니다. 다음 글에서는 인터페이스를 다룹니다. 가상 함수 없이 다형성을 구현하는 Go의 핵심 개념입니다.
📚 시리즈 네비게이션
| 이전 글 | 목차 | 다음 글 |
|---|---|---|
| ← #02 자료구조 | 📑 전체 목차 | #04 인터페이스 → |
| Go 2주 완성 시리즈: | ||
| 커리큘럼 • #01 기본 문법 • #02 자료구조 • #03 객체지향 • #04 인터페이스 • #05 에러 처리 • #06 고루틴·채널 • #07 테스팅 • #08 REST API • #09 context·우아한 종료 |
한 줄 요약: Go는 클래스와 상속 대신 구조체와 합성으로 더 심플하고 유연한 객체지향을 구현합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [Go 2주 완성 #02] Day 3~4: 메모리와 자료구조 - 포인터 연산은 없지만 포인터는 있다
- C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
- C++ 상속과 다형성 | “virtual 함수” 완벽 가이드
이 글에서 다루는 키워드 (관련 검색어)
Go struct 메서드, 포인터 리시버, Embedding 합성, Go OOP, C++ 상속 대체, Golang 객체지향, Go 2주 완성 등으로 검색하시면 이 글이 도움이 됩니다.
실전 팁 (Go)
go fmt,go vet, 필요 시golangci-lint로 기본 품질을 맞춥니다.if err != nil을 습관화합니다. 에러를 삼키면 운영에서 원인 추적이 어렵습니다.go test ./...로 패키지 단위 회귀를 확인합니다. 성능은go test -bench로 측정할 수 있을 때만 다룹니다.
실전 체크리스트 (Go)
코드
- 외부 호출·I/O 실패 시 에러가 처리되거나 로깅되는가?
- 고루틴·채널 사용 시 블로킹·누수 가능성을 검토했는가?
모듈
-
go.mod/go.sum이 팀과 합의된 범위인가?
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ 클래스와 상속 대신 Go의 구조체와 합성을 사용하는 방법. 메서드 리시버(포인터 vs 값)의 차이와 구조체 임베딩을 통한 코드 재사용 패턴을 실전 예제로 학습합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
관련 글
- C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
- C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
- C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
- [Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법 - C++ 개발자의 첫인상
- [Go 2주 완성 #02] Day 3~4: 메모리와 자료구조 - 포인터 연산은 없지만 포인터는 있다
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「[Go 2주 완성 #03] Day 5~6: 클래스 없는 객체지향 - 상속을 버리고 합성을 취하다」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「[Go 2주 완성 #03] Day 5~6: 클래스 없는 객체지향 - 상속을 버리고 합성을 취하다」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.