🎯 Go vs Spring Boot - 이커머스 관점 비교

타임딜 프로젝트(Go + Kafka + Saga)를 직접 짜고 부딪히면서 체득한 내용. 이론이 아닌 실제 장애 포인트 기반 정리.

📑 목차


1. 코드 대응 비교

핵심

Go에서 직접 짜던 것들을 Spring이 숨겨놓은 것. 개념은 동일.

트랜잭션

// Go - 직접 짜야 함
func (s *OrderService) CreateOrder(req OrderRequest) (*Order, error) {
    tx, err := db.Begin()
    if err != nil { return nil, err }
    defer tx.Rollback()
    // ... 로직
    return order, tx.Commit()
}
// Spring - 어노테이션 하나
@Transactional
fun createOrder(req: OrderRequest): Order {
    // 예외 터지면 자동 롤백
}

의존성 주입 (DI)

// Go - 직접 조립 (NewXxx 패턴)
func NewOrderService(repo *OrderRepository, kafka *KafkaService) *OrderService {
    return &OrderService{repo: repo, kafka: kafka}
}
// main.go에서 직접 연결
svc := NewOrderService(repo, kafka)
// Spring - 선언만 하면 자동 주입
@Service
class OrderService(
    private val repo: OrderRepository,  // Spring이 알아서 주입
    private val kafka: KafkaService     // Spring이 알아서 주입
)

Repository (쿼리)

// Go - SQL 직접 작성
func (r *OrderRepo) FindByUserID(id int) ([]*Order, error) {
    rows, err := r.db.Query("SELECT * FROM orders WHERE user_id = ?", id)
    // ... rows 파싱
}
// Spring - 메서드 이름이 곧 SQL
interface OrderRepository : JpaRepository<Order, Long> {
    fun findByUserId(userId: Long): List<Order>
    // SELECT * FROM orders WHERE user_id = ? 자동 생성
}

예외 처리 (전역)

// Go - 라우터마다 직접 처리
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    order, err := h.svc.CreateOrder(req)
    if err != nil {
        http.Error(w, err.Error(), 400)  // 매번 직접
        return
    }
}
// Spring - 한 곳에서 전부 처리 (corsMiddleware 개념)
@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(NotFoundException::class)
    fun handle(e: NotFoundException) = ResponseEntity.status(404).body(...)
    // 모든 핸들러에 자동 적용
}

한눈에 비교

GoSpring
tx.Begin/Rollback/Commit@Transactional
NewOrderHandler(hub)@Service 자동 주입
corsMiddleware@ControllerAdvice
"SELECT * FROM ..."findByUserId()
go removeClient(conn)@Async

2. 초반 vs 후반 압박

Spring Boot
  초반: 설정 많다, 코드 길다, 학습 곡선 있다  →  고통
  후반: 이미 검증된 패턴이 버텨줌              →  안정

Go
  초반: 빠르다, 심플하다, 돌아간다             →  쾌감
  후반: "어? 이 케이스 안 짰는데?"             →  비명

후반에 터지는 순서

1단계  이벤트 유실
       새벽 2시: "주문 들어왔는데 재고 안 빠짐"
       → Kafka publish 타이밍에 서버 재시작
       → Outbox 없으니 추적 불가

2단계  보상 트랜잭션 실패
       "환불했는데 재고가 안 돌아왔어요"
       → rollback 도중 Redis 잠깐 죽음
       → Saga 상태 없으니 어디서 멈췄는지 모름

3단계  중복 결제
       "결제가 두 번 됐어요"
       → 네트워크 타임아웃 → 클라이언트 재시도
       → 멱등성 처리 없었던 것

4단계  상태 미스매치
       "주문은 완료인데 배송이 안 시작됨"
       → 인메모리 상태가 pod 재시작으로 증발

부하테스트 함정

k6 1만 TPS는 을 테스트. 장애는 타이밍에서 난다. 깔끔한 환경 + 네트워크 한 번에 성공 → 장애 안 잡힘.


3. Kafka 써도 되는 곳 vs 안 되는 곳

이커머스 핵심은 즉시 일관성 (Eventual이 아닌 Immediate)

❌ Kafka 쓰면 안 되는 곳 (실패하면 안 됨)
  주문 생성
  재고 차감
  결제 처리
  환불

✅ Kafka 써도 되는 곳 (실패해도 재시도 가능)
  배송 시작 알림
  이메일 / 푸시
  통계 집계
  정산 배치

코드 양의 본질

Go 10줄   → 문제의 30% 해결
Spring 50줄 → 문제의 90% 해결

나머지 70%를 Go로 채우면
→ 결국 100줄 이상
→ 검증 안 된 직접 구현

Spring 보일러플레이트의 정체:

@Transactional    ← 누군가 데이터 날린 경험
멱등키            ← 누군가 중복결제 맞은 경험
PG 트랜잭션 밖   ← 누군가 커넥션 풀 고갈 겪은 경험
Outbox Pattern   ← 누군가 이벤트 유실로 고객 돈 날린 경험

더럽게 도배된 게 아니라
10년치 프로덕션 장애 경험이 코드로 굳어진 것

4. 이커머스 실무 정석 구조

단일 DB 기준 (Saga 불필요)

Saga는 DB가 분리될 때만 필요

단일 DB → @Transactional 하나로 해결 DB 여러 개, 서비스 여러 개 → Saga 필요

@Transactional (짧게 끝냄)
  재고 차감 (Redis atomic DECR)
  주문 상태 = PENDING
  멱등키 저장
커넥션 반납          ← 핵심! PG 대기 전에 반납

PG 호출 (트랜잭션 밖, 2~3초)
  성공 콜백 → 주문 상태 = PAID
  실패 콜백 → 주문 상태 = CANCELLED + 재고 복구
  타임아웃  → 멱등키로 PG 상태 조회 후 결정

Kafka → 배송 / 알림 / 정산 (여기서만 사용)

PG를 트랜잭션 밖에 두는 이유

PG를 트랜잭션 안에 넣으면
  커넥션 획득 → PG 대기 2~3초 → 커넥션 반납

커넥션 풀 20개 + 동시 주문 20명
→ 커넥션 20개 전부 PG 기다리는 중
→ 21번째 요청 → 커넥션 없음 → 장애

PG를 트랜잭션 밖에 두면
  커넥션 획득 → 재고/주문 저장 0.001초 → 커넥션 반납
  PG 대기 2~3초 (커넥션 없이)
→ 동시 2000명도 가능

5. 핵심 결론

짧고 보기 좋은 Go Saga 코드보다 더럽게 도배된 Spring @Transactional이 돈을 지킨다.

코드가 짧다 = 문제를 덜 풀었다 일 가능성을 항상 의심할 것

부하테스트 통과 ≠ 데이터 정합성 보장
k6는 처리량을 보여주고, 장애는 엣지케이스에서 터진다

Saga, Outbox, 분산 트랜잭션은 원래 복잡한 문제
복잡도는 어딘가에 반드시 존재하고
Go는 그걸 개발자한테 떠넘기는 구조

기술 선택 기준

상황선택
DB 하나, 빠른 개발Go + 트랜잭션 직접 관리
DB 하나, 이커머스Spring + @Transactional
DB 여러 개, MSASpring + Saga + Outbox
배송/알림/통계Kafka (어디서든 OK)

목업은 언제나 이상적인 면만 보여준다. 실제 운영환경의 괴리감은 파고들어야 해결 가능하다!