이 글은 회사에서 SI 프로젝트를 진행하면서 마주한 엣지 케이스들의 공통된 원인을 나름대로 분석하고, 그 원인을 줄이기 위한 방법론을 제시하기 위해 썼다.
다만 실제 사례를 공개적으로 작성할 수 없으니, HR/채용 플랫폼의 가상 사례로 대체하였다.
이해를 돕기 위한 이미지는 AI를 통해 만들었다.
1. 도메인의 복잡성이 드러나는 순간들
MSA 환경이나 외부 연동 시스템을 개발하다 보면, 한 엔티티의 변화가 예상치 못한 파급 효과를 일으키는 경우를 자주 마주한다. 기획과 DB 설계 단계에서는 완벽해 보였지만, 구현 단계에서 수많은 엣지 케이스가 튀어나온다.
HR/채용 플랫폼의 가상 사례로 설명해보겠다. ‘기업 계정 정지 및 삭제’ 상황을 가정해 보자. 특정 기업(Company) 계정이 규정 위반으로 정지 처리될 때, 그 기업에 소속된 수많은 ‘인사담당자(Recruiter)’ 개인 계정은 어떻게 처리해야 할까?
담당자 계정을 일괄 비활성화할 것인가? 소속 매핑만 끊고 ‘무소속 회원’으로 둘 것인가? 진행 중이던 면접 일정과 지원서는 즉시 파기할 것인가, 열람만 차단할 것인가?
이런 엣지 케이스는 화면 기획서나 정적인 ERD만으로는 100% 식별하기 불가능하다. 결국 개발이나 테스트 단계에서 이슈가 터지고, 그때마다 방어 로직을 덧대어 시스템을 유지한다.
그렇다면 왜 이런 엣지 케이스를 늘 뒤늦게 발견하는 것일까?
2. 원인 진단: 가려져 있던 라이프사이클
설계를 시작할 때 우리는 자연스럽게 **‘구조(명사)‘**에 집중한다. 화면에 필요한 데이터를 뽑고, ERD를 그리며, API 스펙을 맞춘다. “인사담당자 테이블에 기업 ID(company_id)가 필요하겠구나” 하는 식이다.
하지만 필드와 타입은 데이터가 “무엇인지” 만 알려준다. 그 데이터가 “언제 생기고, 어떤 이벤트를 거쳐, 어떻게 종료되는지” 는 문서에 드러나지 않는다.
엣지 케이스를 늦게 발견하는 진짜 원인은 분명하다. 관계를 맺을 때 ‘데이터가 연결되어 있다’는 정적인 상태만 확인했기 때문이다. ‘상대의 상태가 변할 때 내 상태는 어떻게 변해야 하는가’라는 동적인 반응 정책을 깊게 파고들지 않은 것이다.
3. 기준 1: 엔티티는 ‘도메인 개념의 라이프사이클’이다
엔티티는 단순한 테이블이 아니다. 도메인 개념이 살아 숨 쉬는 ‘라이프사이클’ 그 자체다.
- 과거의 접근: 기획서의 화면과 데이터를 보고, “이 엔티티에는 이런 컬럼이 필요하겠군” 이라며 필드와 타입 위주로 설계한다. 언제 생성되고 어떻게 변경되는 지에 대한 기준과 정의는 뒤로 미룬다.
- 현실의 문제: 막상 개발에 들어가면 “진행(In Progress) 상태일 때 이 요청을 처리해도 되나요?”, “마감(Closed) 상태일 때 필드를 수정해도 되나요?” 같은 질문이 쏟아진다. 상태 변화 규칙이 없으니 즉석에서 판단하고, 로직은 코드 여기저기에 파편화된다.
💡 개선 방향
필드를 뽑기 전에 ‘생성 → 변경 → 종료’로 이어지는 전체 라이프사이클을 먼저 그린다. 각 상태를 넘어갈 때 반드시 지켜야 할 규칙(불변식)을 정의하는 것부터 설계를 시작한다.

4. 기준 2: 관계는 ‘상대방 라이프사이클 변화에 대한 반응’이다
관계는 단순한 외래키(FK) 조인이 아니다. “상대 상태 변화를 어떻게 해석할 것인가”에 대한 약속이다.
- 과거의 접근: 기획서를 보고 “이 데이터는 저 엔티티에 있으니 식별자(ID)만 들고 있다가 조인해서 써야지” 수준으로 관계를 취급했다.
- 현실의 문제: 엣지케이스는 조회가 아니라 상태의 ‘변화(Write/Update)’ 에서 나온다. 상대 엔티티의 라이프사이클을 고려하지 않으면 앞선 예시(기업 계정 정지 시 담당자 처리)처럼 까다로운 이슈를 마주하고, 일정이 급해 땜빵 코드로 막게 된다.
💡 개선 방향
관계를 맺을 때는 식별자 유지가 핵심이 아니다. “상대의 상태가 변했을 때 내 상태도 바꿀 것인가, 무시할 것인가” 하는 반응 방식을 설계 시점에 미리 정의해야 한다.

