# Local Dev Setup — 로컬 개발 환경 설정 로컬에서 앱을 구동할 때 필요한 환경/경로 설정을 둔다. ## ⚠️ 코드·JSP 변경 후 반드시 앱 재빌드·재시작 (가장 흔한 함정) 소스(Java 컨트롤러/매퍼/서비스)나 **JSP(`/WEB-INF/views/*.jsp`)** 를 고쳐도, **돌고 있는 앱은 자동으로 반영되지 않는다.** 변경을 커밋·테스트 통과까지 해도 *실행 중인 인스턴스가 구 버전이면* 화면 동작은 그대로다. > 증상 예: 좋아요/댓글 등 신규 서버 기능을 추가했는데 "고친 게 반영이 안 된다" — 십중팔구 **앱 미재시작**. 특히 클라이언트 동작(예: 로그인 안 했는데도 버튼이 눌리고 카운트가 로컬에서만 변함)이 보이면 구 JSP 가 서빙 중이라는 신호다. 구동 방식별 재배포: | 구동 방식 | 재빌드·재시작 | | --- | --- | | **IDE / 호스트 JVM** (`./mvnw spring-boot:run`, 또는 IntelliJ Run) | 앱 **Stop → 프로젝트 rebuild → 다시 Run**. 컴파일된 컨트롤러 변경은 hot-reload 안 됨(devtools 미사용). JSP 변경도 재기동으로 확실히 반영. | | **docker compose (로컬 dev — 권장)** | 아래 "로컬 도커 dev" 참조. JSP 는 즉시, Java 는 `docker compose restart app`. **이미지 재빌드 불필요**. | | **docker compose (배포 이미지 빌드)** | `docker compose -f docker-compose.yml up -d --build app` — Dockerfile 로 WAR 굽기. 배포/운영용. | | **수동 WAR** | `./mvnw -P dev clean package spring-boot:repackage -DskipTests` 로 실행형 WAR 재패키지(프로필·repackage goal 주의 — 본 문서 함정 참조) 후 재기동. | 확인: 재시작 후 의도한 신규 엔드포인트가 응답하는지 1회 스모크(예: 좋아요는 로그인 상태에서 클릭 → 새로고침·목록에서도 카운트 유지). 미로그인 클릭은 신 코드에선 401(로그인 유도)이 정상 — 더 이상 클라이언트에서 토글되지 않는다. > 코드 변경을 핸드오프할 때는 이 재빌드·재시작 단계를 `needs_user_verification`/배포 후 체크리스트에 **명시**한다. 커밋·테스트 통과 ≠ 실행 인스턴스 반영. ## 로컬 도커 dev (override) vs 배포 이미지 — 분리 로컬은 **이미지 재빌드 없이** 빠르게 돌리고, 배포 이미지는 **별도 경로**로 굽는다. - **로컬 dev**: `docker-compose.override.yml`(git 추적, dev 기본)이 `app` 을 재정의한다 — `maven:3.9-eclipse-temurin-21` 이미지에 소스를 bind-mount(`.:/build`)하고 호스트 `~/.m2` 캐시를 물려 `mvn -o -P dev spring-boot:run` 으로 띄운다. `docker compose` 는 base + override 를 **자동 병합**하므로 평소엔 그냥: ```bash docker compose up -d app # 로컬 dev (override 자동 적용, WAR 안 구움) docker compose logs -f app ``` 반영(이미지 재빌드 0): - **JSP/정적**: 원칙상 저장 즉시(Jasper 가 마운트된 webapp 재컴파일). **단, 반영 안 되는 사례 확인됨** (함정 참조 — 편집 후 `curl` 로 서빙 값이 옛 값이면 즉시반영을 신뢰 말고 `docker compose restart app` 으로 강제 확인). - **Java 코드**: `docker compose restart app` (spring-boot:run 재기동 ~1~2s). 또는 `docker compose exec app mvn -o -P dev -DskipTests compile` 후 restart. - 전제: 호스트 `~/.m2` 에 의존성 캐시 존재(offline `-o`). **새 의존성 추가 시**: override 의 `-o` 를 일시 제거해 1회 온라인 받거나, 호스트에서 `./mvnw -P dev dependency:go-offline` 후 다시 offline. > **의존성/플러그인 버전 변경 후 online prefetch 필요** (함정, 2026-06-29 확인): > SNAPSHOT→GA 등 버전 변경 후 offline 모드(`-o`)로 첫 기동 시 `spring-boot-maven-plugin` 신 버전 resolution 실패 → 컨테이너 즉시 종료. 컨테이너가 죽으면 `exec` 진입 불가. > > **처치**: 버전 변경 후 첫 기동 전 아래 중 하나로 온라인 prefetch 1회 수행. > ```bash > # 방법 A — run --rm 으로 플러그인·의존성 prefetch (컨테이너 내부, 호스트 ~/.m2 마운트됨) > docker compose run --rm app mvn -B dependency:resolve-plugins dependency:resolve -P dev > > # 방법 B — 호스트에서 mvnw 직접 실행 (호스트 JDK 21 필요) > ./mvnw -B -P dev dependency:resolve-plugins dependency:resolve > ``` > prefetch 성공 후 `docker compose up -d app` 으로 offline 재기동하면 정상 동작. - **배포 이미지**: override 를 **제외**하고 base 만 명시한다(자동병합 회피). ```bash docker compose -f docker-compose.yml build app # Dockerfile WAR 굽기 docker compose -f docker-compose.yml up -d --build app ``` > **⚠ 함정**: `docker compose up --build app`(`-f` 없이)을 로컬 dev 환경에서 실행하면 > override 가 `build: !reset null` 로 base 의 Dockerfile 빌드를 비활성화하므로 > **이미지를 새로 구워도 override 가 그 이미지를 무시**한다. > 로컬 dev 에서 Dockerfile 재빌드가 필요한 경우는 거의 없으나, 필요하다면 반드시 > `-f docker-compose.yml` 로 override 를 명시적으로 제외한다. > 로컬 Java 코드 변경 반영은 `docker compose restart app` 으로 충분하다. > > 근거: 세션 20260630-105459 — `docker compose up --build` 가 "No services to build" 반환. Dockerfile 은 멀티스테이지(WAR repackage → `eclipse-temurin:21-jre` + `java -jar`)로 운영과 동일한 산출물을 만든다(프록시 CA·`-P dev`·repackage goal 은 본 문서 함정 참조). > devtools 자동재시작은 도입하지 않았다 — 컨테이너 `spring-boot:run` 에는 소스 자동 재컴파일러가 없어(IDE 부재) 이득이 작고, `3.5.x-SNAPSHOT` devtools 의 offline 해소가 불안정했다. `restart`/`exec compile` 루프가 더 견고하다. > **⚠ 함정**: JSP "저장 즉시 반영"이 실패한 사례 확인됨 — CSS 수치 2줄 수정 후 `curl` 로 서빙 값을 대조했더니 옛 값 그대로였다(수정 파일 mtime 은 최신인데 컴파일된 `index_jsp.class` 는 그 이전 시각). 브라우저 fork 로 "정상 반영"이라 잘못 보고한 원인도 이 stale 렌더링을 봤기 때문(false pass). `docker compose restart app` 으로 강제 재기동하니 즉시 해소됨 — 근본 원인은 미확인(Jasper 재검사 간격 또는 Docker Desktop bind-mount mtime 전파 지연 추정)이나, **재현성 있는 회피책은 확정**: JSP 수정 직후엔 "즉시 반영"을 신뢰하지 말고 `curl` 로 실제 서빙 CSS/마크업을 수정값과 대조 → 불일치 시 `docker compose restart app` 후 재대조. 브라우저 스크린샷만으로 검증하면 이 stale 상태를 놓칠 수 있다. > > 근거: 세션 20260701-093754. ## 업로드 저장 루트 (static 트리 밖) 업로드물(프로필 이미지·게임 WebGL asset)은 **웹서버 정적 서빙 트리(`src/main/resources/static/`) 밖**에 저장한다. 직접 서빙을 차단하고 컨트롤러 권한 게이트를 강제하기 위함이다(보안 하드닝, commit `9041bb7`). ### 경로 규약 | 설정 키 | 값 | 비고 | | --- | --- | --- | | `app.upload.game-storage-path` | `${user.home}/.bibimbap/uploads` | `application.properties` 에 명시. `${user.home}` 는 Spring 런타임 placeholder | - 프로필 이미지: `~/.bibimbap/uploads/profile/{userId}/*.{png,jpg}` — `UploadResourceConfig` 의 `/profile/**` ResourceHandler 가 이 위치를 서빙. - 게임 WebGL asset: `~/.bibimbap/uploads/game/{gameUuid}/**` — `GameAssetController @GetMapping("/game/{gameUuid}/**")` 컨트롤러가 경계 검증 후 서빙(정적 핸들러 아님). ### 디렉토리 생성 런타임에 `Files.createDirectories` 로 자동 생성된다(`GameUploadController`·`UserController`). 첫 업로드 시 `~/.bibimbap/uploads/{game,profile}` 가 만들어지므로 수동 생성 불필요. ### @Value 기본값 주의 5개 클래스(`UploadResourceConfig`·`GameUploadController`·`GameAssetController`·`UserController`·`GameAssetCleanupService`)의 `@Value("${app.upload.game-storage-path:src/main/resources/static}")` **기본값(fallback)은 `src/main/resources/static` 으로 유지**한다. 이유: - `application.properties` 의 명시 설정이 항상 오버라이드하므로 실제 동작 경로는 `~/.bibimbap/uploads`. - 기본값을 홈 경로로 바꾸면 IDE/CI 가 명시 설정 없이 홈 디렉토리에 파일을 생성하는 부작용이 생긴다. - 업로드 테스트(`GameUploadControllerSecurityTest`)는 `@TempDir` + `ReflectionTestUtils.setField` 로 경로를 격리하므로 기본값과 무관. ## 자산 이전 이력 저장 루트가 static 트리 밖으로 이전되면서, **이전부터 `static/profile/` 에 커밋돼 있던 자산은 자동 이동되지 않는다.** 신규 루트로 수동 이전이 필요하다. | 일자 | 이전 대상 | 조치 | | --- | --- | --- | | 2026-06-29 | `src/main/resources/static/profile/8/*` (user 8 프로필 2파일) | `~/.bibimbap/uploads/profile/8/` 로 이동(체크섬 대조 검증) + tracked 원본 git rm. 이후 user 8 프로필은 신규 루트에서 서빙됨. | > `static/game/` 은 이전 시점 비어 있어(커밋된 게임 자산 0) 이전 대상 없음. 향후 저장 루트를 다시 옮기거나 자산을 마이그레이션할 때 이 표에 한 줄씩 기록한다. ## SSRF DNS 캐시 (운영 참고) `SsrfSafeFetcher` 가 `@PostConstruct` 로 `networkaddress.cache.ttl=30` 을 best-effort 설정한다(DNS rebinding TOCTOU 완화). 런타임 반영은 `InetAddressCachePolicy` lazy init 경합에 의존하므로 **결정적 보장이 필요하면 JVM 레벨**(`$JAVA_HOME/conf/security/java.security` 의 `networkaddress.cache.ttl=30`)이 정본이다. 앱 기동 후 `java.security.Security.getProperty("networkaddress.cache.ttl")` 로 반영 여부를 확인할 수 있다(배포 후 체크리스트 참조: `../maintenance/post-deploy-verification-checklist.md`).