Skip to content

<feat> API Response DTO 도입 및 TSID 변경 관련 변경#37

Merged
leonroars merged 2 commits into
mainfrom
refactor_api_dto
Feb 4, 2026
Merged

<feat> API Response DTO 도입 및 TSID 변경 관련 변경#37
leonroars merged 2 commits into
mainfrom
refactor_api_dto

Conversation

@leonroars
Copy link
Copy Markdown
Owner

@leonroars leonroars commented Feb 4, 2026

PR: API Response DTO 도입 및 ID 정밀도 이슈 해결

배경 (Background)

  • 이슈: Frontend(JavaScript) 환경에서 64-bit TSID(Long)를 Number 타입으로 처리 시 정밀도 유실 발생.
  • 현상: 사용자 생성 후 조회 시 ID 불일치로 인한 404/400 발생
  • 추가 발견: Frontend에서 단일 화면 구성을 위해 다수의 API를 개별 호출(Client-side Loop)하는 구조 확인.

변경 사항 (Changes)

1. Response DTO 도입 (interfaces/dto)

  • UserResponse, ConcertResponse 등 8개 DTO 생성.
  • Explicit Contract: DTO 가 도메인 모델의 Long ID를 String으로 명시적 변환 -> 도메인 모델과 API Spec 간 독립성 확보

2. Controller 리팩토링 및 Data Aggregation

  • ConcertReservationController의 반환 타입을 DTO로 전면 교체.
  • Backend-side Aggregation:
    • Frontend의 반복 요청 구조를 Backend Loop로 전환.
    • ReservationResponseseatNumber 포함 등 UI 필요 데이터 조합.
    • Note: Aggregation 로직은 현재 Controller에 위치하며, 직후 Bulk Fetch 최적화 시 Service/Facade Layer로 이관 예정.

문제 해결 방안 의사결정

결정 항목 Option A Option B 최종 결정 판단 근거
ID 직렬화 전략 Jackson Custom Serializer (@JsonSerialize) 별도 Response DTO 도입 DTO Serializer 사용 시 Domain Model과 API가 강하게 결합됨. DTO를 통해 API Contract를 명확히 정의하고 도메인과 분리하는 것이 장기적 유지보수성에 유리하다고 판단.
Aggregation 위치 Frontend (Promise.all) Backend (Controller Loop) Backend DB 접근 횟수는 현재 동일하나, Client-Server 간 HTTP 요청 횟수를 구조적으로 감소시킴 (N회 -> 1회).
최적화 전략 Bulk Fetch까지 한 번에 구현 API 계약 확정 후 최적화 단계적 적용 정밀도 버그 수정이 최우선 과제(Critical). DB 레벨의 최적화(Bulk Fetch)는 별도 PR로 분리하여 변경 리스크를 최소화함.

검증 (Verification)

  • ./gradlew test --tests DtoSerializationTest: DTO의 ID 필드가 String으로 정확히 변환되는지 검증 Pass.
  • Manual Test: 사용자 생성 → 공연 목록 조회(Title 포함) → 예약 및 조회 플로우에서 UnavailableRequestException 없이 정상 동작 확인 / 모든 테스트 통과 확인

Summary by CodeRabbit

Release Notes

  • Refactor

    • Restructured all API response formats to use consistent data transfer objects across user, concert, schedule, seat, reservation, and token endpoints, improving API clarity and consistency.
  • Tests

    • Added serialization tests to validate API response structure and data integrity.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

All public API endpoints in the ConcertReservationController have been refactored to return DTO-based responses instead of domain entities. Eight new DTO classes were introduced (UserResponse, ConcertResponse, ConcertScheduleResponse, SeatResponse, ReservationResponse, TokenResponse, UserPointBalanceResponse, PointHistoryResponse), each with factory methods for converting domain models. Related data fetching logic was added to endpoints where necessary to populate DTOs with associated entities.

Changes

