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] 황승준 미션 제출합니다. #378

Open
wants to merge 70 commits into
base: davidolleh
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
766de30
<FEAT> spring web 의존성 추가
davidolleh Nov 5, 2024
8f583de
<FEAT> index page 응답 구현
davidolleh Nov 5, 2024
ed41d53
<FEAT> index page 응답 테스트 구현
davidolleh Nov 5, 2024
1d148ec
<FEAT> person, reservation entity 구현
davidolleh Nov 5, 2024
58d780c
<FIX> reservation id 자료형 Long으로 변경
davidolleh Nov 5, 2024
4ddedd9
<FIX> person id 자료형 Long으로 변경
davidolleh Nov 5, 2024
a555095
<FEAT> getter 추가 객체 비교 순서 비교 id로 하도록 설정
davidolleh Nov 6, 2024
ceeec41
<FEAT> reservation controller dto 구현
davidolleh Nov 6, 2024
c023e3c
<FEAT> reservation controller 구현
davidolleh Nov 6, 2024
d9ad36b
<FEAT> reservation service 구현
davidolleh Nov 6, 2024
e395263
<FEAT> reservation repository 구현
davidolleh Nov 6, 2024
faa20cf
<FEAT> reservation save 기능 구현
davidolleh Nov 6, 2024
f795d78
<FEAT> ResponseEntity return 하도록 변경
davidolleh Nov 6, 2024
df0f34f
<FEAT> reservation delete 기능 구현
davidolleh Nov 6, 2024
14c2227
<FIX> reservation request, response dto 분리
davidolleh Nov 6, 2024
7dfd9e4
<FIX> return 값 구체적으로 명시
davidolleh Nov 6, 2024
2e56edb
<FIX> file 위치 변경
davidolleh Nov 6, 2024
d7e0290
<FIX> 변수 final 로 변경
davidolleh Nov 6, 2024
8f48143
<FEAT> index 페이지 테스트 구현
davidolleh Nov 6, 2024
c6d091f
<DELETE> controller test 파일들 삭제
davidolleh Nov 6, 2024
75bff4a
<FEAT> exception 메세지 추가
davidolleh Nov 6, 2024
697005d
<FEAT> response header 추가
davidolleh Nov 6, 2024
77a24b6
<FEAT> 예외처리 추가
davidolleh Nov 6, 2024
2aaa48f
<FEAT> 예약 삭제 기능 예외처리
davidolleh Nov 6, 2024
fa1537c
<FEAT> Test code 추가
davidolleh Nov 6, 2024
989cad9
<FIX> 주석 제거
davidolleh Nov 6, 2024
4663949
<FEAT> jdbc, h2 의존성 추가
davidolleh Nov 12, 2024
907e629
<FEAT> h2 database 연결 설정, 테이블 생성
davidolleh Nov 12, 2024
b148d0a
<FEAT> 데이터베이스 연결 테스트
davidolleh Nov 12, 2024
1274a15
<FIX> 파일 이름 수정
davidolleh Nov 12, 2024
4257069
<FIX> 파일 이름 변경
davidolleh Nov 12, 2024
3339d0a
<FIX> 파일 이름 변경
davidolleh Nov 12, 2024
3bab9cb
<FIX> 파일 이름 변경
davidolleh Nov 12, 2024
ed6dc7a
<FEAT> 어노테이션 추가로 의존성 주입
davidolleh Nov 12, 2024
66087ad
<FIX> jdbc template 관련 테스트 분리
davidolleh Nov 12, 2024
005b552
<FEAT> jdbc 조회 테스트 구현
davidolleh Nov 12, 2024
78945f2
<FEAT> H2 database 사용 repository구현
davidolleh Nov 12, 2024
0d68891
<FEAT> jdbc repository insert, delete 기능 구현
davidolleh Nov 12, 2024
fff2d60
<FEAT> jdbc repository insert, delete 테스트 구현
davidolleh Nov 12, 2024
beba048
<FIX> 중복된 익명 함수 필드로 설정
davidolleh Nov 13, 2024
4aae3f0
<FIX> 테스트 통과하도록 수정
davidolleh Nov 13, 2024
33f4f2a
<FIX> 주석 제거
davidolleh Nov 13, 2024
68648b3
<FEAT> 삭제 예외처리 추가
davidolleh Nov 13, 2024
e9a35c2
<FEAT> 예외처리 테스트 추가
davidolleh Nov 13, 2024
8530260
<FIX> 주석 삭제
davidolleh Nov 13, 2024
31ba8b8
<FIX> setter 삭제 후 새로운 객체 생성후 전해주도록 변경
davidolleh Nov 13, 2024
4ef725d
<FEAT> entity id로 값 비교하도록 설정
davidolleh Nov 13, 2024
20480eb
<FEAT> README.md 작성
davidolleh Nov 13, 2024
e6d5f11
<FEAT> README.md 작성
davidolleh Nov 13, 2024
f430a17
<REFACTOR> 정적페이지 리턴, json 리턴 분리
davidolleh Nov 22, 2024
f3a8644
<REFACTOR> ok로 수정
davidolleh Nov 22, 2024
36be467
<REFACTOR> validation 패키치 의존성 추가
davidolleh Nov 22, 2024
313bf0d
<REFACTOR> spring validation으로 dto 필드 예외처리하도록 수정
davidolleh Nov 22, 2024
305af35
<REFACTOR> DB에 맞춰 테스트 코드 수정
davidolleh Nov 22, 2024
351d38a
<REFACTOR> 사용안하는 주석 삭제
davidolleh Nov 22, 2024
0147fd8
<REFACTOR> Comparator삭제
davidolleh Nov 22, 2024
05284cc
<REFACTOR> 이름 변경
davidolleh Nov 25, 2024
5080574
<REFACTOR> 예상치 못한 예외처리 추가
davidolleh Nov 25, 2024
a3fcedb
<REFACTOR> log 의존성 추가
davidolleh Nov 25, 2024
d02b685
<REFACTOR> log 설정 구현
davidolleh Nov 25, 2024
22bd9ca
<REFACTOR> findById 예외 처리 null로 처리
davidolleh Nov 26, 2024
ee65dce
<REFACTOR> DateTimeFormatter 내장 라이브러리 사용
davidolleh Dec 1, 2024
579edb6
<REFACTOR> data.sql 추가
davidolleh Dec 1, 2024
42f2ea8
<REFACTOR> data.sql 설정 추가
davidolleh Dec 1, 2024
e03aad0
<REFACTOR> 테스트 방식 변경
davidolleh Dec 1, 2024
ecd7119
<REFACTOR> 로그 설정
davidolleh Dec 2, 2024
4d775b5
<REFACTOR> 로그백 설정 파일 분리
davidolleh Dec 2, 2024
86d53b8
<REFACTOR> 초기 db setting 파일 위치 변경
davidolleh Dec 2, 2024
39859b2
<REFACTOR> gitignore 로그 추가
davidolleh Dec 2, 2024
1467f7d
<REFACTOR> gloabl exception 로그 처리
davidolleh Dec 2, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ out/

