-
Notifications
You must be signed in to change notification settings - Fork 82
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
[1 - 2단계 방탈출 예약 대기] 몰리(김지민) 미션 제출합니다. #63
Conversation
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
…필드가 형식 오류인지 알려주도록 변경 Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
Co-authored-by: HaiSeong <[email protected]>
reservation.getTheme().getName(), | ||
reservation.getDate(), | ||
reservation.getReservationTime().getStartAt(), | ||
"예약"); |
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.
이번 요구사항에 내 예약 조회 API가 추가되면서 응답 예시가 제시되어 있었습니다.
status라는 예약의 상태를 저장하는 응답 필드가 추가되었는데요!
응답 예시만 파악했을 때, 제시된 예약 필드의 의미를 파악할 수 없었습니다.
- 완료인지 대기인지를 나타내는지 (ex. 완료, 대기)
- 예약 완료인지 혹은 몇 번째 대기인지를 나타내는지 (ex. 예약, 3번째 대기)
따라서 페어와의 상의 후 응답 필드에만 status를 추가하고 값을 하드코딩했습니다.
수월한 다음 단계를 위해 요구사항이 아직 명확하지 않더라도 상태에 대한 필드 또는 객체를 고려하는 것이 좋을까요?
피드백 주시면 반영하겠습니다😀
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.
실제 상황이었다면 기획자와 논의해서 요구사항을 명확히 하고 개발을 이어갔겠지만 미션이라 그렇게 할 수가 없네요 😅 저도 이 부분이 애매하다 생각했는데 몰리와 페어의 의견이 우선 하드코딩 후 요구사항이 명확해지면 구현하는 것이라면 저도 좋습니다. 오히려 불필요한 시간을 줄일 수도 있겠네요. 그렇게 가시죠~
@@ -22,4 +25,9 @@ public MemberController(final MemberService memberService) { | |||
public ResponseEntity<List<FindMembersResponse>> getMembers() { | |||
return ResponseEntity.ok(memberService.getMembers()); | |||
} | |||
|
|||
@GetMapping("/reservations") |
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.
내 예약 조회 API의 endpoint를 /members/reservations
과 같이 작성한 이유
처음 페어와 논의를 할 때, /reservation/members
또는 /reservation/my
처럼 조회하려는 자원이 먼저 오는 것이 어떤가 하는 의견을 들었었는데요.
이 부분에는 동의를 했지만, 내 예약 조회와 같은 API는 마이페이지의 느낌이 강하다고 생각했습니다.
요구사항이 변경되어 내 결제 조회 같은 API들이 추가된다면 "동일하게 /members 아래에 위치하는 것이 조금 더 통일성이 있지 않을까", 또 코드 상에서도 "한 컨트롤러에 있는 것이 관리하기 수월할 것 같다."는 생각을 했습니다.
이 설계나 의견에 대한 파랑의 생각이 궁금합니다!
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.
로그인한 사용자의 자원을 가져오는 경우에 /members
를 앞에 붙였다는 말씀이죠? 저는 좋습니다! 현재 존재하는 나의 예약이나 앞으로 추가될지 모르는 나의 결제내역, 내가 작성한 리뷰 등을 생각해봤을 때 members가 앞에 붙으면 의도한 바를 쉽게 파악할 수 있을 것 같아요. 👍 필요에 따라 path를 통해 로직을 추가해주기도 쉽구요 (ex /members/**
는 LoginInterceptor를 타게 만든다거나..)
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.
몰리 안녕하세요~ 파랑입니다 🐳
JPA를 잘 적용해주셨습니다 👍 몇 가지 리뷰와 질문 남겨놓았어요. 확인해보시고 궁금한 점 있으면 코멘트나 DM으로 편하게 질문주세요~ 이번 미션도 잘 끝내봅시당 파이팅💪
System.out.println(exceptionMessage); | ||
return ResponseEntity.status(HttpStatus.BAD_REQUEST) | ||
.body(ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exceptionMessage)); | ||
public ResponseEntity<ProblemDetail> catchHttpMessageNotReadableException(HttpMessageNotReadableException ex) { |
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.
오호 ProblemDetail
이런 클래스도 있군요!
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.
저번 강의 시간에 배웠습니다 ㅎㅎ
스프링 6에서 추가됐고, 해당 객체를 사용하면 RFC 7807에서 정의된 API 예외 응답 포맷과 동일하게 예외에 대한 정보를 자세하게 클라이언트에게 전달할 수 있다고 하더라구요! 😀
@@ -22,4 +25,9 @@ public MemberController(final MemberService memberService) { | |||
public ResponseEntity<List<FindMembersResponse>> getMembers() { | |||
return ResponseEntity.ok(memberService.getMembers()); | |||
} | |||
|
|||
@GetMapping("/reservations") |
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.
로그인한 사용자의 자원을 가져오는 경우에 /members
를 앞에 붙였다는 말씀이죠? 저는 좋습니다! 현재 존재하는 나의 예약이나 앞으로 추가될지 모르는 나의 결제내역, 내가 작성한 리뷰 등을 생각해봤을 때 members가 앞에 붙으면 의도한 바를 쉽게 파악할 수 있을 것 같아요. 👍 필요에 따라 path를 통해 로직을 추가해주기도 쉽구요 (ex /members/**
는 LoginInterceptor를 타게 만든다거나..)
public Role getMemberRole() { | ||
return role; |
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.
getMemberRole
과 getRole
메서드가 중복되네요.
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.
헉 MemberRole을 rename하면서 실수가 있었네요 😭
바로 수정하겠습니다!! 🙇
reservation.getTheme().getName(), | ||
reservation.getDate(), | ||
reservation.getReservationTime().getStartAt(), | ||
"예약"); |
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.
실제 상황이었다면 기획자와 논의해서 요구사항을 명확히 하고 개발을 이어갔겠지만 미션이라 그렇게 할 수가 없네요 😅 저도 이 부분이 애매하다 생각했는데 몰리와 페어의 의견이 우선 하드코딩 후 요구사항이 명확해지면 구현하는 것이라면 저도 좋습니다. 오히려 불필요한 시간을 줄일 수도 있겠네요. 그렇게 가시죠~
} | ||
|
||
public List<FindMembersResponse> getMembers() { | ||
return memberRepository.findAll().stream() | ||
.map(FindMembersResponse::of) | ||
.toList(); | ||
} | ||
|
||
public List<FindReservationResponse> getReservationsByMember(final AuthInfo authInfo) { |
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.
path 맨 앞에 /members
가 붙었다고 회원의 예약을 가져오는 기능도 MemberService
에 있어야 하는지는 의문이네요. 회원의 예약을 가져오는 기능, 리뷰를 가져오는 기능, 결제내역을 가져오는 기능이 생기면 모두 MemberService에 들어가야 할까요? Member 도메인이 다른 모든 도메인을 의존하게 되는데 문제는 없을까요? 좀 더 고민해보시죠~
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.
다시 생각해보았을 때, 결국 이 요청에서 처리하려는 리소스는 Reservation이기 때문에 ReservationService에서 로직을 처리하는 것이 맞다는 생각이 들었어요. 위치 변경하겠습니다!
Member 도메인이 다른 모든 도메인을 의존하게 된다는 말의 의미를,
Reservation 리소스에 대한 처리를 MemberService가 하게 되어 MemberService의 책임에 다른 도메인도 추가된다. 즉, 다른 도메인 때문에 MemberService가 영향을 받을 수도 있다. 라는 느낌으로 이해를 했는데, 제가 이해한 것이 맞을까요...?
제가 놓친 부분이 있거나 잘 이해하지 못한 부분이 있다면 가감없이 코멘트 부탁드립니다 🙇
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.
넵넵 맞습니다. 단순 서비스 간의 의존을 넘어서 다른 도메인 간의 의존성이 생기게 되는 거죠. 기본적으로 회원이라는 도메인은 다른 모든 도메인에서 참조할 수 밖에 없는 도메인입니다. 로그인 기능만 생각하더라도 그렇겠죠. 그런데 회원 도메인도 다른 도메인들에게 의존해버리면 의존성이 양방향으로 생기게 됩니다. 도메인 간의 의존 방에 대해서도 고민해보면 좋을 것 같습니다!
@@ -40,8 +41,8 @@ public ResponseEntity<List<FindThemeResponse>> getThemes() { | |||
} | |||
|
|||
@GetMapping("/popular") | |||
public ResponseEntity<List<FindPopularThemesResponse>> getPopularThemes(@RequestParam(defaultValue = "10") int size) { | |||
return ResponseEntity.ok(themeService.getPopularThemes(size)); | |||
public ResponseEntity<List<FindPopularThemesResponse>> getPopularThemes(@PageableDefault Pageable pageable) { |
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.
PageableDefault
는 무엇을 위한 애노테이션인가요? 디폴드 값은 무엇인가요?
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.
우선 클라이언트가 request parameters로 sort(정렬할 속성), direction(정렬 방향), page(현재 페이지 번호), size(한 페이지에 표시할 데이터의 크기)
에 대해 값을 지정해주면, PageableHandlerMethodArgumentResolver
가 작동하여 조건에 맞게 생성한 Pageable
객체를 반환받을 수 있어요.
개발자는 전달 받은 Pageable
객체를 JpaRepository
에 인자로 전달하면, 페이징 정보와 페이징 처리가 된 데이터 목록을 받을 수 있는 것으로 알고 있어요.
Pageable
을 사용한 이유는 현재 요구사항은 상위 10개의 테마를 조회하지만, 자주 변경될 수 있는 조건이라고 생각하여 변경이 될 경우에도 페이징 처리를 쉽게 하기 위해 사용을 했습니다😀
@PageableDefault
는 클라이언트가 Pageable에 대한 조건들을 지정하지 않았을 경우에, 각각의 조건들에 대해 Default로 값을 설정해둘 수 있는 어노테이션으로, 디폴트 값은 다음과 같아요.
page: 0
size: 10
sort: 없음 (정렬이 적용되지 않음)
direction: 오름차순

현재 요구사항은 10개의 데이터만을 조회하고, 정렬은 Query를 통해 조회하므로 디폴트값을 그대로 사용해도 될 것 같아 값들을 지정하지는 않았습니다!
spring.jpa.ddl-auto=create-drop | ||
spring.jpa.defer-datasource-initialization=true |
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.
-
spring.datasource.url
- 애플리케이션에서 사용하는 datasource의 url을 나타냅니다.
- jdbc:h2:mem:database 는 jdbc -> DB에 접근하기 위한 API, h2 -> 사용하려는 DB 유형, mem -> inmemory 모드로 실행할 것, database -> DB 이름이 "database"를 의미합니다.
-
spring.h2.console.enabledl
- H2 데이터베이스는 브라우저 형태의 콘솔을 제공하는데, Spring Boot에서는 H2 데이터베이스 콘솔 사용이 기본적으로 비활성화되어 있어 활성화 시키는 옵션입니다.
-
spring.jpa.properties.hibernate.show_sql
- Hibernate가 생성하는 SQL 쿼리문을 로그로 출력하는 설정값입니다.
-
spring.jpa.properties.hibernate.format_sql
- Hibernate가 생성하는 SQL 쿼리문을 읽기 쉽게 포맷팅하는 설정값입니다.
-
spring.jpa.ddl-auto
-
애플리케이션 시작 시 DB를 초기화할 것인지를 지정할 수 있는 옵션입니다.
-
옵션은 none, validate, update, create, create-drop으로 설정할 수 있지만, 프러덕션 DB에서는 보통 validate나 none로 실행하여 쿼리문이 올바른지 검증만 하거나, 재실행하지 않도록 해야 데이터가 날라가지 않습니다.
이번에 다시 한번 학습을 하면서 Spring Boot가 내장 데이터베이스인 h2를 사용하고 Flyway나 Liquibase를 사용하지 않는다면, 기본값으로 create-drop을 설정한다는 것을 알 수 있습니다..!
-
-
spring.jpa.defer-datasource-initialization
- 이번에 처음 알게된 설정값인데요.
- Spring boot는 기본적으로 스크립트 기반
(data.sql(더미데이터)이나 schema.sql(스키마) 같은 파일)DataSource 초기화는 모든 JPA EntityManagerFactory 빈이 생성되기 전에 수행됩니다 - 그런데 Jpa 사용 시, Hibernate 등의 ORM 프레임워크에 의한 스키마 생성이 완료된 후에 스크립트 기반의 데이터 소스 초기화이 되어야 하기 때문에 스키마 생성 후 Script 파일을 수행하도록 지연시키는 설정값입니다.
-
logging.level.org.hibernate.orm.jdbc.bind
- 쿼리문이 날라갈 때 바인딩 되는 파라미터 값을 로그에 출력해주는 설정값입니다!
- 쿼리 확인 시 값이 제대로 들어갔는지를 쉽게 확인하기 위해 추가하였습니다.
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.
비록 지금 서비스를 운영하고 있지는 않지만 운영 환경이라 가정하고 설정을 수정해보는 건 어떨까요?
ddl-auto
와 함께 쿼리가 실행될 때마다 운영환경에서도 쿼리를 출력하는 게 맞는지 고민해보면 좋을 것 같습니다.
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.
현재 가장 많은 쿼리를 출력하는 예약 생성 기능에서만 보더라도 쿼리가 4개가 (ReservationTime, Theme, Member 조회, Reservation 추가) 가 발생하도록 되어있는데요.
한 유저가 여러번의 예약을 생성하고, 더 나아가 그 유저 자체도 수없이 많아진다면, 로그들이 너무 많이 출력되어 오히려 중요한 에러 로그를 확인하지 못하던가 오버헤드가 발생할 수도 있을 것 같아요. 사실 개발 환경이 아닌, 운영 환경에서의 쿼리를 확인할 일도 많이 없을 것 같다는 생각도 듭니다...!
현재는 운영/개발 환경이 분리되어 있지 않기 때문에 파랑의 말씀처럼 운영 단계를 가정하면서 개발하고, 환경을 분리해놓은 테스트에서만 확인 용도로 옵션을 유지해도 좋을 것 같네요!
좋은 피드백 너무 감사합니다 🙇
jdbcTemplate.update("insert into reservation_time (start_at) values ('20:00')"); | ||
jdbcTemplate.update( | ||
"insert into member (name, role, email, password) values ( '몰리', 'USER', '[email protected]', 'hihi')"); | ||
// themeRepository.save(new Theme( "테마이름", "설명", "썸네일")); |
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.
이전 미션에서 테스트를 할 때, 더미데이터를 사용했었는데요.
제대로 활용하지 못했어서 이미 저장된 데이터인지 등 테스트 내부가 아닌 외부 코드들도 확인을 해야하는 것들이 번거롭더라구요😭
그 이후로는 최대한 하나의 메서드만 확인해도 모든 상황을 파악할 수 있도록 하는 것에 신경을 쓰고자 했었습니다
예시로 예약 생성 시에 필요한 자원들이 3개여서 아래 코드가 중복적으로 나타났었는데요.
themeRepository.save(new Theme( "테마이름", "설명", "썸네일"));
reservationTimeRepository.save(new ReservationTime(LocalTime.of(20, 0)));
memberRepository.save(new Member("몰리", Role.USER, "[email protected]", "hihi"));
각 자원이 없는 경우에 대한 예외 테스트를 할 때, 해당 자원이 저장되지 않았다는 것을 주석 처리하여 나타낸다면 "다른 개발자 입장에서 더 쉽게 테스트를 파악할 수 있지 않을까" 라는 생각에 의도적으로 주석을 사용하긴 했습니다..
그런데 주석의 성격 때문에 이런 제 의도를 담기는 어려운 것 같아서, 오히려 더 이해하기 혼란스러울수도 있을 것 같다는 생각도 들어요 🥲
제거하겠습니다! 🙇
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.
네 설명해주신 의도를 나타내려면 다른 방식으로 주석을 작성하는 게 좋을 것 같습니다. 이것만 봐서는 그냥 코딩하다 주석 처리하고 깜빡하고 안 지운 것 같아 보입니다 ㅎㅎ
public interface ReservationRepository { | ||
Reservation save(Reservation reservation); | ||
|
||
public interface ReservationRepository extends JpaRepository<Reservation, Long> { |
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.
Repository에 대한 테스트가 필요해보입니다~ 작성하신 메소드가 의도한대로 쿼리를 잘 만들어주는지 확인해보셨나요?
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.
Jpa로 변경하면서 쿼리 로그는 확인했는데 테스트를 업데이트 못했네요 🥲
추가하겠습니다!
import org.springframework.test.annotation.DirtiesContext; | ||
import org.springframework.test.context.ActiveProfiles; | ||
|
||
@Target({ElementType.TYPE}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@ActiveProfiles("test") | ||
@JdbcTest | ||
@DataJpaTest | ||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) |
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.
JpaRepositoryTest에서 @DirtiesContext
를 삭제하고 테스트를 돌리면 결과가 어떻게 되나요? 왜 그런 결과가 나올까요?
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.
@DirtiesContext
를 삭제할 경우 테스트 간에 격리가 발생하지 않아 실패하는 테스트들이 발생합니다.
기본적으로, 스프링 테스트에서는 한번 로딩되면 캐싱되어, 기존의 Context를 재활용하는데요.
@DirtiesContext
는 이 Context가 더러워졌을 경우를 감지하여 새 Context를 생성해줍니다. BEFORE_EACH_TEST_METHOD
옵션과 함께 사용 시, 각 테스트 메서드가 실행되기 전 컨텍스트를 Dirty 상태로 만들어 컨텍스트를 재생성하게 만듭니다.
따라서 테스트 간 ApplicationContext 를 격리해주기 위해 사용했습니다..!
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.
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.
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
옵션을 추가해서 확인해보니,
@SpringBootTest
만 실행 시
-
- 기본적으로
@SpringBootTest
는 실제 환경처럼 실행되기 때문에 각 save에 대해 Transaction이 Commit 되는 것을 확인할 수 있었습니다. - 또한 기본적으로
@SpringBootTest
는 컨텍스트를 캐싱하여 재활용합니다. 때문에 다른 테스트가 동일한 데이터 소스를 사용하므로 영향을 받아, 실패하게 됩니다.
@SpringBootTest
+@DirtiesContext(BEFORE_EACH_TEST_METHOD)
-
- 1번과 동일하게
@SpringBootTest
를 사용하므로 각 save에 대해 Transaction이 Commit 됩니다. - 하지만,
@DirtiesContext(BEFORE_EACH_TEST_METHOD)
의 영향으로, 1번처럼 같은 컨텍스트를 공유하지 않습니다. 매 테스트 메서드 실행 전 애플리케이션 컨텍스트를 더티한 상태로 만들어버려 애플리케이션 컨텍스트를 재생성합니다. - 따라서, 동일한 컨텍스트에서 실행되지 않아, 테스트 간 격리가 되고 테스트가 통과합니다.
@SpringBootTest
+@Transactional
로 변경 시@DataJpaTest
로 변경 시
따라서 @JpaRepositoryTest
에서는 @DataJpaTest
만 사용해도 되는 것을 알 수 있었습니다 😁
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.
안녕하세요 파랑 🐳
파랑의 꼼꼼한 리뷰 덕분에 무심코 지나갔던 것들도 다시 고민해볼 수 있었어요.
Jpa 사용 경험은 있지만 더 깊이 고민하고 새로운 사실들을 알아가는 과정들이 너무 재미있는 것 같습니다 😁
제가 놓친 부분이나, 부족한 부분이 있다면 가감없이 말씀 해주시면 감사하겠습니다!!! 🙇
이번 리뷰도 잘 부탁드립니다 🙌
jdbcTemplate.update("insert into reservation_time (start_at) values ('20:00')"); | ||
jdbcTemplate.update( | ||
"insert into member (name, role, email, password) values ( '몰리', 'USER', '[email protected]', 'hihi')"); | ||
// themeRepository.save(new Theme( "테마이름", "설명", "썸네일")); |
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개여서 아래 코드가 중복적으로 나타났었는데요.
themeRepository.save(new Theme( "테마이름", "설명", "썸네일"));
reservationTimeRepository.save(new ReservationTime(LocalTime.of(20, 0)));
memberRepository.save(new Member("몰리", Role.USER, "[email protected]", "hihi"));
각 자원이 없는 경우에 대한 예외 테스트를 할 때, 해당 자원이 저장되지 않았다는 것을 주석 처리하여 나타낸다면 "다른 개발자 입장에서 더 쉽게 테스트를 파악할 수 있지 않을까" 라는 생각에 의도적으로 주석을 사용하긴 했습니다..
그런데 주석의 성격 때문에 이런 제 의도를 담기는 어려운 것 같아서, 오히려 더 이해하기 혼란스러울수도 있을 것 같다는 생각도 들어요 🥲
제거하겠습니다! 🙇
public Role getMemberRole() { | ||
return role; |
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.
헉 MemberRole을 rename하면서 실수가 있었네요 😭
바로 수정하겠습니다!! 🙇
@@ -40,8 +41,8 @@ public ResponseEntity<List<FindThemeResponse>> getThemes() { | |||
} | |||
|
|||
@GetMapping("/popular") | |||
public ResponseEntity<List<FindPopularThemesResponse>> getPopularThemes(@RequestParam(defaultValue = "10") int size) { | |||
return ResponseEntity.ok(themeService.getPopularThemes(size)); | |||
public ResponseEntity<List<FindPopularThemesResponse>> getPopularThemes(@PageableDefault Pageable pageable) { |
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.
우선 클라이언트가 request parameters로 sort(정렬할 속성), direction(정렬 방향), page(현재 페이지 번호), size(한 페이지에 표시할 데이터의 크기)
에 대해 값을 지정해주면, PageableHandlerMethodArgumentResolver
가 작동하여 조건에 맞게 생성한 Pageable
객체를 반환받을 수 있어요.
개발자는 전달 받은 Pageable
객체를 JpaRepository
에 인자로 전달하면, 페이징 정보와 페이징 처리가 된 데이터 목록을 받을 수 있는 것으로 알고 있어요.
Pageable
을 사용한 이유는 현재 요구사항은 상위 10개의 테마를 조회하지만, 자주 변경될 수 있는 조건이라고 생각하여 변경이 될 경우에도 페이징 처리를 쉽게 하기 위해 사용을 했습니다😀
@PageableDefault
는 클라이언트가 Pageable에 대한 조건들을 지정하지 않았을 경우에, 각각의 조건들에 대해 Default로 값을 설정해둘 수 있는 어노테이션으로, 디폴트 값은 다음과 같아요.
page: 0
size: 10
sort: 없음 (정렬이 적용되지 않음)
direction: 오름차순

현재 요구사항은 10개의 데이터만을 조회하고, 정렬은 Query를 통해 조회하므로 디폴트값을 그대로 사용해도 될 것 같아 값들을 지정하지는 않았습니다!
System.out.println(exceptionMessage); | ||
return ResponseEntity.status(HttpStatus.BAD_REQUEST) | ||
.body(ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exceptionMessage)); | ||
public ResponseEntity<ProblemDetail> catchHttpMessageNotReadableException(HttpMessageNotReadableException ex) { |
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.
저번 강의 시간에 배웠습니다 ㅎㅎ
스프링 6에서 추가됐고, 해당 객체를 사용하면 RFC 7807에서 정의된 API 예외 응답 포맷과 동일하게 예외에 대한 정보를 자세하게 클라이언트에게 전달할 수 있다고 하더라구요! 😀
public interface ReservationRepository { | ||
Reservation save(Reservation reservation); | ||
|
||
public interface ReservationRepository extends JpaRepository<Reservation, Long> { |
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.
Jpa로 변경하면서 쿼리 로그는 확인했는데 테스트를 업데이트 못했네요 🥲
추가하겠습니다!
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) |
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.
FetchType이 EAGER인 경우, 엔티티를 조회할 때 연관된 엔티티도 함께 조회합니다.
즉, 부모 엔티티를 조회하면 자동으로 자식 엔티티도 함께 로드됩니다.
때문에 부모 엔티티 조회 시에 무조건, 부모 엔티티 쿼리 1개 + 자식 엔티티 조회 쿼리 n개(자식 엔티티 갯수)가 날라가게 됩니다.
반면에 FetchType이 LAZY 인 경우 엔티티를 조회할 때 연관된 엔티티는 실제 사용될 때 조회하는데요.
부모 엔티티를 조회하면 자식 엔티티는 실제 사용될 때까지 로드하지 않기 때문에, 우선적으로는 부모 엔티티 쿼리 1개가 나가고,
자식 엔티티 사용 시점에 자식 엔티티의 쿼리가 날라가므로 자식 엔티티를 사용하지 않는 경우에는 쿼리의 수를 줄일 수 있다고 생각했습니다!
따라서 성능상 이점을 얻고자 LAZY로 설정하였습니다!
import org.springframework.test.annotation.DirtiesContext; | ||
import org.springframework.test.context.ActiveProfiles; | ||
|
||
@Target({ElementType.TYPE}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@ActiveProfiles("test") | ||
@JdbcTest | ||
@DataJpaTest | ||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) |
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.
@DirtiesContext
를 삭제할 경우 테스트 간에 격리가 발생하지 않아 실패하는 테스트들이 발생합니다.
기본적으로, 스프링 테스트에서는 한번 로딩되면 캐싱되어, 기존의 Context를 재활용하는데요.
@DirtiesContext
는 이 Context가 더러워졌을 경우를 감지하여 새 Context를 생성해줍니다. BEFORE_EACH_TEST_METHOD
옵션과 함께 사용 시, 각 테스트 메서드가 실행되기 전 컨텍스트를 Dirty 상태로 만들어 컨텍스트를 재생성하게 만듭니다.
따라서 테스트 간 ApplicationContext 를 격리해주기 위해 사용했습니다..!
spring.jpa.ddl-auto=create-drop | ||
spring.jpa.defer-datasource-initialization=true |
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.
-
spring.datasource.url
- 애플리케이션에서 사용하는 datasource의 url을 나타냅니다.
- jdbc:h2:mem:database 는 jdbc -> DB에 접근하기 위한 API, h2 -> 사용하려는 DB 유형, mem -> inmemory 모드로 실행할 것, database -> DB 이름이 "database"를 의미합니다.
-
spring.h2.console.enabledl
- H2 데이터베이스는 브라우저 형태의 콘솔을 제공하는데, Spring Boot에서는 H2 데이터베이스 콘솔 사용이 기본적으로 비활성화되어 있어 활성화 시키는 옵션입니다.
-
spring.jpa.properties.hibernate.show_sql
- Hibernate가 생성하는 SQL 쿼리문을 로그로 출력하는 설정값입니다.
-
spring.jpa.properties.hibernate.format_sql
- Hibernate가 생성하는 SQL 쿼리문을 읽기 쉽게 포맷팅하는 설정값입니다.
-
spring.jpa.ddl-auto
-
애플리케이션 시작 시 DB를 초기화할 것인지를 지정할 수 있는 옵션입니다.
-
옵션은 none, validate, update, create, create-drop으로 설정할 수 있지만, 프러덕션 DB에서는 보통 validate나 none로 실행하여 쿼리문이 올바른지 검증만 하거나, 재실행하지 않도록 해야 데이터가 날라가지 않습니다.
이번에 다시 한번 학습을 하면서 Spring Boot가 내장 데이터베이스인 h2를 사용하고 Flyway나 Liquibase를 사용하지 않는다면, 기본값으로 create-drop을 설정한다는 것을 알 수 있습니다..!
-
-
spring.jpa.defer-datasource-initialization
- 이번에 처음 알게된 설정값인데요.
- Spring boot는 기본적으로 스크립트 기반
(data.sql(더미데이터)이나 schema.sql(스키마) 같은 파일)DataSource 초기화는 모든 JPA EntityManagerFactory 빈이 생성되기 전에 수행됩니다 - 그런데 Jpa 사용 시, Hibernate 등의 ORM 프레임워크에 의한 스키마 생성이 완료된 후에 스크립트 기반의 데이터 소스 초기화이 되어야 하기 때문에 스키마 생성 후 Script 파일을 수행하도록 지연시키는 설정값입니다.
-
logging.level.org.hibernate.orm.jdbc.bind
- 쿼리문이 날라갈 때 바인딩 되는 파라미터 값을 로그에 출력해주는 설정값입니다!
- 쿼리 확인 시 값이 제대로 들어갔는지를 쉽게 확인하기 위해 추가하였습니다.
@Query(""" | ||
select r, m, rt, t | ||
from Reservation r | ||
join fetch Member m |
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.
일반 join과 달리 대상 객체와 함께 Fetch Join이 걸린 Entity에서도 대해 select를 할 수 있습니다.
현재 해당 메서드를 통해 응답할 때, 연관된 객체에 대해 전부 접근을 하는데요.(/reservation/controller/reponseFindReservationResponse
)
따라서 이런 연관 객체들을 조회해야 한다는 사실을 알고 있기 때문에 Reservation 조회 시, theme, member, reservationTime에 대한 추가 쿼리 없이, 한방 쿼리로 조회하는 것이 성능상으로 좋을 것 같다고 생각했습니다.
즉, 발생하는 쿼리 수를 줄이고자 join fetch
를 사용하였습니다..!
} | ||
|
||
public List<FindMembersResponse> getMembers() { | ||
return memberRepository.findAll().stream() | ||
.map(FindMembersResponse::of) | ||
.toList(); | ||
} | ||
|
||
public List<FindReservationResponse> getReservationsByMember(final AuthInfo authInfo) { |
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.
다시 생각해보았을 때, 결국 이 요청에서 처리하려는 리소스는 Reservation이기 때문에 ReservationService에서 로직을 처리하는 것이 맞다는 생각이 들었어요. 위치 변경하겠습니다!
Member 도메인이 다른 모든 도메인을 의존하게 된다는 말의 의미를,
Reservation 리소스에 대한 처리를 MemberService가 하게 되어 MemberService의 책임에 다른 도메인도 추가된다. 즉, 다른 도메인 때문에 MemberService가 영향을 받을 수도 있다. 라는 느낌으로 이해를 했는데, 제가 이해한 것이 맞을까요...?
제가 놓친 부분이 있거나 잘 이해하지 못한 부분이 있다면 가감없이 코멘트 부탁드립니다 🙇
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.
몰리 안녕하세요~
리뷰 잘 반영해주셨습니다! 꼼꼼히 달아주신 코멘트도 확인했어요.
추가 코멘트 드렸습니다. 다음 리뷰 요청에는 머지할게요. 👍
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) |
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개 + 자식 엔티티 조회 쿼리 n개(자식 엔티티 갯수)가 날라가게 됩니다.
어떤 상황에서 추가 쿼리가 실행되나요? 모든 상황인가요?
기본적으로 LAZY 로딩을 많이 사용하긴 하지만 EAGER가 더 유용한 경우도 있습니다.
추가로 LAZY 로딩을 사용했을 때 N+1 문제 외에도 신경써야할 부분이 있을까요? 트랜잭션과 관련해서 어떤 부분을 신경써야할지 학습해보는 것도 좋을 것 같아요 :) 너무 깊게 파거나 코드에 반영하려고 하진 마시고 시간을 정해두고 간단히만 알아보시죠~
} | ||
|
||
public List<FindMembersResponse> getMembers() { | ||
return memberRepository.findAll().stream() | ||
.map(FindMembersResponse::of) | ||
.toList(); | ||
} | ||
|
||
public List<FindReservationResponse> getReservationsByMember(final AuthInfo authInfo) { |
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.
넵넵 맞습니다. 단순 서비스 간의 의존을 넘어서 다른 도메인 간의 의존성이 생기게 되는 거죠. 기본적으로 회원이라는 도메인은 다른 모든 도메인에서 참조할 수 밖에 없는 도메인입니다. 로그인 기능만 생각하더라도 그렇겠죠. 그런데 회원 도메인도 다른 도메인들에게 의존해버리면 의존성이 양방향으로 생기게 됩니다. 도메인 간의 의존 방에 대해서도 고민해보면 좋을 것 같습니다!
spring.jpa.ddl-auto=create-drop | ||
spring.jpa.defer-datasource-initialization=true |
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.
비록 지금 서비스를 운영하고 있지는 않지만 운영 환경이라 가정하고 설정을 수정해보는 건 어떨까요?
ddl-auto
와 함께 쿼리가 실행될 때마다 운영환경에서도 쿼리를 출력하는 게 맞는지 고민해보면 좋을 것 같습니다.
jdbcTemplate.update("insert into reservation_time (start_at) values ('20:00')"); | ||
jdbcTemplate.update( | ||
"insert into member (name, role, email, password) values ( '몰리', 'USER', '[email protected]', 'hihi')"); | ||
// themeRepository.save(new Theme( "테마이름", "설명", "썸네일")); |
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.
네 설명해주신 의도를 나타내려면 다른 방식으로 주석을 작성하는 게 좋을 것 같습니다. 이것만 봐서는 그냥 코딩하다 주석 처리하고 깜빡하고 안 지운 것 같아 보입니다 ㅎㅎ
import org.springframework.test.annotation.DirtiesContext; | ||
import org.springframework.test.context.ActiveProfiles; | ||
|
||
@Target({ElementType.TYPE}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@ActiveProfiles("test") | ||
@JdbcTest | ||
@DataJpaTest | ||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) |
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.
실제로 제거 후 테스트 해보셨나용?
import roomescape.reservation.model.Reservation; | ||
import roomescape.reservation.repository.ReservationRepository; |
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.
사용하지 않는 import가 남아있네요 👀
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.
안녕하세요 파랑 🐬
저번 리뷰도 덕분에 놓쳤던 부분에 대해 다시 고민해볼 수 있던 시간을 가졌어요 😁
이번 리뷰도 잘 부탁드립니다 🙌
+) 이번 반영에서 추가 작성한 코멘트를 찾아보시기 힘드실 것 같아, 링크 첨부합니다.
#63 (comment)
#63 (comment)
#63 (comment)
#63 (comment)
#63 (comment)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) | ||
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) | ||
public @interface IntegrationTest { |
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.
#63 (comment) 에서 @SpringBootTest
와 @Transactional
에 대해 학습하다가, IntegrationTest
를 다시 보았는데요..! 🤔
IntegrationTest
에서는 2번처럼 @SpringBootTest
+ @DirtiesContext(BEFORE_EACH_TEST_METHOD)
를 사용했었어요.
#63 (comment) 의 2, 3번 사진에서 알 수 있듯이, @Transactional
과 사용할 때에 비해 컨텍스트를 매번 재생성하기 때문에, 거의 7배에 달하는 속도 차이가 난다는 것을 확인할 수 있었어요.
따라서 @IntegrationTest
에서 @DirtiesContext(BEFORE_EACH_TEST_METHOD)
을 제거하고 @Transactional
로 변경하고자 했는데요. 대부분의 통합 테스트가 실패했었습니다 😭
일단, 제 통합 테스트에서 실패했던 이유는 테스트 안에서의 save 메서드들이 전체적으로 실행되지 않다고 생각을 했고, 그 이유가
@Transactional
의 영향인 것 같아 찾아보았는데요.


➡️ 해당 어노테이션을 클래스 레벨에 붙이게 되어 메서드 단위로 트랜잭션이 적용
➡️ test 메서드 내의 save문이 Test worker
라는 쓰레드에서 실행
➡️ 그 후 테스트에서 보낸 요청을 처리하기 위해 실행된 서비스 계층의 테스트는 http-nio-auto-1-exec-1
이라는 쓰레드에서 실행
➡️ 즉, 서로 다른 쓰레드에서 각 로직을 실행
➡️ 그런데 @Transactional
이 메서드에 걸려있기 때문에 메서드가 끝나지 않는 이상, 트랜잭션이 적용 X
➡️ 즉, save문은 실행 됐더라도, 이를 다른 스레드에서 확인할 수 없기 때문에 테스트 실패
결과적으로 현재 RestAssured 처럼 실제 웹 요청을 보내는 경우에는 @SpringBootTest
와 @Transactional
조합이 적절하지 않다는 것을 알게 되었어요.@Transactional
은 매우매우 어렵고도 중요한 내용 같아요...😭
우선은 @DirtiesContext(BEFORE_EACH_TEST_METHOD)
로 사용하되, 통합테스트가 거대해진다면 TestExecutionListener이나 다른 방안에 대해 학습 해본 후 적용을 해보아야겠다는 결론을 내렸습니다..!
혹시 파랑은 테스트 시 DB 격리에 대해 어떤 방법을 주로 사용하시나요??
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.
RestAssured & @Transactional 방금 저장한 데이터를 못찾아요
오호 저도 찾아보니 요런 글도 있네요. 직접 테스트해본 것처럼 RestAssured
와 @Transactional
은 함께 사용하기 어려워 보여요. 이런 경우 기존처럼 @DirtiesContext(BEFORE_EACH_TEST_METHOD)
를 사용하거나 @Sql
애노테이션을 통해 SQL문을 직접 실행시키는 방법, BeforeTest와 AfterTest를 이용하여 직접 데이터 초기화하는 방법 등이 있을 것 같습니다. 지금 제일 간단하게 적용해볼 수 있는 건 @Sql
애노테이션일 것 같습니다. 물론 테스트 클래스 내부에서 어떤 데이터로 초기화가 이루어지는지 알 수 없다는 점은 단점이겠네요 :-)
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.
몰리 안녕하세요~
JPA 옵션부터 테스트까지 열심히 공부하셨네요 👍 1단계는 잘 완성해주셔서 여기서 머지하면 될 것 같아요. 수고하셨습니다 다음 단계에서 봬요~
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) |
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.
열심히 공부해오셨네요 👍 찾아보신 점들에 유의해서 상황에 맞게 옵션을 선택해서 사용하면 좋겠습니다!
import org.springframework.test.annotation.DirtiesContext; | ||
import org.springframework.test.context.ActiveProfiles; | ||
|
||
@Target({ElementType.TYPE}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@ActiveProfiles("test") | ||
@JdbcTest | ||
@DataJpaTest | ||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) |
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.
캡쳐본까지 👍 여러 케이스들을 잘 확인해주셨습니다 👏
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) | ||
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) | ||
public @interface IntegrationTest { |
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.
RestAssured & @Transactional 방금 저장한 데이터를 못찾아요
오호 저도 찾아보니 요런 글도 있네요. 직접 테스트해본 것처럼 RestAssured
와 @Transactional
은 함께 사용하기 어려워 보여요. 이런 경우 기존처럼 @DirtiesContext(BEFORE_EACH_TEST_METHOD)
를 사용하거나 @Sql
애노테이션을 통해 SQL문을 직접 실행시키는 방법, BeforeTest와 AfterTest를 이용하여 직접 데이터 초기화하는 방법 등이 있을 것 같습니다. 지금 제일 간단하게 적용해볼 수 있는 건 @Sql
애노테이션일 것 같습니다. 물론 테스트 클래스 내부에서 어떤 데이터로 초기화가 이루어지는지 알 수 없다는 점은 단점이겠네요 :-)
안녕하세요 파랑~ 🌊
이번 미션 리뷰 받게 된 몰리입니다 🙌
레벨1 때 리뷰를 받았었는데, 다시 뵙게 되어 영광입니다 ㅎㅎ
당시에는 객체지향적 사고를 힘들어 했어서 파랑과의 리뷰 시간을 잘 활용하지 못했던 것 같아 아쉬웠는데, 스프링에서 다시 파랑의 리뷰를 받게 되어 기쁘네요😆
이전의 웹 개발 경험
스프링을 활용하여 3번 정도의 팀 프로젝트를 경험해본 적이 있습니다.
스프링 학습을 기능 구현에 집중하여 사용법을 주로 학습했었어서, 레벨2 때는 전보다 깊게 스프링을 학습하고자 노력하고 있습니다.🔥
참고하실 부분
이번 단계 반영 커밋
패키지 구조
현재 패키지 구조는 도메인형으로 작성되어 있습니다.
코드 리뷰하실 때 미리 참고하시면 더 수월하실 것 같아요 😁
DTO 네이밍
패키지 구조와 관련해 도메인형 구조에서는, 행위가 먼저 나오는 것이 빠르게 인식할 수 있을 것이라고 생각하여 아래와 같은 컨벤션을 따라 작성하였습니다.
추가 질문 사항은 코멘트를 통해 작성하겠습니다.
리뷰 잘 부탁드립니다 🙂