Cohort / File(s) Summary
Controller Refactoring
src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java
All 16 public endpoints updated to return DTO responses. Return types changed from domain entities to corresponding DTO wrappers (e.g., ResponseEntity<User>ResponseEntity<UserResponse>). Response bodies now use DTO factory methods (UserResponse.from(...), etc.). Additional data fetching added for related entities like Concert and Seat to populate DTOs.
DTO Classes
src/main/java/com/slam/concertreservation/interfaces/dto/UserResponse.java, ConcertResponse.java, ConcertScheduleResponse.java, SeatResponse.java, ReservationResponse.java, TokenResponse.java, UserPointBalanceResponse.java, PointHistoryResponse.java
Eight new DTO classes introduced with Lombok @Getter and @Builder annotations. Each includes fields mapping domain entity properties and a static from() factory method for domain-to-DTO conversion. Field sets vary per DTO (e.g., ConcertScheduleResponse includes concertTitle/artist from related Concert).
Serialization Tests
src/test/java/com/slam/concertreservation/interfaces/dto/DtoSerializationTest.java
New test class verifying JSON serialization of DTOs (UserResponse, ConcertResponse). Configures ObjectMapper with JavaTimeModule and asserts proper string preservation of ID fields during JSON serialization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰✨ New DTOs hop into place,
Responses dance with perfect grace,
From entities to data's dress,
The API blooms in cleanliness! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions DTO introduction and TSID-related changes in Korean, which directly aligns with the main objectives of introducing Response DTOs and converting Long IDs to Strings to fix JavaScript precision loss issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Docstrings were successfully generated.
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor_api_dto

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java`:
- Around line 119-129: The controller method getAvailableConcertSchedules causes
an N+1 by calling reservationApp.getConcert(schedule.getConcertId()) per
schedule; fix by collecting unique concert IDs from
getAvailableConcertSchedules(), batch-fetching concerts via a new
reservationApp.getConcertsByIds(Set<Long>) method, build a Map<Long,Concert>,
and then call ConcertScheduleResponse.from(schedule,
concertMap.get(schedule.getConcertId())) when mapping schedules to responses so
each concert is fetched once.
- Around line 207-219: The current getUserReservations controller implementation
causes an N+1 query by calling reservationApp.getSeat(...) for each Reservation;
instead, after calling reservationApp.getUserReservations(...) collect all
distinct seatIds, call a new/appropriate batch method on reservationApp (e.g.,
getSeatsByIds) to fetch all Seat objects at once, build a Map<Long,Seat> keyed
by seatId, and then map reservations to ReservationResponse using
ReservationResponse.from(reservation, seatMap.get(reservation.getSeatId())) so
each seat is retrieved from the map rather than via repeated
reservationApp.getSeat calls.
- Around line 226-236: The cookie set in issueToken (method issueToken, class
ConcertReservationController) lacks secure and SameSite attributes; change the
cookie handling so the token cookie is marked Secure and SameSite (preferably
Strict or Lax) and keep HttpOnly and Path. Replace the current
response.addCookie(new Cookie(...)) usage with a construction that sets secure
(cookie.setSecure(true)) and ensures SameSite is emitted (either build the
Set-Cookie header manually on the HttpServletResponse or use Spring's
ResponseCookie/Servlet API to include SameSite), and make the Secure flag
configurable via a profile/property so non-HTTPS dev remains possible; return
TokenResponse.from(token) unchanged.
🧹 Nitpick comments (3)
src/test/java/com/slam/concertreservation/interfaces/dto/DtoSerializationTest.java (2)

13-37: Consider expanding test coverage for the remaining DTOs.

The tests effectively verify that string IDs are preserved during JSON serialization. However, only 2 of the 8 new DTOs are covered. Given that the PR's primary goal is fixing JavaScript precision loss across all ID fields, consider adding similar tests for the other DTOs (especially TokenResponse, SeatResponse, and ReservationResponse) to ensure comprehensive coverage.

Example test for TokenResponse
`@Test`
`@DisplayName`("TokenResponse IDs should be serialized as Strings")
void tokenResponseSerialization() throws Exception {
    TokenResponse dto = TokenResponse.builder()
            .id("token-uuid-string")
            .userId("1234567890123456789")
            .concertScheduleId("9876543210987654321")
            .status("ACTIVE")
            .expiredAt(LocalDateTime.now())
            .build();

    String json = objectMapper.writeValueAsString(dto);

    assertThat(json).contains("\"userId\":\"1234567890123456789\"");
    assertThat(json).contains("\"concertScheduleId\":\"9876543210987654321\"");
}

28-32: Minor: ConcertResponse test is missing the artist field.

The ConcertResponse DTO has an artist field, but it's not set in the test. While not strictly required for the ID serialization test, setting all fields would make the test more representative and catch any serialization issues with the complete DTO.

Suggested improvement
         ConcertResponse dto = ConcertResponse.builder()
                 .id("9876543210987654321")
                 .name("Test Concert")
+                .artist("Test Artist")
                 .build();
src/main/java/com/slam/concertreservation/interfaces/dto/ConcertScheduleResponse.java (1)

21-32: Consider adding validation for mismatched Concert.

The factory method assumes the caller passes the correct Concert for the given ConcertSchedule. A mismatch (e.g., schedule's concertId != concert.getId()) would silently produce incorrect data.

🛡️ Optional defensive validation
 public static ConcertScheduleResponse from(ConcertSchedule schedule, Concert concert) {
+    if (!schedule.getConcertId().equals(concert.getId())) {
+        throw new IllegalArgumentException("Concert ID mismatch: schedule.concertId=" 
+            + schedule.getConcertId() + ", concert.id=" + concert.getId());
+    }
     return ConcertScheduleResponse.builder()

Comment on lines 119 to 129
@GetMapping("/concerts/schedules/available")
public ResponseEntity<List<ConcertSchedule>> getAvailableConcertSchedules() {
return ResponseEntity.ok(reservationApp.getAvailableConcertSchedules());
public ResponseEntity<List<ConcertScheduleResponse>> getAvailableConcertSchedules() {
List<ConcertSchedule> schedules = reservationApp.getAvailableConcertSchedules();
List<ConcertScheduleResponse> responses = schedules.stream()
.map(schedule -> {
Concert concert = reservationApp.getConcert(schedule.getConcertId());
return ConcertScheduleResponse.from(schedule, concert);
})
.toList();
return ResponseEntity.ok(responses);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

N+1 query issue: Consider batch-fetching concerts.

Each schedule triggers a separate getConcert() call, resulting in N+1 queries. For M schedules, this makes M additional database calls. Per the PR objectives, this is planned for a separate optimization PR, but flagging for visibility.

♻️ Suggested optimization approach for future PR
// Collect unique concertIds first
Set<Long> concertIds = schedules.stream()
    .map(ConcertSchedule::getConcertId)
    .collect(Collectors.toSet());

// Batch fetch all concerts
Map<Long, Concert> concertMap = reservationApp.getConcertsByIds(concertIds)
    .stream()
    .collect(Collectors.toMap(Concert::getId, Function.identity()));

// Map with cached lookups
List<ConcertScheduleResponse> responses = schedules.stream()
    .map(schedule -> ConcertScheduleResponse.from(schedule, concertMap.get(schedule.getConcertId())))
    .toList();
🤖 Prompt for AI Agents
In
`@src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java`
around lines 119 - 129, The controller method getAvailableConcertSchedules
causes an N+1 by calling reservationApp.getConcert(schedule.getConcertId()) per
schedule; fix by collecting unique concert IDs from
getAvailableConcertSchedules(), batch-fetching concerts via a new
reservationApp.getConcertsByIds(Set<Long>) method, build a Map<Long,Concert>,
and then call ConcertScheduleResponse.from(schedule,
concertMap.get(schedule.getConcertId())) when mapping schedules to responses so
each concert is fetched once.

Comment on lines 207 to 219
@GetMapping("/users/{userId}/reservations")
public ResponseEntity<List<Reservation>> getUserReservations(@PathVariable String userId) {
return ResponseEntity.ok(reservationApp.getUserReservations(Long.valueOf(userId)));
public ResponseEntity<List<ReservationResponse>> getUserReservations(@PathVariable String userId) {
List<Reservation> reservations = reservationApp.getUserReservations(Long.valueOf(userId));
List<ReservationResponse> responses = reservations.stream()
.map(reservation -> {
// Seat 정보 조회하여 seatNumber 포함
Seat seat = reservationApp.getSeat(reservation.getSeatId());
return ReservationResponse.from(reservation, seat);
})
.toList();

return ResponseEntity.ok(responses);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

N+1 query issue: Batch-fetch seats for user reservations.

Same N+1 pattern as getAvailableConcertSchedules(). For M reservations, this makes M additional getSeat() calls. Consider batch-fetching seats by their IDs in the future optimization PR.

♻️ Suggested optimization approach for future PR
List<Reservation> reservations = reservationApp.getUserReservations(Long.valueOf(userId));

// Batch fetch all seats
Set<Long> seatIds = reservations.stream()
    .map(Reservation::getSeatId)
    .collect(Collectors.toSet());
Map<Long, Seat> seatMap = reservationApp.getSeatsByIds(seatIds)
    .stream()
    .collect(Collectors.toMap(Seat::getId, Function.identity()));

List<ReservationResponse> responses = reservations.stream()
    .map(r -> ReservationResponse.from(r, seatMap.get(r.getSeatId())))
    .toList();
🤖 Prompt for AI Agents
In
`@src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java`
around lines 207 - 219, The current getUserReservations controller
implementation causes an N+1 query by calling reservationApp.getSeat(...) for
each Reservation; instead, after calling reservationApp.getUserReservations(...)
collect all distinct seatIds, call a new/appropriate batch method on
reservationApp (e.g., getSeatsByIds) to fetch all Seat objects at once, build a
Map<Long,Seat> keyed by seatId, and then map reservations to ReservationResponse
using ReservationResponse.from(reservation,
seatMap.get(reservation.getSeatId())) so each seat is retrieved from the map
rather than via repeated reservationApp.getSeat calls.

Comment on lines 226 to 236
@PostMapping("/queue/tokens")
public ResponseEntity<Token> issueToken(@RequestParam String userId,
public ResponseEntity<TokenResponse> issueToken(@RequestParam String userId,
@RequestParam Long scheduleId,
HttpServletResponse response) {
Token token = reservationApp.issueToken(Long.valueOf(userId), scheduleId);
Cookie cookie = new Cookie("tokenId", token.getId());
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
return ResponseEntity.ok(token);
return ResponseEntity.ok(TokenResponse.from(token));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cookie missing security attributes.

The token cookie lacks secure and SameSite attributes, as flagged by static analysis. This could expose the token over insecure channels and leave the endpoint vulnerable to CSRF.

🔒 Proposed fix for cookie security
     Token token = reservationApp.issueToken(Long.valueOf(userId), scheduleId);
     Cookie cookie = new Cookie("tokenId", token.getId());
     cookie.setHttpOnly(true);
+    cookie.setSecure(true); // Only transmit over HTTPS
+    cookie.setAttribute("SameSite", "Strict"); // CSRF protection
     cookie.setPath("/");
     response.addCookie(cookie);

Note: If the application needs to support non-HTTPS in development, consider making setSecure() configurable via a profile or property.

🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 233-233: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute

(cookie-missing-secure-flag-java)


[warning] 233-233: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)

🤖 Prompt for AI Agents
In
`@src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java`
around lines 226 - 236, The cookie set in issueToken (method issueToken, class
ConcertReservationController) lacks secure and SameSite attributes; change the
cookie handling so the token cookie is marked Secure and SameSite (preferably
Strict or Lax) and keep HttpOnly and Path. Replace the current
response.addCookie(new Cookie(...)) usage with a construction that sets secure
(cookie.setSecure(true)) and ensures SameSite is emitted (either build the
Set-Cookie header manually on the HttpServletResponse or use Spring's
ResponseCookie/Servlet API to include SameSite), and make the Secure flag
configurable via a profile/property so non-HTTPS dev remains possible; return
TokenResponse.from(token) unchanged.

@leonroars leonroars merged commit 8ebdc4e into main Feb 4, 2026
1 of 2 checks passed
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 5, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #38

coderabbitai Bot added a commit that referenced this pull request Feb 5, 2026
Docstrings generation was requested by @leonroars.

* #37 (comment)

The following files were modified:

* `src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/ConcertResponse.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/ConcertScheduleResponse.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/PointHistoryResponse.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/ReservationResponse.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/SeatResponse.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/TokenResponse.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/UserPointBalanceResponse.java`
* `src/main/java/com/slam/concertreservation/interfaces/dto/UserResponse.java`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant