이 문서는 현재 Spring Boot 코드베이스에서 실제로 사용 중인 구조와 앞으로 맞춰야 할 구현 규칙을 정리한다. 일반 Java/Spring 관례보다 이 프로젝트의 기존 패턴을 우선한다.
- 도메인 루트는
com.example.demo.domain.{domainName}형태를 사용한다. 예:display,archive,lounge,user,personalartwork. - 도메인 내부는 기본적으로
presentation,application,domain,infrastructure계층으로 나눈다. presentation에는Controller,docs,mapper,request,response를 둔다.application은command,query,result, 필요 시service,usecase,mapper로 나뉜다.domain은aggregate,entity,vo,type/enums,repository,error를 둔다.infrastructure는persistence,oauth,mail처럼 기술 구현 기준으로 나누고, JPA 구현은persistence와persistence/adapter에 둔다.
- 클래스명은 역할이 드러나게 끝낸다:
DisplayController,UpdateDisplayService,DisplayPresentationMapper,JpaDisplayRepositoryAdapter. - Command/Query/Result record는 유스케이스명 또는 응답 의미를 붙인다:
CreateDisplayCommand,SearchDisplayQuery,DisplayDetailResult. - Repository port는 domain에
{Aggregate}Repository, query port는{Feature}QueryRepository를 사용한다. - Spring Data repository는
SpringData{Name}JpaRepository, 구현 adapter는Jpa{Name}RepositoryAdapter또는Jpa{Name}QueryRepositoryAdapter를 사용한다. - 위 adapter 규칙은
domain/repository의 repository를 구현하는 경우에만 적용한다.application/port의 port를 구현하는 adapter는 port의 역할이 드러나게 명명한다. 예:DisplayContentPublicationPort -> JpaDisplayContentPublicationAdapter,SchoolEmailSenderPort -> SchoolEmailSenderAdapter,DisplayListCacheEvictionPort -> DisplayListCacheEvictor. - 상수는
UPPER_SNAKE_CASE, 임시 인증 값은TEMP_USER_ID처럼 명확히 표시한다.
- Controller는 HTTP 요청/응답 조립만 담당하고, 비즈니스 규칙은 Application Service로 넘긴다.
- 공통 응답은 항상
ApiResponseBody.success(..., request)로 감싼다. - 현재 Swagger 작성 방식은 두 가지가 혼용된다.
- Controller가 docs interface를 implements:
ArchiveDisplayController,LoungePostController,AuthController. - Controller에 직접
@Operation,@ApiResponse작성:DisplayController,HealthController.
- Controller가 docs interface를 implements:
- 권장 통일 방식: 신규 API는
presentation/docs/*ControllerDocs인터페이스에 Swagger 문서를 두고 Controller는implements한다. - Swagger 예시는 응답 래퍼까지 포함해 실제
ApiResponseBody구조와 맞춘다.
- Request/Response DTO는 대부분 Java
record를 사용한다. - Request DTO는 HTTP 입력과 validation 중심으로 유지한다. 도메인 변환은 가능하면 mapper에서 수행한다.
- Command는 쓰기 유스케이스 입력, Query는 조회 조건, Result는 application 출력으로 사용한다.
- Result는 domain/entity를 그대로 노출하지 않고 응답에 필요한 값만 record로 구성한다.
- 조회 결과는
CursorResponse,PaginationResult,ExhibitionResult처럼 내부 record를 활용한다.
- Presentation Mapper는
@Component클래스로 작성하고Request -> Command,Result -> Response변환을 담당한다. - enum 이름이 API와 도메인에서 다르면 mapper에서 switch로 명시 변환한다. 예:
MEDIA -> VIDEO,TASK -> ASSIGNMENTS. - Controller에서 직접 변환 로직을 길게 작성하지 않는다.
- 현재 MapStruct 의존성은 있으나 실제 코드는 대부분 수동 mapper다. 신규 코드는 기존 수동 mapper 스타일을 우선한다.
- Domain과 JPA Entity 간 별도 mapper는 거의 없다. 현재 Aggregate가 JPA Entity 역할도 함께 하므로 adapter는 Spring Data repository에 위임하는 형태가 많다.
- Aggregate/Entity는
@Entity,@Getter, protected 기본 생성자, 명시 생성자 또는 static factory를 사용한다. - 필드 변경은 setter 대신 행위 메서드로 표현한다. 예:
changeContent,publish,delete,restore,replaceImages. - 내부 컬렉션은
List로 관리하고 외부에는Collections.unmodifiableList(...)로 노출한다. - Value Object는
@Embeddable을 사용하고 생성자에서 null/범위/blank 검증을 수행한다. - soft delete는
SoftDeleteBaseEntity또는deletedAt필드와delete()/restore()/isDeleted()계열 메서드로 처리한다.
- Application Service는 유스케이스 흐름 조율, repository 호출, transaction 경계, Result 생성만 담당한다.
- 쓰기 서비스에는
@Transactional, 조회 서비스에는@Transactional(readOnly = true)를 메서드 단위로 붙이는 패턴이 많다. - command 패키지에는 생성/수정/삭제/토글 유스케이스를 둔다.
- query 또는 service 패키지에는 조회 유스케이스를 둔다.
display는application/service + usecase가 있고, 다른 도메인은application/query서비스가 많다. - 시간 기준 로직은
Clock주입을 우선한다. 예: display 조회 서비스.
- domain repository interface는
domain/repository에 둔다. - infrastructure adapter는 domain repository를 구현하고 Spring Data repository에 위임한다.
- Spring Data repository는 infrastructure에만 둔다. domain/application에서 직접 의존하지 않는다.
- 복잡한 조회는 query repository port와 adapter를 분리한다. 예:
SearchDisplayQueryRepository,JpaSearchDisplayQueryRepositoryAdapter. - 단순 존재 확인용 repository도 domain port로 분리하는 패턴이 있다. 예:
UserExistenceRepository,DisplayArtworkExistenceRepository.
- 비즈니스 예외는
BusinessException(BaseErrorCode)로 던진다. - 공통 에러는
GlobalErrorCode, 도메인별 에러는{Domain}ErrorCodeenum으로 둔다. - ErrorCode enum은
BaseErrorCode를 구현하고HttpStatus,code,message를 가진다. GlobalExceptionHandler가 validation, type mismatch, malformed JSON, method/content-type 오류, fallback exception을 공통 처리한다.- 성공/실패 응답 구조는
resultType,success,error,meta를 유지한다.
- Request body 검증은 request record 필드에
@NotBlank,@NotNull,@Size,@Positive,@Min,@Max등을 붙인다. - 조건부 검증은 request record 내부
@AssertTrue메서드로 처리한다. - Query/path parameter 검증은 docs interface 또는 Controller 파라미터에 붙이고 Controller에는
@Validated를 사용한다. - 도메인 불변식은 DTO validation에만 맡기지 말고 Aggregate/VO 생성자 또는 행위 메서드에서도 방어한다.
- 인증 구현 전 임시
userId입력은 명시적으로 TODO 또는 request 필드로 유지하되, 인증 도입 시 제거 대상으로 기록한다.
- Entity/Aggregate에는 주로
@Getter만 사용하고 setter는 노출하지 않는다. - 생성자 주입은 명시 생성자와
@RequiredArgsConstructor가 혼용된다. 신규 코드는 한 클래스 안에서 한 방식만 사용한다. - DTO/Command/Query/Result는 record를 우선한다.
- JPA Entity 기본 생성자는
protected로 둔다. - 포맷팅은 Spotless
googleJavaFormat(),importOrder(),removeUnusedImports(),formatAnnotations()를 따른다. Checkstyle은 현재 없다.
- Controller 통합 테스트는
@SpringBootTest,@AutoConfigureMockMvc,@ActiveProfiles("test"), 필요 시@Transactional을 사용한다. - Repository adapter 테스트는
@DataJpaTest,@Import({Adapter.class, JpaAuditingConfig.class})를 사용한다. - 테스트 메서드명은 설명형 camelCase를 사용한다. 예:
updateDisplayReturnsForbiddenWhenRequesterIsNotTeamLeader. - MockMvc 검증은
status()와jsonPath()로 공통 응답 구조까지 확인한다. - 테스트 fixture는 private static helper로 만든다. 예:
displayWithTeamMembers(),publishedDisplay(...).
- API prefix는
/api/v1을 사용한다. - 리소스 기반 URL을 우선한다. 예:
/api/v1/lounge/posts/{loungePostId},/api/v1/archives/exhibitions. - 생성은
POST, 수정은PATCH, 삭제/취소는DELETE를 주로 사용한다. - 일부 legacy/신규 기능에서 토글 취소를
PATCH로 쓰는 API가 있다. 예:/api/v1/display/like. - 커서 조회는
cursorId,cursor,sizequery parameter를 사용하고 기본 size는 대체로10이다.
- 현재 혼용 방식: Controller 직접 Swagger 어노테이션 방식과
presentation/docsinterface 방식이 함께 존재한다. - 권장 통일 방식: 신규 API는 docs interface에 Swagger를 두고 Controller는 HTTP 매핑과 서비스 호출에 집중한다.
- 이유: Controller가 짧아지고, 문서 예시/응답 설명을 별도 파일에서 관리할 수 있다.
- 현재 혼용 방식: 일부 request record가
toCommand()를 직접 가지고, 일부는 Presentation Mapper가 변환한다. - 권장 통일 방식: 신규/수정 API는 Presentation Mapper에서
Request -> Command변환을 담당한다. - 이유: request DTO가 application/domain 타입에 직접 의존하는 것을 줄이고 변환 규칙을 한 곳에 모을 수 있다.
- 현재 혼용 방식:
display는application/service와usecase를 사용하고, 다른 도메인은application/queryservice가 많다. - 권장 통일 방식: 새 조회 기능은 도메인 기존 패턴을 따른다. display 안에서는 usecase interface가 이미 있으면 구현체를
application/service에 둔다. - 이유: 전체 대규모 이동 없이 도메인 내부 일관성을 유지한다.
- 현재 혼용 방식: 아키텍처 문서는 persistence mapper를 권장하지만, 실제 코드는 Aggregate가 JPA Entity를 겸하는 경우가 많다.
- 권장 통일 방식: 현 구조에서는 불필요한 JpaEntity/Mapper를 새로 만들지 않는다. 외부 테이블 조회 전용 projection/entity가 필요할 때만 infrastructure에 둔다.
- 이유: 현재 코드의 복잡도를 유지하면서 domain이 infrastructure 구현체를 직접 의존하지 않는 선을 지킨다.
- 현재 혼용 방식:
domain/*/domain/error,domain/user/exception,global/error가 함께 사용된다. - 권장 통일 방식: 신규 도메인 에러는 가능하면
{domain}/domain/error/{Domain}ErrorCode에 둔다. - 이유: 도메인별 예외 코드를 찾기 쉽고
BusinessException처리 구조와 맞다.
- 현재 혼용 방식:
build.gradle의 indentation/blank line이 Spotless 적용 전 스타일과 일부 섞여 있다. - 권장 통일 방식: 변경 전후
./gradlew spotlessApply또는 최소./gradlew spotlessCheck를 실행한다. - 이유: CI에서
spotlessJavaCheck/spotlessMiscCheck가 실패하는 것을 방지한다.
새 기능을 구현하거나 기존 코드를 수정할 때 아래 항목은 특히 확인한다. 이 항목들은 현재 코드에서 혼용되거나 컨벤션과 어긋난 사례가 확인된 부분이다.
- 현재 불일치: 일부 Controller/Service는 명시 생성자, 일부는
@RequiredArgsConstructor를 사용한다. - 권장: 한 클래스 안에서는 하나의 방식만 사용한다. 신규 Controller는 도메인 주변 코드가 쓰는 방식을 따른다.
- 수정 필요성: 생성자 주입 방식 자체보다 파일별 일관성이 중요하다.
- 현재 불일치: path/query parameter validation이 docs interface에 있는 경우와 Controller 파라미터에 있는 경우가 섞여 있다.
- 권장: docs interface를 implements하는 Controller는 docs interface에 Swagger와 parameter validation을 함께 둔다.
- 수정 필요성:
@Validated가 Controller에 없으면 parameter validation이 동작하지 않을 수 있으므로 신규 Controller에는 명시한다.
- 현재 불일치:
TEMP_USER_ID상수 사용, request body의userId입력, path/query 기반 임시 사용자 처리가 섞여 있다. - 권장: 인증 전에는 기존 API 흐름을 따르되
TEMP_USER_ID또는 requestuserId사용 이유를 주석/TODO로 남긴다. - 수정 필요성: 인증 도입 시 제거 대상이므로 비즈니스 규칙에 임시 사용자 값을 깊게 섞지 않는다.
- 현재 불일치: API에서는
exhibitionId, 도메인에서는displayId를 쓰는 등 외부 용어와 내부 용어가 다르다. - 권장: 외부 API 용어와 내부 도메인 용어 변환은 Presentation Mapper에서만 수행한다.
- 수정 필요성: Controller나 Service에서 용어 변환이 흩어지면 응답 필드명 변경 시 수정 범위가 커진다.
- 현재 불일치: 좋아요/저장 취소가
DELETE인 API와PATCH인 API가 함께 존재한다. - 권장: 신규 토글 리소스는 생성
POST, 취소DELETE를 우선한다. 기존PATCH /display/like는 호환성 때문에 유지한다. - 수정 필요성: 같은 성격의 토글 API가 서로 다른 HTTP method를 쓰면 프론트엔드 구현과 문서 이해가 어려워진다.
- 현재 불일치: display query repository는
application/query에 있고, 일반 repository는domain/repository에 있다. - 권장: 현재 display 구조에서는 기존 위치를 유지한다. 새 도메인의 저장소 port는 기본적으로
domain/repository에 둔다. - 수정 필요성: 대규모 이동 없이 도메인 내부 일관성을 우선한다. 단, infrastructure 구현체는 반드시 infrastructure에 둔다.
- 현재 불일치:
domain/user/exception과{domain}/domain/error가 함께 존재한다. - 권장: 신규 도메인 에러는
{domain}/domain/error에 둔다. user 도메인의 기존 exception 패키지는 별도 정리 전까지 유지한다. - 수정 필요성: 예외 위치가 분산되면 ErrorCode 검색과 재사용이 어려워진다.
- 현재 불일치:
application/command/*Service,application/query/*Service,application/service/*Service가 도메인별로 다르게 쓰인다. - 권장: 새 코드는 해당 도메인의 기존 패키지 규칙을 먼저 따른다. 도메인 내에서 새로 나눌 때는 쓰기
command, 읽기query를 우선한다. - 수정 필요성: 전체 패키지 재배치보다 도메인 내부 탐색 비용을 줄이는 것이 우선이다.
- 현재 불일치: Aggregate가 JPA Entity를 겸하는 구조와 reference/projection용 JpaEntity가 infrastructure에 따로 있는 구조가 공존한다.
- 권장: 핵심 Aggregate는 현재처럼 JPA 매핑을 직접 가진다. 단순 존재 확인, 외부 도메인 참조, 조회 전용 구조는 infrastructure reference/projection entity를 사용한다.
- 수정 필요성: 불필요한 persistence mapper 추가는 피하되, 다른 도메인 aggregate를 직접 의존하는 persistence 코드는 만들지 않는다.
- 현재 불일치: 일부 request DTO는 enum/command 변환을 직접 갖고, 일부는 mapper에서 처리한다.
- 권장: request DTO는 입력 필드와 validation만 갖고, 도메인 enum/command 변환은 mapper가 담당한다.
- 수정 필요성: presentation 계층이 domain/application 타입에 과도하게 의존하지 않도록 한다.
- 현재 불일치: 일부 response는 result 구조를 거의 그대로 복사하고, 일부는 API 용어로 재구성한다.
- 권장: application result는 유스케이스 결과 용어, response는 API 계약 용어를 사용한다. 변환은 mapper에 둔다.
- 수정 필요성: API 필드명 변경이 application service까지 번지는 것을 막는다.
- 현재 불일치:
application-local.yaml에는 기본값이 있는 환경변수가 있고,application-dev.yaml은 필수 placeholder 위주다. - 권장: dev/운영 프로파일은 누락 시 빠르게 실패하도록 필수 placeholder를 사용한다. local/test만 안전한 기본값을 허용한다.
- 수정 필요성: 클라우드 배포에서 GitHub Secrets에만 값이 있고 CD
.env에 누락되는 문제가 반복될 수 있다.