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

Conversation

davidolleh
Copy link

@davidolleh davidolleh commented Nov 13, 2024

안녕하세요! 잘 부탁드립니다~
미션 하면서 생겨났던 고민, 질문사항 README에 담아두었습니다!

@davidolleh davidolleh closed this Nov 13, 2024
@davidolleh davidolleh reopened this Nov 13, 2024
@davidolleh davidolleh changed the title 안녕하세요! 잘 부탁드립니다~ [Spring JDBC] 신지훈 미션 제출합니다. Nov 13, 2024
@davidolleh davidolleh changed the title [Spring JDBC] 신지훈 미션 제출합니다. [Spring JDBC] 황승준 미션 제출합니다. Nov 13, 2024
Copy link

@be-student be-student left a comment

Choose a reason for hiding this comment

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

일단 어프루브를 누르기는 했지만, 더 학습해보시고 싶거나 한 부분이 있다면 언제든 고쳐서 와주시면 계속 봐드리겠습니다 🙇
수정해주시고 dm 으로 요청해주세요!

- [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 하는 경우는 거의 없다고 보셔도 될 것 같아요!

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

Choose a reason for hiding this comment

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

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

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

Comment on lines 22 to 29
@GetMapping("/reservation")
public String reservationPage() {
return "reservation";
}

@GetMapping("/reservations")
@ResponseBody
public ResponseEntity<List<ReservationResponseDto>> readReservations() {

Choose a reason for hiding this comment

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

요 2가지는 서로 분리하면 어떨까요?
view 를 반환하는 컨트롤러와 json 을 반환하는 메소드가 한 클래스에 같이 있을 이유는 딱히 없을 것 같아요

Comment on lines 30 to 32
return ResponseEntity
.status(HttpStatus.OK)
.body(

Choose a reason for hiding this comment

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

ResponseEntity.ok(객체)
이런 형태로 단순화 가능할 것 같아요!

Comment on lines +33 to +35
reservationService.readReservations().stream().
map(ReservationResponseDto::fromEntity)
.toList()

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 를 넘겨주는 작업을 하긴 했던 것 같아요

Comment on lines 78 to 80
if (count == 0) {
throw new EntityNotFoundException("해당 예약은 존재하지 않스빈다");
}

Choose a reason for hiding this comment

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

이 부분은 구현의 취향 차이인데요
혹시 예외를 던지게 된 이유에 대해서 알 수 있을까요?
결과적으로 db 에는 예약이 없다는 상태가 되는 것은 동일하잖아요?

db 에 id 1 있음 -> 1 삭제 -> db 에 id 1 없음
db 에 id 1 없음 -> 1 삭제 -> db 에 id 1 없음

이런 것들을 보통 멱등성이라고 표현하는데요
1이 없는 상황에서도 1을 삭제하려고 했을 때 예외를 던지는 것은 멱등하지 않은 코드를 짠 것이죠
어느 것이 무조건 맞다는 정답은 없으니 어떤 것을 선택했을 때 그 이유가 있으면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

db 에 id 1 있음 -> 1 삭제 -> db 에 id 1 없음
db 에 id 1 없음 -> 1 삭제 -> db 에 id 1 없음

혹시 이부분에서

db 에 id 1 있음 -> 1 삭제 -> db 에 id 1 없어짐
db 에 id 1 없음 -> 1 삭제 -> db 에 id 1 없음
이지 않나요?

변경된 즉 delete된 행이 있다면
int count = jdbcTemplate.update(query, id);
이것은 0이 아닌 다른 값을 반환할 것이고
변경된 행이 없다면 0을 반환하는 것으로 알고 있는데
삭제하려는 행이 없다는 것 즉 entity가 db에 없다고 알릴 수 있는 방법 아닌가요?

조금 헷갈려서 질문드립니다!

Choose a reason for hiding this comment

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

저는 count 를 반환해주는 것 자체는 좋다고 생각합니다
그런데 이게 과연 예외 상황인가? 에 대해서는 정확하게 잘 모르겠어요
보통 무언가의 이유로 네트워크 응답을 못받고, 재요청하는 것은 아주 잦은 상황이다보니
이런 상황에서 이미 지워진 row를 다시 지우는 요청을 실패로 처리하는 것이 좋을지 아니면 성공으로 처리하는 것이 좋을지에 대해서는 천천히 고민해보셔도 좋을 것 같아요
저도 아직 잘 모르겠네요

Comment on lines 39 to 47
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
query,
new String[]{"id"});
ps.setString(1, reservation.getPerson().getName());
ps.setString(2, reservation.getDate().format(CustomDateTimeFormat.dateFormatter));
ps.setString(3, reservation.getTime().format(CustomDateTimeFormat.timeFormatter));
return ps;
}, keyHolder);

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.

