spring_2기[본캠프]/과제

[파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 7

minwoo95 2026. 4. 22. 22:12

1. 오늘 한 일
오늘은 NovelCraft 프로젝트에서 mentor/mentoring 도메인 고도화 작업과 PR 코드래빗 리뷰 피드백 반영, 테스트 코드 수정을 진행했다.

MentorRegisterRequest 파일 업로드 제거 → 수상/출간 경력 텍스트 입력으로 변경
Mentor 엔티티 certificationFileUrl 필드 제거
getMyMentees N+1 문제 개선 → QueryDSL JOIN으로 v2 구현 (GET /api/v2/mentors/me/mentees)
MentorCareerLevelScheduler 청크 단위(100명) 처리 + ID ASC 정렬 기준 추가 + persistence context flush/clear로 메모리 누적 방지
CustomMentorRepository, CustomMentorshipRepository QueryDSL 구현체 추가
getMyHistory N+1 개선 → QueryDSL JOIN v2 구현 (GET /api/mentorships/v2/me/history)
getReceivedMentoringsV2 N+1 개선 + soft-delete 적용 → QueryDSL JOIN 단일 쿼리로 교체
findReceivedMentoringsWithDetails / findMyHistoryWithMentorNickname null 방어 처리 (삭제된 유저/소설 기본값 대체)
토큰 유효시간 수정 (액세스 토큰 6시간, 리프레시 토큰 24시간)
테스트 코드 전체 수정

2. 트러블슈팅
2-1. MentoringServiceV1 클래스를 찾을 수 없는 컴파일 에러
문제 상황
테스트 파일에서 MentoringServiceV1을 import하고 있어 컴파일 에러가 발생했다.
cannot find symbol: class MentoringServiceV1
원인 분석
테스트 코드가 존재하지 않는 MentoringServiceV1 클래스를 참조하고 있었다. 실제 서비스 클래스명은 MentoringService인데 테스트 작성 시 잘못된 클래스명을 사용한 것이었다.
해결
테스트 파일에서 MentoringServiceV1 → MentoringService로 전체 치환하고 컨트롤러 테스트에서 v1 메서드 호출명도 실제 메서드명에 맞게 수정했다.

 

2-2. Mentor.create() 파라미터 수 불일치
문제 상황
테스트 코드에서 Mentor.create() 호출 시 아래 에러가 발생했다.
method create in class Mentor cannot be applied to given types;
required: Long,CareerLevel,String,String,String,String,String,Integer,Boolean,String,MentorStatus
found:    Long,CareerLevel,String,String,String,String,String,int,boolean,String,<null>,MentorStatus
reason: actual and formal argument lists differ in length
원인 분석
certificationFileUrl 필드를 엔티티에서 제거하면서 Mentor.create() 시그니처가 변경됐는데 테스트 코드에서 기존 시그니처로 null을 넣어 호출하고 있었다.
해결
테스트 파일에서 Mentor.create() 호출하는 곳을 전부 찾아 null 파라미터를 제거했다. MentorServiceTest, MentoringServiceTest, MentorshipServiceTest 세 파일 모두 수정했다.

 

2-3. MentorService.register() 파라미터 수 불일치
문제 상황
테스트에서 mentorService.register(USER_ID, registerRequest, null) 로 호출하는데 실제 서비스 메서드는 2개 파라미터만 받아 컴파일 에러가 발생했다.
원인 분석
파일 업로드 제거 작업으로 register() 메서드에서 MultipartFile 파라미터가 제거됐는데 테스트 코드가 업데이트되지 않은 것이었다.
해결
테스트 파일 전체에서 register(USER_ID, registerRequest, null) → register(USER_ID, registerRequest)로 일괄 수정했다.

 

2-4. getMyStatus_approved_success 테스트 실패
문제 상황
Expected: APPROVED
Actual: PENDING
원인 분석
MentorStatus.PENDING으로 Mentor를 생성해놓고 APPROVED 상태를 검증하는 테스트였다. 생성 시 상태가 이미 PENDING이니 당연히 실패한다.
해결
Mentor.create() 호출 시 MentorStatus.APPROVED로 변경했다.

 

2-5. applyMentorship_slot_full_throws 테스트 실패
문제 상황
정원 초과 예외를 기대했는데 NPE가 발생했다.
Cannot invoke "Mentorship.getId()" because "saved" is null
원인 분석
maxMentees=3으로 슬롯이 가득 차지 않은 상태라 예외가 발생하기 전에 mentorshipRepository.save()까지 도달했고, save()에 mock이 없어 null이 반환되면서 NPE가 발생한 것이었다.
해결
fullMentor 생성 시 maxMentees=0으로 변경해서 실제로 슬롯이 가득 찬 상태를 만들었다.

 

3. 코드래빗 리뷰 반영
3-1. 스케줄러 persistence context 누적 문제
@Transactional이 전체 루프를 감싸고 있어 청크마다 fetch된 엔티티가 메모리에 쌓이는 문제가 있었다. 청크 처리 후 entityManager.flush() / entityManager.clear()를 호출해서 persistence context를 정리했다.
3-2. 스케줄러 페이지네이션 정렬 기준 없음
PageRequest.of(pageNumber, CHUNK_SIZE)에 정렬이 없으면 배치 실행 중 다른 트랜잭션이 데이터를 변경할 때 페이지 경계가 밀려 행 누락/중복이 발생할 수 있었다. Sort.by("id").ascending()을 추가해서 정렬 기준을 고정했다.
3-3. findReceivedMentoringsWithDetails null 노출 문제
v1은 삭제된 유저/소설에 "알 수 없는 사용자", "알 수 없는 소설" 기본값을 내려줬는데, v2 QueryDSL 프로젝션은 LEFT JOIN 결과 null을 그대로 넣어서 클라이언트에서 NPE 가능성이 있었다. Expressions.cases()로 null 방어 처리를 추가했다.
3-4. findMyHistoryWithMentorNickname soft-delete 미적용
findReceivedMentoringsWithDetails는 soft-delete를 적용했는데 findMyHistoryWithMentorNickname은 inner join이라 탈퇴한 멘토 닉네임이 그대로 노출되는 문제가 있었다. leftJoin + isDeleted=false + fallback으로 통일했다.

 

4. 느낀 점
오늘은 코드래빗 리뷰 피드백을 받으면서 배치 처리에서 놓치기 쉬운 부분들을 배웠다. persistence context 누적 문제는 @Transactional이 전체 루프를 감싸는 구조에서 발생하는데 청크 처리를 도입했어도 flush/clear를 빠뜨리면 의미가 없다는 것을 알게 됐다.
정렬 기준 없는 페이지네이션이 배치에서 데이터 누락/중복을 일으킬 수 있다는 점도 새로 알게 됐다. 단순히 페이지 번호와 사이즈만 설정하면 된다고 생각했는데 배치처럼 실행 중 데이터가 변경될 수 있는 환경에서는 정렬 기준 고정이 필수라는 것을 배웠다.
또한 테스트 수정 과정에서 파라미터 수 불일치 에러가 여러 파일에 걸쳐 발생했는데, 엔티티나 서비스 메서드 시그니처가 바뀌면 관련 테스트 파일을 한꺼번에 검색해서 수정하는 습관이 필요하다는 것을 다시 느꼈다.

 

------------------------------------------------------------------------------------------

1. 오늘 한 일
오늘은 NovelCraft 프로젝트의 AWS 인프라 셋팅과 Jenkins CI/CD 파이프라인 구축을 진행했다.
AWS EC2 인스턴스 생성 (Amazon Linux 2023, x86_64)
보안 그룹 생성 및 인바운드 규칙 설정 (SSH 22, HTTP 8080)
IAM Role 생성 및 EC2 연결 (ECR Pull, S3 접근 권한)
EC2 SSH 접속 및 Java 21 (Amazon Corretto) 설치
Jenkins repo 등록 및 Jenkins 2.555.1 설치
Jenkins 서비스 설정 파일 수정 (JENKINS_HOME, JAVA_HOME 경로 수정)
Jenkins 웹 UI 접속 및 초기 플러그인 설치 완료

 

2. 트러블슈팅
2-1. SSH 접속 시 UNPROTECTED PRIVATE KEY FILE 에러
문제 상황
pem 키로 EC2에 SSH 접속 시도했을 때 아래 에러가 발생하며 접속이 거부됐다.
WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions 0644 for 'novel-craft-key.pem' are too open.
Permission denied (publickey)
원인 분석
pem 파일의 권한이 0644로 설정되어 있어 다른 사용자도 읽을 수 있는 상태였다. SSH는 보안상 private key가 본인만 읽을 수 있어야 한다는 규칙이 있어서 접속을 차단한 것이다.
해결
bashchmod 400 "novel-craft-key.pem"
권한을 400(본인만 읽기)으로 변경 후 정상 접속됐다.

 

2-2. Jenkins systemctl 시작 실패
문제 상황
Jenkins 설치 후 sudo systemctl start jenkins 실행 시 아래 에러가 발생했다.
Job for jenkins.service failed because the control process exited with error code.
원인 분석
서비스 파일의 JENKINS_HOME 경로가 Java 경로로 잘못 설정되어 있었다.
# 잘못된 설정
Environment="JENKINS_HOME=/usr/lib/jvm/java-17-amazon-corretto.x86_64"
또한 Java 17이 설치되어 있었으나 Jenkins 2.555 버전은 Java 21을 요구했고, JAVA_HOME도 주석 처리된 상태였다.
해결
Java 17을 제거하고 Java 21로 재설치 후, 서비스 파일을 직접 수정했다.
bash# JENKINS_HOME 경로 수정
Environment="JENKINS_HOME=/var/lib/jenkins"

# JAVA_HOME 주석 해제 및 경로 수정
Environment="JAVA_HOME=/usr/lib/jvm/java-21-amazon-corretto.x86_64"
수정 후 systemctl daemon-reload → systemctl start jenkins 순으로 실행해 정상 구동됐다.

 

2-3. Jenkins 초기 플러그인 다수 설치 실패
문제 상황
Jenkins 웹 UI 초기 셋팅에서 Install suggested plugins 진행 시 Pipeline, Git, Gradle 등 다수의 플러그인이 설치 실패했다.
원인 분석
미러 서버(mirrors.tuna.tsinghua.edu.cn) 타임아웃으로 인한 네트워크 문제였다. EC2의 외부 네트워크 연결은 정상이었으나 특정 미러 서버 응답이 느렸다.
해결
Retry를 통해 일부 플러그인을 추가 설치하고, 나머지는 Continue로 넘어간 후 대시보드에서 수동 설치하는 방향으로 진행했다. CI/CD에 필요한 핵심 플러그인(Pipeline, Git, Gradle, SSH Agent 등)은 이후 Manage Jenkins → Plugins에서 별도 설치 예정이다.

 

3. 느낀 점
오늘은 코드보다 인프라 셋팅에 집중한 하루였다. EC2 생성부터 Jenkins 구동까지 단계별로 진행하면서 각 설정값이 어떤 역할을 하는지 직접 확인할 수 있었다.
특히 Jenkins 서비스 파일의 JENKINS_HOME이 Java 경로로 잘못 설정되어 있었던 부분이 인상 깊었다. 에러 메시지만 봐서는 원인을 알 수 없었고, grep으로 서비스 파일 내용을 직접 뜯어보고 나서야 잘못된 값을 발견할 수 있었다. 앞으로 서비스가 시작되지 않을 때는 로그와 설정 파일을 직접 확인하는 습관을 들여야겠다는 것을 느꼈다.
IAM Role을 EC2에 직접 연결하면 Access Key 없이도 ECR, S3에 접근할 수 있다는 점도 새롭게 알게 됐다. 키를 코드나 설정 파일에 직접 넣는 것보다 훨씬 안전한 방식이라는 걸 이해했다.
내일은 Jenkins 파이프라인 설정과 GitHub Webhook 연동을 진행할 예정이다.