[BE] 공통 도메인 테크 스펙 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki
배경 (Background)
프로젝트 목표
- 서비스 전반의 구조를 파악하고 그 구조들에서 공통적으로 사용되는 로직을 분리하여 개발 생산성을 향상한다.
문제 정의
- 우리 서비스(Devths)는 구글 API, FastAPI 등 백엔드 서버 이외의 다양한 외부 서버와 소통한다.
- 서비스 전반의 구조를 파악하여 개발 과정에서의 혼란을 줄이는 것이 필요하다.
- 일부 기능들은 하나의 도메인이 아닌 여러 도메인에 공통적으로 사용된다.
- 해당 기능들을 정리하여 분리를 용이하게 하고 높은 개발 생산성 및 유지보수성을 확보해야 한다.
설계 및 기술 자료 (Architecture and Technical Documentation)
데이터베이스 스키마(ERD)
https://www.erdcloud.com/d/NyT4nhhqhXwbhRFQP
API 명세(API Specification)
Presigned URL 발급
- AI 챗봇, 게시글, 실시간 채팅 등에서 파일 첨부 시, 백엔드가 파일 바이트를 중계하지 않고 FE가 S3로 직접 업로드할 수 있도록 Presigned URL을 발급한다.
- 업로드(대용량/병렬 업로드 포함) 시 서버 리소스를 보호하기 위해 “URL 발급”과 “메타데이터 등록”을 분리한다.
POST /api/files/presigned
구현 상세
- 인증: Authorization 헤더 검증
- 요청 헤더
Authorization: Bearer {accessToken}를 Spring Security FilterChain에서 검증
- 요청 헤더
1-1. 인증 실패
-
토큰 누락/형식 오류/만료/서명 불일치 등으로 인증 실패 시
-
AuthenticationEntryPoint에서 401 Unauthorized 응답을 반환 -
응답 바디:
message = "인증 실패: 로그인하지 않은 사용자의 접근입니다.",data = null
- 요청 바디 유효성 검증
fileName,mimeType필수 값 검증 (null/blank 체크)mimeType허용 목록 검증(예: pdf, png, jpeg 등)fileName에 경로 탐색 문자열(../) 등 포함 여부 검사 (S3 Key 안전성)
2-1. 검증 실패
- 파라미터 형식/값이 유효하지 않으면 400 Bad Request 반환
message = "유효하지 않은 S3 Key이거나 요청 파라미터가 잘못되었습니다."
- S3 Key 생성 규칙 적용
- 서버에서 업로드 대상 경로를 결정하고
s3Key를 생성 - 예:
resumes/{yyyy}/{MM}/{uuid}_{fileName} - 여기서
fileName은 공백/특수문자 치환, 길이 제한 적용
- 서버에서 업로드 대상 경로를 결정하고
- Presigned URL 생성
- AWS SDK(S3 Presigner)로
PutObjectpresigned URL 생성 - 만료 시간(TTL): 10분
- Content-Type, Content-Length 제한이 필요하면 조건부 서명 정책(또는 업로드 후 검증)을 고려
- AWS SDK(S3 Presigner)로
- 응답 반환
- 200 OK
data.presignedUrl,data.s3Key반환message = "Presigned URL이 생성되었습니다."
에러/예외 매핑
- AWS SDK 예외(권한, 버킷 설정, 네트워크) → 500 Internal Server Error
- 입력값 문제(확장자/mime 불일치 등 정책 위반) → 400 Bad Request
- 인증 문제 → 401
S3 파일 첨부 완료
- FE가 Presigned URL로 S3 업로드를 끝낸 뒤, 업로드된 파일의
s3Key와 메타데이터를 DB에 파일 엔티티로 등록 refType/refId로 ‘어디에 첨부된 파일인지’를 연결(채팅방/게시글/챗봇 등)POST /api/files
구현 상세
- 인증: Authorization 헤더 검증
- 요청 헤더
Authorization: Bearer {accessToken}를 Spring Security FilterChain에서 검증
- 요청 헤더
1-1. 인증 실패
AuthenticationEntryPoint에서 401 반환
- 요청 바디 유효성 검증
- 필수:
originalName,s3Key,mimeType,category,fileSize,refType,sortOrder - 선택:
refId fileSize최소/최대 제한category,refType는 enum 매핑 실패 시 invalidsortOrder는 1 이상
- 필수:
2-1. 검증 실패
- 400 Bad Request 반환
- S3 Key 정책 검증
s3Key가 서버가 발급하는 prefix 규칙을 만족하는지 검증- 예:
resumes/,chat/,posts/등
- 예:
- 참조 무결성 검증 (refType/refId)
refId가 들어오는 경우- refType에 맞는 대상 엔티티 존재 여부 확인 (예:
chatroomRepository.existsById(refId))
- refType에 맞는 대상 엔티티 존재 여부 확인 (예:
- 존재하지 않으면 404
- DB 저장
FileEntity생성 (File(ownerId, s3Key, originalName, mimeType, category, fileSize, refType, refId, sortOrder))fileRepository.save(fileEntity)- 저장 후 생성된
fileId,createdAt확보
- 응답 반환
- 201 Created
data.fileId,data.s3Key,data.createdAt반환
에러/예외 매핑
- Validation/enum 매핑 실패 → 400
- 인증 실패 → 401
- DB 저장 실패/예상치 못한 런타임 예외 → 500
S3 파일 삭제
- “첨부 파일”이라는 비즈니스 개념을 파일 엔티티로 관리하되, 삭제는 파일 엔티티 기준으로 수행한다. (엔드포인트 경로는
/files로 통일) DELETE /api/files/{fileId}
구현 상세
- 인증: Authorization 헤더 검증
- 요청 헤더
Authorization: Bearer {accessToken}를 Spring Security FilterChain에서 검증
- 요청 헤더
1-1. 인증 실패
AuthenticationEntryPoint에서 401 반환
- PathVariable 검증
fileId가 Long 파싱 불가/음수/0 등 → 400 Bad Request
- DB 조회
fileRepository.findById(fileId)로 파일 엔티티 조회
3-1. 파일이 존재하지 않음
orElseThrow()로 도메인 예외 발생 → 404 Not Foundmessage = "존재하지 않는 파일입니다."
- 인가: 소유자 검증
file.ownerId == currentUserId확인
4-1. 인가 실패
- 다르면 403 Forbidden
message = "인가 실패: 본인의 파일에만 접근할 수 있습니다."
- S3 삭제
s3Client.deleteObject(bucket, file.s3Key)호출- S3 삭제 실패 시 정책
- 강한 일관성: S3 삭제 실패면 DB 삭제도 롤백
- DB 삭제(또는 소프트 삭제)
isDeleted=true갱신
- 응답 반환
- 204 No Content
- 바디 없음
에러/예외 매핑
- fileId 파싱/검증 문제 → 400
- 인증 실패 → 401
- 소유자 불일치 → 403
- 파일 없음 → 404
- S3/DB 처리 중 예외 → 500
비동기 작업 상태 확인
- 이미지/PDF 분석, 이력서/면접 리포트 생성 등 시간이 오래 걸리는 작업을 비동기로 처리한다.
- 초기 구현 단순화를 위해 Polling 방식으로 진행 상황을 조회한다.
- 추후 Webhook/WebSocket 등으로 전환 가능
GET /api/tasks/{taskId}
구현 상세
- 인증: Authorization 헤더 검증
- 요청 헤더
Authorization: Bearer {accessToken}를 Spring Security FilterChain에서 검증
- 요청 헤더
1-1. 인증 실패
AuthenticationEntryPoint에서 401 반환
- Path Variable 검증
taskIdLong 파싱/범위 검증 실패 → 400 Bad Requestmessage = "요청 파라미터가 잘못되었습니다."
- DB 조회
taskRepository.findById(taskId)로 비동기 작업 엔티티 조회
3-1. Task가 존재하지 않음
orElseThrow()→ 404 Not Foundmessage = "존재하지 않는 Task입니다."
- 인가: 소유자 검증
task.ownerId == currentUserId확인
4-1. 인가 실패
- 403 Forbidden
message = "인가 실패: 본인의 비동기 작업에만 접근할 수 있습니다."
5-1. PROGRESSING
- 아직 처리 중이면 202 Accepted
data.result = null,isNotified=false등 그대로 반환message = "비동기 작업이 처리 중입니다."
5-2. COMPLETED
- 200 OK
taskType에 따라data.result구조가 달라짐- RESUME/INTERVIEW → 채팅방 메시지 형태의 REPORT 결과
- SCHEDULE → 일정 추출 결과(회사명, 데드라인, 위치, 태그 등)
message는 taskType별 성공 메시지 사용- 예:
"이력서 및 포트폴리오 분석에 성공하였습니다."
- 예:
5-3. FAILED
- 200 OK + status=FAILED
- 실패 사유:
failReason,failMessage포함 - 동시성 고려: 최초 조회 1회만 처리하려면 조건부 업데이트 필요
에러/예외 매핑
- taskId 파싱 오류 → 400
- 인증 실패 → 401
- 소유자 불일치 → 403
- task 없음 → 404
- DB/직렬화/기타 예외 → 500