열린 항목

여기는 아직 정하지 않았거나, 정한 것끼리 부딪히는 지점을 모아두는 곳이다.

표준을 읽다가 빈틈을 발견하면 그 자리에서 여기에 적는다. 대화가 끊겨도 남아야 하기 때문이다. 적을 때는 무엇이 비었는지어떻게 발견했는지를 같이 남긴다. 나중에 다시 읽을 때 “이게 왜 문제였지”를 복원할 수 있어야 한다.

  • 모순 — 두 표준이 서로를 부정한다. 코드를 짜면 그 자리에서 막힌다
  • 미정의 — 정한 적이 없다. 사람마다 다르게 구현하게 된다

닫을 때는 아래 표에서 지우지 않고 닫힌 항목으로 옮긴다. 무엇이 왜 열려 있었는지가 그 표준을 만든 이유이기 때문이다.


요청 한 사이클에서 비어 있는 자리

#항목종류무엇을 정해야 하나파급
1 현재 시각을 얻는 방법 미정의
마감 시각 같은 정책 판단은 Domain이 해야 하는데, Domain은 POJO라 Clock을 주입받을 수 없다. LocalDateTime.now()를 직접 부르면 테스트에서 시각을 고정할 수 없다
중간
2 응답 상태 코드 미정의
HttpApiResponse.success(int status, T data) 오버로드가 예시 코드에 있는데 언제 쓰는지 규칙이 없다. 생성이 200인지 201인지, 삭제가 204인지, 그리고 code200000000으로 고정인 것과 status만 달라지는 관계가 미정
낮음
3 정상 요청 로깅과 추적 키 미정의
로그 규칙이 예외 경로에만 있다. 실패를 Advice에서 한 번 남기기로 했는데 그 로그 줄을 특정 요청과 이을 키가 없다. traceId·MDC를 정한 적이 없다
중간
4 Snapshot·Response의 null 표기 미정의
계층별 검증 표준이 Request@Nullable, Command·Query·DomainorNull 접미사로 갈라놨는데 나머지 둘이 어느 쪽인지 없다
낮음
5 ErrorCode 공통 대역의 경계 미정의
9자리 체계로 넘어오면서 DomainCode.COMMON(0)으로 자리는 잡혔다. 다만 어떤 실패를 공통으로 볼지의 기준 — 예를 들어 CONCURRENT_UPDATE가 공통인지 도메인인지 — 은 정한 적이 없다
낮음

닫힌 항목

무엇이 왜 열려 있었는지가 그 표준을 만든 이유다. 지우지 않고 남긴다.

항목어떻게 닫혔나
인증에 성공한 주체가 Command까지 가는 경로가 없었다
인증 주체 표준. common/security에는 실패 핸들러만 있고 성공 경로는 비어 있었다. LoginUserapi에서 받아 request.toCommand(loginUser)로 넘기고 application에는 Long userId만 보낸다. 본인 확인은 검증 표준의 축이 그대로 답이었다
Repository에 delete가 없어 애그리거트를 어떻게 지우는지 정해진 적이 없었다
삭제 표준. "삭제"가 가리키던 넷 중 셋은 이미 규칙이 있었고 루트 제거만 비어 있었다. 다른 도메인이 참조하는 애그리거트는 지우지 않고, 지울 수 있는 것에만 delete(XxxDomain)을 둬 컴파일러가 막게 했다. 도메인 간 ID 참조에 DB FK를 걸기로 한 것도 여기서 함께 정했다
조인 결과가 infra 밖으로 나갈 타입이 없었다
조회 전용 타입 표준. domainXxxView를 두고 구현체가 Projection·MapperResult를 변환해 내보낸다. domain에 둘 수 있는 것이 셋에서 넷으로 늘었다
여러 Domain을 조합한 결과 타입의 이름 규칙이 없었다
조합도 조인과 같은 XxxView를 채운다. 그래서 "쿼리가 반복되면 옮긴다"는 이주가 채우는 주체만 바꾸는 일이 되고, application 위쪽은 안 바뀐다
동적 검색이면서 조인인 조회의 자리가 없었다
충돌이 아니라 층위가 달랐다. 동적 여부는 인터페이스와 구현 기술을, 조인 여부는 infra 안쪽 타입을 가른다. 밖으로 내보낼 XxxView가 생기면서 그 조합이 끝까지 간다
트랜잭션 경계를 정한 표준이 없는데 세 표준이 그걸 전제로 서 있었다
경계를 XxxService의 UseCase 구현 메서드로 못 박았다. 쓰기는 @Transactional, 조회는 readOnly = true. UseCase에 없는 package-private 내부 메서드에는 붙이지 않는다 — 프록시가 가로채지 못한다
Domain이 POJO인데 save()mergeDomain이 모르는 컬럼이 조용히 null로 덮였다
Domain 영속화 표준. 수정 경로에서 merge를 쓰지 않고 영속 Entity를 꺼내 applyTo로 덮는다. 자식 컬렉션은 id로 대조하고 cascade = ALL + orphanRemoval = true를 건다
잡으라고 정한 OptimisticLockingFailureException이 발생할 조건이 없었다
@Version을 Entity에만 두고 영속 Entity를 직접 수정하는 방식으로 바꾸니 Domain이 version을 모르는 채로 낙관적 락이 정상 동작한다. 예외의 종류 표준에 이 전제를 명시했다
「배치」가 벌크와 Domain 순회 잡을 동시에 가리켰다
벌크와 배치를 새로 쓰고 {도메인}/batchbulk로 리네임. 가르는 질문을 "도메인의 판단이 개입하는가"로 정했다
ErrorCode 뒤 3자리가 두 글에서 다른 뜻이었고, 도메인이 10개까지밖에 안 들어갔다
9자리(상태 3 + 도메인 3 + 일련번호 3)로 넓히고 도메인 번호를 DomainCode enum으로 분리. 공통 대역 침범 문제도 COMMON(0)으로 함께 닫혔다
UseCase 표준이 "Service 출력은 항상 봉투로 감싼다"를 고쳤는데 DTO 표준에 반영되지 않았다
XxxProvider의 반환은 감싸지 않는다는 예외를 DTO 네이밍 표준에 추가했다
XxxSnapshot의 생성 방식이 DTO 생성 표준에 없었다
private 생성자 + package-private 정적 팩토리 from()으로 명시했다
운영에서 -ea가 꺼져 있는데 AssertionError 핸들러를 두는 이유가 규칙 파일에 없었다
도달 불가 분기의 throw new AssertionError-ea와 무관하게 실행된다는 연결을 assert 표준에 넣었다
인덱스에 표준 4개가 빠져 있었다
Null-free 객체 설계, 상속 vs 합성, 팩토리 메소드 VO 생성, Assert를 참조 순서 맨 앞에 넣었다