도메인별 적용 기술 정리하기
튜터님 피드백 및 방향성
[캘린더]
POST /api/calendars/me/records — 독서 기록 등록
적용 기술
CalendarExceptionEnum: PLATFORM 출처인데 서재 미등록 시 BOOK_NOT_IN_LIBRARY 예외 발생 → 400 반환
PK 직접 참조: Library 테이블에서 userId + novelId로 서재 등록 여부 확인, 연관관계 없이 PK로 조회
GET /api/calendars/me/records — 독서 기록 조회
적용 기술
QueryDSL 동적 쿼리: date, novelId가 선택값이므로 BooleanExpression으로 null 조건 자동 제외
PageResponse: 페이징 처리하여 content, totalElements, totalPages 등 반환
PK 직접 참조: novelId, userId를 외래키로 직접 참조해 조회
GET /api/calendars/me — 독서 캘린더 조회
적용 기술
CalendarExceptionEnum: 조회 범위 1년 초과 시 DATE_RANGE_TOO_LARGE → 400 반환
GlobalExceptionHandler + MissingServletRequestParameterException 핸들러: startDate 누락 시 500 대신 400 반환
GET /api/calendars/me/statistics — 월간 통계 조회
적용 기술
CalendarExceptionEnum: 미래 달 조회 시 INVALID_STAT_DATE → 400 반환
JPQL @Query: 월별 집계(총 페이지, 완독 수, 독서일 수 등) 단순 집계 쿼리는 JPA로 처리
MonthlyStatResponse Record: 집계 결과를 Record 클래스 DTO로 반환
이유
통계 조회는 복잡한 동적 조건보다는 month, year가 고정된 단순 집계이므로 QueryDSL 대신 JPQL @Query로 처리했습니다. 복잡도에 맞는 기술을 선택하는 것이 팀 컨벤션이고, 오버엔지니어링을 방지합니다. 미래 달 조회 제한은 존재하지 않는 데이터에 대한 불필요한 쿼리를 사전 차단하기 위한 방어 로직입니다.
N+1, 반복 쿼리 발생을 방지, 불필요한 조인 줄이고 응답 일관성 확보, 복합 유니크 제약과 어플리케이션 중복검사 ==> 중복 서재 등록 방지
--------------------------------------------------------------------------------------------------------
[내서제]
POST /api/libraries — 내서재 담기
적용 기술 1. 메타데이터 스냅샷 저장
문제: 소설 정보(제목, 작가명, 표지)는 작가가 수정할 수 있어서 담기 시점 이후에 변경될 수 있습니다. Novel 테이블을 매번 Join하면 수정된 데이터가 담기 시점과 다르게 노출되고 불필요한 Join 비용이 발생합니다.
해결: 담기 시점의 novelTitle, authorNickname, coverImageUrl을 Library 테이블에 직접 저장합니다. 이후 조회 시 Novel 테이블 Join 없이 Library 단독으로 응답이 가능합니다.
적용 기술 2. DB 유니크 제약 + 애플리케이션 중복 검사 이중 방어
문제: 애플리케이션 레벨의 existsByUserIdAndNovelId() 단독 검사만으로는 동시 요청 시 race condition으로 중복 insert가 발생할 수 있습니다.
해결: Library 테이블에 userId + novelId 복합 유니크 인덱스를 추가하여 DB 레벨에서도 중복을 차단합니다. 애플리케이션 레벨 검사는 409 응답을 위해 유지하고 DB 제약은 동시 요청 방어의 최후 방어선으로 이중 적용하였습니다.
GET /api/libraries/me — 내서재 목록 조회
적용 기술 1. QueryDSL 동적 쿼리
문제: 전체/읽는중/완독/찜/구매 탭 필터와 최신순/제목순 정렬을 JPQL로 구현하면 조건 조합마다 별도 메서드가 필요해 코드 중복이 발생합니다.
해결: QueryDSL을 사용하여 libraryType이 null이면 전체, 값이 있으면 해당 타입만 필터링하는 동적 쿼리를 단일 메서드로 처리합니다. 정렬 조건도 resolveOrder()로 분기하여 확장에 유연한 구조로 설계하였습니다.
적용 기술 2. N+1 문제 해결 — 일괄 집계 쿼리
문제: 각 소설의 총 회차 수를 구하기 위해 항목마다 countByNovelId()를 호출하면 페이지 크기만큼 추가 쿼리가 발생하는 N+1 문제가 생깁니다. 페이지 크기가 12라면 최대 13번의 쿼리가 실행됩니다.
해결: 조회된 Library 목록에서 novelId 집합을 추출한 뒤 countByNovelIds()로 단일 쿼리에서 GROUP BY novelId로 일괄 집계합니다. 결과를 Map으로 변환하여 항목별로 매핑하므로 쿼리가 항상 1회로 고정됩니다. 또한 isDeleted = false 조건으로 soft-delete된 회차를 제외하여 실제 공개된 회차 수만 집계합니다.
적용 기술 3. 엔티티 간 연관관계 제거 + ID 직접 참조
문제: JPA 연관관계를 사용하면 즉시/지연 로딩에 따른 불필요한 쿼리가 발생하고 도메인 간 결합도가 높아져 변경 영향 범위가 넓어집니다.
해결: Library 엔티티가 User, Novel을 객체로 참조하지 않고 userId, novelId를 컬럼으로만 보유하도록 설계하였습니다. 도메인 간 결합을 끊고 필요한 시점에만 QueryDSL Join으로 처리합니다.
GET /api/libraries/me - 내 서재 통합 조회
적용 기술 1 - 단일 API 통합 응답
문제: 소설 서재와 국립도서관 도서 서재를 별도 API로 분리하면 프론트에서 두 번 호출해야 하고 로딩 타이밍이 달라져 UX가 저하됩니다.
해결: 기존 GET /api/libraries/me 하나에서 소설 서재(novels)와 국립도서관 도서 서재(nationalLibraryBooks)를 MyLibraryResponse 하나로 통합하여 반환하도록 설계하였습니다. 프론트 호출 횟수를 줄이고 단일 응답으로 렌더링 구조를 단순화하였습니다.
적용 기술 2 - N+1 방지를 위한 일괄 조회 (findAllById + Map 매핑)
문제: 연관관계 없이 ID로만 관리하는 구조에서 UserBook 목록을 순회하며 Book을 개별 조회하면 N+1 문제가 발생하여 쿼리 횟수가 데이터 수에 비례하여 증가합니다.
해결: UserBook 목록 조회 후 bookId 목록을 추출하여 findAllById로 Book을 단 한 번에 일괄 조회하고, Map으로 변환하여 O(1)로 매핑하는 방식을 적용하였습니다. 조회 쿼리가 항상 2번으로 고정되어 데이터 수와 무관하게 일정한 성능을 유지합니다.
--------------------------------------------------------------------------------------------------------
[국립도서]
구현한 API 및 적용 기술 정리
GET /api/v1/national-library/books/search - 도서 검색
적용 기술 - Redis 캐싱 (RedisTemplate)
문제: 국립중앙도서관은 외부 API라 매 요청마다 호출하면 네트워크 지연으로 응답 속도가 느려지고 API 호출 횟수 제한 리스크가 있습니다. 동일한 검색어에 대한 반복 요청이 많을수록 불필요한 외부 호출이 증가합니다.
해결: 검색어 + 페이지 + 사이즈를 조합한 키로 검색 결과를 Redis에 10분간 캐싱하여 캐시 히트 시 외부 API를 호출하지 않도록 하였습니다. 이를 통해 응답 속도를 개선하고 외부 API 호출 횟수를 최소화하였습니다.
POST /api/v1/national-library/books/shelf - 내 서재 도서 저장
적용 기술 1 - books 테이블 upsert 패턴
문제: 검색 결과에서 선택한 도서를 저장할 때 동일한 ISBN의 도서가 여러 유저에 의해 중복으로 저장될 수 있습니다. 도서 데이터가 중복 적재되면 저장 공간이 낭비되고 데이터 정합성이 깨집니다.
해결: 도서 저장 요청 시 ISBN 기준으로 books 테이블에 이미 존재하는지 먼저 확인하고, 없을 때만 저장하고 있으면 기존 레코드를 재사용하는 방식을 적용하였습니다. 도서 데이터 중복을 방지하고 user_books 테이블에는 유저와 도서의 관계만 저장하도록 설계하였습니다.
적용 기술 2 - 엔티티 간 연관관계 제거 + ID 직접 참조
문제: JPA 연관관계를 사용하면 즉시/지연 로딩에 따른 불필요한 쿼리가 발생하고 도메인 간 결합도가 높아져 변경 영향 범위가 넓어집니다.
해결: UserBook 엔티티가 User, Book을 객체로 참조하지 않고 userId, bookId를 컬럼으로만 보유하도록 설계하였습니다. 도메인 간 결합을 끊고 필요한 시점에만 ID 기반으로 일괄 조회하여 처리합니다.
Redis 캐시 활용 네트워크 지연율 감소, 외부 API 호출 결과 저장 시 upsert 중복적재 방지, 외부 API 장애나 지연 가능성을 고려
--------------------------------------------------------------------------------------------------------
[멘토 도메인]
POST /api/mentors — 멘토 등록 신청
적용 기술 1. careerLevel 기준 자동 승인 처리
문제: 멘토 등록 신청 시 모든 신청을 관리자가 수동으로 검토하면 처리 지연이 발생하고 관리자 부담이 커집니다. 하지만 등급별로 이미 플랫폼 활동 조건이 정해져 있어 일정 조건을 충족한 경우는 자동으로 승인해도 무방합니다.
해결: 신청 시점에 Novel, Episode 테이블을 조회해 PUBLISHED 에피소드 수와 좋아요 합산을 검증합니다. INTRODUCTION은 에피소드 50회 이상, ELEMENTARY는 에피소드 50회 이상 + 좋아요 50개 이상, INTERMEDIATE는 에피소드 100회 이상 + 좋아요 100개 이상이면 즉시 APPROVED로 저장합니다. PROFICIENT는 수상/출간 경력 검증이 필요하므로 관리자 수동 승인으로 PENDING 유지합니다.
적용 기술 2. 리스트 필드 JSON 직렬화 저장
문제: mainGenres, specialFields, mentoringStyles는 복수의 값을 가지는 필드입니다. 별도 테이블로 분리하면 조회 시 항상 Join이 필요하고 수정 시 기존 데이터를 삭제 후 재삽입하는 복잡한 로직이 필요합니다.
해결: ObjectMapper.writeValueAsString()으로 List를 JSON 문자열로 직렬화해 단일 컬럼에 저장합니다. 조회 시에는 objectMapper.readValue()로 역직렬화해 List로 반환합니다. 이 필드들은 단순 목록 조회 외에 복잡한 검색 쿼리가 필요 없고 변경 빈도도 낮아 Join 없이 처리할 수 있는 JSON 직렬화 방식이 적합하다고 판단했습니다.
적용 기술 3. DB 유니크 제약 + DataIntegrityViolationException 처리로 동시성 방어
문제: 멘토 등록 시 PENDING, APPROVED 상태를 사전 조회한 후 저장하는 구조라 동시에 두 요청이 들어오면 둘 다 조회를 통과해 중복 저장이 발생할 수 있습니다. check-then-act 패턴의 전형적인 race condition입니다.
해결: Mentor 엔티티의 userId 컬럼에 @Column(unique = true)를 걸어 DB 레벨에서 중복을 막습니다. 동시 요청으로 두 건이 저장을 시도할 경우 DB에서 DataIntegrityViolationException이 발생하는데, save 호출부를 try-catch로 감싸 해당 예외를 MENTOR_ALREADY_APPROVED로 변환해 409로 응답합니다. 애플리케이션 레벨 검증만으로는 race condition을 막을 수 없어 DB 제약과 예외 처리를 함께 구성했습니다.
적용 기술 4. 정적 팩토리 패턴으로 엔티티 생성
문제: 생성자를 열어두면 팀원이 엔티티를 생성할 때 필드 순서를 실수하거나 필수 초기값 설정을 빠뜨릴 수 있습니다. 특히 Mentor는 생성 시점에 반드시 status가 결정되어야 하는데 이를 강제할 방법이 없습니다.
해결: 생성자를 private으로 막고 Mentor.create()라는 정적 팩토리 메서드만 열어두었습니다. 자동 승인 여부를 결정한 initialStatus를 파라미터로 받아 생성 시점에 반드시 status가 설정되도록 강제합니다. 객체 생성 의도를 메서드 이름으로 명확히 표현할 수 있고 생성 규칙을 한 곳에서 관리할 수 있습니다.
PUT /api/mentors/me — 멘토 정보 수정
적용 기술 1. 부분 업데이트 - null 필드 기존 값 유지
문제: 수정 API에서 전체 필드를 항상 보내도록 강제하면 프론트엔드에서 변경하지 않는 필드까지 모두 채워서 보내야 하는 부담이 생깁니다.
해결: Mentor.update() 메서드에서 null 필드는 기존 값을 유지하도록 구현했습니다. introduction, careerHistory 같은 단순 문자열 필드는 null 여부를 체크하고, mainGenres, specialFields, mentoringStyles 리스트 필드는 등록용 toJson()과 수정용 toJsonForUpdate()를 분리했습니다. toJsonForUpdate()는 null이면 null을 반환해 기존 값을 유지하고, 빈 리스트를 보내면 실제로 직렬화해 명시적으로 비울 수 있도록 했습니다.
GET /api/mentors/me — 내 멘토 프로필 조회
적용 기술 1. JSON 역직렬화로 리스트 필드 복원
문제: DB에 JSON 문자열로 저장된 mainGenres, specialFields, mentoringStyles를 응답 시 List로 변환해야 합니다.
해결: MentorProfileResponse의 parseJson() 메서드에서 objectMapper.readValue()로 JSON 문자열을 List로 역직렬화합니다. 파싱 실패 시 빈 리스트를 반환하도록 예외 처리해 응답이 깨지지 않도록 했습니다.
배치 스케줄러 — 매일 자정 멘토 등급 자동 조정
적용 기술 1. Spring @Scheduled 배치
문제: 멘토 등급은 에피소드 회차와 좋아요 수에 따라 자동으로 올라가야 합니다. 에피소드 발행이나 좋아요 발생 시마다 이벤트로 처리하면 에피소드, 좋아요 도메인에 멘토 도메인 로직이 침투해 결합도가 높아집니다.
해결: Spring의 @Scheduled(cron = "0 0 0 * * *")로 매일 자정에 한 번 APPROVED 상태 멘토 전체를 순회하며 조건을 검사합니다. 각 도메인이 서로를 몰라도 되고 구현이 단순합니다. 현재 트래픽 규모에서 실시간 반영이 필수가 아니라 판단해 별도 인프라 없이 Spring 내장 스케줄러로 충분하다고 결정했습니다.
적용 기술 2. 등급 변경 이력 별도 테이블 저장
문제: 배치로 등급이 변경될 때 Mentor 테이블에 덮어쓰기만 하면 언제 어떤 이유로 등급이 바뀌었는지 추적이 불가능합니다. 관리자 입장에서 멘토 이의 제기 시 근거 자료가 없습니다.
해결: MentorCareerHistory 테이블에 변경 전 등급, 변경 후 등급, 변경 사유, 변경 시각을 함께 저장합니다. 데이터가 누적되는 append-only 구조라 변경 흐름 전체를 추적할 수 있고 삭제나 수정이 없어 관리가 단순합니다.
중복 sessionNumber 발생을 방지하기 위해 비관적 락과 DB 유니크 제약을 함께 적용, 낙관적 락 적용, 상태전이 모델, 성능을 점진적으로 개선 해나 났다 +배치 스케줄러
배치 스케줄러--> 도메인
멘토 등급 자동 조정 청크 단위 고민한거--> 메모리 부하, 영속성 컨텍스트 누적 문제 해결, 배치 페이지네이션 pk 정렬 고정
--------------------------------------------------------------------------------------------------------
[멘토 도메인]
1. GET /api/mentorings/received — 멘토링 접수 목록 조회
적용 기술 1. 페이지네이션 (Spring Data Pageable)
문제: 멘토에게 접수된 멘토링 신청이 많아질수록 전체 목록을 한 번에 반환하면 응답 크기가 커지고 DB 부하가 증가합니다.
해결: Pageable을 적용해 페이지 단위로 조회하고 PageResponse로 감싸 현재 페이지, 전체 페이지 수, 마지막 페이지 여부를 함께 반환합니다.
적용 기술 2. 엔티티 ID 참조 방식 + 개별 조회 (N+1 인지)
문제: 엔티티 간 연관관계를 맺지 않는 설계 원칙상 Mentorship에서 멘티 이름(User)과 소설 제목(Novel)을 바로 JOIN할 수 없습니다.
해결: userRepository.findByIdAndIsDeletedFalse(), novelRepository.findById()로 개별 조회합니다. 현재는 N+1이 발생할 수 있으나 탈퇴한 멘티는 알 수 없는 사용자, 삭제된 소설은 알 수 없는 소설로 graceful하게 처리합니다. 고도화 시 QueryDSL JOIN으로 교체할 위치를 TODO 주석으로 명시해두었습니다.
2. PATCH /api/mentorings/{mentoringId}/mentees/{menteeId}/accept — 멘티 수락
적용 기술 1. 낙관적 락 (@Version)
문제: 멘토가 여러 멘티를 동시에 수락하는 경우 슬롯 차감(decreaseSlot())이 동시에 실행되면 슬롯이 0 이하로 내려가는 race condition이 발생할 수 있습니다
해결: Mentor 엔티티에 @Version을 적용해 낙관적 락을 걸었습니다. 동시에 두 요청이 슬롯을 차감하려 하면 나중에 커밋된 트랜잭션이 OptimisticLockException을 발생시켜 중복 차감을 방지합니다.
적용 기술 2. 상태 검증 (PENDING 체크)
문제: 이미 수락/거절 처리된 멘토링에 중복으로 수락 요청이 들어올 수 있습니다.
해결: mentorship.getStatus() != MentorshipStatus.PENDING 체크로 이미 처리된 요청에는 MENTORING_ALREADY_PROCESSED 예외를 반환합니다.
3. PATCH /api/mentorings/{mentoringId}/mentees/{menteeId}/reject — 멘티 거절
적용 기술 1. 상태 검증 (PENDING 체크)
문제: 이미 수락/거절 처리된 멘토링에 거절 요청이 중복으로 들어올 수 있습니다.
해결: 수락 API와 동일하게 PENDING 상태 체크를 통해 중복 처리를 방지하고 MENTORING_ALREADY_PROCESSED 예외를 반환합니다. 수락과 달리 슬롯 차감이 없으므로 낙관적 락은 적용하지 않았습니다.
4. GET /api/mentorings/{mentoringId}/documents — 원고 다운로드 URL 조회
적용 기술 1. 다운로드 횟수 트래킹
문제: 멘토가 원고를 몇 번 다운로드했는지 추적이 필요합니다.
해결: URL 조회 시 mentorship.increaseManuscriptDownloadCount()를 호출해 다운로드 횟수를 증가시킵니다. 조회와 카운트 증가를 하나의 트랜잭션으로 묶어 일관성을 보장합니다.
적용 기술 2. S3 Presigned URL 연동 준비 (TODO)
문제: 원고 파일 URL을 그대로 노출하면 인증 없이 누구나 접근할 수 있는 보안 문제가 있습니다.
해결: 현재는 manuscriptUrl을 직접 반환하지만 S3 연동 후 s3Service.generatePresignedUrl()로 교체할 위치를 TODO 주석으로 명시해 두었습니다.
5. PATCH /api/mentorings/{mentoringId}/complete — 멘토링 종료
적용 기술 1. 낙관적 락 (@Version) + 슬롯 반환
문제: 멘토링 종료 시 슬롯을 반환(increaseSlot())하는데 동시에 여러 멘토링이 종료되면 슬롯 값이 꼬일 수 있습니다.
해결: Mentor 엔티티의 @Version으로 슬롯 반환 시에도 낙관적 락이 적용되어 동시 수정 충돌을 방지합니다. 수락 시 차감한 슬롯을 종료 시 정확히 반환해 슬롯 정합성을 유지합니다.
적용 기술 2. 상태 검증 (ACCEPTED 체크)
문제: PENDING이나 REJECTED 상태의 멘토링을 종료 처리하려는 잘못된 요청이 들어올 수 있습니다.
해결: mentorship.getStatus() != MentorshipStatus.ACCEPTED 체크로 진행 중인 멘토링만 종료할 수 있도록 제한하고 MENTORING_NOT_ACCEPTED 예외를 반환합니다.
6. GET /api/mentorings/{mentoringId} — 멘토링 상세 조회
적용 기술 1. 엔티티 ID 참조 방식 + Soft Delete 고려 조회
문제: 멘토나 멘티가 탈퇴한 경우에도 멘토링 상세 정보는 조회 가능해야 합니다.
해결: userRepository.findByIdAndIsDeletedFalse()로 조회해 탈퇴한 유저는 알 수 없는 사용자로 graceful하게 처리합니다. 멘토링 데이터 자체는 유지되어 피드백 이력 등을 확인할 수 있습니다.
적용 기술 2. 피드백 목록 시간순 정렬 조회
문제: 피드백이 여러 건일 때 작성 순서대로 보여줘야 멘토링 진행 흐름을 파악할 수 있습니다.
해결: mentorFeedbackRepository.findAllByMentorshipIdOrderByCreatedAtAsc()로 피드백을 시간 오름차순으로 조회합니다.
7. POST /api/mentorings/{mentoringId}/feedbacks — 피드백 작성
적용 기술 1. 세션 횟수 트래킹
문제: 멘토링이 몇 회 진행되었는지 추적이 필요합니다.
해결: 피드백 저장 시 mentorship.increaseSession()을 함께 호출해 총 세션 수를 증가시킵니다. 피드백 저장과 세션 증가를 하나의 트랜잭션으로 묶어 피드백이 저장되면 반드시 세션도 증가하도록 일관성을 보장합니다.
적용 기술 2. 상태 검증 (ACCEPTED 체크)
문제: 진행 중이 아닌 멘토링(PENDING, REJECTED, COMPLETED)에 피드백을 작성하는 잘못된 요청이 들어올 수 있습니다.
해결: mentorship.getStatus() != MentorshipStatus.ACCEPTED 체크로 수락된 멘토링에만 피드백 작성이 가능하도록 제한하고 MENTORING_FEEDBACK_ONLY_ACCEPTED 예외를 반환합니다.
--------------------------------------------------------------------------------------------------------
[고도화 진행한 내용]
POST /api/v1/mentorings/{mentoringId}/feedbacks — 피드백 작성 (V1)
적용 기술 1. 세션 횟수 트래킹
문제: 멘토링이 몇 회 진행되었는지 추적이 필요합니다.
해결: 피드백 저장 시 mentorship.increaseSession()을 함께 호출해 총 세션 수를 증가시킵니다. 피드백 저장과 세션 증가를 하나의 트랜잭션으로 묶어 일관성을 보장합니다.
적용 기술 2. 상태 검증 (ACCEPTED 체크)
문제: 진행 중이 아닌 멘토링에 피드백을 작성하는 잘못된 요청이 들어올 수 있습니다.
해결: mentorship.getStatus() != MentorshipStatus.ACCEPTED 체크로 수락된 멘토링에만 피드백 작성이 가능하도록 제한하고 MENTORING_FEEDBACK_ONLY_ACCEPTED 예외를 반환합니다.
적용 기술 3. 회차별 피드백 이력 관리
문제: 피드백이 몇 회차인지, 어떤 제목의 피드백인지 구분할 방법이 없었습니다.
해결: MentorFeedback에 title과 sessionNumber를 추가해 회차별 피드백 이력을 명확히 관리합니다. sessionNumber는 totalSessions + 1로 계산해 자동 부여합니다.
POST /api/v2/mentorings/{mentoringId}/feedbacks — 피드백 작성 (V2)
적용 기술 1. 비관적 락 (동시성 보호)
문제: 동시에 두 요청이 들어오면 totalSessions + 1을 동시에 읽어 같은 sessionNumber가 중복 저장될 수 있습니다.
해결: findByIdWithLock()으로 멘토링 row에 비관적 락을 걸어 동시 요청을 직렬화합니다. 두 요청이 동시에 들어와도 하나씩 순서대로 처리됩니다.
적용 기술 2. DB 유니크 제약 (이중 방어)
문제: 애플리케이션 레벨의 락만으로는 예외 상황에서 중복 저장을 완전히 막을 수 없습니다.
해결: mentorship_feedbacks 테이블에 (mentorship_id, session_number) 유니크 제약을 추가해 DB 레벨에서도 중복을 차단합니다. 충돌 시 DataIntegrityViolationException을 MENTORING_SESSION_CONFLICT 예외로 변환해 409를 반환합니다.
적용 기술 3. 입력값 길이 검증
문제: title이 200자를 초과하면 DB 예외로 500이 반환될 수 있습니다.
해결: @Size(max = 200)을 추가해 요청 단계에서 400으로 끊어 사용자에게 명확한 오류 메시지를 반환합니다.
GET /api/v1/mentorings/received — 멘토링 접수 목록 조회 (V1)
적용 기술 1. 페이지네이션
문제: 멘토링 신청이 많을 경우 전체 조회 시 성능 저하가 발생할 수 있습니다.
해결: Pageable을 적용해 페이지 단위로 조회합니다. @Min, @Max로 page, size 파라미터를 검증해 잘못된 입력으로 인한 500 에러를 방지합니다.
적용 기술 2. 탈퇴 회원/삭제 소설 Fallback 처리
문제: 멘티가 탈퇴하거나 소설이 삭제된 경우 조회 시 예외가 발생할 수 있습니다.
해결: Optional의 orElse()로 탈퇴한 멘티는 "알 수 없는 사용자", 삭제된 소설은 "알 수 없는 소설"을 반환해 안정적으로 목록을 제공합니다.
GET /api/v2/mentorings/received — 멘토링 접수 목록 조회 (V2)
적용 기술 1. soft-delete 적용
문제: V1의 findById()는 soft-delete 조건을 포함하지 않아 삭제된 소설 제목이 그대로 노출될 수 있습니다.
해결: findByIdAndIsDeletedFalse()로 교체해 삭제된 소설은 "알 수 없는 소설"로 처리합니다.
GET /api/v1/mentorings/{mentoringId} — 멘토링 상세 조회 (V1)
적용 기술 1. 권한 검증
문제: 다른 멘토가 멘토링 상세를 조회하는 잘못된 요청이 들어올 수 있습니다.
해결: mentorship.getMentorId()와 로그인한 멘토의 ID를 비교해 본인의 멘토링만 조회 가능하도록 제한하고 MENTORING_UNAUTHORIZED 예외를 반환합니다.
적용 기술 2. 회차별 피드백 이력 조회
문제: 멘토링 상세에서 피드백 이력을 회차 순서대로 확인해야 합니다.
해결: findAllByMentorshipIdOrderByCreatedAtAsc()로 피드백을 생성 순서대로 조회하고 FeedbackInfo에 title, sessionNumber를 포함해 회차별 내용을 명확히 제공합니다.
GET /api/v2/mentorings/{mentoringId} — 멘토링 상세 조회 (V2)
적용 기술 1. soft-delete 적용
문제: V1의 findById()는 삭제된 소설 제목이 그대로 노출될 수 있습니다.
해결: findByIdAndIsDeletedFalse()로 교체해 삭제된 소설은 "알 수 없는 소설"로 반환합니다.
PATCH /api/v1,v2/mentorings/{mentoringId}/mentees/{menteeId}/accept — 멘티 수락
적용 기술 1. 슬롯 관리
문제: 멘토가 수락할 수 있는 멘티 수는 최대 5명으로 제한됩니다.
해결: 수락 시 mentor.decreaseSlot()으로 잔여 슬롯을 차감합니다. 슬롯 차감과 멘토링 상태 변경을 하나의 트랜잭션으로 묶어 일관성을 보장합니다.
적용 기술 2. 중복 처리 방지
문제: 이미 수락/거절된 멘토링에 다시 수락 요청이 들어올 수 있습니다.
해결: mentorship.getStatus() != MentorshipStatus.PENDING 체크로 PENDING 상태에서만 수락이 가능하도록 제한하고 MENTORING_ALREADY_PROCESSED 예외를 반환합니다.
PATCH /api/v1,v2/mentorings/{mentoringId}/mentees/{menteeId}/reject — 멘티 거절
적용 기술 1. 중복 처리 방지
문제: 이미 처리된 멘토링에 거절 요청이 들어올 수 있습니다.
해결: PENDING 상태에서만 거절 가능하도록 제한하고 MENTORING_ALREADY_PROCESSED 예외를 반환합니다.
적용 기술 2. 거절 시각 기록
문제: 이번 달 거절 건수 통계 집계를 위해 거절 시각이 필요합니다.
해결: mentorship.reject() 호출 시 rejectedAt을 LocalDateTime.now()로 기록합니다. 이를 기반으로 countRejectedThisMonth() 쿼리에서 월별 거절 통계를 정확히 집계합니다.
PATCH /api/v1,v2/mentorings/{mentoringId}/complete — 멘토링 종료
적용 기술 1. 슬롯 반환
문제: 멘토링이 종료되면 해당 슬롯을 다시 다른 멘티에게 열어줘야 합니다.
해결: 종료 시 mentor.increaseSlot()으로 슬롯을 반환합니다. 슬롯 반환과 상태 변경을 하나의 트랜잭션으로 묶어 일관성을 보장합니다.
적용 기술 2. 상태 검증 (ACCEPTED 체크)
문제: PENDING이나 REJECTED 상태의 멘토링을 종료하는 잘못된 요청이 들어올 수 있습니다.
해결: ACCEPTED 상태에서만 종료 가능하도록 제한하고 MENTORING_NOT_ACCEPTED 예외를 반환합니다.
GET /api/v1,v2/mentorings/{mentoringId}/documents — 원고 다운로드 URL 조회
적용 기술 1. 다운로드 횟수 트래킹
문제: 멘토가 원고를 몇 번 다운로드했는지 추적이 필요합니다.
해결: URL 반환 시 mentorship.increaseManuscriptDownloadCount()를 함께 호출해 다운로드 횟수를 기록합니다.
적용 기술 2. 원고 존재 여부 검증
문제: 원고 파일이 없는 멘토링에 다운로드 요청이 들어올 수 있습니다.
해결: manuscriptUrl이 null인 경우 MENTORING_MANUSCRIPT_NOT_FOUND 예외를 반환해 명확한 오류 메시지를 제공합니다.
--------------------------------------------------------------------------------------------------------
[고도화 진행한 내용]
GET /api/mentors/me/mentees — 내 멘티 목록 조회 (v1)
적용 기술 1. 탈퇴/삭제 데이터 방어
문제: 멘티가 탈퇴하거나 소설이 삭제된 경우 응답에 null이 들어갈 수 있습니다.
해결: userRepository.findByIdAndIsDeletedFalse() 조회 후 orElse("알 수 없는 사용자"), novelRepository.findById() 조회 후 orElse("알 수 없는 소설")로 null을 방어합니다.
적용 기술 2. 최근 피드백 날짜 조회
문제: 멘티별로 마지막 피드백 날짜를 보여줘야 합니다.
해결: mentorFeedbackRepository.findTopByMentorshipIdOrderByCreatedAtDesc()로 최신 1건만 조회해서 createdAt을 반환합니다.
GET /api/v2/mentors/me/mentees — 내 멘티 목록 조회 (v2)
적용 기술 1. N+1 문제 해결
문제: v1은 반복문 안에서 멘티, 소설, 피드백을 각각 개별 조회해 멘티가 N명이면 쿼리가 1+3N번 나가는 구조였습니다.
해결: QueryDSL로 mentorship → menteeUser → novel → feedback을 단일 JOIN 쿼리로 처리해 쿼리를 1번으로 줄였습니다.
적용 기술 2. soft-delete 방어
문제: 탈퇴한 유저나 삭제된 소설 조회 시 LEFT JOIN 결과 null이 응답에 그대로 들어갈 수 있습니다.
해결: Expressions.cases()로 쿼리 단에서 직접 null을 방어합니다.
javaExpressions.cases()
.when(menteeUser.nickname.isNull())
.then("알 수 없는 사용자")
.otherwise(menteeUser.nickname)
적용 기술 3. v1 호환성 유지
문제: 기존 v1 API를 바로 교체하면 롤백이 어렵습니다.
해결: v1 엔드포인트는 그대로 유지하고 /api/v2/mentors/me/mentees를 신규 추가해 점진적 전환이 가능하도록 했습니다.
GET /api/v2/mentorings/received — 멘토링 접수 목록 조회 (v2)
적용 기술 1. N+1 문제 해결
문제: v1은 멘토십 목록 조회 후 반복문에서 멘티 정보, 소설 정보를 개별 조회하는 구조였습니다.
해결: QueryDSL로 mentorship → menteeUser → novel을 단일 LEFT JOIN 쿼리로 처리했습니다.
적용 기술 2. soft-delete 적용
문제: 탈퇴한 멘티나 삭제된 소설의 경우 null이 응답에 들어갈 수 있습니다.
해결: JOIN 조건에 isDeleted=false를 추가하고 Expressions.cases()로 null fallback을 처리했습니다.
java.leftJoin(menteeUser).on(menteeUser.id.eq(mentorship.menteeId)
.and(menteeUser.isDeleted.eq(false)))
적용 기술 3. 페이지네이션
문제: 접수 목록이 많아질수록 전체 조회는 성능에 부담이 됩니다.
해결: Pageable을 적용해 페이지 단위로 조회하고 PageImpl로 응답합니다.
GET /api/mentorships/v2/me/history — 내 멘토링 이력 조회 (v2)
적용 기술 1. N+1 문제 해결
문제: v1은 멘토십 목록 조회 후 반복문에서 멘토 정보를 개별 조회하는 구조였습니다.
해결: QueryDSL로 mentorship → mentor → user를 단일 JOIN 쿼리로 처리했습니다.
적용 기술 2. soft-delete 적용
문제: 탈퇴한 멘토의 닉네임이 inner join 구조에서 그대로 노출될 수 있습니다.
해결: inner join → leftJoin으로 변경하고 isDeleted=false 조건과 null fallback을 추가했습니다.
java.leftJoin(user).on(user.id.eq(mentor.userId)
.and(user.isDeleted.eq(false)))
적용 기술 3. 상태 필터링
문제: 전체 이력이 아닌 특정 상태(ACCEPTED, COMPLETED 등)의 이력만 조회할 수 있어야 합니다.
해결: BooleanBuilder로 status가 null이면 전체, 값이 있으면 해당 상태만 필터링합니다.
javaif (status != null) {
where.and(mentorship.status.eq(status));
}
PATCH /api/mentors/me — 멘토 정보 수정
적용 기술 1. 부분 업데이트 (null 허용)
문제: 수정 요청 시 변경하지 않는 필드까지 전부 보내야 하면 불편합니다.
해결: MentorUpdateRequest의 각 필드가 null이면 기존 값을 유지하고, 값이 있을 때만 업데이트합니다.
적용 기술 2. careerHistory 빈 값 검증
문제: careerHistory를 빈 문자열로 보내면 의미없는 데이터가 저장됩니다.
해결: null은 기존 값 유지, 빈 문자열은 MENTOR_CAREER_REQUIRED 예외를 반환합니다.
POST /api/mentors/me — 멘토 등록 신청
적용 기술 1. 등급별 자동 승인 로직
문제: 등급마다 멘토 승인 조건이 다릅니다. INTRODUCTION은 에피소드 50개, ELEMENTARY는 에피소드 50개 + 좋아요 50개, PROFICIENT는 항상 관리자 수동 승인입니다.
해결: resolveInitialStatus()에서 등급별 조건을 switch로 분기해 자동 승인 여부를 결정합니다.
적용 기술 2. 동시 요청 방어
문제: 동시에 여러 요청이 들어오면 중복 등록이 발생할 수 있습니다.
해결: DataIntegrityViolationException 발생 시 MENTOR_ALREADY_APPROVED 예외로 변환해 처리합니다.
적용 기술 3. 중복 신청 방어
문제: 이미 PENDING이거나 APPROVED 상태인 경우 중복 신청이 들어올 수 있습니다.
해결: existsByUserIdAndStatus()로 사전 체크 후 이미 존재하면 예외를 반환합니다.
스케줄러 — 멘토 등급 자동 조정 (매일 자정)
적용 기술 1. 청크 단위 페이지네이션
문제: 승급 대상 멘토를 한 번에 전부 List로 가져오면 데이터가 많을수록 메모리 부하가 커집니다.
해결: 100명씩 페이지 단위로 나눠서 처리합니다.
javaPageRequest.of(pageNumber, CHUNK_SIZE, Sort.by("id").ascending())
적용 기술 2. persistence context 정리
문제: @Transactional 안에서 처리한 엔티티가 배치 종료까지 영속성 컨텍스트에 쌓여 메모리가 누적됩니다.
해결: 청크 처리 후 entityManager.flush() / entityManager.clear()를 호출해 청크마다 메모리를 비웁니다.
적용 기술 3. 정렬 기준 고정
문제: 정렬 기준이 없으면 배치 실행 중 다른 트랜잭션이 데이터를 변경할 때 페이지 경계가 밀려 행 누락/중복이 발생할 수 있습니다.
해결: Sort.by("id").ascending()으로 PK 기준 정렬을 고정합니다.
--------------------------------------------------------------------------------------------------------
[배포]
젠킨스 aws
--------------------------------------------------------------------------------------------------------
[수익환전]
계좌 등록 + 1원 인증
POST /api/revenues/me/account/verify — 계좌 등록 + 1원 인증 코드 발송
POST /api/revenues/me/account/verify/confirm — 인증코드 검증
적용 기술 1. AES/GCM 계좌번호 암호화
문제: 계좌번호를 평문으로 DB에 저장하면 DB 탈취 시 금융 정보가 그대로 노출됩니다.
해결: AES/GCM 양방향 암호화로 계좌번호를 암호화하여 저장하고, 응답 시에는 뒤 4자리만 노출하는 마스킹 처리를 적용했습니다. 복호화는 1원 인증 성공 후 계좌 상세 조회 시에만 수행합니다.
적용 기술 2. BankVerificationClient 인터페이스 분리
문제: 실제 은행 API(useB/CODEF)는 금융규제상 테스트/개발 환경에서 연동이 불가능합니다. 개발 중 매번 실제 API를 호출하면 비용이 발생하고 환경에 따라 동작이 달라집니다.
해결: BankVerificationClient 인터페이스를 분리하고 @Profile({"dev", "local", "test"})가 붙은 LocalBankVerificationClient 시뮬레이션 구현체를 별도로 만들었습니다. 운영 환경에서는 실제 구현체로 교체만 하면 되는 구조입니다.
적용 기술 3. 인증코드 만료/시도 횟수 검증
문제: 인증코드를 무제한으로 시도하거나 만료 이후에도 사용할 수 있으면 보안상 문제가 됩니다.
해결: AccountVerification 엔티티에 expiredAt(5분), attemptCount(최대 5회)를 관리하여 만료 시간 초과 또는 시도 횟수 초과 시 예외를 발생시킵니다.
수익 현황 조회
GET /api/revenues/me — 총 누적 수익 / 총 환전 금액 / 가용 잔액 / 인증된 계좌 정보
적용 기술 1. Redis 캐싱 (TTL 30분)
문제: 수익 현황은 Revenue 테이블 전체를 집계하는 무거운 쿼리입니다. 작가가 대시보드를 반복 조회할 때마다 집계 쿼리가 실행되면 DB 부하가 커집니다.
해결: 첫 조회 시 집계 결과를 Redis에 30분간 캐싱합니다. 이후 동일 요청은 DB를 거치지 않고 Redis에서 바로 반환합니다. 환전 신청 또는 수익 발생 시 해당 캐시를 즉시 무효화하여 데이터 정합성을 유지합니다.
적용 기술 2. Revenue 타입 분리 집계
문제: 수익(EPISODE_SALE, SUBSCRIPTION), 환전(WITHDRAWAL), 환불(REFUND)이 하나의 Revenue 테이블에 혼재되어 있어 가용 잔액 계산 시 타입별 분리 집계가 필요합니다.
해결: sumAmountByAuthorIdAndTypeIn()으로 수익+환불을 합산하고 sumAmountByAuthorIdAndType()으로 환전액을 별도 집계하여 가용 잔액 = 수익합계 - 환전합계로 계산합니다.
환전 신청
POST /api/revenues/me/exchanges — 환전 신청
적용 기술 1. Redis 분산락 (UUID 토큰 기반)
문제: 동시에 여러 환전 요청이 들어오면 잔액 검증을 통과한 요청이 중복으로 처리되어 잔액보다 많은 금액이 환전될 수 있습니다.
해결: Redis SETNX로 lock:withdrawal:{authorId} 키에 UUID 토큰을 5초간 점유합니다. 락 해제 시에는 get 후 delete의 비원자성 문제를 방지하기 위해 Lua Script로 비교와 삭제를 하나의 원자적 연산으로 처리합니다.
적용 기술 2. FeePolicy Enum으로 수수료 정책 분리
문제: 수수료율(3.3%)과 최소 환전 금액(10,000원)을 서비스 코드에 하드코딩하면 정책 변경 시 코드 전체를 수정해야 합니다.
해결: FeePolicy Enum에 수수료율, 최소 환전 금액, 수수료 계산, 실수령액 계산 로직을 캡슐화했습니다. 정책 변경 시 Enum 값만 수정하면 됩니다.
적용 기술 3. Revenue + Withdrawal 한 트랜잭션 처리
문제: Revenue(WITHDRAWAL) 차감 기록과 Withdrawal 생성이 별도 트랜잭션이면 하나만 성공하고 하나가 실패할 경우 잔액 불일치가 발생합니다.
해결: 잔액 확인 → Revenue(WITHDRAWAL) 저장 → Withdrawal 저장을 하나의 @Transactional 안에서 처리하여 원자성을 보장합니다.
환전 내역 조회
GET /api/revenues/me/exchanges — 환전 목록 (상태/기간 필터 + 페이징)
GET /api/revenues/me/exchanges/{id} — 환전 상세 조회
적용 기술 1. QueryDSL 동적 쿼리
문제: 상태 필터, 기간 필터가 선택적으로 적용되어야 하는데 JPQL로 작성하면 조건 조합마다 별도 쿼리가 필요하고 null 체크 분기가 복잡해집니다.
해결: QueryDSL BooleanExpression으로 각 필터 조건을 null 체크 후 동적으로 조합합니다. 또한 requestedAt 단일 정렬 시 동일 시각 레코드에서 페이지 중복/누락이 발생할 수 있어 id DESC를 tie-breaker로 추가했습니다.
수익 분석 통계
GET /api/revenues/me/statistics — 월별/주별 수익 집계
적용 기술 1. Redis 캐싱 + SCAN 기반 패턴 무효화 (TTL 1시간)
문제: 통계는 GROUP BY 집계 쿼리로 부하가 크고, 월별/주별 등 기간 단위로 캐시 키가 달라집니다. 수익 발생 시 해당 작가의 모든 통계 캐시를 무효화해야 하는데 키 패턴이 다양하면 일괄 삭제가 어렵습니다.
해결: statistics:{authorId}:* 패턴으로 캐시 키를 설계하고, 무효화 시 KEYS 대신 SCAN으로 패턴 매칭 키를 순회 삭제합니다. KEYS는 싱글스레드 Redis를 블로킹하지만 SCAN은 커서 기반으로 점진적으로 탐색하여 운영 환경에서도 안전합니다.
관리자 환전 승인/거절
GET /api/admin/exchanges — 전체 환전 목록 조회
GET /api/admin/exchanges/{id} — 환전 상세 조회
PUT /api/admin/exchanges/{id}/approve — 환전 승인
PUT /api/admin/exchanges/{id}/reject — 환전 거절
적용 기술 1. WithdrawalStatus 상태 전이 검증
문제: 이미 완료(COMPLETED)되거나 거절(REJECTED)된 환전 건을 다시 승인/거절하면 데이터 정합성이 깨집니다.
해결: WithdrawalStatus Enum에 VALID_TRANSITIONS Map으로 허용된 전이만 정의하고, Withdrawal 엔티티의 상태 변경 메서드에서 validateTransition()을 호출하여 허용되지 않은 전이 시 IllegalStateException을 발생시킵니다. PENDING → PROCESSING → COMPLETED, PENDING → REJECTED만 허용됩니다.
적용 기술 2. 거절 시 REFUND 타입으로 잔액 복구
문제: 환전 신청 시 Revenue(WITHDRAWAL)로 잔액이 차감됩니다. 거절 시 해당 WITHDRAWAL 레코드를 삭제하거나 수정하면 이력 추적이 불가능합니다.
해결: 거절 시 Revenue를 삭제하지 않고 동일 금액의 Revenue(REFUND)를 새로 생성합니다. 가용 잔액 계산 시 typeIn(EPISODE_SALE, SUBSCRIPTION, REFUND)로 합산하면 자연스럽게 복구된 금액이 반영되고, 환전/거절 이력이 모두 Revenue 테이블에 남습니다.
적용 기술 3. @Secured + 역할 분리
문제: 일반 작가나 독자가 관리자 API에 접근하면 안 됩니다.
해결: @Secured({"ADMIN", "SUPER_ADMIN"})을 컨트롤러 클래스 레벨에 적용하여 ADMIN과 SUPER_ADMIN 역할만 접근 가능하도록 제한했습니다.
RedisTTL 효과, 키 설계, Redis 기반 락, 상태전이, 민감한 금융정보에 대한 핸들링
--------------------------------------------------------------------------------------------------------
[이벤트]
1. POST /api/admin/events — 이벤트 생성 (관리자)
적용 기술 1. 페이지 단위 배치 조회 + Kafka 기반 비동기 알림 발송
문제: 이벤트가 생성되면 전체 독자(READER)에게 알림을 발송해야 한다.
단순하게 userRepository.findAllByRole(READER)로 전체 유저를 한 번에 조회하면
유저 수가 수만 명이 될 경우 메모리에 전부 올라가 OOM이 발생할 수 있고,
이벤트 생성 트랜잭션 자체가 지연되는 문제가 생긴다.
해결: 전체 READER를 1000건씩 페이지 단위로 배치 조회하여 순차적으로 이벤트를 발행했다.
알림 발송은 ApplicationEventPublisher를 통해 이벤트를 등록하고,
@TransactionalEventListener(AFTER_COMMIT)으로 트랜잭션 커밋 이후에 Kafka에 발행되는 구조를 활용했다.
이로 인해 알림 발송 실패가 이벤트 생성 트랜잭션에 영향을 주지 않고, READER에게만 선택적으로 발송된다.
적용 기술 2. Redis 캐시 Evict
문제: 사용자 이벤트 목록 조회에 Redis 캐싱이 적용되어 있기 때문에
새 이벤트가 생성되어도 캐시가 살아있으면 사용자에게 새 이벤트가 노출되지 않는다.
해결: 이벤트 생성 시 사용자용 목록 캐시 전체를 즉시 evict한다.
이후 사용자가 목록을 조회하면 캐시 miss가 발생해 DB에서 최신 데이터를 읽어온 뒤 다시 캐싱된다.
2. GET /api/admin/events — 이벤트 목록 조회 (관리자)
적용 기술. 캐싱 미적용 - 실시간 DB 직접 조회
문제: 관리자는 이벤트의 정확한 현재 상태(참여자 수, 진행 여부 등)를 실시간으로 파악해야 한다.
캐싱을 적용하면 최신 상태가 반영되지 않아 관리 판단에 오류가 생길 수 있다.
해결: 관리자 목록 조회는 캐싱을 전혀 적용하지 않고 항상 DB를 직접 조회한다.
관리자는 소수 인원이라 요청 빈도가 낮으므로 DB 부하 문제가 없다.
사용자 API와 관리자 API를 별도 Service로 분리해 캐싱 전략을 독립적으로 관리했다.
3. GET /api/admin/events/{eventId} — 이벤트 상세 조회 (관리자)
적용 기술. 캐싱 미적용 - 실시간 DB 직접 조회
문제: 관리자 상세 조회는 참여자 현황, 마감 여부 등 실시간 정보가 중요하다.
캐싱된 데이터를 보여주면 관리 목적으로 사용할 수 없다.
해결: 캐싱 없이 항상 DB를 직접 조회한다.
사용자용 상세 조회와 달리 관리자용은 별도 Service 메서드로 분리해 캐싱 로직이 섞이지 않도록 했다.
4. GET /api/admin/events/{eventId}/participants — 이벤트별 참여자 목록 조회 (관리자)
적용 기술. 연관관계 미사용 + FK 컬럼 직접 조회
문제: 프로젝트 컨벤션상 엔티티 간 연관관계(@ManyToOne 등)를 사용하지 않고 FK를 Long 컬럼으로만 관리한다.
EventParticipant에서 Event나 User로의 Join이 필요한 경우 연관관계 없이 처리해야 한다.
해결: EventParticipant 테이블에 event_id, user_id를 Long 컬럼으로 보유하고
Repository에서 findAllByEventId()로 페이징 조회한다.
추가 정보가 필요한 경우 QueryDSL Join으로 처리할 수 있는 구조로 설계했다.
5. GET /api/events — 이벤트 목록 조회 (사용자)
적용 기술. RedisTemplate 캐싱 - TTL 5분
문제: 이벤트 목록 조회는 다수의 사용자가 반복적으로 요청하는 API다.
매 요청마다 DB를 조회하면 이벤트 오픈 직후처럼 트래픽이 몰리는 상황에서 DB 부하가 심해진다.
이벤트 메타 정보(제목, 기간, 포인트 등)는 자주 변경되지 않으므로 캐싱이 적합하다.
해결: RedisTemplate을 사용해 status, 페이지 번호, 페이지 크기, 정렬 조건을 조합한 키로 캐싱한다.
TTL은 5분으로 설정해 짧은 주기로 갱신되게 하고, 이벤트 생성 시 캐시를 evict해 정합성을 유지한다.
캐시 역직렬화 실패 시 자동으로 DB 조회로 전환하는 장애 대응 로직도 포함했다.
6. GET /api/events/{eventId} — 이벤트 상세 조회 (사용자)
적용 기술. 이벤트 상태별 차등 캐싱 전략
문제: 진행 중인 이벤트의 상세 조회는 참여 가능 여부와 현재 상태가 실시간으로 중요하다.
반면 종료된 이벤트는 결과가 확정되어 더 이상 변경되지 않는데도 매 요청마다 DB를 조회하면
이벤트 종료 직후 결과 확인 트래픽이 몰릴 때 DB에 불필요한 부하가 생긴다.
해결: 진행 중인 이벤트는 캐싱 없이 DB를 직접 조회해 실시간 정확성을 보장한다.
종료된 이벤트는 첫 조회 시 Redis에 TTL 7일로 캐싱한다.
이후 동일 이벤트 조회는 전부 Redis에서 응답해 DB 부하 없이 처리된다.
7. POST /api/events/{eventId}/participants — 이벤트 참여 신청 (사용자)
적용 기술 1. Redisson 분산락 - 선착순 동시성 제어
문제: 이벤트 참여 신청은 짧은 시간에 수백 건의 요청이 동시에 몰린다.
동시성 제어 없이 처리하면 maxParticipants를 초과해 참여자가 저장되거나
같은 유저가 중복 참여하는 레이스 컨디션이 발생한다.
DB 비관적 락은 트래픽 집중 시 DB 커넥션을 오래 점유해 병목이 생기므로 부적합하다.
이미 Redis 인프라가 구성되어 있으므로 Redisson 분산락을 선택했다.
TTL(leaseTime) 기반으로 서버 장애 시에도 데드락이 발생하지 않고,
멀티 인스턴스 환경에서도 단일 락으로 동시성을 제어할 수 있다.
해결: 이벤트 ID 단위로 Redisson 분산락을 걸어 한 번에 하나의 요청만 처리한다.
waitTime 5초(최대 대기 시간), leaseTime 3초(자동 해제 시간)로 설정했다.
락 획득 실패 시 TOO_MANY_REQUESTS(429)를 반환해 사용자에게 재시도를 안내한다.
적용 기술 2. DB UniqueConstraint - 이중 동시성 방어
문제: Redisson 락의 leaseTime이 초과되는 극한 상황에서
동시에 두 요청이 락을 획득해 중복 참여가 저장될 가능성이 극히 낮지만 존재한다.
애플리케이션 레벨의 중복 체크만으로는 완전한 방어가 불가능하다.
해결: EventParticipant 테이블에 (event_id, user_id) 복합 UniqueConstraint를 적용했다.
Redisson 락이 1차 방어, DB 제약조건이 2차 방어로 동작하는 이중 구조로 완전한 중복 참여를 차단한다.
적용 기술 3. 포인트 즉시 지급 - 도메인 간 서비스 직접 호출
문제: 선착순 인원 안에 든 사용자에게는 참여 신청 즉시 포인트가 지급되어야 한다.
지급이 지연되면 사용자 경험이 나빠지고 지급 누락 이슈가 발생할 수 있다.
해결: 참여자 저장 직후 같은 트랜잭션 내에서 PointService.chargeEventReward()를 호출한다.
PointHistory에 타입을 EVENT로 기록해 이벤트 보상 이력을 명확히 남긴다.
같은 트랜잭션이므로 참여자 저장과 포인트 지급이 원자적으로 처리되어
참여는 됐는데 포인트가 안 지급되는 불일치 상황이 발생하지 않는다.
Redis 캐싱, 분산락, 유니크제약, kafka를 활용한 이벤트 발행, --> 메모리 고민 대량 사용자 알림 발송시
포폴을위해
1.조회시 성능 지표 가지기
2.동시성이나 데이터 정합성 ->동시성 데이터 정합성 테스트에 관한 수치 자료
3.외부 연동 잘 챙겨가기
4.카프카를 통해 대량 알림 전송 관련 -> 왜 카프카로 넘어오게된 스토리가 필요하고 관련 수치 자료가 필요하다
->카프카 실패시에 대한 재시도 고민, 알림중복발송 을 위한 방지를 위한 고민
5.부하테스트 해보기
6.인프라 쪽으로 더해보던지
7.카프카쪽으로 고도화 해보던지
8.대용량 쿠폰 발행 고민해보기->굳이 여기까지 갈필요는 없을것같다
포인트 -> 증거를 확보하고, 인덱스 설계를 보강하고, 동시성이나 정합성 증거남기기,캐시자체도 내가 어떻게 설계했는데 정리해보기
증거 잘 정리하면 바로 지원서 넣어도 될정도
'spring_2기[본캠프] > 과제' 카테고리의 다른 글
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 15 (0) | 2026.05.06 |
|---|---|
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 14 (0) | 2026.05.04 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 12 (1) | 2026.04.30 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 11 (0) | 2026.04.29 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 10 (0) | 2026.04.28 |