-
Notifications
You must be signed in to change notification settings - Fork 134
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] 황승준 미션 제출합니다. #390
base: davidolleh
Are you sure you want to change the base?
Changes from all commits
766de30
8f583de
ed41d53
1d148ec
58d780c
4ddedd9
a555095
ceeec41
c023e3c
d9ad36b
e395263
faa20cf
f795d78
df0f34f
14c2227
7dfd9e4
2e56edb
d7e0290
8f48143
c6d091f
75bff4a
697005d
77a24b6
2aaa48f
fa1537c
989cad9
4663949
907e629
b148d0a
1274a15
4257069
3339d0a
3bab9cb
ed6dc7a
66087ad
005b552
78945f2
0d68891
fff2d60
beba048
4aae3f0
33f4f2a
68648b3
e9a35c2
8530260
31ba8b8
4ef725d
20480eb
e6d5f11
2a3a59f
b69af71
8b5e45b
6260905
3b685e9
cb72257
dc90448
8991c9d
753473e
98dfab5
1c77107
c00780f
f430a17
f3a8644
36be467
313bf0d
305af35
351d38a
0147fd8
d79f47b
05284cc
5080574
a3fcedb
d02b685
22bd9ca
4767ed0
8b26c63
8ff0ae2
46bb758
836b55d
b580189
5f7eb86
77ad816
a622f68
65442d4
d52825c
f9f7fe5
85d38c0
5ef7aba
83aa536
6b5bddc
3682464
ad5cd1c
cefa3e4
9d7da74
355adbd
1e6a583
5bfdb4f
841d5a9
a07d6ca
a1c0e36
a7c6304
941bd0a
00e7cc5
f3250e9
b2e5533
08c66f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# 간단 스프링 어플리케이션 | ||
|
||
## Core | ||
### 8단계 | ||
- [x] 시간 테이블 설정 | ||
- [x] 시간 조회, 추가, 삭제 기능 추가 | ||
- [x] DB 초기 값 설정 | ||
### 9단계 | ||
- [x] 해당 시간이 존재하는 예약이 남아있을 때, 시간 삭제 못하도록 예외처리 | ||
- [x] 중복된 시간(시간의 time값이 같은) DB에 저장하지 못하도록 예외처리 | ||
### 10단계 | ||
|
||
### 고민사항 | ||
- 프로젝트 구조:<br/> | ||
[도메인 우선 vs 레이어 우선](https://codewithandrea.com/articles/flutter-project-structure/) | ||
<br/> | ||
우선 프로젝트 구조를 잘 가져가면 가져올 수 있는 효과가 무엇이 있는지 궁금합니다 | ||
미션처럼 크기가 작은 프로젝트에서는 레이어 구조 또한 괜찮은 방향 같지만 서비스 크기가 | ||
큰 앱을 개발하는데 있어서는 도메인 단위로 나누는 것이 프로젝트으 복잡성을 줄일 수 있게 되는거 같습니다. | ||
그러나 도메인 단위로 나뉘게 된다면 중복된 Entity(테이블이 서로 연관되어 있을 가능성이 크기 )에 대한 관리를 어떻게 하는지 궁금합니다! | ||
|
||
[Repository 계층, 도메인과 영속성 엔티티 사이의 간극](https://kokodakadokok.tistory.com/entry/Repository-%EA%B3%84%EC%B8%B5-%EB%8F%84%EB%A9%94%EC%9D%B8%EA%B3%BC-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B0%84%EA%B7%B9-%EB%A7%A4%EA%BE%B8%EA%B8%B0) | ||
<br/> | ||
스프링은 기술적으로 편의를 위해서? | ||
데이터베이스 테이블과 Java의 class를 매핑해준 Jpa 기술을 사용하는 것으로 알고 있습니다. | ||
Jpa의 @Entity라는 annotation을 사용하여 정의하는 Class는 Domain과의 간극이 존재한다고 | ||
생각합니다. 아직 이 간극에 대해 완벽히 알지 못하여서 Entity와 Domain을 혼돈해서 사용 | ||
하게 되는거 같습니다. 최소한 Service를 나눌때 Domain기준으로 나누고 싶은데 | ||
Entity(Table)단위로 나뉘게 되는 경향이 강한거 같습니다! | ||
지금은 TimeService와 ReservationService가 나뉘어져 있지만 | ||
TimeService와 ReservationService가 하고 있는 역할은 결국 예약이라는 하나의 도메인에 속한다는 생각이 들어 | ||
이 둘을 하나의 ReservationService로 합할까 고민하게 되었습니다. | ||
|
||
- Entity 패키지를 따로 둔 이유 <br/> | ||
제가 따로 Entity 패키지를 분리한 이유는 Service, Repository, Contoller(controller에서 dto를 entity로 변환하기에) 모두다 | ||
Entity를 바라보기 때문에 분리하게 되었습니다. | ||
Comment on lines
+22
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1️⃣ 질문
저희가 Reservation 같은 아이들을 부를 때 아래와 같이 정말 여러 이름으로 부를 수 있겠네요.
저 용어들이 사용되는 때에 따라서 의미가 약간씩은 다르게 사용되는데요.
이런 관점에서 Time과 Reservation이 같은 도메인 문제를 해결하기 위한 엔티티들이라면 지금 제가 생각했을 때는, Service들의 문제라기 보단, RDBMS가 아니라 인메모리에 저장한다고 생각하고 table 개념을 제거하고 설계해보는 것도 방법이겠어요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2️⃣ 질문
그렇군요. 이견 없습니다. 위에서 레이어드 아키텍처의 가치에도 크게 벗어나지 않습니다. 조금 더 자세한 리뷰는 상희님 리뷰 참고하면 좋겠어요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
하나의 도메인으로 정의했다고 하나의 서비스만 사용할 것인가? X |
||
|
||
|
||
## JDBC | ||
### 5단계 | ||
- [x] 데이터베이스 설정 | ||
- [x] 데이터베이스 연결 | ||
|
||
### 6단계 | ||
- [x] 데이터 조회하기 | ||
|
||
### 7단계 | ||
- [x] 데이터 추가하기 | ||
- [x] 데이터 삭제하기 | ||
- [x] 데이터 삭제 잘못된 요청시 예외처리 | ||
|
||
### 7 단계 고민 | ||
- Entity id에 setter 함수를 두면 위험성이 크다는 생각이 들어 새로운 객체를 생성해서 return 해야 되겠다 라는 생각을 가지게 되었습니다 | ||
과연 spring은 새로 db에 생성된 entity에 관해 어떻게 id를 주입하나 궁금함군요 🤔 | ||
|
||
### 질문사항 | ||
- 어플리케이션 실행중 어디서든 exception이 발생하면 ControllerAdvice를 exception과 관련된 응답을 전달해줍니다. | ||
대부분의 exception을 최종적으로 ControllerAdvice에서 처리를 하다 보니 Service, Repository, Entity 등등 아무데서나 exception을 | ||
던질 수 있겠다라는 착각을 하게 되는것 같습니다. 혹시 exception은 보통 어느 layer에서 처리를 하는지 궁금하며 예외처리는 어떻게 관리 | ||
되는지 궁금합니다! | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package roomescape.api; | ||
|
||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
import roomescape.api.dto.TimeRequestDto; | ||
import roomescape.api.dto.TimeResponseDto; | ||
import roomescape.entity.Time; | ||
import roomescape.service.TimeService; | ||
|
||
import java.util.List; | ||
|
||
@RestController | ||
@RequestMapping("/times") | ||
public class TimeController { | ||
|
||
private final TimeService timeService; | ||
|
||
public TimeController( | ||
@Autowired TimeService timeService | ||
) { | ||
this.timeService = timeService; | ||
} | ||
@GetMapping | ||
public ResponseEntity<List<TimeResponseDto>> readTimes() { | ||
List<Time> times = timeService.readReservationTimes(); | ||
List<TimeResponseDto> timeResponseDtos = times.stream() | ||
.map(TimeResponseDto::fromEntity) | ||
.toList(); | ||
|
||
return ResponseEntity | ||
.ok() | ||
.body(timeResponseDtos); | ||
} | ||
|
||
@PostMapping | ||
public ResponseEntity<TimeResponseDto> createTime(@RequestBody TimeRequestDto timeRequestDto) { | ||
Time time = timeService.createReservationTime(TimeRequestDto.toEntity(timeRequestDto)); | ||
TimeResponseDto timeResponseDto = TimeResponseDto.fromEntity(time); | ||
|
||
String headerName = "Location"; | ||
String headerValue = "/times/" + timeResponseDto.id(); | ||
|
||
return ResponseEntity | ||
.status(HttpStatus.CREATED) | ||
.header(headerName, headerValue) | ||
.body(timeResponseDto); | ||
} | ||
|
||
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> deleteTime(@PathVariable Long id) { | ||
timeService.deleteReservationTime(id); | ||
|
||
return ResponseEntity.noContent().build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,29 @@ | ||
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.DateTimeFormat; | ||
import roomescape.entity.Time; | ||
import roomescape.util.CustomDateTimeFormat; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalTime; | ||
import java.time.format.DateTimeFormatter; | ||
import java.util.Arrays; | ||
|
||
public record ReservationRequestDto( | ||
@NotBlank | ||
@NotNull | ||
String name, | ||
@NotBlank | ||
@NotNull | ||
String date, | ||
String time | ||
@NotNull | ||
Long time | ||
) { | ||
|
||
public ReservationRequestDto { | ||
checkValidation(name, date, time); | ||
} | ||
|
||
private void checkValidation(String name, String date, String time) { | ||
blanksValidation(name, date, time); | ||
} | ||
|
||
private void blanksValidation(String... fields) { | ||
boolean isBlankExists = Arrays.stream(fields) | ||
.anyMatch(String::isBlank); | ||
|
||
if (isBlankExists) { | ||
throw new IllegalArgumentException("요청 인자의 값을 빈값일 수 없습니다"); | ||
} | ||
} | ||
|
||
|
||
public Reservation toEntity() { | ||
return new Reservation( | ||
new Person(this.name), | ||
LocalDate.parse(date, DateTimeFormatter.ofPattern(DateTimeFormat.dateFormat)), | ||
LocalTime.parse(time, DateTimeFormatter.ofPattern(DateTimeFormat.timeFormat)) | ||
LocalDate.parse(date, CustomDateTimeFormat.dateFormatter), | ||
new Time(time) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,19 @@ | ||
package roomescape.api.dto; | ||
import roomescape.entity.Reservation; | ||
import roomescape.util.DateTimeFormat; | ||
|
||
import java.time.format.DateTimeFormatter; | ||
import roomescape.util.CustomDateTimeFormat; | ||
|
||
public record ReservationResponseDto( | ||
Long id, | ||
String name, | ||
String date, | ||
String time | ||
TimeResponseDto time | ||
) { | ||
public static ReservationResponseDto fromEntity(Reservation reservation) { | ||
return new ReservationResponseDto( | ||
reservation.getId(), | ||
reservation.getPerson().getName(), | ||
reservation.getDate().format(DateTimeFormatter.ofPattern(DateTimeFormat.dateFormat)), | ||
reservation.getTime().format(DateTimeFormatter.ofPattern(DateTimeFormat.timeFormat)) | ||
reservation.getDate().format(CustomDateTimeFormat.dateFormatter), | ||
TimeResponseDto.fromEntity(reservation.getTime()) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package roomescape.api.dto; | ||
|
||
import roomescape.entity.Time; | ||
|
||
import java.time.LocalTime; | ||
|
||
public record TimeRequestDto( | ||
String time | ||
) { | ||
public static Time toEntity(TimeRequestDto timeRequestDto) { | ||
return new Time( | ||
LocalTime.parse(timeRequestDto.time) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package roomescape.api.dto; | ||
|
||
import roomescape.entity.Time; | ||
import roomescape.util.CustomDateTimeFormat; | ||
|
||
public record TimeResponseDto( | ||
Long id, | ||
String time | ||
) { | ||
|
||
public static TimeResponseDto fromEntity(Time reservationTime) { | ||
return new TimeResponseDto( | ||
reservationTime.getId(), | ||
reservationTime.getTime().format(CustomDateTimeFormat.timeFormatter) | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저번에 이야기 나눴을 때, 아키텍쳐를 많이 고민했다고 애기하셨었는데 이 질문에서 딥하게 고민하고 계신 것이 느껴지네요. 👍 👍 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1️⃣ 질문
프로젝트 구조를 도메인 중심으로 가져갈지 레이어 중심으로 가져갈지 보다, 이 질문을 우선적으로 해주셔서 정말 감사합니다. 정말 훌륭합니다. 🎉
그쵸. 효과에 공감하고 느껴야 동기가 생기겠죠. 구조, 아키텍쳐 같은 최소 약속을 지키면서 코딩을 하는 것은 매우 번거로운 일이니까요.
그런 말이 있습니다 (진짜 있는지 모름)
개발자는 움직이는 기차의 바퀴를 바꾸는 것이다
모든 것을 허물고 다시 구조를 내 맘대로 올리는 것은 그리 어려운 일은 아니겠어요.
하지만, 우리가 개발자로서 하는 일은 대부분 다른 사람(혹은 과거의 나)이 만든 코드를 이해하고 기존의 비즈니스가 잘 유지된 상태로 개선을 해나가야합니다.
작은 토이 프로젝트도 몇주 몇달만 되어도 엄청난 복잡도가 생길거에요.
저는
구조를 잘 가져간다
는 것은의존성을 잘 설정한다
인 것 같은데요.의존성
은영향(변경)이 전파
된다는 의미와 또 같은 의미입니다.예를 들어 이런 의존 관계가 있겠죠.
이는,
Repository의 문제가 생기면 Service에도 문제가 생긴다는 의미입니다.
Time에게 변경이 생기면 Reservation에도 자연스레 변경이 생기겠죠.
(예를들면 Time의 클래스명을 ReservationTime으로 바꾼다고 생각해보면 되겠어요.)
구조를 잘 가져가게 되면 유지보수 시, 변경사항의 전파를 예측하기 좋겠죠.
(영상자료)toss SLASH 22 - 김재민님 / 지속 성장 가능한 코드를 만들어가는 방법
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2️⃣ 질문
(구조를 패키지로 많이 나타내므로 이하 패키지라는 표현을 많이 사용 예정)
오! 레이어 중심으로 구조를 가져가는 것도 정말 직관적이고 좋죠!
꼭 규모가 큰 도메인이라 해도 도메인 중심으로 패키지를 나누지 않을 수 있습니다.
레이어 별 나누고 그 안에서 도메인을 따로 나눌 수 있어요. (정답은 없는 것이니까요)
승준님이 그렇게 느끼신 것은 합리적이라 생각합니다.
아마도 규모가 클 수록 팀별로 도메인을 따로 관리할 가능성이 있겠죠.
그렇다 보면,
A팀은 예약 시스템,
B팀은 채팅 시스템,
C팀은 결제 시스템,
...
이런식으로 팀별로 도메인이 나눠질 수 있습니다
서비스 전체로 보면 예약을 하면서 채팅을하고 결제를 하겠지만, 그를 관리하는 사람(팀)이 달라지면 경계가 필요합니다.
이때부턴 도메인 중심적인 사고를 하면서 구조를 짜볼 필요가 있겠죠.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3️⃣ 질문
답변에 앞서
중복된 Entity(테이블이 서로 연관되어 있을 가능성이 크기)
라는 표현이 어떤 의미인지 정확하게 이해하지 못했습니다.제가 이해한 질문은
도메인 중심 설게에서 다른 패키지로 나뉘어버린 Entity간 연관관게는 어떻게 핸들링하냐
입니다.이 질문이라고 생각하고 답변하겠습니다.
일단, 대부분의 조직에서는
예약 팀
이결제 팀 엔티티 자체
를 직접 참조하지 않을 것입니다.결제팀 코드 수정에 예약팀 코드가 영향을 받는 등의 의존성 문제로 참조를 끊는 것이죠.
다만,
예약
에서어떤 결제건이 연관
되었는지 알아야한다면, 참조를 하긴 해야겠죠.직접 참조하는 것보단 약한 참조인 id만을 갖고 있을 수 있겠네요. 이를 간접 참조라고도 합니다.
프로젝트 구조를 도메인 중심적으로 가져가기로 했고,
그 도메인 모델들이 경계가 구분되어서,
다른 패키지에 속했다면,
그 둘은 크게 서로 신경 쓸 필요가 없다는 의미입니다.
이 정도의 질의(메시지)는 던져볼 수 있겠네요. 👍
나 아이디가 1인 예약인데, 내 결제는 아이디가 a래. a결제 결제 상태좀 알려줄래?
이렇게 고민을 깊게 해보는 자세 정말 좋습니다.
질문하신 내용이 이 미션에서 고민할 수 있는 영역 중 꽤 고수준의 영역이라고 생각합니다.
특히 3번 질문답에는 너무 깊게 매몰되기 보다는 그렇구나 정도로 넘어가도 좋겠어요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
모두가 봤으면 하는 훌륭한 리뷰에요.
좋은 질문과 좋은 답변입니다..👍 감사합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저의 가려웠던 부분을 시원하게 긁어 주는 리뷰였습니다 :) 감사드립니다...
계속해서 보고 또 보는 것 같네요 ㅎㅎ