Post

루퍼스 2주차 WIL — 설계 의사결정과 문서화

루퍼스 2주차 WIL

이번 주 핵심 인사이트

설계는 점진적 진화다 - 고도화보다 동작하는 코드 우선

  • “처음부터 고도화를 생각하지 말고, 조금씩 조각을 한다고 생각하라”
  • 동작하는 코드를 먼저 만들고 점진적으로 개선하는 것이 완벽한 초기 설계보다 실용적

기술적 결정의 “왜”를 설명할 수 있어야 한다

주문-결제 API 분리를 고려한다면?

  • 고객 경험: 결제 실패 시 다른 결제수단으로 재시도 가능
  • 트랜잭션 제어: 주문 생성과 결제 처리를 독립적으로 관리
  • 에러 처리: 결제 실패해도 주문은 유지되어 복구 용이

OrderItem을 Entity로 선택한다면?

  • 가까운 미래 요구사항 대응: 부분 반품, 개별 배송 추적
  • 독립된 상태 관리: 개별 아이템별 상태 변화 추적
  • 느슨한 결합: OrderItem 변경이 Order에 영향 없음

좋아요 API를 멱등하게 설계한다면?

  • 동시성 안정성: 같은 요청 반복 시에도 일관된 결과
  • 사용자 경험: 네트워크 불안정 상황에서도 안전한 재시도

설계 문서는 커뮤니케이션 도구다

  • 읽는 사람 입장에서 생각: “글쓰기처럼 보는 사람을 고려하는 게 설계의 핵심”
  • 의사결정 기록: “왜 그 시점에 그런 결정을 했는지” 남기기 (ADR)
    • 목적: 미래의 협업자가 맥락을 이해하고 더 나은 결정을 내릴 수 있도록 하기 위한 문서

2주차 핵심 학습 내용

1. 도메인 설계 철학

Entity vs Value Object 선택 기준

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 초기 판단 - 식별자 불필요하니 VO
@Embeddable
class OrderItem {
    private Long productId;
    private int quantity;
    private BigDecimal price;
}

// 재판단 - 확장성 고려해 Entity
@Entity
class OrderItem {
    @Id
    private Long id;  // 개별 추적 가능

    @ManyToOne
    private Order order;

    private Long productId;
    private int quantity;
    private BigDecimal price;
    private String status;  // 상태 관리 가능 (배송중, 반품 등)
}

판단 기준

기준Entity 선택VO 선택
식별자독립 조회/추적 필요부모에 종속
생명주기독립적 상태 변화부모와 동일
비즈니스 로직개별 상태 관리 필요단순 데이터 묶음
확장성부분 반품, 개별 추적단순 조회만

학습 과정

  • 초기 판단: 일부만 보고 애매하게 느낌 (식별자만 고려)
  • 학습 후: 세부적인 차이 확인 (생명주기, 비즈니스 로직, 확장성)
  • 결과: 선택 기준이 명확해짐 - 확장 가능성 고려 시 Entity가 적절

핵심 깨달음

“VO → Entity 리팩토링은 실무에서 거의 안 한다” - 처음부터 확장 가능성 고려

도메인 분리 기준 (Aggregate)

분리 판단 기준: “독립적으로 존재 가능한가?”

Aggregate란?

  • 트랜잭션 일관성을 보장하는 객체 묶음
  • 하나의 루트 엔티티(Root Entity)를 통해서만 외부 접근
  • 함께 변경되고, 함께 저장되는 단위

같은 애그리게이트

  • 생명주기가 완전히 종속된 관계
  • 루트 엔티티 없이는 의미 없음
  • 트랜잭션 경계를 함께 함
  • 예: 주문-주문아이템, 장바구니-장바구니아이템

별도 애그리게이트

  • 독립적인 생명주기
  • 서로 다른 트랜잭션으로 관리
  • 각자의 루트 엔티티 보유
  • 예: 사용자-포인트, 상품-주문

