feat: 게시판 CUD API 구현, 추천 기능, 조회수, CSRF, 게시판 레이아웃 프레임 및 README 업데이트 - #12
Conversation
…, DTO 방어적 복사, 계층 분리 및 API 버저닝
🤖 Gemini AI PR Code Review반갑습니다! 백엔드 아키텍처와 보안, 그리고 성능 관점에서 서비스의 안정성을 단단하게 다져나가는 두 번째 스프린트 PR이군요. 게시판 도메인의 비회원/회원 익명 포스팅, CSRF 방어, 커스텀 비밀번호 마스킹 및 페이징 최적화까지 서비스의 확장성과 보안 기틀을 다지기 위한 많은 고민이 소스코드 곳곳에 잘 녹아있습니다. 시니어 아키텍트로서 이번 PR이 실제 프로덕션 환경(High-Traffic & Scale-Out)에 배포되었을 때 발생할 수 있는 잠재적 장애 요인과 아키텍처적 위험을 스스로 인지하고 극복할 수 있도록, 정교하고 따뜻하게 코드 리뷰를 전달해 드립니다. 🔍 1. [PR 구현 목적 ↔ 실제 코드 대조 분석]
🏛️ 2. [잠재적 위협 & 아키텍처 딥다이브 (Security & System Risks)]🚨 위협 1:
|
yyy9942
left a comment
There was a problem hiding this comment.
고생하셨어요 코멘트 단 내용 확인해서 답변 달아주세요.
아래 내용은 codex가 잡아준 내용이니, 이 내용도 참조해보세요.
-
H2/MySQL 설정 불일치
application.yml:16
[Blocker] Docker MySQL이 실제 검증에 사용되지 않습니다.
Docker Compose에서는 MySQL을 실행하지만 애플리케이션 기본 datasource는 H2를 바라보고 있습니다. 따라서 Compose를 실행해도 백엔드는 MySQL이 아닌 H2에 연결되고, MySQL DDL·제약조건 오류를 발견할 수 없습니다.
H2와 MySQL 설정을 프로필로 분리하고, Docker 프로필에서는 MySQL datasource를 사용하도록 구성해주세요. 수정 후 실제 연결 JDBC URL과 MySQL 환경에서 게시글 작성·삭제 및 익명 추천을 검증한 결과도 남겨주세요. -
게시글 deleted_at 누락
ddl.sql:114
[Blocker] 엔티티의 Soft Delete SQL과 DDL이 일치하지 않습니다.
Post의 @SQLDelete는 삭제 시 deleted_at = NOW()까지 실행하지만 현재 post 테이블에는 deleted_at 컬럼이 없습니다. 이 DDL로 생성한 MySQL에서 삭제 API를 호출하면 존재하지 않는 컬럼 오류가 발생합니다.
게시글과 댓글 엔티티의 Soft Delete SQL을 DDL과 전부 대조해주세요. 수정 후 실제 MySQL에서 삭제 API를 호출하여 is_deleted, status, deleted_at이 기대한 값으로 변경되는지 확인해주세요. -
익명 추천 스키마 불일치
ddl.sql:136
[Blocker] 현재 DDL에서는 비로그인 익명 추천을 저장할 수 없습니다.
엔티티는 익명 추천일 때 member_id=null을 허용하고 anonymous_voter_id를 사용하지만, DDL은 member_id NOT NULL이며 anonymous_voter_id, writer_ip 컬럼도 없습니다.
엔티티와 DDL의 컬럼, null 허용 여부, 회원 추천 UNIQUE 제약, 익명 추천 UNIQUE 제약을 일치시켜주세요. H2 테스트뿐 아니라 실제 MySQL에서 회원 추천과 익명 추천의 생성·취소·중복 요청을 검증해주세요. -
댓글 작성 API 주소 오류
page.tsx:210
[Blocker] 백엔드 API 계약과 주소가 달라 댓글 작성이 404로 실패합니다.
백엔드는 /api/v1/posts/{publicId}/comments를 제공하지만 여기서는 /api/posts/{publicId}/comments를 호출하고 있습니다. 백엔드 테스트와 프론트 빌드가 각각 성공해도 실제 연동에서는 실패합니다.
이 라인만 수정하기보다 API base URL과 /api/v1을 공통 모듈로 관리해주세요. 수정 후 일반 댓글과 대댓글을 브라우저에서 각각 작성하고 실제 요청 URL과 응답 상태를 확인해주세요. -
댓글 삭제 주소·비밀번호 노출
page.tsx:253
[Blocker] 삭제 주소와 익명 비밀번호 전달 방식 모두 수정이 필요합니다.
백엔드는 /api/v1/comments/{id}를 제공하지만 여기서는 /api/comments/{id}를 호출해 404가 발생합니다. 또한 익명 비밀번호를 URL 쿼리 문자열에 추가하면 Nginx, 서버, APM, 프록시 로그 등에 평문으로 남을 수 있습니다.
API 주소를 통일하고 비밀번호는 request body DTO로 전달해주세요. 정상 비밀번호는 성공, 잘못된 비밀번호는 403, 요청 URL에는 비밀번호가 포함되지 않는 것을 검증해주세요. -
게시글 삭제 비밀번호 RequestParam
PostController.java:93
[Blocker] 민감정보를 URL로 받는 API 계약을 변경해주세요.
anonymousPassword를 @RequestParam으로 받으면 모든 클라이언트가 비밀번호를 URL에 담아야 합니다. 프론트만 수정해서는 해결되지 않으므로 백엔드 계약 자체를 request body 방식으로 변경해야 합니다.
삭제 요청 DTO를 만들고 로그인 작성자·관리자·비회원 익명 작성자의 권한 분기를 각각 테스트해주세요. 특히 관계없는 로그인 사용자가 익명 비밀번호를 알 경우 삭제 가능한지도 정책을 명확히 검증해주세요. -
댓글 삭제 비밀번호 RequestParam
CommentController.java:48
[Blocker] 댓글 삭제도 게시글과 동일한 보안 계약을 사용해야 합니다.
CommentDeleteRequest DTO가 존재하지만 실제 컨트롤러에서는 사용하지 않고 비밀번호를 @RequestParam으로 받고 있습니다.
사용되지 않는 DTO를 실제 API에 적용하고 게시글·댓글 삭제의 비밀번호 전달 규칙을 통일해주세요. 정상 비밀번호, 잘못된 비밀번호, 로그인 작성자, 관리자 케이스를 컨트롤러 테스트에 추가해주세요. -
숨김·차단 게시글 목록 노출
PostRepositoryCustomImpl.java:42
[Blocker] isDeleted=false만으로는 공개 게시글을 판별할 수 없습니다.
현재 조건에서는 HIDDEN, BLOCKED, DRAFT 게시글도 삭제 상태만 아니라면 일반 사용자 목록에 노출됩니다. 상세 접근만 차단해도 목록에서 제목·작성자·카운트가 노출될 수 있습니다.
일반 사용자 목록에는 status=NORMAL 조건을 적용해주세요. 댓글 조회·작성과 추천 API도 동일한 상태 정책을 사용하는지 확인하고, 상태별 접근 테스트를 추가해주세요. -
QueryDSL 성능 설명 불일치
MemberRepositoryCustomImpl.java:25
[Major] “단일 조인 DTO 프로젝션”이라는 설명과 구현이 다릅니다.
실제 구현은 회원 엔티티, 리조트 이름, 라이딩 스타일 이름을 각각 조회하므로 총 3개 쿼리입니다. 현재 구조는 한 회원당 고정된 3개 쿼리라 전형적인 N+1은 아니지만, 단일 쿼리 최적화라고 설명할 수도 없습니다.
3개 쿼리를 의도적으로 선택했다면 선택 이유와 트레이드오프를 문서에 적어주세요. 단일 쿼리가 목표라면 실제 projection과 중복 row 조합 방식을 다시 설계하고 쿼리 횟수도 테스트해주세요. -
가상 스레드 중복 설정
AsyncConfig.java:16
[Major] 가상 스레드 설정의 적용 범위와 중복 여부를 설명해주세요.
spring.threads.virtual.enabled=true를 설정하면서 별도의 taskExecutor도 등록했습니다. Spring Boot 자동 실행기와 직접 등록한 실행기 중 어떤 것이 Servlet 요청과@Async작업에 각각 사용되는지 확인이 필요합니다.
또한 가상 스레드는 플랫폼 스레드 비용을 줄이지만 DB 커넥션, 메모리, 외부 API 제한까지 해결하지는 않습니다. 따라서 “Native OOM 원천 차단” 표현은 수정해주세요. 현재@Async대상과 가상 스레드를 선택한 근거, DB 커넥션 풀이 고갈될 때의 제한 전략도 설명해주세요.
|
conflicts 나는것도 해결하기! rebase나 merge를 진행해주세요. |
…e/CommentService.java Co-authored-by: Jun Jeong <46917538+yyy9942@users.noreply.github.com>
4.경로가 /api/posts/...로 잘못 호출되던 부분을 /api/v1/posts/{publicId}/comments로 수정했습니다. 동시에 프론트엔드 곳곳에 하드코딩되어 있던 API 주소들을 모아서 관리할 수 있도록 frontend/app/lib/api.ts 공통 모듈을 만들고, 전체 페이지가 API_ENDPOINTS를 참조하도록 했습니다. 빌드와 API 연동도 정상 통과합니다.
Servlet HTTP 요청: spring.threads.virtual.enabled=true에 의해 Tomcat 서블릿 컨테이너가 모든 인바운드 HTTP 요청을 가상 스레드(virtual-0, virtual-1...)로 처리합니다. 물리적 한계 및 과장 표현 정정: @async 대상 및 가상 스레드 선택 근거: DB 커넥션 풀 고갈 방지 전략: |
yyy9942
left a comment
There was a problem hiding this comment.
고생하셨어요 정리 조금만 더 하고 머지해주세요.






