22 KiB
| kind | title | description | perspective | valid_starting_point_for | owner | stability | last_reviewed | source | superseded_note |
|---|---|---|---|---|---|---|---|---|---|
| analysis | bibimbap 종합 코드 분석 (아키텍처/보안/품질/도메인) | Spring Boot 3 MVC + MyBatis + PostgreSQL + JSP 웹앱의 4차원(D1~D4) 전면 read-only 분석. 보안 기초·기술부채·도메인 인벤토리·미결 결정 정리. | neutral | 현황 파악, 보안 결정 입력(login/signup CSRF·세션 쿠키·SNAPSHOT 고정), 기술부채 정리(dead code 제거·서비스 레이어 추출), 미완성 기능(게임 댓글/좋아요) 방향 결정 | art | snapshot | 2026-06-16 | work-session 20260616-111711 (research-advisor, analysis.md) 에서 distill | 후속 결정으로 전제가 바뀌면 이 줄을 갱신한다 — 현재 없음 |
bibimbap 종합 코드 분석 (D1~D4)
분석 방식: 30개 Java 파일 + 16개 JSP + 6개 MyBatis 매퍼 전수 직접 읽기(Read/Grep/Bash). 외부 자료 미사용 → 모든 사실은 코드 직접 확인(
확인됨). 추정 항목은추정/미확인으로 명시했으며 사실로 격상하지 않았다.출처: work-session
20260616-111711의research/analysis.md를 정제한 정본 기록.
요약
Java/Maven 웹앱(Spring Boot 3 MVC + MyBatis + PostgreSQL, JSP 뷰) 전면 read-only 분석. 4개 차원으로 정리한다: 아키텍처(D1), 보안(D2, 최우선), 코드품질/기술부채(D3), 도메인/기능(D4).
상위 결론: 보안 기초는 의외로 탄탄(SQLi 표면 0, PBKDF2 견고, CSRF 더블서밋, zip-slip/path-traversal/zip-bomb 방어, XSS escape 일관)하나, 프로토타입 잔재성 dead code 와 미완성 기능이 화석으로 남아 신규 개발자에게 혼란·인증 함정을 유발한다. 서비스 레이어 부재로 fat controller 가 발생했고, 테스트가 거의 전무하다.
주요 발견 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 레이어 존재" 를 전제했다면 충돌(아래 종합 판단 참조). - 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-jasperfor JSP,spring-boot-starter-tomcatprovided). 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(placeholderyour_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 불가. (참조: db-update-query-generator.md)- 커버리지 갭: 컨트롤러·매퍼·인증·업로드·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 만 구현(추정). 상세: user-signup-schema.md
- 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_deletesoft-delete +is_visible노출제어 +sort_order정렬. 모든 조회가is_delete IS NOT TRUE필터(확인됨, 일관).
사용자 플로우
- 비로그인: 홈에서 게임 탐색/검색 → 게임 상세(WebGL 플레이, 댓글은 로컬), 모집 목록/상세 열람.
- 가입(이메일+비번 8자+약관) → 로그인(세션, remember) → 프로필(닉네임/아바타 변경).
- 로그인 사용자: 게임 등록(zip 업로드→썸네일→메타 저장), 본인 게임 수정/삭제, 모집글 작성.
- 인가: 게임 수정/삭제는 작성자만(소유권 체크).
종합 판단 (상위 패턴 · 충돌 · 갭)
강점: 보안 기초가 의외로 탄탄하다 — 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). → 후속 작업에서 전제 재확인 필요.
미해결 / open_questions
- 게임 좋아요·댓글의 서버 영속화는 의도된 미완성인가, 폐기된 기능인가? (스키마·매퍼는 완비, 엔드포인트만 없음) — 코드만으로 의도 판별 불가(
미확인). - 세션 쿠키 Secure/SameSite·HTTPS 강제는 배포 톰캣/리버스프록시 설정에 의존 — 저장소 코드 밖이라
미확인. 프로덕션 설정 확인 필요. provider/provider_user_id컬럼 = 소셜로그인 확장 예정 스키마인지(현재 email 전용) —추정.- spring-boot 3.5.14-SNAPSHOT 을 의도적으로 SNAPSHOT 유지하는 이유(특정 미릴리스 픽스 의존?) —
미확인. 로컬 빌드 영향(온라인 의존·오프라인 빌드 불가)은 usage/local-setup.md §1·§7 참조. - 의존성 CVE 스캔 미수행(외부 조회 안 함) — 별도
mvn dependency-check또는 OSV 조회 권장.
관련 링크 (References)
- 출처(work-session):
.atp/work-session/20260616-111711/research/analysis.md - db-update-query-generator.md —
DbUpdateQueryGeneratorTest운영 도구(D3 테스트 항목 관련) - user-signup-schema.md —
users/user_auth_identities스키마 상세(D4 데이터 모델 관련) - 카테고리 분류 기준: development/document-category-classification.md