Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4ee1510
web 기능 및 thymeleaf 템플릿 엔진 의존성 추가
Nov 5, 2025
84a59b5
index.html 파일명 규칙 대신 / 매핑을 통해 home.html 을 초기화면으로 반환하도록 구현
Nov 5, 2025
0599c64
예약 조회 기능 테스트를 위한 기본 Reservation 데이터 정의
Nov 5, 2025
6cc4f11
예약 페이지 응답 및 예약 목록 조회 기능 구현
Nov 5, 2025
db298e1
1,2단계 테스트 코드
Nov 5, 2025
65a2b86
README파일 추가
Nov 6, 2025
e813153
README 프로젝트 구조 추가
Nov 6, 2025
5e335e0
dependencies scope 별 그룹 정리
tae-wooo Nov 8, 2025
9f06209
test: 3, 4단계 테스트 코드 추가
tae-wooo Nov 13, 2025
f60f1ac
refactor: RoomescapeController를 controller 패키지로 이동
tae-wooo Nov 13, 2025
3f95a1c
feat: 예약 생성 및 삭제 기능 추가
tae-wooo Nov 13, 2025
bdd3ca1
refactor: Reservation 엔티티에 유효성 검증 로직 추가
tae-wooo Nov 13, 2025
f01d97b
feat: 예약 관련 예외 클래스 및 전역 예외 처리 추가
tae-wooo Nov 13, 2025
11013d9
feat: 예약 생성 요청을 위한 ReservationRequest DTO 추가
tae-wooo Nov 13, 2025
8f76975
docs: README 수정 및 프로젝트 구조 설명 보완
tae-wooo Nov 13, 2025
3444ef2
refactor: ReservationController에서 응답 DTO 적용 및 JSON 응답 구조 개선
tae-wooo Nov 16, 2025
cc0031a
refactor: RoomescapeController를 PageController로 이름 변경하고 View 전용 컨트롤러로…
tae-wooo Nov 16, 2025
1513c89
feat: DTO로 입력 형식 검증을 이동하고 도메인에 비즈니스 검증 추가
tae-wooo Nov 16, 2025
2dee21f
feat: 예외 JSON 응답을 위한 ErrorResponse 레코드 추가
tae-wooo Nov 16, 2025
4b8a3bc
feat: ReservationRequest DTO에 입력값 검증 추가
tae-wooo Nov 16, 2025
904a3ba
feat: ReservationResponse 응답 DTO 추가
tae-wooo Nov 16, 2025
e817519
feat: 예외 JSON 응답 통일 및 DTO/날짜 파싱 예외 처리 추가
tae-wooo Nov 16, 2025
f44976c
fix: 테스트 요구사항에 맞도록 예약 조회 초기 상태 및 예시 데이터 보정
tae-wooo Nov 16, 2025
8e01e11
feat: Spring Validation 의존성 추가
tae-wooo Nov 16, 2025
abc6834
Merge branch 'tae-wooo' into roomescape-taewoo
tae-wooo Nov 16, 2025
7d8306a
docs: README 업데이트 (프로젝트 구조 및 기능 정리)
tae-wooo Nov 20, 2025
84f96c4
feat: ReservationController에 Service 계층 분리하여 책임 명확화
tae-wooo Nov 20, 2025
b2b9e5c
feat: JDBC 기반 데이터 저장을 위해 ReservationDao 계층 추가
tae-wooo Nov 20, 2025
913fe55
refactor: DB 스키마에 맞춰 Reservation의 date/time 타입을 String으로 변경
tae-wooo Nov 20, 2025
624819d
refactor: ReservationResponse DTO를 Reservation의 String 타입 필드에 맞게 수정
tae-wooo Nov 20, 2025
fccc8ff
feat: DB 연동 과정에서 발생하는 기술적 예외(DataAccessException) 글로벌 처리 추가
tae-wooo Nov 20, 2025
29d8ef3
feat: ReservationService 도입으로 비즈니스 로직을 Controller에서 분리
tae-wooo Nov 20, 2025
2c3cca4
test: step 5, 6, 7 기능 검증용 테스트 코드 추가
tae-wooo Nov 20, 2025
dc60143
feat: H2 데이터베이스 적용을 위한 설정 및 의존성 추가
tae-wooo Nov 20, 2025
1bc32dc
필요없는 파일
tae-wooo Nov 20, 2025
0764299
Merge branch 'tae-wooo' into roomescape-taewoo2
tae-wooo Nov 21, 2025
ed57a7a
refactor: 예외 처리 메서드 네이밍 통일
tae-wooo Nov 23, 2025
c66dc1d
refactor: SimpleJdbcInsert 적용으로 insert 메서드 가독성과 유지보수성 개선
tae-wooo Nov 23, 2025
e718f80
refactor: 예외 처리 구조를 FailMessage 기반으로 통합
tae-wooo Nov 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'org.springframework.boot:spring-boot-starter-jdbc'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'

