Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spring Core] 김이화 미션 제출합니다. #309

Open
wants to merge 14 commits into
base: ihwag719
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ 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'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testImplementation 'org.assertj:assertj-core:3.20.2'
}

test {
Expand Down
75 changes: 19 additions & 56 deletions src/main/java/roomescape/controller/ReservationController.java
Original file line number Diff line number Diff line change
@@ -1,77 +1,40 @@
package roomescape.controller;

import org.springframework.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
import roomescape.exception.InvalidReservationException;
import roomescape.exception.NotFoundReservationException;
import roomescape.model.Reservation;
import roomescape.dto.ReservationRequestDto;
import roomescape.dto.ReservationResponseDto;
import roomescape.service.ReservationService;

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

@Controller
@RestController
@RequestMapping("/reservations")
public class ReservationController {

private List<Reservation> reservations = new ArrayList<>();
@Autowired
private final ReservationService reservationService;

private AtomicLong index = new AtomicLong(1);

@GetMapping("/reservation")
public String reservation() {
return "reservation";
public ReservationController(ReservationService reservationService) {
this.reservationService = reservationService;
}

@GetMapping("/reservations")
@ResponseBody
public List<Reservation> getReservations() {
return reservations;
@GetMapping
public List<ReservationResponseDto> getReservations() {
return reservationService.getAllReservations();
}

@PostMapping("/reservations")
@ResponseBody
public ResponseEntity<Reservation> createReservation(@RequestBody Reservation reservation) {
validateReservation(reservation);

Long id = index.getAndIncrement();
String name = reservation.getName();
String date = reservation.getDate();
String time = reservation.getTime();

Reservation newReservation = new Reservation(id, name, date, time);
reservations.add(newReservation);

return ResponseEntity.created(URI.create("/reservations/" + newReservation.getId())).body(newReservation);
@PostMapping
public ResponseEntity<ReservationResponseDto> createReservation(@RequestBody ReservationRequestDto requestDto) {
ReservationResponseDto responseDto = reservationService.createReservation(requestDto);
return ResponseEntity.created(URI.create("/reservations/" + responseDto.getId())).body(responseDto);
}

@DeleteMapping("/reservations/{id}")
@ResponseBody
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteReservation(@PathVariable Long id) {
boolean removed = reservations.removeIf(r -> r.getId() == id);
if (!removed) {
throw new NotFoundReservationException("예약을 찾을 수 없습니다: " + id);
}
reservationService.deleteReservation(id);
return ResponseEntity.noContent().build();
}

@ExceptionHandler({InvalidReservationException.class, NotFoundReservationException.class})
public ResponseEntity<String> handleException(RuntimeException e, WebRequest request) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}

private void validateReservation(Reservation reservation) {
if (reservation.getName() == null || reservation.getName().isEmpty()) {
throw new InvalidReservationException("이름이 필요합니다.");
}
if (reservation.getDate() == null || reservation.getDate().isEmpty()) {
throw new InvalidReservationException("날짜가 필요합니다.");
}
if (reservation.getTime() == null || reservation.getTime().isEmpty()) {
throw new InvalidReservationException("시간이 필요합니다");
}
}
}
38 changes: 38 additions & 0 deletions src/main/java/roomescape/controller/TimeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package roomescape.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import roomescape.domain.Time;
import roomescape.service.TimeService;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/times")
public class TimeController {

@Autowired
private final TimeService timeService;
public TimeController(TimeService timeService) {
this.timeService = timeService;
}

@GetMapping
public List<Time> getTimes() {
return timeService.getAllTimes();
}

@PostMapping
public ResponseEntity<Time> createTime(@RequestBody Time time) {
Time newTime = timeService.createTime(time);
return ResponseEntity.created((URI.create("/times/" + newTime.getId()))).body(newTime);
}

Choose a reason for hiding this comment

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

domain 객체(Time)를 Controller-Service-DAO 모든 계층에서 사용하고 있는 것 같아요. 만약 테이블 구조가 변경되어 domain 클래스가 변경된다면 무슨 문제가 생길 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

모든 객체에서 동일한 도메인 객체(Time)를 사용하면, 테이블 구조가 변경될 때 해당 도메인 클래스도 변경되어야 하는데 이 변경은 모든 계층에 영향을 미치게 되어 각 계층의 코드도 함께 수정해야 합니다. 이렇게 되면 코드의 유연성을 저하시키고 도메인 객체의 변경이 빈번하게 발생하면 코드 수정 및 배포 과정이 복잡해질 수도 있습니다. 또한 테스트 코드도 함께 수정하게 되면서 테스트의 복잡성을 증가시키고 테스트 유지보수 비용도 높이게 됩니다.
즉, 모든 계층이 동일한 도메인 객체에 의존하기 때문에 여러 계층에 전파되어 시스템 결합도를 높이고 코드 유지보수가 어려워집니다. 또한 각 계층은 서로 다른 책임을 가지지만 동일한 도메인 객체를 공유하면 각 계층의 책임이 명확히 분리되지 않아 응집도를 낮추고 코드의 명확성을 떨어뜨립니다. 코드 개선해서 리팩토링하겠습니다!


@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTime(@PathVariable Long id) {
timeService.deleteTime(id);
return ResponseEntity.noContent().build();
}
}
14 changes: 14 additions & 0 deletions src/main/java/roomescape/controller/viewController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package roomescape.controller;

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

@Controller
@RequestMapping("/reservations")
public class viewController {
@GetMapping("/reservationpage")
public String reservationPage() {
return "new-reservation";
}
}

Choose a reason for hiding this comment

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

클래스 이름은 대문자로 시작하는 것이 관례입니다!

Choose a reason for hiding this comment

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

추가로 해당 URI로도 실행했을 때 화면이 표시되나요?

Copy link
Author

@ihwag719 ihwag719 Jul 15, 2024

Choose a reason for hiding this comment

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

헉 대문자로 얼른 고치겠습니다 해당 URl을 실행했을 때 화면이 표시됩니다!
스크린샷 2024-07-15 오후 6 39 49

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import roomescape.exception.NotFoundReservationException;
import roomescape.domain.Reservation;
import roomescape.domain.Time;

import java.sql.PreparedStatement;
import java.util.List;

@Repository
public class ReservationDAO {

@Autowired
private JdbcTemplate jdbcTemplate;

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.

생성자, 세터, 필드 중 한가지로 의존성을 주입하면 된다고 생각하고 필드 주입을 사용했습니다. 다음부터는 생성자를 통해 의존성을 주입하도록 하겠습니다!

  1. 생성자 주입은 의존성을 명시적으로 주입하기 때문에 단위 테스트 작성이 쉬워집니다.
  2. 생성자 주입은 객체 생성 시 필요한 모든 의존성을 명확하게 전달받기 때문에 객체 생성 과정이 더 명확해지고 코드 구조가 단순해져서 유지보수성과 가독성을 향상시키는 데 도움이 됩니다.
  3. 생성자 주입을 사용하면 의존성을 생성자에서만 주입받기 때문에 주입된 필드를 final로 선언할 수 있고 객체의 불변성을 유지할 수 있습니다.
  4. 생성자 주입은 컴파일 오류를 발생시켜 순환 의존성을 방지할 수 있습니다. 순환 의존성은 두 개 이상의 빈 또는 클래스가 서로를 직접 또는 간접적으로 의존하고 있는 상황을 말하는데, 이러한 상황에서 의존성을 주입할 때 컴파일 오류를 발생시킵니다.


public ReservationDAO(JdbcTemplate jdbcTemplate, TimeDAO timeDAO) {
this.jdbcTemplate = jdbcTemplate;
}

private final RowMapper<Reservation> rowMapper = (resultSet, rowNum) -> {
Time time = new Time(
resultSet.getLong("time_id"),
resultSet.getString("time_value")
);
return new Reservation(
resultSet.getLong("reservation_id"),
resultSet.getString("name"),
resultSet.getString("date"),
time
);
};

public List<Reservation> findAllReservations() {
String sql = "SELECT r.id as reservation_id, r.name, r.date, t.id as time_id, t.time as time_value " +
"FROM reservation as r " +
"inner join time as t on r.time_id = t.id";
return jdbcTemplate.query(sql, rowMapper);
}

Choose a reason for hiding this comment

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

Suggested change
public List<Reservation> findAllReservations() {
String sql = "SELECT r.id as reservation_id, r.name, r.date, t.id as time_id, t.time as time_value " +
"FROM reservation as r " +
"inner join time as t on r.time_id = t.id";
return jdbcTemplate.query(sql, rowMapper);
}
public List<Reservation> findAllReservations() {
String sql = """
SELECT r.id as reservation_id, r.name, r.date, t.id as time_id, t.time as time_value
FROM reservation as r inner join time as t on r.time_id = t.id
""";
return jdbcTemplate.query(sql, rowMapper);
}
  • 이런식으로 텍스트 블럭을 사용하면 +연산자 없이 더 깔끔히 선언할 수 있을 것 같아요.
  • Java-Multiline-String

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 insert(Reservation reservation) {
Time time = reservation.getTime();
KeyHolder keyHolder = new GeneratedKeyHolder();

jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO time(time) VALUES (?)", new String[]{"id"});
ps.setString(1, time.getTime());
return ps;
}, keyHolder);

Long timeId = keyHolder.getKey().longValue();

jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO reservation(name, date, time_id) VALUES (?, ?, ?)", new String[]{"id"});
ps.setString(1, reservation.getName());
ps.setString(2, reservation.getDate());
ps.setLong(3, timeId);
return ps;
}, keyHolder);

Long reservationId = keyHolder.getKey().longValue();
return new Reservation(reservationId, reservation.getName(), reservation.getDate(), new Time(timeId, time.getTime()));
}