핵심: 절대적인 기준은 없다

  • Address 예시:
    • 일반 서비스: Address를 VO로 사용 (단순 데이터)
    • 우체국 서비스: Address를 Entity로 관리 (주소 추적/변경 이력 필요)
  • 비즈니스 요구사항에 따라 같은 개념도 다르게 설계

2. API 설계 패턴

주문-결제 API 설계 시 선택지

핵심: 정답은 없다. 요구사항에 따라 선택하고, 근거를 설명할 수 있어야 한다.

설계 선택지

선택지구조장점단점
1. API 통합POST /orders
(주문+결제 한 번에)
- 구현 단순
- 클라이언트 호출 1번
- 긴 트랜잭션
- Lock 과다
- 결제 실패 시 전체 롤백
2. 트랜잭션 분리POST /orders
(내부에서 주문/결제 트랜잭션 분리)
- API 1개 유지
- 트랜잭션 경계 제어
- 내부 복잡도 증가
- 부분 실패 처리 복잡
3. API 분리POST /orders
POST /orders/{id}/payment
- 명확한 책임 분리
- 재시도 용이
- 고객 경험 개선
- 클라이언트 복잡도 증가
- 상태 관리 필요

선택 기준 예시

케이스 1: 간단한 서비스 (MVP)

  • 선택: API 통합
  • 이유: 구현 속도 우선, 트래픽 적음, 복잡도 최소화

케이스 2: 결제 실패 빈번

  • 선택: API 분리
  • 이유:
    • 결제 실패 시 주문까지 롤백? - 고객이 다른 결제수단 선택 불가
    • 주문 유지 - 재시도 가능
    • 고객 경험 개선

설계 시 질문할 것

  1. 결제 실패 시 주문은 어떻게 되어야 하는가?
  2. 고객이 다른 결제수단을 시도할 수 있어야 하는가?
  3. 트랜잭션 길이가 시스템 성능에 영향을 주는가?
  4. 클라이언트 복잡도 증가를 감수할 수 있는가?

좋아요 API 멱등성 설계

멱등성에 대한 오해와 이해

초기 의문

  • 첫 요청: 201 Created
  • 재요청: 200 OK (또는 409 Conflict)
  • HTTP 상태 코드가 다른데 멱등한가?

학습 후 이해

  • 멱등성의 정의: “몇 번을 요청해도 결과(리소스 상태)가 동일”
  • HTTP 상태 코드가 달라도 됨
  • 중요한 건: 서버의 최종 상태가 동일한가

예시

1
2
3
4
1번 좋아요 → product_like 생성 (201)
2번 좋아요 → product_like 그대로 유지 (200/409)
3번 좋아요 → product_like 그대로 유지 (200/409)
→ 결과: 좋아요 1개 (멱등함)

멱등성 구현 방법 선택지

방법구현장점단점
1. DB 제약조건UNIQUE(product_id, user_id)- DB 레벨 보장
- 구현 단순
- 예외 처리 필요
- DB 부하
2. 애플리케이션 체크SELECT → 존재 확인 → INSERT- 세밀한 제어
- 응답 커스터마이징
- Race Condition 가능
- 코드 복잡도 증가
3. 락 방식SELECT FOR UPDATE → INSERT- Race Condition 방지
- DB 제약 없이도 가능
- 성능 오버헤드
- 데드락 가능성
- 트랜잭션 길이 증가
4. 혼합 방식락 or 체크 + UNIQUE 제약- 이중 안전장치
- 명확한 응답
- 복잡도 최대

구현 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 방법 1: DB 제약조건 (예외 처리)
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"product_id", "user_id"}))
class ProductLike {
    // Duplicate 시도 시 예외 발생 → 409 Conflict 반환
}