runtimeOnly 'com.h2database:h2'
}

test {
Expand Down
71 changes: 71 additions & 0 deletions et --hard 5e335e0b40bec777c2c887a4361dd6d0be5d1f0d
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
commit db4464b2ee2776fc1a7e2c33ac06db51dca35c96 (HEAD -> roomescape-taewoo, origin/roomescape-taewoo)
Author: 김태우 <[email protected]>
Date: Sun Nov 9 04:45:22 2025 +0900

Reservation 클래스 record 타입으로 전환

commit 5e335e0b40bec777c2c887a4361dd6d0be5d1f0d
Author: 김태우 <[email protected]>
Date: Sun Nov 9 04:29:07 2025 +0900

dependencies scope 별 그룹 정리

commit e81315342c391df2ce1b226f975f500dca48c502
Author: taewoo <[email protected]>
Date: Thu Nov 6 16:17:56 2025 +0900

README 프로젝트 구조 추가

commit 65a2b867e22ee4e1c972ae64fb560d3069e2d329
Author: taewoo <[email protected]>
Date: Thu Nov 6 15:54:16 2025 +0900

README파일 추가

commit db298e1d1f6dc6fbaa4eb3e5644e8671a1b76e1a
Author: taewoo <[email protected]>
Date: Thu Nov 6 06:35:18 2025 +0900

1,2단계 테스트 코드

commit 6cc4f11270dbd5587e645d58012541d95b7e8e3a
Author: taewoo <[email protected]>
Date: Thu Nov 6 06:34:50 2025 +0900

예약 페이지 응답 및 예약 목록 조회 기능 구현

commit 0599c6420f34983907795296d8e5bc82db75ea61
Author: taewoo <[email protected]>
Date: Thu Nov 6 06:33:49 2025 +0900

예약 조회 기능 테스트를 위한 기본 Reservation 데이터 정의

commit 84a59b597b453e7c1b9768671b8f552aa0820eb2
Author: taewoo <[email protected]>
Date: Thu Nov 6 04:11:06 2025 +0900

index.html 파일명 규칙 대신 / 매핑을 통해 home.html 을 초기화면으로 반환하도록 구현

commit 4ee151079690731ed3b64b3d94a513ed8580dc8c
Author: taewoo <[email protected]>
Date: Thu Nov 6 04:09:41 2025 +0900

web 기능 및 thymeleaf 템플릿 엔진 의존성 추가

commit 13c25ffbfd35945a01ec7efd07f0e56638ca0082 (origin/zinyan, origin/wzrabbit, origin/wfs0502, origin/wateralsie, origin/uoehisx, origin/taeyeonroyce, origin/tae-wooo, origin/stonecau, origin/sseung3424, origin/sohvun, origin/shin-mallang, origin/programming-alpaca, origin/nyeroni, origin/nova0128, origin/nonactress, origin/newvh, origin/mintcoke123, origin/marriedsenior, origin/main, origin/leeyoonjoo, origin/leegwichan, origin/kyy00n, origin/kyuwon-choi, origin/kwakminu, origin/kokodak, origin/kimsky247-coder, origin/ke-62, origin/kang1221, origin/jxxxxxn, origin/jihyeonanan, origin/jelee2555, origin/idle2534, origin/idealhyun, origin/hong-sile, origin/gy102912, origin/fanngineer, origin/corjqnrl, origin/chemistryx, origin/breadquokka, origin/boyekim, origin/bingle625, origin/bbggr1209, origin/HEAD, origin/8parks, main)
Author: boorownie <[email protected]>
Date: Tue Aug 15 01:12:26 2023 +0900

default

commit 94aebe2cc437605d73f15fc851e9504a976c3c37
Author: boorownie <[email protected]>
Date: Tue Aug 15 01:12:02 2023 +0900

init

commit ef8a7256668457b8b9be3685163a937c2b1e6070
Author: 류성현 <[email protected]>
Date: Tue Aug 15 01:16:06 2023 +0900

Initial commit
40 changes: 35 additions & 5 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
- /reservation 요청 시 예약 관리 페이지가 응답할 수 있도록 구현
- /reservations 요청 시 예약 목록을 JSON 데이터로 조회할 수 있도록 구현


## step 3 예약 추가 / 취소

- API 명세를 따라 예약 추가 API 와 삭제 API를 구현
Expand All @@ -22,21 +21,33 @@
- 예약 관련 API 호출 시 에러가 발생하는 경우 중 요청의 문제인 경우 Status Code를 400으로 응답
예를 들면 예약 추가 시 필요한 인자값이 비어있는 경우 혹은 삭제 할 예약의 식별자로 저장된 예약을 찾을 수 없는 경우가 있다

## step 5 데이터베이스 적용하기

- h2 데이터베이스를 활용하여 데이터를 저장하도록 수정

## step 6

- 예약 조회 API 처리 로직에서 저장된 예약을 조회할 때 데이터베이스를 활용

## step 7

- 예약 추가/취소 API 처리 로직에서 데이터베이스를 활용하도록 수정
- 기존에 사용하던 List 및 AtomicLong 을 제거
- 예약 관리 기능이 정상 동작하도록 기능을 완성

---

## 프로젝트 구조

### Reservation


- 예약 정보를 표현하는 도메인 객체
- 생성 시 이름/날짜/시간 검증을 수행하여 유효한 예약만 생성되도록 보장

- DB에서 조회한 예약은 newReservationFromDb 로 생성
- 신규 예약은 createReservation 로 생성하여 검증 포함

### ReservationController

- /reservation 요청 시 reservation.html 응답
- /reservations 요청 시 예약 목록 조회 API 응답(JSON)
- /reservations POST 요청에서 예약 추가
- /reservations/{id} DELETE 요청에서 예약 삭제
Expand All @@ -46,19 +57,38 @@

- Entry Point

### RoomescapeController
### PageController

- / 요청 시 home.html 응답
- /reservation 요청시 reservation.html 응답


### Reservation Service

- 도메인 객체 생성
- DAO를 통해 예약 추가·조회·삭제 처리
- 삭제 시 영향받은 row 수를 확인하여, 없으면 NotFoundReservationException 발생

### RerservationDao

- KeyHolder 를 사용해 예약 추가 시 생성된 ID 반환
- 전체 예약 조회/삭제 기능 구현

### ReservationRequest

- 예약 생성 요청을 받을 때 사용하는 DTO
- name/date/time 값에 대해 NotBlank 검증 수행

### ReservationResponse

- Reservation 도메인을 응답 형태로 변환하는 DTO


### GlobalExceptionHandler

- 예약 생성/삭제 시 발생하는 예외를 처리
- 잘못된 요청에 대해 400 응답 반환
- DB 관련 예외(DataAccessException)는 500으로 내부 서버 오류 처리

---

Expand Down
31 changes: 0 additions & 31 deletions src/main/java/roomescape/Reservation.java

This file was deleted.

32 changes: 0 additions & 32 deletions src/main/java/roomescape/ReservationController.java

This file was deleted.

12 changes: 0 additions & 12 deletions src/main/java/roomescape/RoomescapeController.java

This file was deleted.

29 changes: 14 additions & 15 deletions src/main/java/roomescape/controller/ReservationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,46 @@
import roomescape.domain.Reservation;
import roomescape.dto.ReservationRequest;
import roomescape.dto.ReservationResponse;
import roomescape.exception.NotFoundReservationException;

import roomescape.service.ReservationService;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;

@RestController
public class ReservationController {
private final List<Reservation> reservations = new ArrayList<>();
private final AtomicLong index = new AtomicLong(1);

private final ReservationService reservationService;

public ReservationController(ReservationService reservationService) {
this.reservationService = reservationService;
}


@PostMapping("/reservations")
public ResponseEntity<ReservationResponse> createReservations(@Valid @RequestBody ReservationRequest request) {
Copy link

@c0mpuTurtle c0mpuTurtle Nov 21, 2025

Choose a reason for hiding this comment

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

단일 예약을 생성하는 API지만 /reservations라는 복수형 엔드포인트를 사용하신 이유가 궁금합니다.
컬렉션을 기준으로 복수형을 사용하는 컨벤션을 적용하신 건가요?
(단순 궁금)

Copy link
Author

Choose a reason for hiding this comment

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

처음에는 단일 예약 생성 API라 /reservation이 더 적절하다고 생각했습니다.
하지만 테스트 코드가 /reservations 기준으로 작성되어 있었고,
REST API 설계를 간단히 살펴보니 컬렉션 리소스에 POST 요청을 보내
새로운 항목을 추가할 때 복수형을 사용하는 패턴도 흔히 사용되는 것을 확인했습니다.
그래서 이번 구현에서는 /reservations를 선택하게 되었습니다.

Reservation newReservation = Reservation.createReservation(index.getAndIncrement(), request.name(), request.date(), request.time());
reservations.add(newReservation);
Reservation newReservation = reservationService.registerReservation(request.name(), request.date(), request.time());

return ResponseEntity.created(URI.create("/reservations/" + newReservation.getId())).
body(ReservationResponse.from(newReservation));
}
Comment on lines 30 to 36

Choose a reason for hiding this comment

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

스크린샷 2025-11-16 오전 3 16 28 사진을 참고하여 이 부분의 Spring MVC 동작과정을 설명해볼까요?
  • 키워드 : controller로 Data 반환

Copy link
Author

Choose a reason for hiding this comment

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

요청이 들어오면 먼저 DispatcherServlet이 실행되고,
HandlerMapping을 통해 해당 요청을 처리할 Controller 메서드를 찾습니다.

그 다음 HandlerAdapter가 선택된 Handler(Controller)를 실행할 수 있도록 준비하며,
이 과정에서 ArgumentResolver가 @RequestBody, @Valid 등을 해석해
요청 본문의 JSON 데이터를 ReservationRequest DTO로 변환하고 검증합니다.

Controller가 호출되면 비즈니스 로직 처리를 위해 Service를 호출하고,
Service는 DAO를 통해 DB에 데이터를 저장한 뒤 생성된 Reservation 객체를 반환합니다.

응답 단계에서는 ResponseEntity 반환 형식에 따라 HttpMessageConverter가 동작해
도메인 객체를 JSON 형태의 HTTP Response로 변환하여 클라이언트에게 전달합니다.



@GetMapping("/reservations")
public ResponseEntity<List<ReservationResponse>> readReservations() {
List<Reservation> reservations = reservationService.getReservations();

List<ReservationResponse> responses = reservations.stream().
map(ReservationResponse::from).toList();

return ResponseEntity
.ok()
.body(responses);
}

@DeleteMapping("/reservations/{id}")
public ResponseEntity<Void> deleteReservations(@PathVariable Long id) {
Reservation reservation = reservations.stream()
Comment on lines 46 to -48

Choose a reason for hiding this comment

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

POST에서는 @RequestBody를, DELETE에서는 @PathVariable을 사용하셨는데,
각각의 전달 방식을 선택한 기준이 무엇이었는지 공유가능할까요?

Copy link
Author

@tae-wooo tae-wooo Nov 23, 2025

Choose a reason for hiding this comment

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

아래와 같이 POST 요청에서는 새로운 예약 정보를 생성해야 하기 때문에
사용자로부터 이름, 날짜, 시간 등의 데이터를 전달받아야 합니다.

POST /reservations HTTP/1.1
Content-Type: application/json

{
  "date": "2023-08-05",
  "name": "브라운",
  "time": "15:40"
}

이처럼 생성에 필요한 데이터가 요청 본문(JSON)에 담겨 전달되므로,
@RequestBody를 사용해 해당 값을 매핑하도록 구현했습니다.

반대로 DELETE 요청은 이미 존재하는 특정 예약을 제거하는 용도이기 때문에
추가 데이터가 필요하지 않고, 삭제할 리소스를 구분할 수 있는 식별자(id)만 있으면 충분합니다.

그래서 URI 템플릿(/reservations/{id})을 사용해 리소스를 표현하고,
@PathVariable로 id 값을 바인딩해 삭제하도록 구현했습니다.

.filter(it -> Objects.equals(it.getId(), id))
.findFirst()
.orElseThrow(() -> new NotFoundReservationException("예약된 기록이 없습니다."));

reservations.remove(reservation);

reservationService.delete(id);
return ResponseEntity.noContent().build();

}
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/roomescape/dao/ReservationDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.dao;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import roomescape.domain.Reservation;

import java.util.List;
import java.util.Map;

@Repository
public class ReservationDao {
Comment on lines +11 to +12

Choose a reason for hiding this comment

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

DAO 잘 만들어주셨습니다!

  • DAO 위에 @Repository 어노테이션을 붙인 이유는 무엇인가요?
  • DAO와 @Repository의 차이는 무엇일까요?

Copy link
Author

Choose a reason for hiding this comment

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

처음에는 다른 DAO 구현 예시들을 참고하면서 @Repository 어노테이션을 그대로 따라 사용했습니다.
그러나 리뷰에서 “왜 @repository를 사용했는가?”라는 질문을 받으면서,
사용 이유에 대해 명확히 알고 싶어 관련 내용을 학습해보았습니다.

DAO는 데이터베이스 접근 로직을 담당하는 계층이기 때문에,
스프링에서 해당 클래스가 DB와 상호작용하는 영속성 계층임을 명확히 표현하기 위해
@Repository 어노테이션을 사용했습니다.

물론 기능적으로 보면 @Repository는 내부적으로 @Component를 포함하고 있기 때문에,
@Component만 사용해도 스프링 빈으로 등록되어 DI는 가능합니다.

하지만 @Repository는 단순한 빈 등록 이상의 장점을 제공합니다.

첫 번째로, 해당 클래스가 "DB 접근을 담당하는 영속성 계층"이라는 의미를 코드 레벨에서 명확하게 드러내기 때문에
역할 분리가 명확해지고 유지보수 시 가독성이 향상됩니다.

두 번째로, @Repository가 붙은 클래스에서 발생하는 데이터 접근 예외들은
스프링이 제공하는 DataAccessException 계열의 unchecked 예외로 변환됩니다.
덕분에 상위 계층(Service, Controller)은 JDBC의 SQLException과 같은 구체적인 DB 기술 예외를 알 필요 없이,
일관된 방식으로 예외를 처리할 수 있습니다.

정리하자면,

  • @Component → 빈 등록 목적
  • @Repository → 빈 등록 + 영속성 계층 의미 명확화 + 예외 변환 기능 제공

따라서 DAO와 같은 영속성 계층에서는 @Repository를 사용하는 것이 더 적합하다고 이해했습니다.

Copy link
Author

Choose a reason for hiding this comment

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

DAO와 Repository는 모두 데이터 접근 계층에서 사용되는 개념이지만, 접근 관점과 역할에서 차이가 있습니다.

DAO(Data Access Object)는 데이터베이스 기술에 맞춰 CRUD 작업을 수행하는 객체로, SQL 기반 접근 방식에 초점을 둡니다.
즉, 데이터베이스 테이블을 중심으로 데이터를 저장하고 조회하는 방식에 가깝습니다.

반면 Repository는 도메인 객체를 저장하고 조회하는 '저장소'라는 개념에 더 가깝습니다.
DB 기술보다는 도메인 모델을 중심으로 설계되며, 마치 컬렉션처럼 엔티티를 다루는 데에 더 집중합니다.

정리하면,

  • DAO는 CRUD 구현과 데이터베이스 접근 기술에 가까운 패턴이고
  • Repository는 도메인 모델을 중심으로 데이터를 다루기 위한 더 높은 추상화 계층입니다.

예를 들어, 아래와 같은 Repository 인터페이스는 구현체가 아닌 도메인 관점의 행동에 집중하고 있습니다.
즉, 어떻게 저장할지(JDBC)는 감추고, 무엇을 저장하고 조회할지에 초점을 둡니다.

public interface UserRepository {
    User getByUserId(int id);
    void createByName(String name);
    void updateByUserId(int id, User user);
    void removeByUserId(int id);
}


private final JdbcTemplate jdbcTemplate;

public ReservationDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public Long insert(Reservation reservation) {
SimpleJdbcInsert insertActor = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("reservation")
.usingGeneratedKeyColumns("id");
Map<String, Object> parameters = Map.of(
"name", reservation.getName(),
"date", reservation.getDate(),
"time", reservation.getTime()
);
Number id = insertActor.executeAndReturnKey(parameters);
return id.longValue();
}

public List<Reservation> findAll() {
String sql = "select id,name,date,time from reservation";
return jdbcTemplate.query(sql,
(resultSet, rowNum) -> {
Reservation reservation = Reservation.newReservationFromDb(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("date"),
resultSet.getString("time")

);
return reservation;
});
}


public int delete(Long id) {
return jdbcTemplate.update("delete from reservation where id = ?", id);
}
}
Loading