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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 방탈출 예약 시스템

## 주요 기능
### 1단계: 홈화면
- localhost:8080 요청 시 아래 화면과 같이 어드민 메인 페이지가 응답할 수 있도록 구현한다.

### 2단계: 예약 조회
- /reservation 요청 시 예약 관리 페이지가 응답할 수 있도록 구현한다.
- 예약 관리 페이지 로드 시 호출되는 예약 목록 조회 API를 구현한다.
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
Copy link

Choose a reason for hiding this comment

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

spring-boot-starterspring-boot-starter-web의 차이에대해 설명해주세요!
추가로 implementationtestImplementation에 대해서도 설명 부탁드립니다~

Copy link
Author

Choose a reason for hiding this comment

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

  1. spring-boot-starter vs spring-boot-starter-web

Spring Boot에서는 spring-boot-starter라는 편리한 의존성 조합을 제공합니다.
프로젝트에 설정해야하는 다수의 의존성들을 starter가 이미 포함하고 있기 때문에 starter에 대한 의존성 추가만으로도 프로젝트를 시작하거나 새로운 기능을 추가할 수 있습니다.
하지만 웹 서버가 포함되어 있지 않아 주로 웹 기능이 없는 단순한 콘솔 애플리케이션이나 백그라운드 작업을 만들 때 사용합니다.

spring-boot-starter-webspring-boot-starter의 모든 기능을 포함하고, 추가로 웹 애플리케이션 개발에 필요한 기능을 포함합니다. 내장 Tomcat 웹 서버, Spring MVC 프레임워크, JSON 변환기가 포함되어있어 @Controller를 사용하고 웹 서버를 띄우려면 spring-boot-starter-web이 반드시 필요합니다.

  1. implementation vs testImplementation

implementationtestImplementation은 Gradle이 의존성을 관리하는 범위를 지정합니다
implementation는 src/main/java에 있는 메인 애플리케이션 코드를 컴파일하고 실행할 때 필요한 라이브러리입니다. 이 의존성은 최종적으로 빌드되는 실행 파일에 포함됩니다.

testImplementation는 src/test/java에 있는 테스트 코드를 컴파일하고 실행할 때만 필요한 라이브러리입니다. 테스트를 실행할 때는 사용되지만, 최종 실행 파일에는 포함되지 않습니다.

implementation "joda-time:joda-time:2.2"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

test {
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
4 changes: 3 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
30 changes: 18 additions & 12 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
Expand Down Expand Up @@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
Expand Down Expand Up @@ -83,7 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
Expand Down Expand Up @@ -111,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""


# Determine the Java command to use to start the JVM.
Expand All @@ -130,26 +133,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
Expand Down Expand Up @@ -198,16 +204,16 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.

set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"

# Stop when "xargs" is not available.
Expand Down
26 changes: 14 additions & 12 deletions gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
Expand Down Expand Up @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

Expand All @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*

:end
@rem End local scope for the variables with windows NT shell
Expand Down
1 change: 0 additions & 1 deletion src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ public class RoomescapeApplication {
public static void main(String[] args) {
SpringApplication.run(RoomescapeApplication.class, args);
}

}
12 changes: 12 additions & 0 deletions src/main/java/roomescape/controller/HomeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package roomescape.controller;

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

@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
Comment on lines +8 to +11
Copy link

Choose a reason for hiding this comment

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

요 메서드의 작동 원리도 한번 설명해주세요!

Copy link
Author

Choose a reason for hiding this comment

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

reservation()메서드와 같은 방식으로 작동합니다

  1. 클라이언트가 "/" URL로 GET 요청을 보냅니다.
  2. Spring은 @GetMapping("/") 어노테이션을 보고, 이 요청을 home() 메서드와 연결시킵니다.
  3. home() 메서드가 실행되고, 문자열 "home"을 반환합니다.
  4. Spring은 이 반환된 문자열 "home"을 View의 이름으로 해석합니다.
  5. Spring은 Thymeleaf 템플릿 엔진을 사용해 templates/home.html 파일을 찾아 렌더링하고, 완성된 HTML 페이지를 클라이언트에게 응답합니다.

}
34 changes: 34 additions & 0 deletions src/main/java/roomescape/controller/ReservationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package roomescape.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import roomescape.domain.Reservation;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

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

Choose a reason for hiding this comment

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