5. 기준 3: 외부 시스템과의 관계 (통제 불가능한 라이프사이클)
엔티티가 관계를 맺는 대상은 시스템 내부에 국한되지 않는다. 외부 시스템과의 관계가 중요한 이유는 상대방의 라이프사이클을 전혀 제어할 수 없기 때문이다.
- 사례 (외부 채용 포털 연동): HR/채용 시스템의 ‘채용 공고’와 외부 채용 포털(사람인, 잡코리아 등)의 ‘채용 공고’는 긴밀하게 연결된다. 하지만 외부 포털의 ‘공고 심사(블라인드 및 가이드라인 위반 심사 등)’ 상태는 우리가 임의로 수정할 수 없다. 외부 상태 변화를 전달받는 통신 방식(Webhook 등) 역시 통제 불가능하며 제약 조건에 맞춰야 한다.
- 대응: 외부 연동 설계 시, 통신 과정에서 상태가 어긋나는 것을 기본값으로 상정해야 한다. 누가 진실의 원천(Source of Truth)인지 결정하고, 어긋남을 맞추기 위한 주기적인 배치(Batch) 재동기화를 설계에 반드시 포함해야 한다.
6. 기준 4: 관계의 강도를 결정하는 세 축
상대의 변화에 어떻게 반응할지 결정할 때, 다음 세 가지 축을 기준으로 삼는다. 이를 조합하면 통신 방식과 모듈 경계가 자연스럽게 도출된다.
- 방향 (Direction): “누가 누구의 변화에 반응하는가?”
단방향을 지향한다. 양방향은 서로를 자극해 무한 루프나 갱신 충돌을 일으킬 위험이 크다. 내부 도메인이라면 무조건 단방향으로 풀어 결합도를 낮춰야 한다. - 동기화 수준 (Sync Level): “언제까지 맞춰야 하는가?”
- 즉시: 절대 뒤틀리면 안 되는 경우 (단일 트랜잭션).
- 결과적: 일시적 지연이 용인되는 경우 (비동기 이벤트, 모듈 분리).
- 배치: 정해진 주기에만 맞추면 되는 경우 (느슨한 결합).
- 모든 것을 ‘즉시’ 맞추려 하면 시스템은 모노리스가 된다.
- 변동 빈도 (Volatility): “얼마나 자주 반응해야 하는가?”
상태가 자주 변하는 개념과 거의 변하지 않는 마스터성 개념이 있다. 결과적 동기화를 택했더라도, 변동 빈도가 너무 높다면 통신 비용과 장애 리스크(유지 비용 = 통신 거리 × 빈도)를 고려해 같은 모듈로 묶는 것을 고려해야 한다.
동기화 수준 × 변동 빈도 의사결정 매트릭스
| 동기화 수준 (Sync) \ 변동 빈도 (Volatility) | 낮음 ~ 보통 | 매우 높음 |
|---|---|---|
| 즉시 (Immediate) (1초의 오차도 불가) | 같은 모듈 (단일 트랜잭션) | 같은 모듈 (단일 트랜잭션) |
| 결과적 (Eventual) (지연 용인, 비동기) | 다른 모듈 (이상적인 모듈 분리) | 같은 모듈 (예외적 통합) (분산 통신 오버헤드 방어) |
| 배치 (Batch) (주기적 재동기화) | 완전 분리 (가장 느슨한 결합) | 완전 분리 (가장 느슨한 결합) |
7. 모듈의 책임: 라이프사이클의 소유권
모듈은 자신에게 속한 엔티티들의 라이프사이클을 온전히 소유해야 한다.
- 소유의 의미: 엔티티의 상태(생성/변경/종료)를 직접 바꿀 수 있는 곳은 그 엔티티를 소유한 모듈 하나뿐이어야 한다. 다른 모듈은 직접 상태를 바꾸는 대신, 소유 모듈에게 “상태 변경을 요청” 하거나 발생한 변화에 “반응” 만 해야 한다.
- 절대 금기 사항: 일정이 급하다고 남의 리포지토리를 직접 호출하거나 다른 모듈의 테이블을 직접 수정하면 어떻게 될까? 라이프사이클 통제 로직이 파편화되고, 앞서 언급한 ‘상태 변화의 엣지 케이스 누락’이 거대하게 재현된다.
결론적으로 모듈이 외부에 공개해야 하는 것은 단순한 ‘데이터’가 아니라, “엔티티의 상태 변화를 일으키거나 반응하는 인터페이스(트리거)“여야 한다.
8. 앞으로 적용해보기
앞으로 기획을 분석하고 시스템을 설계할 때, 다음 세 가지를 가장 먼저 질문하려 한다.
- 엔티티 식별: “단순히 어떤 컬럼이 필요한가가 아니라, 이 개념은 언제 생성되고, 어떻게 변화하며, 어떻게 종료되는가?”
- 관계 파악: “대상 엔티티의 상태가 변할 때, 우리는 어떻게 반응해야 하는가? (방향/동기화 수준/변동 빈도 고려)”
- 경계와 책임: “관계의 강도에 따라 모듈 경계를 어떻게 설계할 것인가? 트레이드오프는 무엇인가?”
여기까지가 수많은 엣지 케이스를 마주하며 정리해 본 ‘라이프사이클 기반의 설계 기준’ 이다.