bibimbap/docs/analysis/2026-06-16-project-analysis.md

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-111711research/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 codeService/Request/Result/ErrorResult 어디서도 상속·호출 안 됨. Service.ChackService 는 세션키 "id"(line 12) 사용, 활성 코드는 "userId" 사용 → 재사용 시 인증 우회 함정. abstracts/Service.java:12
5 MED(부채) orphan 깨진 JSP fragmentWEB-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 GameCatalogstub(빈 배열), 레거시 정적 게임 카탈로그 폴백(현재 무력)
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.propertiesspring.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+#{}. 검색: searchVisibleGamesILIKE CONCAT('%', #{query}, '%') 로 안전(GamesMapper.java:85-87).
  • 단, 테스트 코드 DbUpdateQueryGeneratorTestString.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.jspforphan 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_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). → 후속 작업에서 전제 재확인 필요.

미해결 / 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 조회 권장.

관련 링크 (References)