Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a26d3bd
gradle: gradle 의존성 추가
mintcoke123 Nov 6, 2025
33475a6
feat: 새로운 컨트롤러 추가
mintcoke123 Nov 6, 2025
2759859
feat: 예약 목록 조회 api 생성
mintcoke123 Nov 6, 2025
c32009e
refactor: 사용하지 않는 메서드 제거
mintcoke123 Nov 6, 2025
7489cf5
refactor: Reservation dto를 record로 관리
mintcoke123 Nov 7, 2025
7543356
refactor: Reservation dto를 record로 관리
mintcoke123 Nov 7, 2025
565d8f2
refactor: eol 준수
mintcoke123 Nov 9, 2025
be87cab
refactor: eol 준수
mintcoke123 Nov 12, 2025
e1fcf2b
chore: 깃 초기화
mintcoke123 Nov 13, 2025
b1b4fb9
chore: editorconfig 추가
mintcoke123 Nov 13, 2025
f8655df
refactor: 폴더구조 변경
mintcoke123 Nov 13, 2025
d75c0dd
refactor: ReservationList 도메인객체 생성
mintcoke123 Nov 13, 2025
2426350
refactor: ResponseEntity를 도입하여 요구사항에 맞게 controller 변경
mintcoke123 Nov 13, 2025
59d2619
feat: 내부 dto 작성
mintcoke123 Nov 13, 2025
3470311
feat: 페이지 반환 컨트롤러 작성
mintcoke123 Nov 13, 2025
f385424
feat: 예외처리코드 작성, ReservationList에서 exception을 throw 하도록 변경
mintcoke123 Nov 13, 2025
0343da9
chore: requestDto의 역할에 맞게 파일명 변경
mintcoke123 Nov 13, 2025
afb174a
test: 4단계 테스트 작성
mintcoke123 Nov 13, 2025
b6d22f2
chore: 변경한 dto명 적용
mintcoke123 Nov 13, 2025
7931473
chore: remove editorconfig for clean diff
mintcoke123 Nov 13, 2025
46a253d
feat: ReservationList 도메인 리스트를 서비스와 도메인으로 분리
mintcoke123 Nov 15, 2025
b92cdfb
feat: 예외 핸들러 추가
mintcoke123 Nov 15, 2025
70a9b67
refactor: 404 에러 분리
mintcoke123 Nov 15, 2025
102f026
refactor: AtomicLong 적용용
mintcoke123 Nov 15, 2025
2d7ef51
refactor: IllegalArgumentException적용
mintcoke123 Nov 15, 2025
4e37450
refactor: 에러 수정
mintcoke123 Nov 15, 2025
9268065
Merge branch 'mintcoke123' into step-4
mintcoke123 Nov 16, 2025
31af366
refactor: long 자료형으로 변경
mintcoke123 Nov 16, 2025
1982c76
refactor: logger 적용
mintcoke123 Nov 16, 2025
f59f320
Merge branch 'step-4' of https://github.com/mintcoke123/spring-roomes…
mintcoke123 Nov 20, 2025
9743e4a
gradle: 의존성 추가
mintcoke123 Nov 20, 2025
10a094a
chore: 잘못 옮긴 파일 삭제
mintcoke123 Nov 20, 2025
5c852cc
feat: 데이터베이스 스키마 설정
mintcoke123 Nov 20, 2025
9aa4ca9
test: 테스트코드 변경
mintcoke123 Nov 20, 2025
75bd872
test: 테스트코드 추가
mintcoke123 Nov 20, 2025
63c9eff
test: 칠단계 테스트코드 추가
mintcoke123 Nov 20, 2025
10c8b9f
refactor: dao와의 연결
mintcoke123 Nov 20, 2025
0b545da
refactor: Simplejdbcinsert 사용
mintcoke123 Nov 25, 2025
9759c2d
refactor: 어노테이션명 변경
mintcoke123 Nov 25, 2025
9a5996a
refactor: logger 적용, dto 리팩토링
mintcoke123 Nov 25, 2025
45fcdef
Merge branch 'mintcoke123' into step-5to7
mintcoke123 Nov 25, 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 @@ -16,11 +16,14 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

developmentOnly 'org.springframework.boot:spring-boot-devtools'

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

runtimeOnly 'com.h2database:h2'
}