// 방법 2: 애플리케이션 체크
@PostMapping("/products/{id}/likes")
public ResponseEntity<Void> likeProduct(@PathVariable Long id, @UserId String userId) {
    ProductLike like = productLikeRepository.findByProductIdAndUserId(id, userId);

    if (like != null) {
        return ResponseEntity.ok().build();  // 200 OK (멱등성 보장)
    }

    productLikeRepository.save(new ProductLike(id, userId));
    return ResponseEntity.status(HttpStatus.CREATED).build();  // 201 Created
}

// 방법 3: 락 방식 (Pessimistic Lock)
@Transactional
@PostMapping("/products/{id}/likes")
public ResponseEntity<Void> likeProduct(@PathVariable Long id, @UserId String userId) {
    // SELECT FOR UPDATE로 Row Lock 획득
    Product product = productRepository.findByIdWithLock(id);

    ProductLike like = productLikeRepository.findByProductIdAndUserId(id, userId);
    if (like != null) {
        return ResponseEntity.ok().build();
    }

    productLikeRepository.save(new ProductLike(id, userId));
    return ResponseEntity.status(HttpStatus.CREATED).build();
}

Race Condition 고려사항

문제 상황

  • 동시에 두 요청이 “좋아요 없음” 확인 → 둘 다 INSERT 시도
  • 결과: 중복 생성 가능

대응 방향

  1. DB 제약조건: DB 레벨에서 중복 차단 (두 번째 INSERT 실패)
  2. 락 사용: 동시 접근 제어 (먼저 온 요청 처리 완료까지 대기)
  3. 혼합: 락으로 대부분 방지 + DB 제약으로 최종 안전장치

선택 기준

  • 간단한 서비스 + 트래픽 낮음: 애플리케이션 체크 (명확한 응답)
  • 대규모 트래픽 + 단순 구조: DB 제약조건 (구현 단순, DB 레벨 보장)
  • 트랜잭션 제어 필요: 락 방식 (세밀한 제어, 데드락 주의)
  • 엔터프라이즈: 혼합 방식 (안정성 최우선)

확장성 고려사항: 규모에 따른 읽기/쓰기 트레이드오프

핵심: 읽기가 훨씬 빈번한 상황에서 쓰기 비용을 높여 읽기 비용을 낮춤

요구사항 가정

  • 사용자: 1만 명
  • 상품당 좋아요: 1만 개
  • 목록 페이지에서 각 상품의 좋아요 수 표시 필요

문제 상황

  • 매번 COUNT(*) 쿼리 실행 → 읽기 부하 급증
  • 목록 10개 → 10번의 집계 쿼리
  • 페이지 로딩마다 반복

읽기쓰기 트레이드오프 성능비교

해결: 비정규화 (좋아요 수 별도 저장)

항목ProductLike 테이블 (정규화)ProductLikeCount 테이블 (비정규화)
용도좋아요 생성/삭제 (쓰기)좋아요 수 조회 (읽기)
데이터product_id, user_idproduct_id, like_count
쓰기 비용낮음 (INSERT/DELETE만)높음 (UPDATE 추가)
읽기 비용높음 (매번 COUNT)낮음 (단순 SELECT)

트레이드오프

  • 장점: 읽기 성능 대폭 개선 (집계 → 조회)
  • 단점: 쓰기 복잡도 증가 (좋아요 토글 시 count도 업데이트)

3. 설계 문서화 전략

핵심 인사이트: 문서는 독자를 위한 것

모든 문서화의 목표는 읽는 사람이 쉽게 이해할 수 있도록 작성하는 것:

  • 미래의 나, 팀원, 새로운 개발자가 컨텍스트를 빠르게 파악할 수 있어야 함
  • 복잡도를 관리하고, 의사결정 배경을 명확히 기록
  • 친절한 문서는 유지보수 비용을 대폭 줄임

시퀀스 다이어그램 작성 원칙

독자 중심 복잡도 관리

  • 50+ 라인: 과도함 → 읽는 사람 부담 증가
  • 성공/실패 케이스 분리: 한 다이어그램에 모든 분기 넣지 말 것
  • 핵심 + 상세 분리: 핵심 플로우만 보여주고 상세 플로우는 별첨

