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 JDBC] 남해윤 미션 제출합니다. #379

Open
wants to merge 7 commits into
base: haeyoon1
Choose a base branch
from
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
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'

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'
}

test {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/roomescape/controller/HomeController.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.RequestMapping;

@Controller
public class HomeController {

@RequestMapping("/")
public String home() {
return "home";
}

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

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
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.ResponseBody;
import roomescape.dto.ReservationRequestDto;
import roomescape.dto.ReservationResponseDto;
import roomescape.service.ReservationService;

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

@Controller
public class ReservationController {

private final ReservationService reservationService;

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

// 홈화면
@GetMapping("/reservation")
public String reservationPage() {
return "reservation";
}

//예약 조회
@ResponseBody
@GetMapping("/reservations")
public ResponseEntity<List<ReservationResponseDto>> list() {
List<ReservationResponseDto> reservations = reservationService.getAllReservations();
return ResponseEntity.ok(reservations);
}

//예약 추가

Choose a reason for hiding this comment

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

저는 개인적으로 이렇게 메소드 주석보다는 클래스 레벨에 어떤 책임을 가지고 있는지 적는 것을 선호합니다
코드 레벨에서 당연히 바로 알 수 있는 내용일 경우에 전체 주석의 신뢰도를 떨어뜨려서 오히려 보기 힘들 수도 있는 악영향을 끼칠 수도 있거든요

@ResponseBody
@PostMapping("/reservations")
public ResponseEntity<ReservationResponseDto> create(@RequestBody ReservationRequestDto newReservationDto) {

ReservationResponseDto reservation = reservationService.createReservation(newReservationDto);

return ResponseEntity.created(URI.create("/reservations/" + reservation.getId()))
.body(reservation);
}

//예약 삭제
@DeleteMapping("/reservations/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
reservationService.deleteReservation(id);
return ResponseEntity.noContent().build();
}
}
45 changes: 45 additions & 0 deletions src/main/java/roomescape/dao/ReservationDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package roomescape.dao;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import roomescape.entity.Reservation;

import java.util.List;

@Repository
public class ReservationDao {

private final JdbcTemplate jdbcTemplate;

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

public List<Reservation> findAll(){
String sql = "SELECT id, name, date, time FROM reservation";

return jdbcTemplate.query(
sql, (resultSet, rowNum) -> new Reservation(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("date"),
resultSet.getTimestamp("time").toLocalDateTime()
));
}

public Reservation insert(Reservation reservation) {
String sql = "INSERT INTO reservation(name, date, time) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, reservation.getName(), reservation.getDate(), reservation.getTime());

String query = "SELECT id FROM reservation ORDER BY id DESC LIMIT 1";
Long id = jdbcTemplate.queryForObject(query, Long.class);

return new Reservation(id, reservation.getName(), reservation.getDate(), reservation.getTime());
}

public void delete(Long id) {
String sql = "DELETE FROM reservation WHERE id = ?";
jdbcTemplate.update(sql, id);
}
Comment on lines +40 to +43

Choose a reason for hiding this comment

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

보통 delete 를 해야 하는 경우에 실제 delete 를 하는 경우는 거의 없는데요
특히 예약이나 돈 거래와 같은 금전이 엮여있는 경우는 거의 절대로 하지 않는다고 보셔도 됩니다
보통 deleted 나 removed, status, enable 과 같은 뭔가 상태를 표현할 수 있는 column 을 두고, 그 column 에 값을 업데이트 하는 방법으로 많이 하게 되는데요

조회를 할 때는 진짜 전체를 조회하거나, 현재 활성화 되어있는 전체를 조회하는 방향으로 진행하게 됩니다
이를 soft delete 이라고 하는데요
한번 이렇게 바꿔보시는 것도 좋을 것 같아요!

soft delete 를 왜 해야 하는지에 대해서도 정리해서 댓글에 달아주시면 공부하는 과정에 많은 도움이 될 것 같아요


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

import java.time.LocalDateTime;

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

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

public String getName() {
return name;
}

public String getDate() {
return date;
}

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

import java.time.LocalDate;
import java.time.LocalDateTime;

public class ReservationResponseDto {
private Long id;
private String name;
private String date;
private LocalDateTime time;

public ReservationResponseDto(Long id, String name, String date, LocalDateTime time) {
this.id = id;

Choose a reason for hiding this comment

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

이 필드의 유무에 따라서 dto 를 분리하는지 아닌지가 나뉠겁니다

적어도 이제 각 목적별로는 dto 를 분리해야지 나중에 특정 경우에는 어떤 데이터가 내려가고, 어떤 경우는 아니고 했을 때 대응하기가 쉬워져요

Copy link
Author

Choose a reason for hiding this comment

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

예를 들면 id랑 name만 필요한경우, id, name, date만 필요한경우, id, name, date, time모두 필요한 경우가 있다면, 세개의 Dtoclass를 만들어서 해당 경우에 필요한 Dto 클래스를 골라 써야 한다는 말로 이해했는데 맞나요?!

Choose a reason for hiding this comment

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

넵 맞습니다!
만약 그걸 명확하게 구분해서 사용해야한다면 이상적으로는 구분하는 것이 맞겠죠

ex) 만약 date 를 바꿀 수 있는 경우는 관리자밖에 없다
이런 케이스는 해도 괜찮을 것 같아요

만약 이런 케이스가 아니라 단순히 date 가 있을 수도 있고, 없을 수도 있다 정도의 레벨에서는 같이 두고, nullable 하게 처리하는 것도 괜찮은 방법입니다!

타입 지정은 완벽하게 정답이 있기 보다는 프로젝트의 성숙도에 따라서 더 달라지는 부분이라서 예시 상황마다 다를 것 같아요
대부분의 경우에 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 LocalDateTime getTime() {
return time;
}
}
37 changes: 37 additions & 0 deletions src/main/java/roomescape/entity/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package roomescape.entity;

import java.time.LocalDateTime;

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

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

public Reservation(String name, String date, LocalDateTime time) {
this(null, name, date, time);
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public String getDate() {
return date;
}

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

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

@RestControllerAdvice
public class ExceptionHandler {

@org.springframework.web.bind.annotation.ExceptionHandler(NotFoundReservationException.class)
public ResponseEntity<String> handleNotFoundReservationException(NotFoundReservationException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}

@org.springframework.web.bind.annotation.ExceptionHandler(InvalidValueException.class)
public ResponseEntity<String> handleInvalidReservationException(InvalidValueException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}
}
8 changes: 8 additions & 0 deletions src/main/java/roomescape/exception/InvalidValueException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package roomescape.exception;

public class InvalidValueException extends RuntimeException {

Choose a reason for hiding this comment

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

이 프로젝트에서 다루는 공통 예외가 있으면 어떨까요?
ex) RoomscapeException

그래서 모든 예외는 저 RoomscapeException 을 상속하는 구조로 만들어지는거죠
그랬을 때의 장점은 라이브러리에서 터지는 예외와 실수로 catch 하지 않은 우리 서비스 로직에서 터지는 exception 하고 구분이 안될 것 같아요

ControllerAdvice 쪽에서도 RoomscapeException 를 처리하는 ExceptionHandler 와 Exception 을 처리하는 ExceptionHandler 을 서로 분리해서 서로 다른 처리를 할 수 있을 것이고요

Copy link
Author

Choose a reason for hiding this comment

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

이미 InvalidValueException랑 NotFoundReservationException는 RuntimeException를 상속 받고 있는데 그러면 RuntimeException 대신 새로 만든 RoomscapeException를 상속받는 클래스로 수정하라는 말씀이신가요?

그랬을 때의 장점은 라이브러리에서 터지는 예외와 실수로 catch 하지 않은 우리 서비스 로직에서 터지는 exception 하고 구분이 안될 것 같아요

ControllerAdvice 쪽에서도 RoomscapeException 를 처리하는 ExceptionHandler 와 Exception 을 처리하는 ExceptionHandler 을 서로 분리해서 서로 다른 처리를 할 수 있을 것이고요

그리고 이 부분이 잘 이해가 되지 않습니다..!ㅠㅠ ControllerAdvice는 사용하지 않은 것 같은데.... 설명부탁드립니다!

Choose a reason for hiding this comment

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

이미 InvalidValueException랑 NotFoundReservationException는 RuntimeException를 상속 받고 있는데 그러면 RuntimeException 대신 새로 만든 RoomscapeException를 상속받는 클래스로 수정하라는 말씀이신가요?

graph TD;
InvalidValueException --> RoomscapeException --> RuntimeException
NotFoundReservationException --> RoomscapeException
Loading

와 같은 형태가 되면 가장 이상적일 것 같아요!
말씀해주신 부분이 맞습니다

그리고 이 부분이 잘 이해가 되지 않습니다..!ㅠㅠ ControllerAdvice는 사용하지 않은 것 같은데.... 설명부탁드립니다!

제가 말씀드린 ControllerAdvice = RestControllerAdvice 입니다!
https://tecoble.techcourse.co.kr/post/2020-08-17-custom-exception/
여기에 4번 항목을 보시면 조금 더 이해가 될 것 같아요!


public InvalidValueException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class NotFoundReservationException extends RuntimeException {
public NotFoundReservationException(String message) {
super(message);
}
}
34 changes: 34 additions & 0 deletions src/main/java/roomescape/repository/ReservationRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package roomescape.repository;

import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import roomescape.dao.ReservationDao;
import roomescape.entity.Reservation;

import java.util.List;

@Repository
public class ReservationRepository {

private final ReservationDao reservationDao;

Choose a reason for hiding this comment

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

보통 dao 의 네이밍은

Suggested change
private final ReservationDao reservationDao;
private final ReservationJdbcTemplateDao or MysqlReservationDao

와 같이 어떤 클래스를 사용했는지를 명확하게 적어주는 편이 좋은 것 같더라고요

repository 와 dao 가 나온 이유도 이와 조금 더 맞을 것 같은데요
repository -> db 종류에 구애받지 않는다
dao -> db 종류에 구애받는다
라고 봤을 때 dao 의 느낌상 하나의 db or 라이브러리에 무조건 종속되다보니 네이밍에서부터 드러내주는 것이 좋은 것 같아요


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

public List<Reservation> findAll() {
return reservationDao.findAll();
}

//예약 추가
public Reservation insert(Reservation reservation) {
return reservationDao.insert(reservation);
}

//예약 삭제
public void delete(Long id) {
reservationDao.delete(id);
}
}

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

import org.springframework.stereotype.Service;
import roomescape.dto.ReservationRequestDto;
import roomescape.dto.ReservationResponseDto;
import roomescape.entity.Reservation;
import roomescape.repository.ReservationRepository;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class ReservationService {

private final ReservationRepository reservationRepository;

public ReservationService(ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}

public List<ReservationResponseDto> getAllReservations(){
return reservationRepository.findAll()
.stream()
.map(this::toResponseDto)
.collect(Collectors.toList());
}

public ReservationResponseDto createReservation(ReservationRequestDto requestDto){

Reservation reservation = new Reservation(
requestDto.getName(),
requestDto.getDate(),
requestDto.getTime()
);
Reservation savedReservation = reservationRepository.insert(reservation);
return toResponseDto(savedReservation);
}

public void deleteReservation(Long id){
reservationRepository.delete(id);
}

private ReservationResponseDto toResponseDto(Reservation reservation) {
return new ReservationResponseDto(
reservation.getId(),
reservation.getName(),
reservation.getDate(),
reservation.getTime()
);
}
}
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 @@
server.port=8080
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
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,

Choose a reason for hiding this comment

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

여기도 VARCHAR 형태가 아닌 TimeStamp 형태로 바꿔보면 어떨까요?

Choose a reason for hiding this comment

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

이번에는 date 와 time 을 한번 통일시켜보면 어떨까요?
날짜, 시간의 정보가 대부분의 경우에 같이 필요하지, 어느 한 가지만 필요한 경우는 많이 없을 것 같아요!

time Timestamp NOT NULL,
PRIMARY KEY (id)
);
Loading