public int delete(Long id) {
String sql = "DELETE FROM reservation WHERE id = ?";
int row = jdbcTemplate.update(sql, id);
if (row == 0) {
throw new NotFoundReservationException(id);
}
return row;
}
}
65 changes: 65 additions & 0 deletions src/main/java/roomescape/dao/TimeDAO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package roomescape.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import roomescape.domain.Time;

import java.sql.PreparedStatement;
import java.util.List;

@Repository
public class TimeDAO {

@Autowired
private JdbcTemplate jdbcTemplate;

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

private final RowMapper<Time> rowMapper = (resultSet, rowNum) -> {
Time time = new Time(
resultSet.getLong("id"),
resultSet.getString("time"));
return time;
};

public List<Time> findAllTime() {
String sql = "SELECT id, time FROM time";
return jdbcTemplate.query(sql, rowMapper);
}

public Time findByTimeValue(String timeValue) {
String sql = "SELECT id, time FROM time where time = ?";
try {
return jdbcTemplate.queryForObject(sql, rowMapper, timeValue);
} catch (EmptyResultDataAccessException e) {
return null;
}
}

public Time insert(Time time) {

KeyHolder keyHolder = new GeneratedKeyHolder();

jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO time(time) VALUES (?)", new String[]{"id"});
ps.setString(1, time.getTime());
return ps;
}, keyHolder);

Long id = keyHolder.getKey().longValue();
return new Time(id, time.getTime());
}

public void delete(Long id) {
String sql = "DELETE FROM time WHERE id = ?";
jdbcTemplate.update(sql, id);
}
}
32 changes: 32 additions & 0 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package roomescape.domain;

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

public Reservation(Long id, String name, String date, Time 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 Time getTime() {
return time;
}

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

public class Time {
private Long id;
private String time;

public Time(Long id, String time) {
this.id = id;
this.time = time;
}

public Long getId() {
return id;
}

public String getTime() {
return time;
}
}
25 changes: 25 additions & 0 deletions src/main/java/roomescape/dto/ReservationRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package roomescape.dto;

public class ReservationRequestDto {
private String name;
private String date;
private String time;

public ReservationRequestDto(String name, String date, String time) {
this.name = name;
this.date = date;
this.time = time;
}

public String getName() {
return name;
}

public String getDate() {
return date;
}

public String getTime() {
return time;
}
}
Loading