표현해야 할 것

  • 데이터 변환 과정 (DTO 레이어별 변화)
  • 트랜잭션 경계 (어디서 시작/종료되는지)
  • 비동기 처리 구간 (응답 타이밍 이해를 위해)

피해야 할 것

  • 추상화 레벨 혼재 (하이레벨 개념 + 메서드 호출 세부사항)
  • 과도한 상세화 (모든 메서드 호출까지 표현)
  • 읽기 어려운 복잡한 분기

실무 ERD 컨벤션

명명 규칙과 표준 컬럼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 독자가 이해하기 쉬운 컨벤션
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    ref_user_id BIGINT NOT NULL,  -- 참조임을 명시 (ref_)
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL,
    deleted_at TIMESTAMP NULL      -- Soft Delete용
);

-- 의도가 불명확한 컨벤션
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,               -- 참조인지 자체 필드인지 애매
    deleted_yn CHAR(1)            -- 삭제 시점 추적 불가
);

컨벤션 배경

  • ref_ 접두사: 외래키 참조임을 명확히 표현 (읽는 사람이 관계 즉시 파악)
  • deleted_at (not deleted_yn):
    • 삭제 시점 추적 가능 → 복구 시 유용
    • 언제 삭제되었는지 추적 가능 (감사 로그)
  • created_at / updated_at: 모든 테이블 필수 (데이터 변화 추적)

실무에서 자주 쓰는 패턴

  • 상태 관리: status (VARCHAR) 또는 state (ENUM)
  • 버전 관리: version (BIGINT) - Optimistic Lock
  • 감사 필드: created_by, updated_by - 누가 변경했는지

ADR (Architecture Decision Records)

ADR이 필요한 이유

  • “왜 그 시점에 그런 결정을 했는지” 기록 → 컨텍스트 손실 방지
  • 트레이드오프 명시 → 미래 유지보수자가 함부로 바꾸지 않도록
  • 의사결정 과정 공유 → 팀 학습 자료

ADR 권장 템플릿 (경량 문서 방식)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# ADR-001: OrderItem을 Entity로 설계

**상태**: 승인 | 작성일: 2024-11-09

## 배경
- 문제: OrderItem을 VO로 설계할지 Entity로 설계할지 결정 필요
- 제약: 부분 반품, 개별 배송 상태 추적 가능성 고려
- 고려 요소: 향후 확장 가능성 vs 초기 복잡도

## 결정
OrderItem을 Value Object가 아닌 **Entity로 설계**

## 근거

**대안 비교**:

| 옵션 | 장점 | 단점 |
|------|------|------|
| VO | - 구현 단순<br/>- 성능 우수 | - 확장 제약<br/>- 상태 관리 불가 |
| Entity | - 확장 가능<br/>- 개별 상태 관리 | - 초기 복잡도 증가 |

**Entity 선택 이유**:
1. 부분 반품 요구사항 예상 (개별 OrderItem 추적 필요)
2. 개별 배송 상태 추적 필요 (배송 중, 배송 완료 등)
3. VO → Entity 리팩토링은 실무에서 거의 불가능 (DB 마이그레이션 비용 高)

## 결과/영향

**장점**:
- 향후 확장 요구사항 대응 용이
- 개별 OrderItem 상태 관리 가능

**단점**:
- 초기 설계 복잡도 증가
- 약간의 성능 오버헤드

**리스크**: 없음 (충분히 검증된 패턴)

**후속 작업**: OrderItem 관련 Repository, Service 레이어 구현

## 관련
- 이슈: #123 (부분 반품 기능 검토)
- 참조: DDD Aggregate 설계 패턴 문서

실무 팁

  • 중요한 아키텍처 결정만 ADR로 작성 (사소한 결정은 제외)
  • 트레이드오프를 명확히 기록 (왜 A를 선택하고 B를 버렸는지)
  • 컨텍스트 변경 시 ADR 업데이트 (결정 무효화 시 명시)