직접 id값을 하드코딩 해 넣지 않고, 자동으로 증가시켜주는 것 이 정말 좋네요!

Copy link
Author

Choose a reason for hiding this comment

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

만약 long을 쓰면, 여러 사용자가 동시에 예약 요청을 보낼 때 ID 번호가 겹치거나 누락되는 오류가 생길 수도 있을 것 같습니다. 감사합니다!


public ReservationController() {
reservations.add(new Reservation(index.getAndIncrement(), "브라운", LocalDate.parse("2023-01-01"), LocalTime.parse("10:00")));
reservations.add(new Reservation(index.getAndIncrement(), "브라운", LocalDate.parse("2023-01-02"), LocalTime.parse("11:00")));
reservations.add(new Reservation(index.getAndIncrement(), "브라운", LocalDate.parse("2023-01-03"), LocalTime.parse("12:00")));
}

@GetMapping("/reservation")
public String reservation() {
return "reservation";
}
Comment on lines +25 to +28
Copy link

Choose a reason for hiding this comment

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

해당 메서드의 작동 원리를 한번 설명해주시겠어요?!

Copy link
Author

Choose a reason for hiding this comment

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

@GetMapping은 HTTP GET 요청을 특정 자바 메서드와 연결시키는 어노테이션입니다.

  1. 클라이언트가 /reservation URL로 GET요청을 보냅니다.
  2. Spring은@GetMapping("/reservation")어노테이션을 보고, 이 요청을 reservation() 메서드와 연결시킵니다.
  3. reservation() 메서드가 실행되고, 문자열 "reservation"을 반환합니다.
  4. 반환된 문자열 "reservation"을 View의 이름으로 해석합니다.
  5. Spring은 Thymeleaf 템플릿 엔진을 사용해 templates/reservation.html 파일을 찾아 렌더링하고, 완성된 HTML 페이지를 클라이언트에게 응답합니다.


@GetMapping("/reservations")
public ResponseEntity<List<Reservation>> getReservations() {
return ResponseEntity.ok().body(reservations);
}
Comment on lines +30 to +33
Copy link

@haeyoon1 haeyoon1 Nov 8, 2025

Choose a reason for hiding this comment

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

  1. @Controller의 주 역할이 뭘까요?
  2. /reservations@GetMapping("/reservation") 와 다르게 JSON만 반환하고있어요! 작성하신 코드에서 응답은 어떤 방식으로 처리될까요?

Copy link
Author

Choose a reason for hiding this comment

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

@Controller의 주 역할

@Controller 어노테이션은 해당 클래스가 웹 애플리케이션의 컨트롤러임을 나타냅니다.
Spring은 클래스 내부를 스캔하여 @GetMapping, @PostMapping 같은 매핑 정보를 찾아내고, 실제 웹 요청이 들어왔을 때 해당 메서드와 연결시켜 줍니다.

getReservations()의 응답 처리 방식

이 메서드는 ResponseEntity<List<Reservation>>를 반환합니다.
ResponseEntity객체를 반환하면 Spring은 뷰 리졸버 대신 메시지 컨버터를 사용합니다.
이 컨버터가 List<Reservation> 자바 객체를 JSON 문자열로 변환합니다.
응답의 Content-Type을 application/json으로 설정하고, 변환된 JSON 데이터를 응답 본문에 담아 전송합니다.

Copy link

@haeyoon1 haeyoon1 Nov 9, 2025

Choose a reason for hiding this comment

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

위에 각 메서드의 작동 흐름을 설명해주셨을 때 작성해주셨듯이, @Controller의 기본적인 역할은 웹 요청을 처리하는 것이에요!
이번 미션에서는 return "reservation"; 를 했을 시 reservation.html파일을 랜더링 해주는 등, view를 랜더링 하는 역할을 주로 수행한 것 같아요.

하지만 해당 메서드의(getReservations) 경우에는 view 템플릿을 반환하는 것 이 아닌 json을 직접 반환하는 메서드예요. 이때 사용할 수 있는 어노테이션이 @RequestBody 입니다!

  1. @ResponseBody 어노테이션에 대해 학습하고, 그 내용을 공유해주세요! (언제 사용되는지, 어떤 역할을 사용하는지 등)
  2. 말씀해주신대로 현재 코드는 ResponseEntity<List<Reservation>>를 반환하여 JSON 형태의 응답을 내려주고 있습니다.
    이때 ResponseEntity는 어떤 역할을 하는지, 그리고 단순히 @ResponseBody로 반환하는 경우와 어떤 차이가 있는지 학습해보세요!
  3. @ResponseBody를 학습하셨다면 @Controller + @ResponseBody를 대체할 수 있는 어노테이션이 있습니다! 어떤 어노테이션인지, 내부적으로 어떻게 동작하는지도 함께 정리해보면 좋을 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

  1. @ResponseBody 어노테이션

