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

[4 - 9단계 - 방탈출 예약 관리] 구름(김민수) 미션 제출합니다. #108

Merged
merged 29 commits into from
Apr 24, 2024

Conversation

alstn113
Copy link
Member

@alstn113 alstn113 commented Apr 22, 2024

안녕하세요 범블비! 구름 ⛅️ 입니다!

4-9 미션 구현은 오랜시간이 걸리지 않았던 것 같습니다.
거의 시간의 80퍼를 테스트를 어떻게 작성하면 좋을지 고민했던 것 같습니다.

데이터베이스와 함께 테스트를 해야했기 때문에 다음의 전략을 사용했습니다.

repository 부분은 @SpringBootTest@Transactional의 조합을 사용했습니다.
service 부분은 mockito를 이용해서 repository를 mocking해서 테스트했습니다.
controller부분은 BaseControllerTest에서 truncate query를 만들어서 실행했습니다.

어느 부분이 문제이고, 어떤 방법들이 있는지 메타인지가 되지 않아서 다양한 코드들을 찾아보면서 학습하고 있습니다.

다음으로 service 계층에서 dto를 리턴해야할 지, entity를 리턴해야할 지 모르겠습니다.
제 결론은 그냥 dto를 default로 하되, 부분적으로 entity를 쓰는게 맞다고 생각합니다. 왜냐하면 service에서 service를 사용하는 경우도 있기 때문입니다.
아니면 controller는 webDto를, service는 ServiceDto를 사용하는 방법도 있습니다. 그러나 이것은 너무 복잡해서 사용하지 않을 것 같습니다.

미리 리뷰 감사합니다. 범블비!

Copy link

@ddaaac ddaaac left a comment

Choose a reason for hiding this comment

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

안녕하세요 구름 👋

구현 잘 해주셨네요.

몇 가지 코멘트 남겼으니 확인해주세요.

import org.springframework.web.bind.annotation.RestController;
import roomescape.dto.ReservationRequest;
import roomescape.dto.ReservationResponse;
import roomescape.service.ReservationService;

@RestController
@RequestMapping("/reservations")
Copy link

Choose a reason for hiding this comment

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

@RequestMapping을 제거해주신 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

너무 당연하게 공통된 부분을 @RequestMapping으로 분리했었습니다.
그러나 전체 URL을 검색할 때 불편함이 있지 않겠냐는 말에 설득된 것 같습니다.

Comment on lines +21 to +23
public ReservationTimeController(ReservationTimeService reservationTimeService) {
this.reservationTimeService = reservationTimeService;
}
Copy link

Choose a reason for hiding this comment

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

알고 계실 수 있겠지만 생성자 주입에 대한 글을 읽어봐도 좋을 것 같네요.

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. 단일 책임의 원칙
  2. DI Container와의 낮은 결합도
  3. 필드의 불변성 보장
  4. 순환 의존 방지

다시 한 번 짚고 넘어 갑니다!

