Skip to content

[Feat/#72] 내 배정 사물함 조회 API 추가 - #73

Merged
jjunh33 merged 5 commits into
mainfrom
feat/#72-my-locker-applications
Sep 29, 2026
Merged

jjunh33 merged 5 commits into
mainfrom
feat/#72-my-locker-applications

Conversation

@leegain1

@leegain1 leegain1 commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

⚠️ #71(feat/#70-locker-application) 위에 쌓은 PR이라 base를 그 브랜치로 두었다. #65 → #71 순서로 머지되면 base를 main으로 바꾼다.

🎯 해결하려는 문제가 무엇인가요?

사물함을 신청(#71)한 뒤 내게 배정된 사물함과 지난 회차 이력을 볼 API가 없다.

Method Endpoint 설명
GET /v1/app/lockers/applications 내 배정 사물함 조회 (지난 회차 이력 포함)
// 200 — 배정된 사물함이 있는 경우
{
  "applications": [
    {
      "lockerApplicationId": 25,
      "lockerPeriodId": 3,
      "lockerPeriodName": "2026-2학기",
      "applicationStatus": "ASSIGNED",
      "appliedAt": "2026-09-05T13:59:00",
      "usageStartDate": "2026-09-07",
      "usageEndDate": "2026-12-15",
      "lockerLabel": "B-25"
    }
  ]
}
// 200 — 없는 경우: "data": null

❓ 왜 해결해야 하나요?

신청은 즉시 배정이라 학생은 신청 직후부터 "내 사물함" 화면에서 배정 결과를 확인해야 한다. 응답의 lockerPeriodId로 기존 구역 목록·구역 상세 조회를 호출해 사물함 위치도 보여준다.

⭐ 어떻게 해결했나요?

서비스 → API → 테스트 순으로 쌓고 리뷰 반영 커밋을 더했다. 각 커밋이 독립적으로 빌드된다.

조회는 각각, 조합은 서비스에서

LockerApplicationServiceImpl.getApplicationsByMemberId(memberId)
 1. 회원의 신청 전체 (applied_at DESC, id DESC)   — 없으면 여기서 빈 목록 반환
 2. getPublishedPeriods                  — 게시된 회차 일괄 조회. 게시를 내린 회차의 신청은 결과에서 뺀다
 3. getAssignedLockersIncludingRemoved  — 사물함 일괄 조회. 철거(삭제)된 사물함도 지난 신청의 이름을 보여줘야 한다
 4. LockerApplicationResult(신청, 사물함, 회차) — 필드를 복사하지 않고 도메인 객체를 묶어 돌려준다

coding-style.md 2-6절대로 레포지토리는 각각 돌려주고 서비스가 짝짓는다. 신청은 LockerApplicationRepository, 회차·사물함은 LockerRepository에서 읽는다. 회차·사물함은 IN 조회로 한 번에 읽어 N+1이 없다. 응답 DTO(MyLockerApplicationListResponse.Item)가 묶음에서 필요한 값을 꺼낸다.

배정 상태는 운영 회차가 판정한다

값 조건
ASSIGNED (배정완료) 오늘 ≤ 사용 종료일 (사용 시작 전 포함)
EXPIRED (이용종료) 오늘 > 사용 종료일

LockerPeriod.isUsageEnded(today)가 판정하고 응답을 만들 때 LockerApplicationStatus.from(period, today)가 상태를 만든다(#65 SectionAvailabilityStatus.from과 같은 모양). 신청이 곧 배정이라 명세의 APPLIED(신청완료)는 발생 경로가 없어 두지 않았다.

트랜잭션 — @Transactional을 걸지 않은 판단

"여러 조회가 반드시 동일한 시점의 데이터를 바라봐야 하는지"를 기준으로 판단했다.

메서드 쿼리 트랜잭션 근거
LockerApplicationServiceImpl.getApplicationsByMemberId 3개 (신청 → 회차 → 사물함), 신청이 없으면 1개 없음 세 조회가 같은 시점을 볼 필요가 없다(아래 표). 쓰기가 없다.
LockerApplicationServiceImpl.apply (#71) 조회 3 + INSERT 1 @Transactional 쓰기. 유니크 위반 시 전체 롤백이 필요하다. 이 PR에서 변경 없음.

조회 사이에 데이터가 바뀌어도 결과가 틀리지 않는다

조회 사이에 바뀌는 것 결과
회차 게시를 내림 그 신청이 빠진다 — 어느 시점 기준으로도 맞는 결과
회차명·사물함 이름 수정 바뀐 이름이 보인다
신청 취소·변경 기능이 없다
새 신청 추가 신청을 가장 먼저 읽어 이후 조회에 영향 없음

실측 (일회용 MySQL 8.4 컨테이너 + general log, API 1회 호출, 첫 호출 제외)

readOnly = true + findAllById (처음 구현) readOnly만 제거 최종: 트랜잭션 없음 + findAllByIdIn
신청 있음 8개: READ ONLY, autocommit=0, SELECT 3, COMMIT, autocommit=1, READ WRITE 8개: SELECT 2 → findAllById가 자체 트랜잭션을 열어 관리 문장 5 + SELECT 1 SELECT 3개
신청 없음 6개: 관리 문장 5 + SELECT 1 1개 SELECT 1개
  • 선언 쿼리 메서드(findAllByMemberIdOrderBy..., findAllByIdInAndIsPublishedTrue)는 자체 트랜잭션을 만들지 않았다.
  • 반면 상속한 findAllById는 SimpleJpaRepository의 클래스 레벨 readOnly 트랜잭션을 연다. 어노테이션만 빼면 신청이 있는 경우 문장 수가 그대로(8개)여서, 사물함 일괄 조회를 선언 쿼리 findAllByIdIn으로 바꿨다. is_deleted 조건이 없어 삭제된 사물함 포함 동작은 같다.

트랜잭션을 빼서 잃는 것

항목 지금 손실 설명
스냅샷 일관성 없음 위 표처럼 각 건이 그 시점에 맞는 결과다
DB 세션 읽기 전용 보호 없음 쓰기가 없다
Hibernate 읽기 전용 최적화 미미 트랜잭션이 없으면 flush 자체가 없다
전파 없음 나중에 UseCase가 감싸면 합류한다

한 스냅샷이 필요한 조회(예: 전체 개수)가 추가되면 다시 판단하도록 메서드 주석에 남겼다.

🧩 이 PR의 한계 & 트레이드오프

  • coding-style.md 2-7절("조회 전용은 readOnly")과 다르다. 문서에 예외를 넣을지는 팀 논의 결과를 따른다.
  • 배정 상태는 서버 날짜(LocalDate.now()) 기준이다. 서버 타임존이 KST가 아니면 종료일 경계가 어긋날 수 있다.
  • JPA 통합 테스트가 없다. 일회용 MySQL 8.4 컨테이너로 V1~V11 적용, ddl-auto: validate, 응답 JSON(최신순·상태·삭제된 사물함 이름·비공개 회차 제외·data: null)과 위 쿼리 수를 확인했다. 일회용 검증이라 커밋에는 넣지 않았다.
  • 성공 메시지는 공통 기본값("요청에 성공했습니다.")이다. 명세의 "현재 배정된 사물함이 없습니다."를 쓰려면 ApiResponse를 고쳐야 해 명세 쪽을 맞춘다.

⛓️ 기존 기능에 미치는 영향

  • 스키마 변경 없음(#71의 V11 (member_id, applied_at) 인덱스를 쓴다).
  • 레포지토리 메서드 3개 추가: 신청은 LockerApplicationRepository.findByMemberId, 회차·사물함은 LockerRepository.findPublishedPeriodsByIds·findLockersByIdsIncludingDeleted. 기존 테스트의 가짜 레포지토리에 스텁을 추가했다.
  • 구역 조회·구역 상세·사물함 신청의 동작 변화 없음. clean build(ModularityTests.verify() + DomainImplAccessTests + 테스트) 통과.

🔀 Edge Case & 실패 시나리오

상황 처리
신청 내역 없음 data: null (회차·사물함 조회 없이 SELECT 1개)
게시를 내린 회차의 신청 결과에서 제외
신청 후 삭제된 사물함 사물함 이름을 그대로 보여준다
사용 시작 전 ASSIGNED
사용 종료일 당일 / 다음 날 ASSIGNED / EXPIRED
인증 없음 401

📋 검토한 대안과 선택 이유

  1. @Transactional(readOnly = true) 유지 — 컨벤션 문서와 맞지만, 스냅샷이 필요 없는데 요청마다 관리 문장 5개를 더 낸다.
  2. 어노테이션만 제거 — 상속한 findAllById가 트랜잭션을 열어 신청이 있으면 문장 수가 그대로다(8개). 효과가 없어 선언 쿼리 교체까지 했다.
  3. 신청·회차·사물함을 조인한 한 쿼리 — 문장은 1개가 되지만 레포지토리가 조합을 맡게 되어 coding-style.md 2-6절과 어긋나고, 서비스 조합을 DB 없이 테스트할 수 없다.
  4. 빈 목록 { "applications": [] } — 명세가 data: null이라 따랐다.

💬 리뷰 포인트

  • [r] 트랜잭션을 걸지 않은 판단 — 위 "조회 사이에 바뀌는 것" 표와 실측 기준으로 봐주세요. 팀 논의 결과에 맞춰 조정하겠습니다.
  • [c] findAllById 대신 findAllByIdIn — 같은 결과를 내는 메서드를 트랜잭션 때문에 따로 선언했다. 이유를 JPA 레포지토리 주석에 남겼다.
  • [c] 내 신청 내역에 지난 회차 이력까지 포함 — 명세 설명은 "현재 회차", 예시는 지난 회차(EXPIRED) 포함이라 예시를 따랐다.
  • [a] 서비스는 신청 API와 같은 LockerApplicationResult(신청·사물함·회차 도메인 객체 묶음)를 돌려준다. 응답과 1:1로 필드를 복사하던 읽기 모델(LockerApplicationSummary)은 리뷰를 반영해 제거했다.

@leegain1 leegain1 self-assigned this Sep 27, 2026
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: billilge/stream-server/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e5438c05-7d9d-4fc2-a2c8-1f07eaf6c5d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

/**
* 회원의 사물함 신청 내역 한 건. 신청에 어느 회차의 어떤 사물함이었는지와 배정 상태를 덧붙인 읽기 모델이다.
*/
public record LockerApplicationSummary(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vo의 의미에 대해 공부해보면 좋을 거 같아요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vo가 뭔지, vo와 dto의 차이가 뭐가있는지 찾아봤는데 LockerApplicationSummary는 vo가 아니었습니다..
vo는 식별자 없이 값 자체로 같고 다름이 정해지고, 스스로 규칙을 지키는 객체인데
이 클래스는 applicationId라는 식별자를 가지고 있고, 응답 DTO와 필드를 1:1로 복사만 하고 있었습니다.

그래서 제거했고, 서비스는 신청·사물함·회차 도메인 객체를 그대로 묶은 LockerApplicationResult를 돌려주도록 바꿨습니다.
응답에 필요한 값은 응답 DTO가 꺼내 쓰고, 배정 상태는 LockerApplicationStatus.from(period, today)로 판정하는 걸로 확인했습니다!!

applications.stream().map(LockerApplication::getLockerPeriodId).distinct().toList())
.stream()
.collect(Collectors.toMap(LockerPeriod::getId, Function.identity()));
Map<Long, Locker> lockers = lockerRepository.findLockersByIdsIncludingDeleted(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사물함 조회에 deleted를 포함한 이유가 궁금합니다. 전체적으로 맥락에 맞추어 메서드로 분리해보는 것도 좋을 거 같아요. 지금 너무 이 메서드에 뭐가 많은 거 같습니다. LockerApplicationSummary가 꼭 필요한지도 한 번 고민해 주세요!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted는 삭제된 사물함을 포함한 건 지난 회차 이력 때문에 포함했습니다!
이용이 끝난 뒤 사물함이 soft delete되어도 그 신청의 사물함 이름(예: "A-14")은 보여줘야 해서 포함했습니다.

말씀대로 한 메서드에 너무 많은 일이 있어서 게시된 회차 조회와 배정된 사물함 조회로 나누고, 삭제 포함 이유는 메서드 주석으로 남겼습니다. 확인해주시면 감사하겠습니다!

@leegain1
leegain1 force-pushed the feat/#72-my-locker-applications branch from f6fc802 to 048f55b Compare September 28, 2026 07:57
@jjunh33
jjunh33 force-pushed the feat/#70-locker-application branch from cdd3eaa to fb9e2da Compare September 29, 2026 11:20
Base automatically changed from feat/#70-locker-application to main September 29, 2026 11:21
신청·회차·사물함을 각각 읽어 서비스에서 짝짓고, 게시를 내린 회차의 신청은
뺀다. 지난 신청의 사물함 이름을 보여줘야 해서 사물함은 삭제된 것도 읽는다.
신청은 LockerApplicationRepository, 회차·사물함은 LockerRepository에서 읽는다.

신청이 곧 배정이라 신청 대기 상태는 두지 않고, 사용 종료일이 지났는지로
ASSIGNED/EXPIRED를 가른다. 판정은 운영 회차가 소유한다.

세 조회가 같은 시점을 볼 필요가 없어 트랜잭션을 걸지 않는다. 사물함 일괄
조회는 상속한 findAllById가 자체 읽기 전용 트랜잭션을 열기 때문에 선언 쿼리
findAllByIdIn으로 둬서 SELECT만 나가게 한다.
GET /v1/app/lockers/applications 로 게시된 회차에서 배정된 내 사물함과 지난
회차 이력을 신청 일시 최신순으로 조회한다.

배정된 사물함이 없으면 명세대로 빈 목록 대신 data를 null로 내려준다.
신청 내역이 저장소의 최신순을 유지하며 회차·사물함 정보를 붙이는지, 게시를
내린 회차의 신청을 빼는지, 삭제된 사물함의 이름도 보여주는지, 신청이 없으면
추가 조회 없이 빈 목록을 돌려주는지 확인한다.

배정 상태는 사용 종료일 당일까지 ASSIGNED, 다음 날부터 EXPIRED인지 경계값으로
확인한다.
신청 내역 조회 한 메서드가 회차·사물함 일괄 조회와 짝짓기를 모두 하고 있어
길고, 삭제된 사물함을 왜 포함하는지도 드러나지 않았다.

게시된 회차(getPublishedPeriods)와 배정된 사물함(getAssignedLockersIncludingRemoved)
조회를 나누고, 이용이 끝난 뒤 철거된 사물함도 지난 신청의 이름을 보여주려고
포함한다는 이유를 메서드 주석에 남긴다.
LockerApplicationSummary는 신청·회차·사물함 필드를 응답 DTO(Item)와 1:1로
복사만 하는 객체였다. 식별자를 갖고 스스로 지키는 규칙도 없어 값 객체로 둘
이유가 없고, 필드가 늘 때마다 도메인과 응답 두 곳을 고쳐야 한다.

서비스는 신청 API와 같은 LockerApplicationResult(신청·사물함·회차 도메인 객체
묶음)를 돌려주고, 응답 DTO가 필요한 값을 꺼낸다. 배정 상태는 응답을 만들 때
LockerApplicationStatus.from(period, today)으로 판정한다.
@jjunh33
jjunh33 force-pushed the feat/#72-my-locker-applications branch from e0d5457 to 8f020b8 Compare September 29, 2026 11:28
@jjunh33
jjunh33 merged commit 59c5d5e into main Sep 29, 2026
@jjunh33
jjunh33 deleted the feat/#72-my-locker-applications branch September 29, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

내 배정 사물함 조회 API 추가

3 participants