4. 실무 마인드셋

점진적 설계 철학

핵심: 과도한 설계보다 실용적 접근

피해야 할 것

  • 처음부터 완벽한 설계 추구 → 실행 지연
  • 불필요한 추상화 계층 미리 만들기

지향할 것

  • 동작하는 코드를 먼저 만들고 점진적 개선
  • 필요할 때 리팩토링

활용할 수 있는 방법들

  • POC (Proof of Concept): 간단한 구현으로 도메인 모델 검증 후 정식 설계
  • 반복적 리팩토링: 동작 확인 → 개선 → 재검증
  • 점진적 추상화: 패턴이 보일 때 추상화 (미리 예측하지 말고)

비기능 요구사항 고려 시점

핵심: DB 종류에 따라 비기능 요구사항 구현 방식이 달라진다

기본적으로 깔고 가야 할 것

동시성 (Concurrency)

  • 왜 기본인가: 웹 애플리케이션은 기본적으로 멀티 스레드 환경
  • Race Condition은 DB 종류와 무관하게 발생 가능
  • 애플리케이션 레벨에서도 제어 가능 (Lock, 동기화 등)

멱등성 (Idempotency)

  • 왜 기본인가: 네트워크 불안정성은 항상 존재
  • 클라이언트 재시도, 타임아웃 등은 일상적 시나리오
  • 결제/주문 같은 중요 API는 멱등성 없으면 치명적

상황에 따라 결정

일관성 (Consistency) - (DB 의존성 높음)

  • 문제: 초기에 “강한 일관성” 요구사항 넣으면 DB 선택 제약
    • RDBMS: 강한 일관성 쉬움 (ACID 트랜잭션)
    • NoSQL: 최종 일관성 기본 (강한 일관성 어려움)
  • 결정 시점: DB 선택 후 or 데이터 특성 파악 후
  • : 조회수는 최종 일관성 OK, 결제는 강한 일관성 필수

가용성 (Availability)

  • 문제: SLA 99.9% vs 99.99%에 따라 인프라 복잡도 급증
  • DB 다중화, 장애 복구 전략이 DB 종류에 따라 천차만별
    • RDBMS Replication vs NoSQL Sharding
  • 결정 시점: 실제 트래픽 규모 파악 후

실무 팁

  • 동시성/멱등성은 설계 초기부터 고려 (나중에 추가하기 어려움)
  • 일관성/가용성은 요구사항 + DB 선택 후 구체화 (무조건 넣으면 과도한 제약)

문서화 원칙

핵심: 관리 포인트를 최소화하라

문서화의 본질

  • 화이트보드 토론도 문서화: 그리고 지우고 다음날 대화하는 것도 커뮤니케이션 기록
  • 실제 문서 파일은 최소화: 파일로 남기는 순간 유지보수 대상이 됨
  • 너무 많은 문서는 부채: 업데이트 안 되면 오히려 혼란 야기

오래 살아남는 문서의 특징

  • 변경 빈도가 낮음: ADR (아키텍처 결정), 핵심 설계 원칙
  • 본질적 가치: 시스템의 “왜”를 설명 (구현 세부사항이 아닌)
  • 독립적: 코드 변경에 영향받지 않는 내용

실무 기준

문서화할 것

  • ADR (중요한 의사결정과 트레이드오프)
  • 전체 시스템 아키텍처 (큰 그림)
  • 신규 팀원 온보딩 가이드

문서화 안 할 것

  • 코드 읽으면 알 수 있는 것
  • 자주 변경되는 세부 구현
  • 자명한 로직 설명

핵심: 문서는 적을수록 좋다. 필요한 것만 남기고 관리 부담을 줄여라.


커리어 & 성장 전략

5. 3~5년차 개발자 역량

핵심 인사이트: “일머리”가 중요하다는 걸 알았다

