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] 안준영 미션 제출합니다. #306

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
316a28e
1단계 - 홈화면 구성
Junyeong-An Jun 27, 2024
86dcc4d
2단계 - 예약 조회
Junyeong-An Jun 27, 2024
633f3fa
3단계 - 예약 추가/취소
Junyeong-An Jun 27, 2024
cc436f5
4단계 - 예외 처리
Junyeong-An Jun 27, 2024
9f2966a
4단계 - 예외 처리 Reservation추가
Junyeong-An Jun 30, 2024
fcfc498
테스트 모두 통과하도록 수정
Junyeong-An Jul 1, 2024
ddb26a9
불필요 공백 제거
Junyeong-An Jul 1, 2024
598103c
Test 코드에 @DisplayName추가
Junyeong-An Jul 1, 2024
be66739
thymeleaf 의존성 추가
Junyeong-An Jul 2, 2024
08c7387
Feat: 5단계: 데이터베이스 적용하기
Junyeong-An Jul 4, 2024
6c5bfc9
Feat: 6단계: 데이터 조회하기
Junyeong-An Jul 4, 2024
35a3e5a
[Spring MVC] 안준영 미션 제출합니다. (#283)
Junyeong-An Jul 4, 2024
cc73923
Feat: 7단계: 데이터 추가/삭제하기
Junyeong-An Jul 4, 2024
48c3027
Feat: 불필요 코드 삭제
Junyeong-An Jul 4, 2024
deb6d02
Merge branch 'junyeong-an' into junyeong-an
Junyeong-An Jul 4, 2024
ce883c0
Feat: static 제거
Junyeong-An Jul 7, 2024
e6ad343
Merge branch 'junyeong-an' of https://github.com/Junyeong-An/spring-r…
Junyeong-An Jul 7, 2024
b7b92b0
Feat: insert 예외 처리 추가
Junyeong-An Jul 8, 2024
c5b300e
Refactor: 테스트 코드명 영어로 변환
Junyeong-An Jul 8, 2024
1a7d4cb
Refactor: id값 없을 경우 예외 처리
Junyeong-An Jul 8, 2024
520a3db
Feat: Dto 추가
Junyeong-An Jul 10, 2024
9a92583
Feat: Time insert,delete,get 구현
Junyeong-An Jul 11, 2024
878d9dd
Feat: ReservationService 추가
Junyeong-An Jul 11, 2024
80f0f20
Feat: 9단계 기존 코드 수정
Junyeong-An Jul 11, 2024
976c1de
Feat: 10단계 계층화 리팩토링
Junyeong-An Jul 11, 2024
12561a7
Refactor: 계층화 리팩토링
Junyeong-An Jul 11, 2024
7246d92
Refactor: 테스트 코드명 수정
Junyeong-An Jul 11, 2024
bb9ddc2
Refactor: 필요없는 import문 제거
Junyeong-An Jul 11, 2024
cadabba
Feat: Time 반환 Dto 추가
Junyeong-An Jul 11, 2024
d55847c
Feat: 테스트 코드 설명 추가
Junyeong-An Jul 11, 2024
4f515e3
Refactor: 예약 서비스에서 Dto 사용추가
Junyeong-An Jul 11, 2024
e47f4a0
refactor: 계층화 분리
Junyeong-An Jul 14, 2024
6d117be
remove: set메서드 제거
Junyeong-An Jul 22, 2024
f93e66f
refactor: header 로직 분리
Junyeong-An Jul 24, 2024
cbec7ba
refactor: try-catch 문 제거
Junyeong-An Jul 24, 2024
57bbf2e
feat: Transactional적용
Junyeong-An Jul 24, 2024
ee69a26
refactor: toList()리팩토링
Junyeong-An Jul 24, 2024
8c9ff70
refactor: 필요없는 공백 및 어노테이션 제거
Junyeong-An Jul 25, 2024
3a3a85b
refactor: 메서드명 일관성있게 수정
Junyeong-An Jul 25, 2024
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'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
runtimeOnly 'com.h2database:h2'
}

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

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;
import roomescape.domain.Reservation;
import roomescape.dto.ReservationDto;
import roomescape.service.ReservationService;

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

