시작 절차와 PR 규칙은 CONTRIBUTING.md를 따릅니다. 이 문서는 백엔드 소스 패키지의 배치 기준입니다.
- 빈 패키지를 미리 만들지 않습니다.
.gitkeep으로 골격만 커밋하지 않고, 실제 클래스가 생길 때 그 패키지를 만듭니다. 이 문서가 "만들어질 위치"의 단일 기준입니다. - 새 클래스는 이 문서에 정의된 위치에만 둡니다. 표에 없는 새 도메인·계층이 필요하면 파일을 만들기 전에 이 문서를 먼저 갱신하고 팀과 합의합니다.
- 구조 자체가 리뷰 대상입니다. 규칙에서 벗어난 배치는 PR 본문 "리뷰 포인트"에 근거를 남깁니다.
루트 패키지는 com.pinlog.pinlogback입니다. 그 아래를 도메인(feature) 축과 공통(global) 축 둘로만 나눕니다.
com.pinlog.pinlogback
├─ PinlogBackApplication # 부트스트랩 (단독 파일)
├─ domain/ # 도메인별 세로 슬라이스
│ └─ {domain}/
│ ├─ controller # HTTP 진입점 (@RestController)
│ ├─ service # 도메인 로직·트랜잭션 경계
│ ├─ repository # 영속성 (Spring Data JPA)
│ ├─ entity # JPA Entity (@Entity)
│ ├─ dto # 요청·응답 DTO
│ └─ exception # 그 도메인의 규칙 위반·상태 예외 (global/exception과 구분)
└─ global/ # 도메인을 가로지르는 공통 관심사
├─ common # 공용 상수·enum·유틸·기반 타입 (BaseEntity 등)
├─ config # Spring 설정 클래스 (@Configuration)
├─ exception # 공통 예외 정의·전역 핸들러
├─ response # 공통 응답 타입 (ApiResponse·ErrorResponse·CursorPage·Cursor)
├─ web # 서블릿·MVC 확장 (ResponseBodyAdvice·Filter)
└─ security # 인증·인가. 지금은 인증 스텁(@LoginMember·MemberPrincipal·리졸버)만 있다
response와 web을 나눈 기준: response는 직렬화되는 계약 타입(응답 JSON의 형태 그 자체)이고, web은 그 계약을 요청·응답 파이프라인에 적용하는 장치입니다. 공통 응답 타입을 추가할 때는 response, advice·filter·interceptor·argument resolver는 web에 둡니다.
각 도메인은 하나의 애그리거트를 담당하고, 위 하위 계층(controller·service·repository·entity·dto·exception)을 필요한 것만 만듭니다. exception은 그 도메인에서만 의미가 있는 규칙 위반·상태 예외를 담습니다 — 여러 도메인이 함께 쓰는 것과 전역 핸들러는 global/exception입니다.
| 도메인 | 책임 | 비고 |
|---|---|---|
member |
회원 계정·프로필 | 공개 프로필·소개를 포함합니다. 별도 profile 도메인을 만들지 않습니다 |
place |
장소 마스터 | |
record |
방문·기록과 기록 본문(Context) | Context는 record 하위에 둡니다. 별도 context 도메인을 만들지 않습니다 |
collection |
컬렉션·큐레이션 | |
follow |
팔로우 관계와 공개 책장 탐색 | 공개 책장 탐색(ShelfController)은 경로가 /v1/feed/collections/{collectionId}/shelf지만 이 도메인에 둡니다 — 추천 계산을 하나도 거치지 않고, 응답에 요청자 기준 팔로우 상태가 실리며, 재사용하는 조회·판정이 FollowService가 쓰는 것과 같습니다. 경로 접두어와 도메인이 어긋난 유일한 사례이며 의도된 것입니다(BD-43) |
feed |
피드 조회·서빙 API | 관측 로그 core.feed_event 테이블은 AI 소유(V102) — 재정의 금지, 조회만 |
auth |
인증·인가 | Jira 작업의 인증 PR에서 생성됐습니다. 하위 계층은 controller(로그인 진입·재발급·로그아웃) · service(세션 토큰 발급·회전·폐기) · dto · exception · client(공급자 연결 해제 호출)이며, repository·entity는 없습니다 — 회원·소셜 계정은 member가 소유합니다. client를 global/security/oauth에 두지 않은 이유는 아래 "인증·보안 경계"에 있습니다 |
search |
개인 자연어 검색 조회 API | Record를 돌려주지만 record에 두지 않습니다 — 진입 경로(/v1/search/records)와 조립 규칙(FastAPI 응답의 Core 재검증)이 Record CRUD와 다릅니다. FastAPI 호출 자체는 ai가 맡고 이 도메인은 그 결과를 검증·조립만 합니다 |
ai |
FastAPI AI Server 연동과 ai 스키마 접근 |
애그리거트가 아니라 파트 경계입니다. 하위 계층은 repository(백엔드가 ai에 쓰는 SQL과 응답 조립용 읽기) · client(내부 API 호출) · event(커밋 이후 훅) · scheduler(시간이 촉발하는 훅) · service(요청 조립·트랜잭션 경계) · exception(호출 실패를 도메인 오류로 옮김)이며, controller·entity는 없습니다 — 외부 진입점이 아니고 남의 스키마를 엔티티로 고정하지 않습니다. event와 scheduler를 가른 기준은 무엇이 호출을 촉발하는가이고, 그에 따라 트랜잭션 경계도 다릅니다 — event는 남의 트랜잭션이 커밋된 뒤에 얹히고, scheduler는 자기 트랜잭션을 열고 닫습니다(Jira 작업) |
검토 필요:
member행의 "공개 프로필·소개를 포함합니다"는docs/static/06_데이터모델_및_무결성.md2.1(익명 서비스이므로 저장하는 개인정보가 없다)과 실제 구현체(domain/member/entity/Member— 개인정보·프로필 컬럼 없음)에 모두 반합니다. CLAUDE.md 9번 규칙에 따라 임의로 고치지 않고 충돌로 기록만 남깁니다.위 목록은 취소된 PR #8의 도메인 골격을 기준으로 정리하되,
profile은member에,context는record에 통합했습니다. 경계가 커져 분리가 필요해지면 이 문서를 먼저 갱신하고 팀과 합의합니다.
controller는service만 호출합니다.repository·entity를 직접 다루지 않습니다.- Entity를 요청/응답으로 직접 노출하지 않습니다. 경계에서
dto로 변환합니다. 세부는 API 규약을 따릅니다. - DTO가 늘어나면 도메인
dto아래를dto/request·dto/response로 나눌 수 있습니다. 나눌 때는 도메인 전체에 일관되게 적용합니다. - 두 도메인이 함께 쓰는 코드는 어느 한 도메인에 두지 않고
global/common으로 올립니다. 다만 한 도메인에서만 쓰는 코드를 미리global에 두지 않습니다. - Flyway migration은 소스 패키지가 아니라
src/main/resources/db/migration에 두며, 버전 소유 구간은 데이터베이스 규약을 따릅니다. - 소유자용과 공개용 DTO는 상속 없이 별개로 정의합니다. 리포지토리 메서드명에도
ForOwner/Public을 명시합니다. 상속이나 조건부 직렬화는 부모에 필드가 추가될 때 조용히 개인정보를 노출합니다.
결정 배경: BD-13 공개 경계를 쿼리·DTO 분리로 강제한 이유 — 실수했을 때 유출이 아니라 컴파일 오류로 실패하게 만든다
인증은 Jira 작업에서 인증 PR 계약에 따라 한 PR로 들어왔습니다. global/security는 책임별 하위 패키지로 나뉘어 있습니다.
| 패키지 | 책임 | 언제 도는가 |
|---|---|---|
security/oauth |
공급자와의 OAuth2 흐름 (OAuth 클라이언트 역할) | 로그인 진입·콜백 |
security/token |
세션 토큰 서명·검증과 쿠키 전달 (BFF 역할) | 로그인 성공·재발급 |
security/authentication |
요청을 인증 주체로 변환, principal 계약 (리소스 서버 역할) | 모든 요청 |
security/error |
필터 체인이 직접 만드는 401·403 응답 | 인증·인가 실패 |
- 의존은 한 방향입니다.
oauth와authentication이token을 쓰고,error는 어느 쪽도 쓰지 않습니다. 반대 방향 참조가 생기면 경계가 잘못된 것이므로 클래스를 옮길 자리를 다시 봅니다. - 발급(
token)과 검증(authentication)을 가른 기준은 수명과 호출 빈도입니다. 발급은 로그인 시점에 한 번, 검증은 모든 요청에서 돕니다. - 보안 설정은
global/config가 아니라global/security안에 둡니다. 설정과 그 설정이 조립하는 구현이 떨어져 있으면 한쪽만 고치게 됩니다.security/SecurityConfig— 하위 패키지 넷을 필터 체인으로 조립하는 유일한 지점이라 루트에 둡니다.security/token/JwtProperties— 소비자(JwtKeyProvider·JwtTokenProvider·AuthCookies)와 같은 패키지입니다.security/authentication/LoginMemberArgumentResolverConfig—@LoginMember리졸버를 MVC에 등록합니다.WebMvcConfigurer는 여러 개가 공존할 수 있으므로, 일반 MVC 설정이 필요해지면global/config에 따로 만들면 됩니다. 하나의 거대한 configurer로 모으지 않습니다.
- 어느 하위 패키지에도 속하지 않는 것은
security루트에 둡니다(현재CsrfCookieFilter하나). 억지로 끼워 넣지 않습니다 — 이름이 거짓말이 되는 쪽이 더 비쌉니다. - 공급자 연결 해제 호출은
security/oauth가 아니라domain/auth/client에 둡니다. 위 표의 기준이 "언제 도는가"인데security/oauth는 전부 필터 체인이 로그인 진입·콜백에서 돌리는 것입니다. 연결 해제는 탈퇴 흐름이 능동적으로 부르는 외부 API 호출이라 그 기준에 들어맞지 않습니다.ai/client(내부 API 호출)와 같은 성격으로 보고 도메인 쪽에 둡니다(BD-48).
하위 패키지에 클래스를 추가할 때:
@NullMarked는 하위 패키지로 상속되지 않습니다. 새 하위 패키지를 만들면package-info.java에 직접 선언해야 하고, 빠뜨리면 컴파일은 통과하지만@Nullable표기가 조용히 무의미해집니다(BD-29).
테스트는 대상 클래스와 같은 패키지 경로를 src/test/java에 둡니다. DB가 필요한 테스트는 PostgreSQL Testcontainers를 사용합니다. 세부는 테스트 규약을 따릅니다.