📌 개요 (Overview)
feature/sprint02-board➔main#12(2차 MVP 개발 타겟 - 게시판 도메인, 비회원 익명 포스팅, CSRF 보안)🛠️ 주요 변경 사항 (What Changed)
POST /api/v1/posts,GET /api/v1/posts/{publicId})Post엔티티,PostCategory,PostImage,PostReaction,PostStatusEnum 설계.POST /api/v1/posts) 시 회원/비회원 익명 작성 지원 및anonymousPassword(BCrypt) 암호화 저장.GET /api/v1/posts/{publicId}) 시Member+PostCategoryJOIN FETCH(조인 페치) 단일 쿼리 최적화.PUT /api/v1/posts/{publicId}) 시 로그인 세션(publicId), 로그인한 익명 작성자 패스워드 생략, 비회원 익명 패스워드 검증 권한 분기.DELETE /api/v1/posts/{publicId}) 시 Soft Delete (is_deleted = true,status = 'DELETED') 적용 및 최고 관리자(ROLE_ADMIN) 패스워드 우회 삭제 권한 부여.DeleteConfirmModal.tsx)prompt()팝업을 100% 제거하고 비밀번호 마스킹(●●●●) 및 실시간 에러 피드백을 지원하는DeleteConfirmModal.tsx커스텀 모달 UI 구축.TopNav로그인 세션 연동 (GET /api/v1/members/me)으로 로그인 여부를 동적 감지하여[닉네임]님프로필 및Sign Out버튼으로 동적 전환.CookieCsrfTokenRepository)CookieCsrfTokenRepository.withHttpOnlyFalse()연동으로XSRF-TOKEN쿠키 ↔X-XSRF-TOKEN헤더 이중 검증(Double Submit Cookie) 적용.viewed_posts세션 쿠키(30분 유효기간,HttpOnly) 핑거프린팅을 활용하여 새로고침 및 Strict Mode 중복 호출에 의한 조회수 무분별 폭증 차단.PostRepositoryCustomImpl)PostRepositoryCustomImpl구동으로 카테고리, 스키장 리조트(resortId), 검색 조건(제목/본문/작성자), 정렬(최신순/추천순/조회순) 동적 커스텀 쿼리 연동.MAX_OFFSET_PAGE가드)과 모바일 무한스크롤을 위한 Cursor 기반 페이징(CursorUtils) 듀얼 제공.💡 핵심 기술 의사결정 및 트레이드오프 (Technical Rationale)
CookieCsrfTokenRepositoryDouble Submit Cookie CSRF 보안 적용: 세션 쿠키(JSESSIONID) 환경에서 악의적인 외부 사이트의 위조 CUD 요청(CSRF 공격)을 차단하기 위해withHttpOnlyFalse()로XSRF-TOKEN을 발급하고, 요청 시X-XSRF-TOKEN헤더를 검증하여 SOP(동일 출처 정책) 기반 보안 강화.viewed_posts쿠키 핑거프린팅 기반 30분 중복 조회수 방지: 단일 조회가 발생할 때마다 DBviewCount를 무분별하게 올려 1회 접근에 조회수가 3씩 튀는 현상을 막기 위해, 브라우저 세션 쿠키에 조회한 게시글 ID를 대괄호 핑거프린팅 방식으로 기록하여 30분간 중복 카운팅 억제.ROLE_ADMIN) 패스워드 우회 삭제 권한: Hard Delete 시 발생하는 데이터 복구 불가 및 어뷰징 이력 추적 단점을 극복하고자 Soft Delete(status = DELETED)를 채택하였으며, 최고 관리자(ROLE_ADMIN)는 작성자가 설정한 익명 비밀번호 없이도 즉시 삭제 가능하도록 권한 행렬 분기 설계.PostListResponsevsPostDetailResponse): 목록 페이징 조회 시 수십~수백 KB의 대용량 본문(content)을 매번 패치하여 발생하는 DB I/O 병목과 네트워크 대역폭 낭비를 막고자, 목록 DTO에는 핵심 메타데이터만 얹어 트래픽을 90% 이상 절감.🧪 테스트 및 검증 결과 (Verification & QA)
.\gradlew.bat test실행 결과 총 51개 테스트 100% PASS (BUILD SUCCESSFUL)PostServiceTest: 게시글 작성, 상세 조회(조회수 쿠키 검증), 수정/삭제 권한 분기 테스트 검증.PostRepositoryCustomTest: QueryDSL 카테고리/검색/페이징 쿼리 검증.WebCookieManagerTest:viewed_posts및 익명 투표 쿠키 생성/파싱 검증.npm run build실행 결과 100% PASS (✓ Compiled successfully)ROLE_ADMIN) 비밀번호 우회 삭제 동작 검증 완료.✅ PR 체크리스트 (Checklist)
comment) 도메인 관련 코드가 PR 커밋 및 변경에서 100% 제외되었는지docs/study/) 및 불필요한 파일이 git 커밋에서 제외되었는지README.md및 작업 기록지(work.md), API 명세서(05.api-spec.md)가 최신 상태로 업데이트되는지