### VS Code ###
.vscode/

/log
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 간단 스프링 어플리케이션

## JDBC
### 5단계
- [x] 데이터베이스 설정
- [x] 데이터베이스 연결

### 6단계
- [x] 데이터 조회하기

### 7단계
- [x] 데이터 추가하기
- [x] 데이터 삭제하기
- [x] 데이터 삭제 잘못된 요청시 예외처리

### 7 단계 고민
- Entity id에 setter 함수를 두면 위험성이 크다는 생각이 들어 새로운 객체를 생성해서 return 해야 되겠다 라는 생각을 가지게 되었습니다

Choose a reason for hiding this comment

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

jpa 를 기준으로 보면, save 시점에 알아서 값을 추가해주게 됩니다!
find 를 했을 때는 자동으로 id 를 채워진 상태로 오기 때문에, id 를 직접 set 하는 경우는 거의 없다고 보셔도 될 것 같아요!

과연 spring은 새로 db에 생성된 entity에 관해 어떻게 id를 주입하나 궁금함군요 🤔

### 질문사항
- 어플리케이션 실행중 어디서든 exception이 발생하면 ControllerAdvice를 exception과 관련된 응답을 전달해줍니다.
대부분의 exception을 최종적으로 ControllerAdvice에서 처리를 하다 보니 Service, Repository, Entity 등등 아무데서나 exception을
던질 수 있겠다라는 착각을 하게 되는것 같습니다. 혹시 exception은 보통 어느 layer에서 처리를 하는지 궁금하며 예외처리는 어떻게 관리
되는지 궁금합니다!
Comment on lines +20 to +24