오 schema.sql 까지 두시다니 진짜 많이 학습하신 티가 나네요

그러면 한 단계 더 가서 data.sql 을 통해서 초기화 하는 방법도 학습을 해보셨을까요?
혹은 다른 방식으로 초기 데이터를 넣어주는 과정도 학습을 해보셨을까요?
ex) ApplicationRunner, ConsoleRunner, data.sql 로 inmemoryReservationRepository 처럼 초기화를 진행해주셔도 좋을 것 같아요

Copy link
Author

@davidolleh davidolleh Dec 1, 2024

Choose a reason for hiding this comment

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

데이터를 초기화 하는 방법이 ApplicationRunner, ConsoleRunner, data.sq로 정말 다양하게 있더군요...

이번 미션에서는 data.sql을 사용해서 데이터를 초기화 하는 연습을 진행해 봤습니다!

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)

Choose a reason for hiding this comment

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

https://velog.io/@tjddus0302/Spring-DirtiesContext
dirtiest context 는 성능상 단점이 있다보니 성능상 단점이 있는데요
다른 방식으로 테스트간 격리를 진행해주실 수 있으실까요?

Copy link
Author

Choose a reason for hiding this comment

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

이것은 따로 공부가 길게 필요할거 같아 공부를 조금 뒤로 미뤄놨습니다! 꼭 공부해 보겠습니다~

Copy link
Author

@davidolleh davidolleh Dec 1, 2024

Choose a reason for hiding this comment

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

dirtiest context 대신 @Sql, InitializingBean등 다양한 방법이 있더군요!
@Sql, InitializingBean 둘 중 InitializingBean은 EntitiyManager를 사용해야 되어서 @Sql을 사용하기로 했습니다

다만 @Sql을 사용하여 truncate 문과 함께 reservation id의 auto_increament 값을 1로 초기화 하는 mysql문도 함께 사용하고 싶었지만 auto_increament 문법이 먹히지 않았습니다!
혹시 truncate 대신 drop table + create table을 사용하는 것은 어떻게 생각하시나요?

Choose a reason for hiding this comment

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

h2 와 mysql 의 차이인데요
h2 의 경우에는 truncate 시에 id 를 1로 롤백해주게 됩니다
mysql 의 경우에는 아니고요

저는 직접 id=1 과 같은 것을 지정해주는 것 보다는 자동으로 증가하는 id 이고, 1이 아닐 수 있다는 것을 생각해서 isEqualTo(1) 과 같은 코드를 사용하지 않는 것이 좋은 방향일 것 같아요

보통 truncate 를 매번 하게 되면개발환경 데이터가 너무 자주 날아가서 세팅이 귀찮아지는 문제가 있어서 저는 둘다 좋아하지 않습니다


@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@SuppressWarnings("NonAsciiCharacters")

Choose a reason for hiding this comment

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

이거 보기 좋네요 👍

@davidolleh
Copy link
Author

안녕하세요 피드백 반영하면서 생겨났던 질문들 댓글로 남겨보았습니다!

Copy link

@be-student be-student left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!

@@ -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.

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

Comment on lines +33 to +35
reservationService.readReservations().stream().
map(ReservationResponseDto::fromEntity)
.toList()