@ResponseBody@controller 클래스 내의 메서드가 반환하는 값을 HTTP 응답 본문에 직접 담도록 지시하는 어노테이션입니다. Spring 컨트롤러에서 HTML 페이지가 아닌, JSON 같은 순수 데이터를 반환하고 싶을 때 사용됩니다.
@ResponseBody 어노테이션이 있으면 Spring은 ViewResolver를 작동시키지 않고, HttpMessageConverter를 작동시켜 메서드가 반환하는 객체를 JSON 형식의 문자열로 변환하여 HTTP 응답 본문에 써넣어 클라이언트에게 전송합니다.

  1. ResponseEntity의 역할과 @ResponseBody와 차이
  1. ResponseEntity의 역할
    REsponseEntity는 HTTP 응답을 세밀하게 제어하기 위한 클래스입니다.
    상태 코드, 헤더, 본문을 모두 하나의 객체로 캡슐화하여 반환할 수 있습니다.

  2. ResponseEntity vs @ResponseBody

  • ResponseEntity
    객체 자체에 상태 코드, 헤더 정보, 본문을 모두 담아서 반환하여 상태 코드를 명시적으로 설정하거나, 헤더를 추가하는 것이 매우 유연합니다.
    하지만, @ResponseBody 방식보다 작성할 코드가 조금 더 많습니다.

  • @ResponseBody
    어노테이션 하나만 추가하면 되므로 간단합니다.
    HTTP 헤더를 설정하기가 어렵습니다. HTTP 상태 코드를 설정하려면 별도의 어노테이션을 추가해야 합니다.

  1. @Controller + @ResponseBody

두 어노테이션을 합친 것이 @RestController 입니다. 데이터만 반환하는 컨트롤러를 만들 때 사용됩니다.
클래스에 @RestController를 붙이면, Spring은 @Controller가 붙은 것으로 인지하여 빈으로 등록합니다. 동시에 @ResponseBody가 붙은 것으로 인지하여, 해당 클래스의 모든 메서드에 @ResponseBody의 기능을 일괄 적용합니다. 따라서 메서드마다 @ResponseBody를 붙이지 않아도 자동으로 JSON을 반환하게 됩니다.

Choose a reason for hiding this comment

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

👍 잘 정리해주셨어요!
그렇다면, 해당 메서드에서는 @Controller이 아니라 @RestController가 더 적합해보이는데 어떻게 생각하시나요?

}
38 changes: 38 additions & 0 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package roomescape.domain;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDate;
import java.time.LocalTime;

public class Reservation {
private Long id;
private String name;
private LocalDate date;

@JsonFormat(pattern = "HH:mm")
private LocalTime time;
Comment on lines +13 to +14
Copy link

Choose a reason for hiding this comment

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

응답 JSON의 시간 포맷을 지정해주셨네요!👍

Copy link
Author

Choose a reason for hiding this comment

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

LocalTime을 그대로 반환하면 11:00:00처럼 초 단위까지 나와서, HH:mm 형식으로 포맷팅하기 위해 @JsonFormat을 추가했습니다! 감사합니다


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

public Long getId() {
return id;
}

public String getName() {
return name;
}

public LocalDate getDate() {
return date;
}

public LocalTime getTime() {
return time;
}
}
19 changes: 19 additions & 0 deletions src/test/java/roomescape/MissionStepTest.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
package roomescape;

import io.restassured.RestAssured;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import static org.hamcrest.core.Is.is;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class MissionStepTest {

@Test
@DisplayName("메인 페이지가 정상적으로 응답한다.")
void 일단계() {
RestAssured.given().log().all()
.when().get("/")
.then().log().all()
.statusCode(200);
}

@Test
@DisplayName("예약 페이지 및 목록 조회 API가 정상적으로 응답한다.")
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(3)); // 아직 생성 요청이 없으니 Controller에서 임의로 넣어준 Reservation 갯수 만큼 검증하거나 0개임을 확인하세요.
}
}