---
phase: research
agent: research-advisor
agent_version: 2
generated_at: 2026-06-16T11:17:11Z
concerns:
  - "abstracts 패키지(Service/Request/Result/ErrorResult)는 전부 dead code이며 세션키('id' vs 'userId')가 활성 코드와 불일치 — 향후 재사용 시 인증 우회 함정. requirements 전제(레이어 구조)와 충돌 가능: 실제 서비스 레이어는 존재하지 않음(컨트롤러가 매퍼 직접 호출)."
  - "POST /login, POST /signup 에 CSRF 검증 부재(다른 모든 mutation 엔드포인트는 검증함) — login CSRF 가능. design 단계로 넘기기 전 보안 결정 필요."
concerns_checked: true
source_confidence: high
workers_spawned: 0
---

# 조사 결과 — bibimbap 종합 분석 (D1~D4)

> 분석 방식: 30개 Java 파일 + 16개 JSP + 6개 MyBatis 매퍼 전수 직접 읽기(Read/Grep/Bash). 외부 자료 미사용 → 모든 사실은 코드 직접 확인(`확인됨`). 추정 항목은 명시.

## 주제
Java/Maven 웹앱(Spring Boot 3 MVC + MyBatis + PostgreSQL, JSP 뷰) 전면 read-only 분석: 아키텍처(D1), 보안(D2, 최우선), 코드품질/기술부채(D3), 도메인/기능(D4).

---

## 주요 발견 Top 10