Choose a reason for hiding this comment

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

이 부분은 현실적으로 말하면 프로젝트 바이 프로젝트가 너무 심하고, 팀 바이 팀이 너무 심해요
대부분의 경우에 throw 를 했다 -> 잘못되었다는 응답을 준다
이 공식이 작동하다보니 그냥 공통 레이어에서 handler 를 추가하는 경우가 많은데요

다르게 말하자면 throw 를 했다 -> 잘못된 응답을 줄 필요가 없다 (자체적으로 롤백을 할 수 있다)
정도로 생각된다면 이는 직접 catch 를 서비스 레이어에서 하는 편인 것 같아요


8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-validation'
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'
}


test {
useJUnitPlatform()
}
61 changes: 61 additions & 0 deletions src/main/java/roomescape/api/ReservationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package roomescape.api;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import roomescape.api.dto.ReservationRequestDto;
import roomescape.api.dto.ReservationResponseDto;
import roomescape.entity.Reservation;
import roomescape.service.ReservationService;

import java.util.List;

@RestController
public class ReservationController {

private final ReservationService reservationService;

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

@GetMapping("/reservations")
public ResponseEntity<List<ReservationResponseDto>> readReservations() {
return ResponseEntity
.ok()
.body(
reservationService.readReservations().stream().
map(ReservationResponseDto::fromEntity)
.toList()
Comment on lines +28 to +30

Choose a reason for hiding this comment

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

controller 레이어에서 dto 를 만드는 것과, service 레이어에서 dto 를 만드는 것중 전자를 선택한 이유가 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

개인적인 생각으로 dto로 형변환하는 것은 핵심 비지니스 로직으로 생각하지 않는거 같습니다.
service에서는 entity로만(Spring에서는 jpa기술을 사용함으로써 entity를 domain class로 사용하기에) 작업해야지 라는 생각이 있습니다!

Choose a reason for hiding this comment

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

오 그런 근거가 있다면 좋은 것 같아요!

회사 기준을 봤을 때는 그냥 그때그때 바꾼다가 가장 일반적인 케이스긴 하거든요?
보통 하나의 엔티티만을 조회할 때는 진행해주신 것처럼 controller 에서 바꾸고
여러개의 엔티티의 데이터를 모아야 한다면 dto 를 넘겨주는 작업을 하긴 했던 것 같아요

);
}

@GetMapping("/reservations/{id}")
public ResponseEntity<ReservationResponseDto> readReservations(@PathVariable Long id) {
Reservation reservation = reservationService.readReservation(id);

return ResponseEntity
.ok()
.body(ReservationResponseDto.fromEntity(reservation));
}

@PostMapping("/reservations")
public ResponseEntity<ReservationResponseDto> createReservation(
@RequestBody @Valid ReservationRequestDto reservationDto
) {
ReservationResponseDto response =
ReservationResponseDto.fromEntity(reservationService.createReservation(reservationDto.toEntity()));

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

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

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

@Controller
public class StaticPageController {
@GetMapping("/")
public String mainPage() {
return "home";
}


@GetMapping("/reservation")
public String reservationPage() {
return "reservation";
}
}
30 changes: 30 additions & 0 deletions src/main/java/roomescape/api/dto/ReservationRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package roomescape.api.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import roomescape.entity.Person;
import roomescape.entity.Reservation;
import roomescape.util.CustomDateTimeFormat;

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

public record ReservationRequestDto(
@NotBlank
@NotNull
String name,

Choose a reason for hiding this comment

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

@NotNull
@Notblank를 활용한 spring validation 을 써도 좋을 것 같아요

@NotBlank
@NotNull
String date,
@NotBlank
@NotNull
String time
) {
public Reservation toEntity() {
return new Reservation(
new Person(this.name),
LocalDate.parse(date, CustomDateTimeFormat.dateFormatter),

Choose a reason for hiding this comment

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

보통 CustomDateTimeFormat 같은 것은 "hh:MM" 과 같이 특이한 형태일때만 정의하는 것 같아요
java 의 기본 라이브러리가 아닌 다른 라이브러리 코드에서는 저렇게 의존성을 격리하는 차원에서 두기도 하지만, 아닌 경우에는 의존하는 것이 더 깔끔한 경우가 많은 것 같아요

LocalTime.parse(time, CustomDateTimeFormat.timeFormatter)
);
}
}
23 changes: 23 additions & 0 deletions src/main/java/roomescape/api/dto/ReservationResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package roomescape.api.dto;
import roomescape.entity.Reservation;
import roomescape.util.CustomDateTimeFormat;

public record ReservationResponseDto(
Long id,
String name,
String date,
String time
) {
public static ReservationResponseDto fromEntity(Reservation reservation) {
if (reservation == null) {
return null;
}

return new ReservationResponseDto(
reservation.getId(),
reservation.getPerson().getName(),
reservation.getDate().format(CustomDateTimeFormat.dateFormatter),
reservation.getTime().format(CustomDateTimeFormat.timeFormatter)
);
}
}
13 changes: 13 additions & 0 deletions src/main/java/roomescape/entity/Person.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package roomescape.entity;

public class Person {

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.

처음에 미션을 읽었을때 Person또한 데이터베이스에 �테이블로 등록 될거라는 생각에 만들었는데 결국 사용하지 않더군요...
추후에 만들어 질 수 있겠다 싶어서 남겨 두었습니다!

Choose a reason for hiding this comment

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

저는 이런 경우에는 사용하지 않는 것을 추천드려요!
오히려 코드의 복잡도가 많이 올라가는 문제가 생겨서요

private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}
54 changes: 54 additions & 0 deletions src/main/java/roomescape/entity/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package roomescape.entity;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Objects;

public class Reservation {
private Long id;
private Person person;
private LocalDate date;
private LocalTime time;

public Reservation(Long id, Person person, LocalDate date, LocalTime time) {
this.id = id;
this.person = person;
this.date = date;
this.time = time;
}
public Reservation(Person person, LocalDate date, LocalTime time) {
this.id = 0L;
this.person = person;
this.date = date;
this.time = time;
}

public Long getId() {
return id;
}

public Person getPerson() {
return person;
}

public LocalDate getDate() {
return date;
}

public LocalTime getTime() {
return time;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reservation that = (Reservation) o;
return Objects.equals(id, that.id);

Choose a reason for hiding this comment

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

오 id 만 같으면 같은 예약이다라고 둔 이유가 있을까요?
좋은 선택인데, 이유가 궁금했어요

Copy link
Author

@davidolleh davidolleh Nov 25, 2024

Choose a reason for hiding this comment

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

DB에서는 id(PK)는 중복될 수 없다. 즉 하나의 테이블을 표현하는 Spring 객체에서 id가 같다면 두 객체는 같은 테이블의 같은 데이터를 표현해주고 있는 것이다.

id 필드 말고 다른 필드 또한 equals, hashcode함수에 함께 사용해서 비교할 수 있지 않을까?

  • 영속화 개념
  1. jpa entity는 엔티티를 지연 로딩 하기 위해서 엔티티 객체를 상속 받은 Proxy 객체를 사용한다. Proxy객체는 id외의 모든 field값은 null일텐데 id와 다른 필드들을 equals, hashcode에 함께 사용한다면 비교 개념상 오류가 발생할 수 있지 않을까?

  2. 이렇게 되면 사용자가 만든 연관관계가 존재하는 모든 객체에 equals와 hashcode를 추가해줘야 하는 불편함이 생길 수 있지 않을까...즉, 불필요한 코드를 추가하는 정도가 되는 것이 아닐까 생각합니다.

Copy link
Author

Choose a reason for hiding this comment

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

https://www.korecmblog.com/blog/jpa-equals-and-history

1번의 영감을 받은 블로그입니다!

Choose a reason for hiding this comment

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

dm 으로 왔다갔다 했던 내용인데, Set 이나 HashSet 에 넣는 경우에는 id 를 재정의 하지만, 그 외에는 하지 않는 것이 좋다고 생각하고요
그 이유는 모든 객체에 다 그 행동을 해야하는데, 너무 귀찮고, 코드 읽기가 힘들어져서 주의하는 편입니다!
equals 로 비교할 때는 id 를 꺼내서 비교하고요

이렇게 하게 된 이유는 추후에 어떻게 변경될지 모른다는 점이 가장 큰데요
Reservation 을 상속하는 무언가의 객체 (ex jpa 의 프록시) 의 객체가 나왔을 경우에 equals 가 제대로 동작하나? 와 같이 어떤 방식으로 변경될지 모르다보니
확실히 절대로 바뀌지 않는다는 전제로 equals 를 믿고 가는 것을 잘 하지 않는 것 같아요

}

@Override
public int hashCode() {
return Objects.hashCode(id);
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/exception/BusinessException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

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

public class EntityNotFoundException extends BusinessException {
public EntityNotFoundException(String message) {
super(message);
}
}
52 changes: 52 additions & 0 deletions src/main/java/roomescape/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.exception;

import org.slf4j.Logger;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import roomescape.util.LoggerUtils;

import java.util.stream.Collectors;

@ControllerAdvice
public class GlobalExceptionHandler {

Choose a reason for hiding this comment

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

이 프로젝트에서 공통적으로 사용되는 BusinessException 을 잡아주는 친구도 있으면 좋을 것 같아요!

private final Logger logger = LoggerUtils.logger(GlobalExceptionHandler.class);

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e, WebRequest request) {
logger.info(e.getMessage());
return ResponseEntity.badRequest().body(e.getMessage());
}

@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<String> handleExceptionNotFoundException(EntityNotFoundException e) {
logger.info(e.getMessage());
return ResponseEntity.badRequest().body(e.getMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String errorFields = e.getBindingResult().
getFieldErrors()
.stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())

Choose a reason for hiding this comment

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

default message 가 있다면 이 부분을 직접 정의해도 좋을 것 같아요
@NotNull 같은 어노테이션 안에 메시지를 정의할 수 있을거에요
현재 값은 뭐고, 어떤 제약조건을 클라이언트에 보여주고 싶은지와 같은 것들을 정의할 수 있을것 같아요

.collect(Collectors.joining(", "));

logger.info(errorFields);
return ResponseEntity.badRequest().body(errorFields);
}

@ExceptionHandler(BusinessException.class)
public ResponseEntity<String> handleBusinessException(BusinessException e) {
logger.warn(e.getMessage());
return ResponseEntity.internalServerError().body(e.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
logger.error(e.getMessage());
return ResponseEntity.internalServerError().body(e.getMessage());
}
}
Loading