Choose a reason for hiding this comment

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

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

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

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.

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

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

import java.util.List;

@Repository
public class JDBCReservationRepositoryImpl implements ReservationRepository {

Choose a reason for hiding this comment

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

넵 저는 그렇게 생각합니다
사람마다 기준은 정말 많이 달라서 다르게 볼 수도 있는 영역이에요

}

@Override
public Reservation findById(Long id) {

Choose a reason for hiding this comment

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

  1. 혹시 시도해봤던 코드는 어떤 것인지 알 수 있을까요?

  2. 이 경우에는 상황마다 다를 것 같아요
    보통 Service 에서 Controller 로 널이 이동되는 케이스라면 실제 클라이언트로의 응답에서도 null 로 보내는 케이스일 것 같아서, 이런 케이스는 딱히 처리하지 않을 것 같아요

{
 null
}

과 같은 형태는 json 이 아니다보니 당연히 안될거고요

{
 response:null
}

와 같은 형태로 보통 표현하게 될 것 같아요

보통 null 처리 로직은 service 에서 null 이거나, entity이거나를 넘겨주고
이를 적용해주신 것처럼 dto 나 controller 레이어에 추가하는 것도 자주 봤던 것 같아요
아니면 service에서 dto 를 미리 다 만들어주는 것도 자주 쓰는 것 같고요

@SuppressWarnings("NonAsciiCharacters")
public class JDBCStepTest {

private static final Logger log = LoggerUtils.logger(JDBCStepTest.class);

Choose a reason for hiding this comment

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

보통 자바의 lombok 을 쓴다는 가정하에 @Slf4j 어노테이션으로 log.info 같은 것을 바로 사용할 수 있을 것 같아요

아니라면 private static final Logger log = LoggerFactory.getClass(this.class);
같이 가져가는 것도 좋을 것 같아요

import org.slf4j.LoggerFactory;

public class LoggerUtils {
public static <T> Logger logger(Class<T> clazz) {

Choose a reason for hiding this comment

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

보통 Util 의 경우에는 최대한 사용하지 않는 것을 권장드려요
보통 이 util 이 모이고 모여서 아무도 수정할 수 없는 영역을 만들어 내게 되더라고요

Comment on lines 7 to 18
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}

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

Choose a reason for hiding this comment

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

보통 기본 appender 에서 사용되는 정보나 예전에 강의때 설명드린
MDC 정보를 활용해서 로깅하는 것이 일반적인 것 같아요
traceId 로 검색하시면 될 것 같아요

로그 레벨은 아주 잘 설정해주신 것 같아요

Comment on lines 78 to 80
if (count == 0) {
throw new EntityNotFoundException("해당 예약은 존재하지 않스빈다");
}

Choose a reason for hiding this comment

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

저는 count 를 반환해주는 것 자체는 좋다고 생각합니다
그런데 이게 과연 예외 상황인가? 에 대해서는 정확하게 잘 모르겠어요
보통 무언가의 이유로 네트워크 응답을 못받고, 재요청하는 것은 아주 잦은 상황이다보니
이런 상황에서 이미 지워진 row를 다시 지우는 요청을 실패로 처리하는 것이 좋을지 아니면 성공으로 처리하는 것이 좋을지에 대해서는 천천히 고민해보셔도 좋을 것 같아요
저도 아직 잘 모르겠네요

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)

Choose a reason for hiding this comment

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

h2 와 mysql 의 차이인데요
h2 의 경우에는 truncate 시에 id 를 1로 롤백해주게 됩니다
mysql 의 경우에는 아니고요

저는 직접 id=1 과 같은 것을 지정해주는 것 보다는 자동으로 증가하는 id 이고, 1이 아닐 수 있다는 것을 생각해서 isEqualTo(1) 과 같은 코드를 사용하지 않는 것이 좋은 방향일 것 같아요

보통 truncate 를 매번 하게 되면개발환경 데이터가 너무 자주 날아가서 세팅이 귀찮아지는 문제가 있어서 저는 둘다 좋아하지 않습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants