Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

}

test {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/roomescape/RoomescapeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package roomescape;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class RoomescapeController {
@GetMapping("/")
public String home() {
return "home";
}
}
94 changes: 94 additions & 0 deletions src/main/java/roomescape/controller/ReservationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// ReservationController.java
package roomescape.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.jdbc.core.JdbcTemplate;

import roomescape.model.Reservation;
import roomescape.exception.BadRequestReservationException;

import javax.sql.DataSource;
import java.util.*;

@RestController // RestController로 변경
@RequestMapping("/reservations")
public class ReservationController {
private JdbcTemplate jdbcTemplate;

public ReservationController(DataSource dataSource, JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

@GetMapping
public List<Reservation> getReservation() {
String sql = "select id, name, date, time from reservation";
List<Reservation> reservations = jdbcTemplate.query(
sql, (resultSet, rowNum) -> {
Reservation reservation = new Reservation(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("date"),
resultSet.getString("time")
);
return reservation;
}
);
return reservations;
}

@PostMapping
public ResponseEntity<Reservation> addReservation(@RequestBody Reservation newReservation) {
// 예외 조건 추가
if (newReservation.getName() == null || newReservation.getName().isBlank() ||
newReservation.getDate() == null || newReservation.getDate().isBlank() ||
newReservation.getTime() == null || newReservation.getTime().isBlank()) {
throw new BadRequestReservationException("Required fields are missing.");
}

String sql = "INSERT INTO reservation(name, date, time) VALUES (?, ?, ?)";

int insertCount = jdbcTemplate.update(
sql,
newReservation.getName(),
newReservation.getDate(),
newReservation.getTime()
);

if (insertCount <= 0) {
throw new RuntimeException("Failed to insert reservation");
}

Long generatedId = jdbcTemplate.queryForObject("SELECT MAX(id) FROM reservation", Long.class);
Copy link

@juanxiu juanxiu Nov 23, 2025

Choose a reason for hiding this comment

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

현재 SELECT 구문이 원자성을 보장하는지 고민해봐야할 것 같습니다. 만약 두 개의 INSERT 문이 연속적으로 실행되고, 그 다음에 SELECT 로 조회한다면 먼저 들어간 레코드의 id가 하나 증가한 채로 조회될 것 같아요. 또는 삭제된 레코드 때문에 MAX가 실제 생성된 ID와 다를 수 있겠네요. 트랜잭션의 특징을 더 알고 싶다면 이 글을 보는 것도 좋을 것 같아요!

그리고 JDBC 의 keyholder 를 사용해서 리팩터링하는 게 좋을 것 같습니다. 이 글을 참고해보실래요?


Reservation reservation = new Reservation(
generatedId,
newReservation.getName(),
newReservation.getDate(),
newReservation.getTime()
);

return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/reservations/" + reservation.getId())
.body(reservation);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteReservation(@PathVariable Long id) {
int deletedCount = jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); if (deletedCount > 0) {
return ResponseEntity.noContent().build();
} else {
throw new BadRequestReservationException("Reservation not found.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class BadRequestReservationException extends RuntimeException {
public BadRequestReservationException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package roomescape.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ReservationExceptionHandler {
@ExceptionHandler(BadRequestReservationException.class)
public ResponseEntity<Void> handleBadRequest(BadRequestReservationException e) {
return ResponseEntity.badRequest().build();
}
}
31 changes: 31 additions & 0 deletions src/main/java/roomescape/model/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.model;

public class Reservation {
private Long id;
private String name;
private String date;
private 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() {
return id;
}

public String getName() {
return name;
}

public String getDate() {
return date;
}

public String getTime() {
return time;
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# h2-console 활성화 여부
spring.h2.console.enabled=true
#db url
spring.datasource.url=jdbc:h2:mem:database
8 changes: 8 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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)
);
144 changes: 143 additions & 1 deletion src/test/java/roomescape/MissionStepTest.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
package roomescape;

import io.restassured.RestAssured;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import roomescape.model.Reservation;

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

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
Expand All @@ -16,4 +30,132 @@ public class MissionStepTest {
.then().log().all()
.statusCode(200);
}

@Test
void 이단계() {
RestAssured.given().log().all()
.when().get("/reservation")
.then().log().all()
.statusCode(200);

RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200)
.body("size()", is(2)); // 아직 생성 요청이 없으니 Controller에서 임의로 넣어준 Reservation 갯수 만큼 검증하거나 0개임을 확인하세요.
}

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

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

RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200)
.body("size()", is(1));

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

RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200)
.body("size()", is(0));
}

@Test
void 사단계() {
Map<String, String> params = new HashMap<>();
params.put("name", "브라운");
params.put("date", "");
params.put("time", "");

// 필요한 인자가 없는 경우
RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(params)
.when().post("/reservations")
.then().log().all()
.statusCode(400);

// 삭제할 예약이 없는 경우
RestAssured.given().log().all()
.when().delete("/reservations/1")
.then().log().all()
.statusCode(400);
}

@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);
}


}