Comment on lines +41 to +54
public List<Reservation> findAll() {
String sql = """
SELECT
r.id as reservation_id,
r.name,
r.date,
t.id as time_id,
t.start_at as time_value
FROM reservation as r
inner join reservation_time as t
on r.time_id = t.id
""";

return jdbcTemplate.query(sql, rowMapper);
Copy link

Choose a reason for hiding this comment

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

ReservationTimeRepository도 존재하는 상황에서 ReservationRepository에서 findAll을 수행하면 ReservTime까지 join할거라는 사실을 클라이언트에서 쉽게 알 수 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

ReservationTime이 Reservation의 속성이라서 굳이 알 필요가 없다고 생각합니다.
만약에 진짜 바꿔야한다면 findAllWithTime으로 메서드 명을 변경할 것 같습니다!

Copy link

Choose a reason for hiding this comment

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

이번 미션에서 자세히 다룰 내용은 아니라 간단하게 코멘트 남겨보면
책임과 예측 가능성 측면서에서 한 번쯤 더 고민해보셔도 좋을 것 같네요.

try {
ReservationTime reservationTime = jdbcTemplate.queryForObject(sql, rowMapper, id);
return Optional.of(reservationTime);
} catch (EmptyResultDataAccessException e) {
Copy link

Choose a reason for hiding this comment

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

처리 꼼꼼하게 잘 해주셨네요 👍

@@ -4,6 +4,7 @@
import roomescape.domain.Reservation;

public interface ReservationRepository {
Copy link

Choose a reason for hiding this comment

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

Repository를 인터페이스로 분리해서 얻은 장점이 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

확장성이 좋습니다.
실제로 1-3단계에서 4-9단계로 쉽게 변경할 수 있었습니다.

Copy link

Choose a reason for hiding this comment

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

확장성이 좋다는게 구체적으로 어떤 의미일까요?
만약 ReservationRepository가 인터페이스가 아니고 클래스였을 때와 지금처럼 인터페이스일 때와는 어떤 차이가 있을까요?

import roomescape.repository.ReservationTimeRepository;

@Service
@Transactional(readOnly = true)
Copy link

Choose a reason for hiding this comment

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

@Transactional(readOnly = true)를 사용했을 때는 어떤 장점이 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

읽기 전용 트랙잭션임을 명시합니다.
데이터베이스에서 읽기 작업만 수행하고 데이터를 변경하지 않습니다.
이로 인해 성능 향상이 있습니다.

import roomescape.BaseTest;

@Sql(scripts = "classpath:truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
Copy link

Choose a reason for hiding this comment

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

테스트에서 DB 초기화 로직을 호출하는 건 위험합니다. 잘 와닿지 않을 수도 있는 얘기지만 테스트 환경에도 데이터들이 존재하는데요, 이런 데이터들이 테스트 한 번 돌린다고 모두 사라지면 곤란하거든요. 테스트로 생성된 데이터만 날리게 구현해주시면 좋을 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

초기 데이터들이 많을 경우에는 값을 세팅하는데 시간이 많이 들 것 같습니다.
이런 경우에는 truncate 코드를 지우고 @AfterEach를 이용해서 @BeforeEach를 통해 생성했던 값들을 지울 것 같습니다.

하지만 이번 미션같은 경우에는 truncate로 모든 값을 밀어버리는게 제일 단순해서 사용했던 것 같습니다.

Comment on lines 6 to 8
@Transactional
public abstract class BaseRepositoryTest extends BaseTest {
}
Copy link

Choose a reason for hiding this comment

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

테스트의 계층이 복잡해지면 어떤 문제가 생길지도 고민해보시면 좋을 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

같은 계층끼리 테스트에서 비슷한 코드가 생길 것 같다고 짐작해서 만들어봤는데 당장은 필요가 없을 것 같네요

Comment on lines 20 to 22
class ReservationServiceTest extends BaseServiceTest {

@Mock
Copy link

Choose a reason for hiding this comment

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

mocking과 transactional은 같이 쓰일 수 있을까요? 런던파 vs 고전파에 대한 글을 읽고 구름이 만든 테스트는 어떤 테스트를 하고 싶은건지 고민해보시면 좋을 것 같습니다

Copy link
Member Author

@alstn113 alstn113 Apr 23, 2024

Choose a reason for hiding this comment

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

repository는 @Transactional을 이용한 테스트 간의 격리는 고전파를 의도한 것 같습니다.
service는 Mock을 이용한 테스트 간의 격리는 런던파를 의도한 것 같습니다.

@Transactional을 잘못 붙인 것 같네요. 감사합니다!

Comment on lines +1 to +4
logging:
level:
org.springframework:
jdbc: DEBUG
Copy link

Choose a reason for hiding this comment

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

테스트 코드에만 debug log를 사용하게 만들어주셨군요 😀

@alstn113
Copy link
Member Author

컨트롤러에서는 @WebMvcTest를 이용해서 테스트를 진행했습니다.
인수테스트에서 다이나믹 테스트를 할려고 했는데 딱히 시나리오가 떠오르지 않네요.

Copy link

@ddaaac ddaaac left a comment

Choose a reason for hiding this comment

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

안녕하세요 구름 👋

리뷰 반영 잘 해주셨습니다.

몇 가지 코멘트 남겼으니 확인해주세요.

import roomescape.domain.ReservationTime;

public record ReservationTimeResponse(
Long id,
Copy link

Choose a reason for hiding this comment

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

프런트 코드를 자세히 보지는 않았는데 time의 id를 사용하는 곳이 있나요?

Copy link
Member Author

@alstn113 alstn113 Apr 23, 2024

Choose a reason for hiding this comment

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

삭제 할 때 필요해요 :)

@@ -4,6 +4,7 @@
import roomescape.domain.Reservation;

public interface ReservationRepository {
Copy link

Choose a reason for hiding this comment

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

확장성이 좋다는게 구체적으로 어떤 의미일까요?
만약 ReservationRepository가 인터페이스가 아니고 클래스였을 때와 지금처럼 인터페이스일 때와는 어떤 차이가 있을까요?

import roomescape.fixture.Fixture;

@SpringBootTest
@Transactional
Copy link

Choose a reason for hiding this comment

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

테스트에 사용하는 @Tx가 적당한가에는 여러 논의 들이 있습니다. 한 번쯤 고민해보셔도 좋을 것 같아요.

Copy link
Member Author

@alstn113 alstn113 Apr 23, 2024

Choose a reason for hiding this comment

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

확장성이 좋다는게 구체적으로 어떤 의미일까요?
만약 ReservationRepository가 인터페이스가 아니고 클래스였을 때와 지금처럼 인터페이스일 때와는 어떤 차이가 있을까요?

예를 들어, 인터페이스를 만들고 InMemoryRepository, JdbcRepository를 만들고 bean으로 등록해서 상황에 따라 다르게 사용할 수 있습니다.
클래스와는 다르게 다형성을 활용해 유연하게 전환할 수 있습니다.

테스트에 사용하는 @tx가 적당한가에는 여러 논의 들이 있습니다. 한 번쯤 고민해보셔도 좋을 것 같아요.

저 글에서 결론은 일반적인 트랜잭션 사용 코드가 아닌 트랜잭션과 관련된 코드에서는 주의가 필요하다는 것이겠군요.
다양한 방법들에 대해서 알았습니다. 감사합니다!

Comment on lines +48 to +49
BDDMockito.given(reservationService.getAllReservations())
.willReturn(List.of(ReservationResponse.from(reservation)));
Copy link

Choose a reason for hiding this comment

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

일반 mockito 대신 BDDMockito를 사용해주신 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

given - when - then을 통해 테스트의 가독성을 높여 더 의도를 명확히 하고 싶었습니다!

import roomescape.fixture.Fixture;
import roomescape.service.ReservationTimeService;

@WebMvcTest(ReservationTimeController.class)
Copy link

Choose a reason for hiding this comment

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

@SpringBootTest@WebMvcTest는 어떤 점이 다를까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

@SpringBootTest는 스프링 부트 전체 컨텍스트를 로드합니다. 그래서 실제 애플리케이션을 시작하고, 실제 데이터베이스와 통합하여 테스트할 수 있습니다.

@WebMvcTest는 웹 계층과 컨트롤러와 관련된 빈들만 로드합니다.
컨트롤러의 요청과 응답을 테스트하고, 컨트롤러에서 사용하는 서비스나 다른 컴포넌트들을 목킹하여 테스트할 수 있습니다.

@Test
@DisplayName("어드민 페이지를 조회한다.")
void adminPage() {
ExtractableResponse<Response> response = RestAssured.given().log().all()
Copy link

Choose a reason for hiding this comment

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

RestAssured와 mockMvc의 차이도 가볍게 정리하고 넘어가면 좋을 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

사용목적, 의존성, 속도, 목적 등의 차이를 다시 한 번 짚고 넘어갑니다!

Comment on lines +21 to +30
@BeforeEach
void environmentSetUp() {
RestAssured.port = port;
}

@AfterEach
public void environmentAfterEach() {
List<String> truncateQueries = getTruncateQueries(jdbcTemplate);
truncateTables(jdbcTemplate, truncateQueries);
}
Copy link

Choose a reason for hiding this comment

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

상속된 테스트에서 @BeforeEach @AfterEach는 어떻게 동작할까도 한 번쯤 체크해보시면 좋을 것 같습니다.

Copy link
Member Author

@alstn113 alstn113 Apr 24, 2024

Choose a reason for hiding this comment

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

상속관계에서 @BeforeEach@AfterEach는 다음의 순서로 실행되네요.

부모 클래스 @BeforeEach
자식 클래스 @BeforeEach
자식 클래스 @AfterEach
부모 클래스 @AfterEach

Comment on lines +28 to +29
@Autowired
private ObjectMapper objectMapper;
Copy link

Choose a reason for hiding this comment

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

스프링에서 주입시켜주는 ObjectMapper가 어디서 오는지도 가볍게 체크해보시면 좋을 것 같아요. 이 리뷰는 넘어가도 좋습니다.

Copy link

@ddaaac ddaaac left a comment

Choose a reason for hiding this comment

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

안녕하세요 구름 👋

구현 잘 해주셨습니다.
이번 단계는 머지해도 될 것 같네요.

추가적으로 궁금한 부분이 있으면 DM 주세요!

Comment on lines +25 to 29
@GetMapping("/reservations")
public ResponseEntity<List<ReservationResponse>> getAllReservations() {
List<ReservationResponse> reservationResponses = reservationService.getAllReservations();

return ResponseEntity.ok(reservationResponses);
Copy link

Choose a reason for hiding this comment

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

@RestController를 사용하고 있다면 꼭 통일성 부여를 위해 ResponseEntity를 사용할 필요는 없다고 생각해요. 200 상태코드를 사용하거나 헤더를 붙여줄 필요가 없다면 바로 객체를 반환하는게 보기 좋지 않을까도 고민해보시면 좋겠네요.

Comment on lines +41 to +54
public List<Reservation> findAll() {
String sql = """
SELECT
r.id as reservation_id,
r.name,
r.date,
t.id as time_id,
t.start_at as time_value
FROM reservation as r
inner join reservation_time as t
on r.time_id = t.id
""";

return jdbcTemplate.query(sql, rowMapper);
Copy link

Choose a reason for hiding this comment

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

이번 미션에서 자세히 다룰 내용은 아니라 간단하게 코멘트 남겨보면
책임과 예측 가능성 측면서에서 한 번쯤 더 고민해보셔도 좋을 것 같네요.

Comment on lines +16 to +18
@SpringBootTest
@Transactional
class JdbcReservationRepositoryTest {
Copy link

Choose a reason for hiding this comment

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

db 레이어를 테스트할 수 있는 다른 방법도 알아볼까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

Jbdc로 해봐야겠네요 ㅎㅎ

@ddaaac ddaaac merged commit 4f445dd into woowacourse:alstn113 Apr 24, 2024
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.

None yet

2 participants