아키텍처 원칙

로그인

User

Paging

🗄️ JPA 설계

연관관계 원칙

배포

파일

댓글

방식: 완전 REST API (@RestController + JS fetch)

게시글(SSR)과 달리 댓글은 detail.html 안에서 JavaScript로 비동기 렌더링함. | 기능 | URL | HTTP | 응답 | |——|—–|——|——| | 목록 (페이징) | GET /api/communities/{id}/comments?page=0&size=10 | GET | JSON (PageResponse) | | 작성 | POST /api/communities/{id}/comments | POST | JSON (201 Created) | | 수정 | PUT /api/communities/{id}/comments/{commentId} | PUT | JSON | | 삭제 | DELETE /api/communities/{id}/comments/{commentId} | DELETE | JSON |

detail.html에서의 동작

페이지 로드 시 loadComments(0) JS 함수가 호출되어 fetch로 댓글 목록을 JSON으로 받아온다. renderComments()로 댓글 목록을 DOM으로 렌더링하고, renderPagination()으로 페이징 버튼을 렌더링한다.

로그인 체크 (Interceptor가 아닌 Controller 자체 처리)

CommentApiController에서 세션의 loginUser가 null이면 401 UNAUTHORIZED JSON 응답을 반환한다. Interceptor는 SSR 페이지에 적용하여 redirect하고, REST API는 401 JSON 응답이 적절하므로 Controller에서 직접 처리한다.

📁 파일

파일 종류 구분 (FileEntity)

FileController (@Controller) — 브라우저 직접 요청

| 기능 | URL | 용도 | |——|—–|——| | 다운로드 | GET /files/download/{fileId} | 첨부파일 다운로드 (브라우저 파일 저장 다이얼로그) | | 이미지 서빙 | GET /uploads/{filename} | 에디터 본문 이미지 표시 |

정석은 WebConfig의 addResourceHandlers()로 정적 리소스 매핑하는 것이지만 Controller에서 일관 처리

FileApiController (@RestController) — JS fetch 호출

| 기능 | URL | HTTP | 용도 | |——|—–|——|——| | 목록 조회 | GET /api/files?refId={refId}&refType={refType}&usage={usage} | GET | 첨부파일/에디터 이미지 목록 | | 업로드 | POST /api/files/upload | POST | 에디터 이미지 즉시 업로드. 첨부파일은 글 등록 컨트롤러에서 파일 업로드 |

FileController와 FileApiController를 분리한 이유: @Controller@RestController를 섞으면 전역 에러 처리(@ControllerAdvice vs @RestControllerAdvice)가 제대로 분리되지 않음. 브라우저 직접 요청(다운로드, 이미지)은 FileController, JS fetch 요청(목록, 업로드)은 FileApiController로 나누어 각각 GlobalExceptionHandler(error.html), ApiExceptionHandler(JSON ErrorResponse)가 올바르게 적용됨.

Community

| 동작 | 처리 위치 | 방식 | |——|———–|——| | 첨부파일 업로드 | POST /community/write, POST /community/{id}/edit | form submit (multipart) → fileService.saveUploadedFiles() | | 첨부파일 삭제 | POST /community/{id}/edit | form submit (deleteFileIds[]) → fileService.deleteFilesSafely() (해당 글 파일인지 체크 후 고아 처리, 물리 파일은 스케줄러가 정리) |

물리 파일 저장 경로

공통 예외 처리

커스텀 예외 계층

RuntimeException을 상속하는 BusinessException (추상 클래스, status와 errorCode 공통 필드)이 있고, 이를 상속하는 EntityNotFoundException(404), AccessDeniedException(403), BusinessRuleException(400), DuplicateResourceException(409)이 있다.

@Responebody와 공통에러처리

Service에서 예외 사용

ApiExceptionHandler — REST API용 (적용됨)

@RestControllerAdvice(annotations = RestController.class)로 등록되어 있으며, @RestController에서만 동작. GlobalExceptionHandler와 같은 예외를 잡지만 JSON(ErrorResponse)으로 응답한다.

{ "status": 404, "errorCode": "NOT_FOUND", "message": "게시글을(를) 찾을 수 없습니다: 99" }

화면이 바뀌는게 아니라 fetch/axios의 콜백(.then/.catch)으로 응답이 가므로, 프론트에서 alert이든 토스트 메시지든 JS에서 에러 처리 가능.

구분 GlobalExceptionHandler ApiExceptionHandler
대상 @Controller (SSR) @RestController (API)
응답 error.html 렌더링 JSON (ErrorResponse)
받는 쪽 브라우저 (페이지 전환) fetch/axios 콜백

ErrorResponse

REST API 에러 응답 DTO. 모든 API 에러 상황에서 일관된 JSON 구조로 응답.

스케줄러

설정

고아객체 처리

FileCleanupScheduler — 고아 파일 물리 삭제

소프트/하드 삭제

| 대상 | 삭제 방식 | 이유 | |——|———–|——| | User | 소프트 삭제 (isDeleted=true) | 계정 복구 가능성, 탈퇴 유예 기간 등 | | Community | 하드 삭제 (DELETE) | 즉시 삭제, 복구 불필요 | | Comment | 하드 삭제 (DELETE) | 게시글 삭제 시 FK 때문에 먼저 삭제 | | File | 고아 처리 (refId=0) → 스케줄러가 물리 삭제 + DB 삭제 | 물리 파일과 DB 분리 삭제 |

외부API 호출 이랑 엑셀까지 하자 … 어려운거 아니니까..