@Controller
public class RoomescapeController {

private final ReservationService reservationService;

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

@GetMapping("/reservation")
public String reservation() {
return "new-reservation";
}

@GetMapping("/reservations")
@ResponseBody
public ResponseEntity<List<ReservationDto>> getAllReservations(){
List<ReservationDto> reservations = reservationService.getAllReservations();
return new ResponseEntity<>(reservations, HttpStatus.OK);
}

@PostMapping("/reservations")

Choose a reason for hiding this comment

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

@GetMapping@DeleteMapping에서는 @ResponseBody 어노테이션을 사용하셨는데 @PostMapping에만 사용하지 않은 이유가 있나요??

Copy link
Author

Choose a reason for hiding this comment

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

  • ResponseEntity를 반환하는 경우 http응답 본문으로 사용되는 것을 Spring이 처리해주므로 @ResponseBody 애노테이션이 필요하지 않는다고 합니다..!

public ResponseEntity<?> createReservation(@RequestBody ReservationDto reservationDto) {
try {
Reservation reservation = reservationService.addReservation(reservationDto);
URI location = UriComponentsBuilder.fromPath("/reservations/{id}")
.buildAndExpand(reservation.getId())
.toUri();
return ResponseEntity.created(location).body(reservation);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
Comment on lines +36 to +47

Choose a reason for hiding this comment

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

createReservation() 메서드는 예약을 생성하는 요청과 그 응답에 집중하도록 하고 Location 필드를 위해 URI를 만드는 부분은 따로 메서드로 분리하거나 아래와 같이 응답 구조를 변경하면 가독성에 도움이 될 것 같아요!

return ResponseEntity.status(HttpStatus.CREATED)
                .header("Location 헤더 설정")
                .header("필요한 다른 헤더들~~")
                .body("본문 객체");

Choose a reason for hiding this comment

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

추가로 Controller에서 try-catch 블럭을 통해 예외를 잡아서 처리하는 것보다는 미션 학습 자료에 있는 @ControllerAdvice 어노테이션에 대해 학습하고 전역 예외 처리에 대해 고민해보면 좋을 것 같아요!


@DeleteMapping("/reservations/{id}")
@ResponseBody
public ResponseEntity<Reservation> deleteReservation(@PathVariable int id){
try {
reservationService.deleteReservation(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(null);
}
}

Junyeong-An marked this conversation as resolved.
Show resolved Hide resolved
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
}
52 changes: 52 additions & 0 deletions src/main/java/roomescape/controller/TimeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.controller;

import java.util.List;
import org.springframework.http.HttpHeaders;
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.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.dto.TimeDto;
import roomescape.dto.TimeResDto;
import roomescape.service.TimeService;

@RestController
@RequestMapping("/times")
public class TimeController {
private final TimeService timeService;

public TimeController(TimeService timeService) {
this.timeService = timeService;
}

@PostMapping
public ResponseEntity<TimeResDto> addTime(@RequestBody TimeDto timeDto) {
TimeResDto timeResDto = timeService.addTime(timeDto);
HttpHeaders headers = createHeader("/times/" + timeResDto.id());
return new ResponseEntity<>(timeResDto, headers, HttpStatus.CREATED);
}
Junyeong-An marked this conversation as resolved.
Show resolved Hide resolved

private HttpHeaders createHeader(String path) {
HttpHeaders headers = new HttpHeaders();
headers.add("Location", path);
return headers;
}

@GetMapping
public ResponseEntity<List<TimeResDto>> getAllTimes() {
List<TimeResDto> times = timeService.getAllTimes();
return new ResponseEntity<>(times, HttpStatus.OK);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTime(@PathVariable int id) {
timeService.deleteTime(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
58 changes: 58 additions & 0 deletions src/main/java/roomescape/dao/RoomDAO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package roomescape.dao;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import roomescape.domain.Reservation;
import roomescape.domain.Time;

import java.util.List;

@Repository
public class RoomDAO {
private final JdbcTemplate jdbcTemplate;

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

public List<Reservation> findAll() {
return jdbcTemplate.query("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",
(resultSet, rowNum) -> new Reservation(
resultSet.getInt("reservation_id"),
resultSet.getString("name"),
resultSet.getString("date"),
new Time(resultSet.getInt("time_id"), resultSet.getString("time_value"))
));
}
Comment on lines +19 to +28

Choose a reason for hiding this comment

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

문자열을 더하기 연산으로 나누다 보니 가독성이 조금 떨어지는 것 같아요.


public void insert(Reservation reservation) {
jdbcTemplate.update("INSERT INTO reservation (name, date, time_id) VALUES (?, ?, ?)",
reservation.getName(), reservation.getDate(), reservation.getTime().getId());
}

public void deletebyId(int id) {
jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id);
}

public int getId(Reservation reservation) {
return jdbcTemplate.queryForObject("SELECT id FROM reservation WHERE name = ? AND date = ? AND time_id = ?",
Integer.class, reservation.getName(), reservation.getDate(), reservation.getTime().getId());
}

public Reservation findById(int id) {
try {
return jdbcTemplate.queryForObject("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 WHERE r.id = ?",
(resultSet, rowNum) -> new Reservation(
resultSet.getInt("reservation_id"),
resultSet.getString("name"),
resultSet.getString("date"),
new Time(resultSet.getInt("time_id"), resultSet.getString("time_value"))
), id);
} catch (EmptyResultDataAccessException e) {
throw new IllegalArgumentException("찾는 id가 존재하지 않습니다!");
}
}
}
47 changes: 47 additions & 0 deletions src/main/java/roomescape/dao/TimeDAO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package roomescape.dao;

import java.util.List;
import java.util.Optional;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import roomescape.domain.Time;

@Repository
public class TimeDAO {
private final JdbcTemplate jdbcTemplate;

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

public void insert(Time time) {
jdbcTemplate.update("INSERT INTO time (time) VALUES (?)",
time.getTime());
}

public Optional<Integer> getId(Time time) {
return jdbcTemplate.query(
"SELECT id FROM time WHERE time = ? LIMIT 1",
(rs, rowNum) -> rs.getInt("id"),
time.getTime()
).stream().findFirst();
}

public List<Time> findAll() {
return jdbcTemplate.query("SELECT * FROM time", (rs, rowNum) ->
new Time(rs.getInt("id"), rs.getString("time"))
);
}

public void deleteById(int id) {
jdbcTemplate.update("DELETE FROM time WHERE id = ?", id);
}

public Optional<Time> findByTime(String time) {
return jdbcTemplate.query(
"SELECT id, time FROM time WHERE time = ? LIMIT 1",
(rs, rowNum) -> new Time(rs.getInt("id"), rs.getString("time")),
time
).stream().findFirst();
}
}
31 changes: 31 additions & 0 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.domain;

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

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

public String getName() {
return name;
}

public int getId() {
return id;
}

public String getDate() {
return date;
}

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

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

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

public String getTime() {
return time;
}

public void setTime(String time) {
this.time = time;
}

public int getId() {
return id;
}
}
12 changes: 12 additions & 0 deletions src/main/java/roomescape/dto/ReservationDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package roomescape.dto;

import roomescape.domain.Time;

public record ReservationDto(
String name, String date, Time time
) {
public static ReservationDto from(String name, String date, Time time) {
return new ReservationDto(name, date, time);
}
}

Choose a reason for hiding this comment

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

record와 정적팩토리 메서드를 사용하신 이유가 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

  • record를 사용하면 자동으로 생성자, 접근자 메서드를 생성해주어 불필요한 코드를 줄이고 가독성을 높일 수 있습니다. 간결하고 불변 객체를 만들기 위해 사용되므로 dto를 만드는데 사용하는 게 목적에 맞겠다 싶어 사용했습니다.
  • 정적팩토리 메서드는 생성자와 달리 메서드에 이름을 부여할 수 있어 생성 의도를 명확히 전달할 수 있고 생성 로직을 캡슐화 하고 반환 타입을 유연하게 할 수 있다는 장점이 있어 사용했습니다.

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

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

import roomescape.domain.Time;

public record TimeResDto(
int id, String time
) {
public static TimeResDto from(Time time) {
return new TimeResDto(time.getId(), time.getTime());
}
}
53 changes: 53 additions & 0 deletions src/main/java/roomescape/service/ReservationService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package roomescape.service;

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import roomescape.dao.RoomDAO;
import roomescape.domain.Reservation;
import roomescape.domain.Time;
import roomescape.dto.ReservationDto;
import roomescape.dto.TimeResDto;

@Service
public class ReservationService {
private final RoomDAO roomDAO;
private final TimeService timeService;
Comment on lines +13 to +16

Choose a reason for hiding this comment

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

ReservationService에서 RoomDAO 객체와 TimeService 객체를 주입받는 이유가 다른 이유도 있을 수 있지만, 새로운 예약을 생성할 때 Time을 찾아서 Reservation 객체에 넣고 save() 하기 위한 것 같아요.

  • 다른 Service와 의존 관계를 가지는 것이 어떤 장점과 단점이 있을까요?
  • 추가적으로 Time 객체를 만들어서 Reservation 객체에 추가하여 저장시키는 것보다는 Time의 id값을 DAO에게 전달한다면 sql 쿼리로 충분히 저장할 수 있을 것 같은데 준영은 어떻게 생각하나요?

Copy link
Author

@Junyeong-An Junyeong-An Jul 25, 2024

Choose a reason for hiding this comment

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

서비스에서 다른 서비스단을 의존성 주입 받으면 재사용성을 할 수 있다는 장점이 있지만 mvc패턴이 제대로 지켜지지 않아 복잡도가 증가하고 단위 테스트를 할 때도 의존성이 증가해 어려움이 있습니다.
다시 코드를 살펴보니 굳이 Time객체를 만드는 것보다 id의 값으로 받아 전달하는 게 맞는 것 같습니다...!😅 감사합니다


public ReservationService(RoomDAO roomDAO, TimeService timeService) {
this.roomDAO = roomDAO;
this.timeService = timeService;
}

@Transactional(readOnly = true)
public List<ReservationDto> getAllReservations() {
return roomDAO.findAll().stream()
.map(reservation -> new ReservationDto(reservation.getName(), reservation.getDate(),
reservation.getTime()))
.collect(Collectors.toList());
}
Comment on lines +23 to +29

Choose a reason for hiding this comment

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

Suggested change
@Transactional(readOnly = true)
public List<ReservationDto> getAllReservations() {
return roomDAO.findAll().stream()
.map(reservation -> new ReservationDto(reservation.getName(), reservation.getDate(),
reservation.getTime()))
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<ReservationDto> getAllReservations() {
return roomDAO.findAll().stream()
.map(reservation -> new ReservationDto(reservation.getName(), reservation.getDate(),
reservation.getTime()))
.toList();
}
  • collect(Collectors.toList()) vs toList() 중에서 전자를 선택하신 이유가 있나요?

Copy link
Author

@Junyeong-An Junyeong-An Jul 25, 2024

Choose a reason for hiding this comment

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

  • Collectors.toList()는 가변 리스트를 반환하지만 toList()는 불변 리스트를 반환합니다. 컬렉션을 수정할 필요가 있는 경우 Collectors를 사용하고 불변성을 유지하고 싶을 경우 toList()를 사용한다고 합니다. 결과값을 불변성으로 보장하기 위해 toList()를 사용하는 것이 좋아보이는 것 같습니다 감사합니다! 😊
  • collect(Collectors.toList())toList()의 차이를 사실 잘 모르고 사용했는데 이번 기회에 공부하면서 제대로 알게 된 것 같습니다!


@Transactional
public Reservation addReservation(ReservationDto reservationDto) {
String name = reservationDto.name();
String date = reservationDto.date();
String timeString = reservationDto.time().getTime();

TimeResDto timeResDto = timeService.findByTime(timeString);
Time time = new Time(timeResDto.id(), timeResDto.time());

Reservation reservation = new Reservation(0, name, date, time);
roomDAO.insert(reservation);
return reservation;
}

@Transactional
public void deleteReservation(int id) {
if (roomDAO.findById(id) != null) {
roomDAO.deletebyId(id);
} else {
throw new IllegalArgumentException("삭제할 예약이 없습니다.");
}
}
}
Loading