test {
Expand Down
12 changes: 0 additions & 12 deletions src/main/java/roomescape/HomeController.java

This file was deleted.

4 changes: 0 additions & 4 deletions src/main/java/roomescape/Reservation.java

This file was deleted.

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

This file was deleted.

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

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

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

@Repository
public class ReservationDao {

Choose a reason for hiding this comment

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

동현님이 생각하시는 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는 service레이어 이전에 데이터베이스와 직접 상호작용하는 계층으로, 순수한 데이터 접근 로직을 담당합니다.
Repository는 도메인 컬렉션처럼 동작하는 추상화라고 하는군요! 처음 알았습니다.
dao의 어노테이션을 Repository라고 지은 이유는, dao 계층을 표현할 때에는 @Repository 어노테이션을 사용한다는 레퍼런스를 참고했기 때문입니다.

https://whitekeyboard.tistory.com/178

찾아보니 @Repository 어노테이션은 JDBC 에서 발생하는 SQLException 같은 걸 스프링이 DataAccessException 계열 예외로 변환해 주는 역할을 수행한다고 하네요


private final JdbcTemplate jdbcTemplate;
private final SimpleJdbcInsert simpleInsert;

public ReservationDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.simpleInsert = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("reservation")
.usingGeneratedKeyColumns("id");
}

private final RowMapper<Reservation> rowMapper = (resultSet, rowNum) ->

Choose a reason for hiding this comment

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

👍

new Reservation(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("date"),
resultSet.getString("time")
);

public List<Reservation> findAll() {
return jdbcTemplate.query(
"SELECT id, name, date, time FROM reservation",
rowMapper
);
}

public long insert(String name, String date, String time) {
Map<String, Object> params = new HashMap<>();
params.put("name", name);
params.put("date", date);
params.put("time", time);

Number key = simpleInsert.executeAndReturnKey(params);
return key.longValue();
}

Choose a reason for hiding this comment

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

현재 insert 메서드에서 KeyHolder와 JDBC 템플릿을 사용하여 데이터 삽입 후 자동 생성된 id를 가져오는 구현 방식을 잘 구현해주신 것 같아요 👍

Simplejdbcinsert라는 객체를 들어보셨을까요? 참고 사항으로 말씀드리자면, 해당 객체를 사용한다면 코드가 좀 더 간결해진다는 장점이 있습니다.

Copy link
Author

Choose a reason for hiding this comment

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

저번 스터디에서 진행했습니다! 코드 길이도 짧아지고 가시성도 좋아지네요. 적용 완료했습니다!


public Reservation findById(long id) {
return jdbcTemplate.queryForObject(
"SELECT id, name, date, time FROM reservation WHERE id = ?",
rowMapper,
id
);
}

public int deleteById(long id) {
return jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id);
}
}
6 changes: 3 additions & 3 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package roomescape.domain;

