프로젝트 단위의 심야자습 신청을 구현하였습니다. 사용자는 프로젝트 심야자습 신청 시 날짜와 시간, 프로젝트 개요를 입력하고 팀원을 선택 후 신청합니다. 학생이 신청하거나 교사의 상태 변경(수락·거절)시 프로젝트 리더를 포함한 참가자들의 신청 상태를 모두 변경해야 하였습니다.
트러블 슈팅
데이터베이스 설계 시 다음과 같은 고민을 하였습니다.
기존에도 개인 심야자습 신청 기능은 존재하였습니다. 초기 설계 시 이미 있던 일반 심야자습 테이블을 사용하고, 프로젝트들은 night_study_project 테이블을 만들어 FK로 프로젝트 아이디를 받아와 저장하였습니다. 초기 테이블 구조는 다음과 같습니다.
초기 테이블 구조
<night_study>
id
type (개인 심자와 프로젝트 심자 1, 2를 구분하는 ENUM)
content (자습 내용)
do_need_phone (휴대폰 사용 여부)
reason_for_phone (휴대폰 사용 이유)
status (신청 상태 - ALLOWED, PENDING, REJECTED)
reject_reason (신청 거절 시 사유)
startAt (시작 날짜)
endAt (종료 날짜)
fk_project_id (일반 심자 시 null)
fk_student_id (신청 학생)
fk_teacher_id (상태 변경 시 교사 ID)
<night_study_project>
id
type
name
description
startAt
endAt
room (ENUM)
status
fk_leader_id
문제 배경
하지만 여기에서 문제를 발견하였습니다. 일반 심야자습과 프로젝트 단위의 심야자습은 독립되어 클라이언트에서 사용되는 경우가 많았습니다. 예를 들어, 전체 조회를 하여도 일반 심야자습과 프로젝트 심야자습은 각각 따로 검색합니다. JPA가 심야자습들을 찾을 시 project_id의 유무로 심야자습 종류를 구분해야만 하였고, 요청이 될 때마다 불필요한 where 조건이 추가되어야 했습니다.
코드리뷰 중 잦은 반복문 사용과 SQL 쿼리 발생으로 인하여 JPA n+1 문제가 발생하였고 시간복잡도가 높다는 것을 파악하였습니다.
개선 후 테이블 구조
<night_study>
id
content (자습 내용)
do_need_phone (휴대폰 사용 여부)
reason_for_phone (휴대폰 사용 이유)
status (신청 상태 - ALLOWED, PENDING, REJECTED)
reject_reason (신청 거절 시 사유)
start_at (시작 날짜)
end_at (종료 날짜)
fk_project_id (일반 심자 시 null)
fk_student_id (신청 학생)
fk_teacher_id (상태 변경 시 교사 ID)
<night_study_project_member>
id
name
description
room
type (시간대를 구분하기 위함)
status
startAt
endAt
rejectReason
fk_teacher_id
<night_study_project_member>
id
role (LEADER, MEMBER)
fk_student_id
fk_project_id
해결 방법
night_study, night_study_project, night_study_project_member 테이블로 데이터베이스 정규화를 하여 SQL 최적화를 하였습니다.
반복문보다는 JPA와 JPQL에서 in과 join, exists 등을 이용하여 시간복잡도를 줄였습니다. 또한 코드의 리팩터링으로 평균 응답 시간이 ≈522.6ms → 73ms 로 7배 이상(약 737.5%) 줄였습니다.
이 결과로 night_study_project 조회 시 불필요한 쿼리(join, where 등)가 발생하지 않았습니다.