일머리란?

  • 유연성: 고집 부릴 때 vs 내려놓을 때 구분
  • 팀워크: 팀 미션을 위해 자신을 내려놓고 부스팅
  • 효율성: 두 번 말하지 않게 만드는 커뮤니케이션
  • 태도: 일을 재밌게 만드는 능력

더 알고 싶은 것

  • 그럼 일머리는 구체적으로 뭐지?
  • 어떻게 기를 수 있지?
  • 실무에서 어떻게 발휘하지?

6. 면접 준비 전략

핵심: 알려주는 경험을 많이 해야한다.

왜 중요한가

  • 내가 아는 걸 타인에게 이해시키는 능력 = 면접의 본질
  • 비유법 활용 = 깊은 이해의 증거
  • 연습 없이 면접관을 이해시키려는 건 치사함

실천 방법

  • 루퍼스, 동료들과 설명 연습을 많이 하기
  • 복잡한 개념을 쉽게 설명하는 연습

7. 이력서 작성 전략

핵심: 수치화보다 비즈니스 임팩트

단순 수치의 함정

사례수치비즈니스 임팩트
A1시간 → 1분 (59분 단축)원래 1시간마다 돌던 배치 → 사실 문제 없었음
B10분 → 9분 (1분 단축)10분마다 돌아야 하는데 간당간당 → 안정성 확보

핵심: B가 비즈니스 임팩트가 더 크다!

돋보이는 이력서 작성법

평범한 이력서돋보이는 이력서
숫자만 나열
(응답 시간 50% 개선)
“그래서 뭐가 달라졌는데?”
(불안정한 서비스 → 안정화)
기술 스택 나열
(Spring, Redis 사용)
“그래서 뭐를 얻었는데?”
(트래픽 10배 증가 대응 가능)
텍스트만 나열시각적 요소 활용
(다이어그램, 흐름도 등)

이력서 차별화 포인트

  • 아키텍처 고민 과정의 시각화 추가
  • 읽는 사람이 쉽게 이해할 수 있는 구조

현실 체크

  • 이력서가 아무리 좋아도 면접에서 떨어질 수 있다
  • 포트폴리오를 멋지게 만들어도 설명 못 하면 의미 없다

=> 핵심: 이력서 + 면접 + 기술력 다 잘해야 한다


더 알아보면 좋을 것들

이번 주 학습을 통해 발견한 역량 확장 영역

표준화된 개발 문서 포맷

  • SRS (Software Requirements Specification): 소프트웨어 요구사항 명세서
  • ADR (Architecture Decision Records): 아키텍처 의사결정 기록
  • API 명세서: OpenAPI/Swagger 표준
  • 시스템 설계 문서: 아키텍처 다이어그램, 플로우차트, ERD, 시퀀스 다이어그램, 유즈케이스

역량 확장 포인트: 팀 간 커뮤니케이션 능력, 문서화 표준 이해


도메인 주도 설계 (DDD)

큰 그림 설계 (어떻게 나눌까)

  • Bounded Context: 시스템을 의미 있는 경계로 나누기 (주문 시스템 vs 결제 시스템)
  • Ubiquitous Language: 개발자와 비즈니스 팀이 같은 용어 쓰기

세부 구현 설계 (어떻게 만들까)

  • Entity: 식별자가 있는 객체 (Order, User)
  • Value Object: 값으로만 구분되는 객체 (Address, Money)
  • Aggregate: 함께 변경되는 객체 묶음

역량 확장 포인트: 복잡한 비즈니스 로직을 체계적으로 설계하는 능력


대용량 트래픽 처리

  • 캐싱 전략: Redis 활용 (Look-Aside, Write-Through, Write-Behind)
  • 부하 분산: 로드 밸런싱, 샤딩, 레플리케이션
  • 성능 최적화: 인덱싱, 쿼리 최적화, 비동기 처리
  • 모니터링: APM, 로그 분석, 장애 대응

역량 확장 포인트: 실무 규모의 시스템 설계 및 운영 경험

This post is licensed under CC BY 4.0 by the author.