public class Reservation {
private final long id;
private final Long id;
private final String name;
private final String date;
private final String time;

public Reservation(long id, String name, String date, String time) {
public Reservation(Long id, String name, String date, String time) {
this.id = id;
this.name = name;
this.date = date;
this.time = time;
}

public long getId() {
public Long getId() {
return id;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

@RestControllerAdvice
public class ReservationExceptionHandler {

private static final Logger logger = Logger.getLogger("테스트용");

@ExceptionHandler({
InvalidReservationRequestException.class,
IllegalArgumentException.class,
Expand All @@ -23,6 +30,10 @@ public ResponseEntity<Void> handleBadRequest(RuntimeException e) {

@ExceptionHandler(NotFoundReservationException.class)
public ResponseEntity<Void> handleNotFound(NotFoundReservationException e) {
String stackTrace = Arrays.stream(e.getStackTrace())
.map(StackTraceElement::toString)
.collect(Collectors.joining("\n"));
logger.log(Level.INFO, stackTrace);
return ResponseEntity.status(404).build();
}

Expand Down
53 changes: 27 additions & 26 deletions src/main/java/roomescape/service/ReservationList.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package roomescape.service;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import roomescape.dao.ReservationDao;
import roomescape.dto.ReservationResponse;
import roomescape.dto.ReservationRequest;
import roomescape.exception.InvalidReservationRequestException;
Expand All @@ -9,54 +10,54 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;

@Component
@Service
public class ReservationList {

private final Map<Long, Reservation> idToReservation = new HashMap<>();
private AtomicLong index;
public ReservationList() {
this.index = new AtomicLong(0);
private final ReservationDao reservationDao;
private static final Logger logger = Logger.getLogger(ReservationList.class.getName());

public ReservationList(ReservationDao reservationDao) {
this.reservationDao = reservationDao;
}

public List<ReservationResponse> findAll() {
List<Reservation> results = reservationDao.findAll();
List<ReservationResponse> responses = new ArrayList<>();
for (Reservation reservation : idToReservation.values()) {
for (Reservation reservation : results) {
responses.add(toResponse(reservation));
}
return responses;
}

public ReservationResponse create(ReservationRequest request) {
if (request.name() == null || request.name().isBlank()
|| request.date() == null || request.date().isBlank()
|| request.time() == null || request.time().isBlank()) {
throw new InvalidReservationRequestException(
"잘못된 예약 요청입니다. " +
"name=" + request.name() +
", date=" + request.date() +
", time=" + request.time()
);
|| request.date() == null || request.date().isBlank()
|| request.time() == null || request.time().isBlank()) {
logger.warning("Invalid request: " + request);
throw new InvalidReservationRequestException("잘못된 예약 요청입니다.");
}

long newId = index.incrementAndGet();
Reservation reservation = new Reservation(newId, request.name(), request.date(), request.time());
idToReservation.put(newId, reservation);
return toResponse(reservation);
long id = reservationDao.insert(request.name(), request.date(), request.time());
Reservation saved = reservationDao.findById(id);
return toResponse(saved);
}
Copy link

@SANGHEEJEONG SANGHEEJEONG Nov 22, 2025

Choose a reason for hiding this comment

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

1️⃣ 동현님이 응답 DTO를 사용하시는 이유가 무엇인지 궁금합니다!

2️⃣ 이런 식으로 조립하여 사용하고 계신 것 같은데요,

a. findAll 에서는 Entity -> Response DTO
b. create 에서는 (id를 제외한 나머지 필드) Request DTO -> Response DTO

b번과 같은 경우 요청 DTO에서 응답 DTO를 바로 조립하고 있습니다.
만약 DB에 기본값을 설정하는 경우가 생길 때, 이 방식에는 어떤 문제가 발생할 수 있을까요?

아래는 위의 문제를 해결하기 위한 방법의 힌트입니다.
Image

Copy link
Author

@mintcoke123 mintcoke123 Nov 25, 2025

Choose a reason for hiding this comment

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

  1. 응답 dto를 사용한 이유는, dto로 도메인 모델과 API 응답 모델을 분리해 변경에 대한 영향도를 줄이는 것이 이후 리팩토링에 더욱 효과적이라고 생각했기 때문입니다.
  2. 지금 코드에서는 말씀주신대로 dto 생성 시 서비스에서 request 값으로 바로 Response를 만들고 있었기 때문에 db의 기본값이 달라지면(ex: string 트리밍 등) 실제 저장된 값과 응답이 달라질 수 있다는 것을 알게 되었습니다!

저장 직후 DB에서 방금 저장한 레코드를 다시 조회해 그 값으로 응답을 생성하도록 로직을 변경하였습니다!
수정했습니다!


public void delete(long id) {
if (!idToReservation.containsKey(id)) {
throw new NotFoundReservationException("해당 분실물을 찾을 수 없습니다: " + id);
int updated = reservationDao.deleteById(id);
if (updated == 0) {
throw new NotFoundReservationException("해당 예약을 찾을 수 없습니다: " + id);
}
idToReservation.remove(id);
}

private ReservationResponse toResponse(Reservation reservation) {
return new ReservationResponse(reservation.getId(), reservation.getName(), reservation.getDate(), reservation.getTime());
return new ReservationResponse(
reservation.getId(),
reservation.getName(),
reservation.getDate(),
reservation.getTime()
);
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

spring.datasource.url=jdbc:h2:mem:database
spring.datasource.driver-class-name=org.h2.Driver
9 changes: 9 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE reservation
(
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
date VARCHAR(255) NOT NULL,
time VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);

80 changes: 79 additions & 1 deletion src/test/java/roomescape/MissionStepTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import static org.hamcrest.Matchers.is;
import io.restassured.http.ContentType;
import java.util.Map;
import java.util.HashMap;


import io.restassured.http.ContentType;

import java.util.Map;
import java.util.HashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;
import java.sql.SQLException;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import roomescape.domain.Reservation;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class MissionStepTest {
Expand All @@ -36,7 +54,6 @@ public class MissionStepTest {
.body("size()", is(0));
}


@Test
void 삼단계() {
Map<String, String> params = new HashMap<>();
Expand Down Expand Up @@ -92,4 +109,65 @@ public class MissionStepTest {
.then().log().all()
.statusCode(404);
}

@Autowired
private JdbcTemplate jdbcTemplate;

@Test
void 오단계() {
try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
assertThat(connection).isNotNull();
assertThat(connection.getCatalog()).isEqualTo("DATABASE");
assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

@Test
void 육단계() {
jdbcTemplate.update(
"INSERT INTO reservation (name, date, time) VALUES (?, ?, ?)",
"브라운",
"2023-08-05",
"15:40"
);

List<Reservation> reservations = RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200).extract()
.jsonPath().getList(".", Reservation.class);

Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);

assertThat(reservations.size()).isEqualTo(count);
}

@Test
void 칠단계() {
Map<String, String> params = new HashMap<>();
params.put("name", "브라운");
params.put("date", "2023-08-05");
params.put("time", "10:00");

RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(params)
.when().post("/reservations")
.then().log().all()
.statusCode(201)
.header("Location", "/reservations/1");

Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
assertThat(count).isEqualTo(1);

RestAssured.given().log().all()
.when().delete("/reservations/1")
.then().log().all()
.statusCode(204);

Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
assertThat(countAfterDelete).isEqualTo(0);
}
}