| # | 심각도 | 발견 | 위치 |
|---|--------|------|------|
| 1 | INFO(긍정) | **SQL 인젝션 표면 0** — 6개 매퍼 전 쿼리가 파라미터 바인딩 `#{}` 만 사용. `${}` 문자열 보간 단 1건도 없음(java/xml 전수 grep). 검색어도 `ILIKE CONCAT('%', #{query}, '%')` 로 안전. | mapper/*.java 전체 |
| 2 | INFO(긍정) | **비밀번호 해싱 견고** — PBKDF2WithHmacSHA256, 210,000 iterations, 16B salt, 256bit, `MessageDigest.isEqual`(상수시간 비교). 형식 `pbkdf2_sha256$iter$salt$hash`. | UserController.java:542-576 |
| 3 | MED | **POST /login, /signup CSRF 미검증** — logout/profile/game/recruit 모든 mutation은 `CsrfTokens.isValid()` 검사하나 login·signup은 누락 → login-CSRF 가능. | UserController.java:118-160(login), 65-116(signup) |
| 4 | MED(부채) | **abstracts 패키지 전체 dead code** — `Service`/`Request`/`Result`/`ErrorResult` 어디서도 상속·호출 안 됨. `Service.ChackService`는 세션키 `"id"`(line 12) 사용, 활성 코드는 `"userId"` 사용 → 재사용 시 인증 우회 함정. | abstracts/Service.java:12 |
| 5 | MED(부채) | **orphan 깨진 JSP fragment** — `WEB-INF/jsp/fragments/header.jspf` 가 Spring Security 태그(`<sec:authorize>`, `${_csrf}`)와 JSTL 사용하나 (a)어디서도 include 안 됨 (b)Spring Security 의존성 자체가 pom에 없음 (c)라우트(`/games/new`,`/register`)·브랜딩('비빔밥')이 활성앱(`/game/new`,`/signup`,'bibimbap')과 불일치. 이전 프로토타입 잔재. | jsp/fragments/header.jspf:1-25 |
| 6 | MED(부채) | **GameCatalog 완전 stub** — 모든 배열이 `{}` 빈 배열, `COUNT=0`. `GameController.gameDetail` 의 폴백 분기(line 111-128)는 영구 도달 불가(`isValidId`는 항상 false). 죽은 분기 + 죽은 클래스. | game/GameCatalog.java:8-22, controller/api/GameController.java:111-128 |
| 7 | MED(부채) | **게임 댓글: DB 스키마·매퍼 존재하나 미연결** — `game_comments` 테이블, `GameCommentsMapper`(get/add/update), `GameCommentData` 모두 존재하지만 **댓글 작성/조회 컨트롤러 엔드포인트 없음**. 실제 댓글은 JSP 내 localStorage 클라이언트 전용(서버 미저장). `softDeleteGameComments`만 게임 삭제 시 호출됨. | mapper/GameCommentsMapper.java, views/game-detail.jsp:907-1000 |
| 8 | LOW | **WebApplicationFirewall/인증필터 부재 — 세션 기반 임시방편 인증** — Spring Security 미사용. 각 컨트롤러가 수동으로 `session.getAttribute("userId")` 체크. 인가는 리소스 소유권 체크(`userId.equals(game.getUserId())`)로 일관 처리됨(양호)하나 중앙 집중 필터 없음 → 신규 엔드포인트에서 체크 누락 위험. | 전 컨트롤러 |
| 9 | LOW | **생성 산출물 git 추적** — `src/test/db/dev-to-live-update.sql`(테스트가 생성하는 dev→live 마이그레이션 진단)이 커밋됨. 실제 비밀값은 없고(`-- DATA DIFF ... password_hash` 주석뿐) 비밀 누출 아님. 단 생성물은 비추적이 적절. | src/test/db/dev-to-live-update.sql:54 |
| 10 | INFO | **XSS 방어 일관 적용(비관습적)** — JSTL `<c:out>` 0건이나, 모든 JSP가 Java scriptlet 변수 할당 시점에 `HtmlUtils.htmlEscape()` 적용 후 출력. 사용자 콘텐츠 sink(게임명/제작자/모집글/닉네임/이메일/창작노트/검색어) 전수 확인 결과 escape 누락 없음. CSP·X-Content-Type-Options 도 WebGL asset 응답에 설정. | views/*.jsp 전체, GameAssetController.java:60-66 |

---

## D1 — 아키텍처/구조

### 패키지별 책임 (1줄)

| 패키지 | 역할 |
|--------|------|
| (root) | `BibimbapApplication`(@SpringBootApplication main), `ServletInitializer`(WAR 배포용 SpringBootServletInitializer) |
| abstracts | **dead code** — 미사용 제네릭 Service/Request/Result/ErrorResult 베이스 (확인됨: 참조 0) |
| config | `UploadResourceConfig` — 업로드 경로의 `/profile/**` 정적 리소스 핸들러 등록 |
| controller | 페이지(뷰) 컨트롤러 — `WebMvcController`(공통 페이지+에러), `RecruitController`(모집 페이지+API 혼합) |
| controller/api | API/mutation 컨트롤러 — Game CRUD, GameUpload(zip/썸네일/파일), GameAsset(WebGL 서빙), User(인증/프로필), 예외 핸들러 |
| data | 순수 POJO DTO 6종 (Lombok 미사용 — 수동 getter/setter) |
| game | `GameCatalog` — **stub(빈 배열)**, 레거시 정적 게임 카탈로그 폴백(현재 무력) |
| mapper | MyBatis `@Mapper` 인터페이스 6종 — 어노테이션 기반 SQL(XML 매퍼 0) |
| security | `CsrfTokens` — 세션 기반 CSRF 토큰 발급/검증 유틸(static) |

### 요청 흐름
- **페이지 렌더**: Controller → (서비스 레이어 없음) → Mapper → DB → ModelAndView/Model → `/WEB-INF/views/*.jsp` (ViewResolver prefix/suffix는 application.properties).
- **API mutation**: `@PostMapping`/`@DeleteMapping` → CSRF 검증 → 세션 인증 → 입력 검증 → Mapper → JSON ResponseEntity.
- **서비스 레이어 부재**(확인됨): 컨트롤러가 매퍼를 직접 주입·호출. `@Transactional`은 컨트롤러 메서드에 부착. → requirements가 "service 레이어 존재"를 전제했다면 충돌(concern 기재).
- **JSP 렌더 방식**(확인됨): JSTL 미사용. Java scriptlet(`<%%>`/`<%=%>`)으로 `request.getAttribute()` 직접 캐스팅 + `HtmlUtils.htmlEscape()`.

### 레이어별 핵심 클래스

| 레이어 | 클래스 | 역할(1줄) |
|--------|--------|----------|
| 진입 | BibimbapApplication / ServletInitializer | main 부트 / WAR 배포 부트 |
| 페이지 | WebMvcController | `/`,`/{pageName}`(화이트리스트), `/error`, 프로필·게임등록 뷰 게이팅 |
| 페이지+API | RecruitController | `/recruit`(목록), `/recruit/new`(폼+생성 POST), `/recruit/{id}`(상세) |
| API | GameController | 게임 생성/수정/삭제(soft) + 상세/편집 뷰 + 소유권 인가 |
| API | GameUploadController | WebGL zip 업로드(zip-slip 방어), 썸네일·일반파일 업로드 |
| API | GameAssetController | `/game/{uuid}/**` WebGL 정적 서빙 + CSP/인코딩 헤더 |
| API | UserController | signup/login/logout/프로필(닉네임·아바타) + PBKDF2 해싱 |
| API | ApiExceptionControllerAdvice | 업로드 초과·일반 예외 → JSON 에러(스택 비노출) |
| 매퍼 | Games/RecruitPosts/Users/UserAuthIdentities/GameComments/GameLikes Mapper | 어노테이션 SQL CRUD |

### Spring/MyBatis 배선·프로파일
- `@SpringBootApplication` 단일. MyBatis는 `mybatis-spring-boot-starter` 자동 배선 + `@Mapper` 스캔(별도 @MapperScan 불필요, 확인됨).
- 프로파일: pom `dev`/`live` 프로파일이 `app.profile` 치환 → `application.properties`의 `spring.profiles.active=@app.profile@`(빌드 필터링) → `spring.config.import`로 `${profile}/db.properties` 로드. dev/live는 PostgreSQL 동일 호스트·다른 schema(`currentSchema=dev|live`).
- DB props: `db.properties.example`만 git 추적, 실제 `db.properties`는 .gitignore 처리(확인됨: gitignore `### Local secrets ###`).
- 앱 진입: embedded Tomcat(`spring-boot-starter-web`) + WAR(`tomcat-embed-jasper` for JSP, `spring-boot-starter-tomcat` provided). web.xml 없음(Servlet 3+ initializer).

---

## D2 — 보안 (최우선)

### 인증/인가
- **메커니즘**(확인됨): Spring Security 미사용(의존성 없음). `HttpSession` 속성 기반 커스텀. login 시 `request.changeSessionId()`로 세션 고정 공격 방어(UserController.java:152). remember 옵션으로 세션 타임아웃 30분/30일 분기.
- **인가**(확인됨): 리소스 mutation마다 `userId.equals(resource.getUserId())` 소유권 체크 일관 적용(GameController updateGame:183, deleteGame:239, editView:142). role 기반 인가는 미구현(role 컬럼은 저장만, 'USER' 고정).

### 자격증명/시크릿
- 하드코딩 시크릿 **없음**(확인됨): tracked 파일은 `*.example`(placeholder `your_username/your_password`)뿐. 실제 db.properties는 gitignore. application.properties에 비밀값 없음.
- 비밀번호 저장: PBKDF2-SHA256 210k iter(상기 Top#2). 평문·약한해시 없음.

### SQL 인젝션 (전수 — `${` vs `#{`)
- **전 매퍼 `${}` 보간 0건**(확인됨, grep 전수). 모든 동적값은 `#{}` 또는 `@Param`+`#{}`. 검색: `searchVisibleGames`도 `ILIKE CONCAT('%', #{query}, '%')`로 안전(GamesMapper.java:85-87).
- 단, **테스트 코드** `DbUpdateQueryGeneratorTest`는 `String.formatted()`로 schema명을 SQL에 직접 삽입(line 333,369) + `quoteIdentifier`/`escapeSql`로 수동 이스케이프. 이는 프로덕션 경로 아님(테스트 전용, surefire에서 제외됨)이나 패턴상 주의(LOW, 입력이 상수 'dev'/'live'라 실위험 없음 — `확인됨`).

### JSP XSS / CSRF
- **XSS**(확인됨): `<c:out>` 0건이나 모든 사용자 콘텐츠 sink가 `HtmlUtils.htmlEscape()` 처리(index.jsp:506-507, recruit-detail.jsp:전 필드, recruit-list.jsp:286-292, profile.jsp:16-18, header.jsp:11, game-detail.jsp:7-21, game-register.jsp:13-17, theme-init.jsp:7). raw `<%=` sink들도 추적 결과 모두 escape된 변수 또는 안전값(ctx, id, String.format)임을 확인.
- 게임 댓글: 클라이언트 JS가 `textContent`로 렌더(game-detail.jsp:970,983) → DOM XSS 안전. 단 서버 미저장.
- **CSRF**(확인됨): 커스텀 더블서밋 — `theme-init.jsp`가 `<meta name="csrf-token">` + `window.BibimbapCsrf.headers()`로 `X-CSRF-Token` 헤더 주입. 검증은 `CsrfTokens.isValid()`(헤더 또는 `_csrf` 파라미터, 세션값과 `.equals` 비교). **단 login/signup 미적용**(Top#3, MED).
- 세션/쿠키: 기본 JSESSIONID. `Secure`/`SameSite` 명시 설정 없음(application.properties 미존재 → 컨테이너 기본값. 프로덕션 HTTPS에서 Secure 미설정이면 LOW~MED 위험 — **추정**: 배포 톰캣 설정 미확인이라 코드만으로 단정 불가).

### 파일 업로드 보안 (양호)
- zip-slip 방어: `target.startsWith(targetDir)` 정규화 검증(GameUploadController.java:128,200,266-269).
- zip bomb 방어: entry 수 8000 / 압축해제 512MB 상한(line 40-41,262,297).
- path traversal: 프로필 아바타·에셋 서빙 모두 `normalize()`+`startsWith(root)` 검증(UserController.java:248-263, GameAssetController.java:101-104). UUID 검증으로 게임 디렉토리 화이트리스트.
- 콘텐츠 타입: 확장자 화이트리스트(png/jpg/webp/gif). **추정 한계**: MIME/확장자만 검사, 매직바이트 미검증 → polyglot 업로드 가능성(LOW, WebGL asset은 nosniff+CSP로 완화).

### 정정 (orientation 대비)
- orientation은 "JSP under WEB-INF/jsp(+fragments)"를 활성 뷰처럼 기재했으나, 실제 활성 뷰는 `WEB-INF/views/*.jsp`이고 `WEB-INF/jsp/fragments/header.jspf`는 **orphan dead 잔재**(Top#5)임을 확인.

---

## D3 — 코드 품질/기술부채

| 항목 | 발견 | 위치 |
|------|------|------|
| Dead code | abstracts 패키지 4클래스 전부 미사용; GameCatalog 빈 stub+도달불가 분기; header.jspf orphan | Top#4,5,6 |
| 미완성 기능 | game_comments 스키마·매퍼·DTO 존재하나 컨트롤러 미연결(localStorage 임시) | Top#7 |
| 레이어 위반 | 서비스 레이어 부재 → 컨트롤러가 비즈니스 로직+매퍼 직접 호출(fat controller). UserController 629라인(해싱·파일IO·세션관리·검증 혼재) | UserController.java 전체 |
| 중복 | `sessionUserId()` 동일 메서드가 4개 컨트롤러에 복붙(RecruitController:155, GameController:291, GameUploadController:222, UserController:325, WebMvcController:118). `trimToNull/trimToEmpty`, `imageExtension/profileImageExtension`, UUID 정규화도 중복 | 다수 |
| 미사용 의존성 | Lombok이 pom+컴파일러 플러그인에 선언되었으나 data 클래스는 수동 getter/setter(Lombok 미활용) | pom.xml:74-79, data/*.java |
| 오타/네이밍 | 뷰 파일명 `errer.jsp`(error 오타), 메서드명 `ChackService`(Check 오타), 필드 `is_login`(자바 네이밍 컨벤션 위반 snake_case) | views/errer.jsp, abstracts/Service.java:7,11 |
| 미사용 코드 | `WebMvcController.isMobileDevice()` 정의만 되고 호출 없음(line 130) | WebMvcController.java:130 |
| 에러 처리 | API는 일관(ResponseEntity+JSON, 스택 비노출). 단 `Result` 생성자의 `System.out.println("잘못된 status")`(line 21)는 로깅이 아닌 stdout | abstracts/Result.java:21 |
| 매직값 | 역할/참여유형/상태 한글 문자열 하드코딩 Set(RecruitController:25-27); status 1000("NULL USERS") 같은 비표준 코드 | RecruitController.java:25-27 |
| 설정 위험 | 업로드 1GB 상한(application.properties), `max-swallow-size=-1`(무제한) → DoS 표면(**추정**: 인증 게이트 뒤라 완화되나 큰 값) | application.properties |

### 테스트
- `BibimbapApplicationTests.contextLoads()` — 스모크만(빈 본문).
- `DbUpdateQueryGeneratorTest` — 실제 단위테스트 아님. dev/live DB 직접 연결해 schema/data diff SQL을 `src/test/db/dev-to-live-update.sql`로 출력하는 **운영 도구**(surefire에서 제외, pom 명시). 실DB 필요 → CI 불가.
- **커버리지 갭**: 컨트롤러·매퍼·인증·업로드·CSRF 로직 단위/통합 테스트 전무. 보안 핵심(PBKDF2 검증, zip-slip, 소유권 인가) 미검증.

### pom 위생
- **spring-boot 3.5.14-SNAPSHOT**(확인됨) — SNAPSHOT은 비재현·비프로덕션. spring-snapshots 저장소 의존. 안정 릴리스로 고정 필요(MED 부채, **추정**: 보안패치 추적 어려움).
- Java 21, MyBatis starter 3.0.5, PostgreSQL(runtime), 의존성 수 적고 깔끔. CVE 직접 스캔 미수행(외부 조회 안 함 — `미확인`).

---

## D4 — 도메인/기능

### 기능 인벤토리 (기능 → 엔드포인트 / 매퍼 / 뷰 / 데이터)

| 기능 | 엔드포인트 | 매퍼 | 뷰 | 데이터/테이블 |
|------|-----------|------|-----|--------------|
| 게임 목록/검색 | GET `/`(q 검색) | GamesMapper.getVisibleGames/searchVisibleGames | index.jsp | games, users |
| 게임 상세 | GET `/game/{id}` | getGame | game-detail.jsp | games, users |
| 게임 등록 | GET `/game/new`(뷰), POST `/game/new` | addGame, nextSortOrder | game-register.jsp | games |
| 게임 수정/삭제 | GET/POST `/game/{id}/edit`, DELETE `/game/{id}` | updateGame, softDeleteGame, softDeleteGameComments, deleteGameLikes | game-register.jsp(edit) | games, game_comments, game_likes |
| WebGL 업로드 | POST `/api/game-files`, `/webgl-zip`, `/thumbnail`, GET `/ping` | — (파일시스템) | (AJAX) | 업로드 디렉토리 |
| WebGL 서빙 | GET `/game/{uuid}/**` | — | (iframe) | 파일시스템 |
| 팀원 모집 목록/상세 | GET `/recruit`, `/recruit/{id}` | getVisibleRecruitPosts, getRecruitPost | recruit-list.jsp, recruit-detail.jsp | recruit_posts, users |
| 모집글 작성 | GET/POST `/recruit/new` | addRecruitPost, nextSortOrder | recruit-form.jsp | recruit_posts |
| 회원가입/로그인/로그아웃 | POST `/signup`,`/login`,`/logout` | Users/UserAuthIdentities add/get/update | signup.jsp, login.jsp | users, user_auth_identities |
| 프로필 | GET `/profile`(뷰), POST `/profile/nickname`,`/profile/avatar` | getUser, updateUser, getGamesByUserId | profile.jsp | users, user_auth_identities |
| 정적 페이지 | GET `/terms`,`/operation-policy` | — | terms.jsp, operation-policy.jsp | — |
| 게임 댓글 | **(엔드포인트 없음)** | GameCommentsMapper(미연결) | game-detail.jsp(localStorage) | game_comments(미사용) |
| 게임 좋아요 | **(엔드포인트 없음)** | GameLikesMapper(deleteGameLikes만 호출) | game-detail.jsp(JS baseLikes) | game_likes(쓰기경로 없음) |

### 데이터 모델 (매퍼 SQL에서 역추출 — `확인됨`)
- **users**(id, display_name, canonical_email, avatar_url, role, status, last_login_at, created_at, updated_at, is_delete)
- **user_auth_identities**(id, user_id, provider, provider_user_id, email, password_hash, display_name, avatar_url, last_login_at, created_at, updated_at, is_delete) — provider='email' 고정, 소셜 확장 의도된 스키마(provider/provider_user_id)이나 email만 구현(**추정**)
- **games**(id, user_id, name, creator_note, git_url, webgl_path, thumbnail_url, like_count, is_visible, sort_order, created_at, updated_at, is_delete)
- **recruit_posts**(id, user_id, project_name, genre, summary, role, project_status, participation_type, expected_period, team_members, contact, description, reference_url, deadline_at, is_visible, sort_order, created_at, updated_at, is_delete)
- **game_comments**(id, game_id, nickname, content, created_at, deleted_at, is_delete) — 매퍼는 완성, 미연결
- **game_likes**(id, game_id, user_key, created_at)
- 공통 패턴: `is_delete` soft-delete + `is_visible` 노출제어 + `sort_order` 정렬. 모든 조회가 `is_delete IS NOT TRUE` 필터(확인됨, 일관).

### 사용자 플로우
1. 비로그인: 홈에서 게임 탐색/검색 → 게임 상세(WebGL 플레이, 댓글은 로컬), 모집 목록/상세 열람.
2. 가입(이메일+비번 8자+약관) → 로그인(세션, remember) → 프로필(닉네임/아바타 변경).
3. 로그인 사용자: 게임 등록(zip 업로드→썸네일→메타 저장), 본인 게임 수정/삭제, 모집글 작성.
4. 인가: 게임 수정/삭제는 작성자만(소유권 체크).

---

## 종합 판단 (상위 패턴 · 충돌 · 갭)

**강점**: 보안 기초가 의외로 탄탄하다 — SQLi 0, PBKDF2 견고, CSRF 더블서밋, zip-slip/path-traversal/zip-bomb 방어, XSS escape 일관, 세션 고정 방어, 소유권 인가 일관. 코드가 "보안을 의식하고 작성"된 흔적이 뚜렷.

**핵심 부채 패턴**: 프로토타입 잔재가 곳곳에 화석으로 남음 — (a)abstracts 미사용 프레임워크 시도, (b)GameCatalog 정적 카탈로그→DB 마이그레이션 후 stub만 잔존, (c)Spring Security 기반 header.jspf orphan, (d)game_comments/game_likes 서버 기능 미완성(스키마만). 이들은 "구현되다 만/대체된" 코드로, 신규 개발자에게 혼란·인증함정(abstracts의 'id' 키)을 유발.

**아키텍처 갭**: 서비스 레이어 부재로 fat controller(특히 UserController 629줄). 중복 헬퍼(sessionUserId 5중복) → 공통 유틸/베이스 추출 여지. 테스트 거의 전무가 가장 큰 리스크(보안 로직 회귀 무방비).

**보안 미결 결정**: login/signup CSRF 적용 여부, 세션쿠키 Secure/SameSite, spring-boot SNAPSHOT 고정 — design 단계 보안 결정 필요.

**requirements 충돌 가능**: "레이어드 아키텍처(controller/service/mapper)" 전제가 있었다면, 실제로 service 레이어는 없음(abstracts는 dead). → concern 기재.

---

## 미해결 / open_questions

1. 게임 좋아요·댓글의 서버 영속화는 의도된 미완성인가, 폐기된 기능인가? (스키마·매퍼는 완비, 엔드포인트만 없음) — 코드만으로 의도 판별 불가(`미확인`).
2. 세션 쿠키 Secure/SameSite·HTTPS 강제는 배포 톰캣/리버스프록시 설정에 의존 — 저장소 코드 밖이라 `미확인`. 프로덕션 설정 확인 필요.
3. `provider`/`provider_user_id` 컬럼 = 소셜로그인 확장 예정 스키마인지(현재 email 전용) — `추정`.
4. spring-boot 3.5.14-SNAPSHOT을 의도적으로 SNAPSHOT 유지하는 이유(특정 미릴리스 픽스 의존?) — `미확인`.
5. 의존성 CVE 스캔 미수행(외부 조회 안 함) — 별도 `mvn dependency-check` 또는 OSV 조회 권장.
