서버
Activity 서버
Activity 서버
활동 기록: 다른 서버(notes)가 gRPC ActivityService/Record 로 남기고, 앱(admin)이 HTTP GET /api/activity 로 본다.
Spring Boot 4.1 · Java 25 · Gradle 멀티 모듈. HTTP :8001 · gRPC :9001, DB 는 Postgres(docker/docker-compose.middleware.yaml, 먼저 pnpm infra).
레포 전체 규칙은 루트 AGENTS.md, 이 파일이 이 서버에서는 우선한다. 명령은 이 폴더에서 돈다.
구조
레이어(docs/architecture.md 서버 레이어)는 패키지와 Gradle 모듈로 나눈다. api · domain · support 는 실행 앱 core/core-api 의 패키지,
storage · clients 는 모듈이다. 의존은 api → domain → storage · clients 방향만, 패키지는 모두 app 아래.
core/core-api/ # 실행 앱(bootJar)폴더 구조
- proto → gRPC 코드(protobuf 플러그인), openapi 태스크
src/main/
java/app/
- 진입점: app 아래 전부(core · storage)를 읽고, 엄격한 JSON(strictStrings)
core/
api/
- HTTP 라우트 + record DTO (Bean Validation = 검증 = 문서)
- gRPC 서비스 구현 + 요청 검증
- domain 레이어: 업무 로직 (HTTP · gRPC 공용)
- support: HTTP 오류 형식
- 포트 · 바인딩 · DB 연결 (환경 변수)
- Flyway. 서버가 뜰 때 적용
- 공통 설정: 랜덤 포트 HTTP · gRPC, 테스트 DB(activity_test), 테스트 앞뒤로 TRUNCATE
- pnpm gen: 실제 서버를 띄워 contract/openapi/activity.json (DB 없이)
- activity/GET_specs (경로 변수는 하위 패키지: activity/id/GET_specs)
- grpc/activityservice/Record_specs (패키지는 소문자)
선택한 경로
규칙
- 쓰기는 JSON 본문만 (
@PostMapping·@PutMapping(consumes = JSON), 아니면ApiErrors가 400). 모르는 필드는 record 가 버린다. - 오류는
{ "error" }(ApiErrors): 검증 실패 · 잘못된 JSON · 잘못된 경로 값({id}가 숫자가 아니거나@Positive위반) 400, 그 외 502, 없는 경로 404, 다른 메서드 405. - 경로 값에 제약(
@Positive)이 있으면 본문 검증도HandlerMethodValidationException으로 온다(MethodArgumentNotValidException이 아니다). - 바인딩은
HOST(server.address·spring.grpc.server.address, 기본 127.0.0.1). 밖에 열 때0.0.0.0, 그 전에 인증 · TLS. - DB 연결은
PGHOST·PGPORT·PGDATABASE(기본activity) ·PGUSER·PGPASSWORD(application.properties).
명령
./gradlew :core:core-api:bootRun # pnpm dev
./gradlew test # pnpm test
./gradlew :core:core-api:openapi # pnpm gen → contract/openapi/activity.json (DB 없이 돈다)
./gradlew checkFormat compileJava compileTestJava # pnpm lint (포맷 + 컴파일러 경고를 오류로)
./gradlew format # pnpm format (spring-javaformat, 탭)
docker build --build-context workspace=../.. . # 이미지 (이 폴더에서)
계약
- springdoc 이 컨트롤러 · record · Bean Validation(
@NotNull= required,@Size= maxLength)에서 문서를 만든다. - springdoc 은 테스트 의존성이다. 운영 앱에는 문서 경로가 없고,
OpenApiExport가 실제 서버를 랜덤 포트에 띄워/v3/api-docs를 받아 쓴다.@CoreApiTest를 쓰지 않고 Flyway 를 꺼서 DB 없이 돈다(pnpm typecheck도 gen 을 먼저 돌린다). - 같은 단순 클래스명 둘은 springdoc 이 경고 없이 하나로 덮어쓴다. DTO 는
{도메인}{역할}(Activity·ActivityList, 저장소 행은ActivityEntity)처럼 서버 안에서 유일하게. - 응답 record 도 필드에
@NotNull을 붙여야 문서에서 required 다(원시 타입long도).OffsetDateTime은string(date-time). - operationId 는 메서드 이름이라
get대신listActivity처럼 짓는다(겹치면get_1처럼 번호가 붙어 불안정해진다). - 문자열 필드에 숫자 · 불리언이 오면 400 이다(Jackson 강제 변환을 끔,
strictStrings). 끄면{"text": 1}이"1"로 저장된다.
gRPC
core/core-api가../../../../contract/proto에서 생성한다. protobuf-gradle-plugin 에plugins { create("grpc") }선언만 두면 Boot 플러그인이 protoc · grpc-java 버전과 태스크를 맞춘다(직접generateProtoTasks를 쓰면 중복으로 실패한다).- 생성 코드(
build/generated)는 포맷 검사에서 뺀다. - 요청 검증은 서비스 구현(
ActivityGrpcService)에서 하고, 실패는onError(Status.INVALID_ARGUMENT...)로 바로 돌려준다. 여기서 처리하지 않은 예외는 UNKNOWN. domain 예외를 status 로 바꿔야 하면GrpcExceptionHandlerBean 하나를 support 에 둔다.
테스트
@CoreApiTest:RANDOM_PORT+TestRestTemplate, gRPC 포트 0, 테스트 DBactivity_test(운영과 같은 Postgres),CleanDatabase가 테스트 앞뒤로 모든 표를TRUNCATE한다(Flyway 이력은 둔다). 서버는 다른 스레드라@Transactionalrollback 을 가정하지 않는다.- 파일
core/core-api/src/test/java/app/activity/GET_specs.java, 경로 변수{id}는 하위 패키지(app/activity/id/GET_specs.java), gRPC 는app/grpc/activityservice/Record_specs.java(Java 패키지는 소문자).@DisplayName("GET /api/activity")·@DisplayName("/app.activity.v1.ActivityService/Record"), 메서드void 기록을_최근_것부터_반환한다(). - 준비 · 저장 상태 확인은 저장소(
ActivityRepository)로. id 는 DB 가 매기고TRUNCATE가 되돌리지 않으니 저장소가 돌려준 id 를 쓴다(1 을 가정하지 않는다). - gRPC 클라이언트는
@LocalGrpcServerPort+ManagedChannel+ blocking stub. - 응답은
ParameterizedTypeReference<Map<String, Object>>처럼 타입을 적어 받는다(원시Map은-Werror에 걸린다). 작은 JSON 정수는Integer로 읽히니 기대값도(int) a.id(), 시각은 문자열이라OffsetDateTime.parse로 비교한다. 본문 없는 204 는String.class로 받아null을 본다. - 외부 클라이언트는
@MockitoBean대신 Fake 를@TestConfiguration의@PrimaryBean 으로(IntegrationTestConfig를 만들어@CoreApiTest에@Import). 실제 Bean 이 만들 때 외부에 접속하면 조건부 등록으로 막는다. hook 이@MockitoBean *Client를 막는다.
주의
- 스키마 변경은 새
V{n}__*.sql. main 에 들어간 마이그레이션은 고치지 않는다(체크섬이 어긋나 서버가 뜨지 않는다, hook 이 막는다). - javac 경고는 오류다.
serial만 끈다(예외 클래스는 직렬화하지 않는다).