안녕하세요. 우빈님! 캐시를 처음 적용해 보아서 캐시를 포함한 비지니스 로직을 어떻게 작성하는 것이 좋을까? 에 대한 고민이 있어 글 남깁니다...! AS IS @Transactional public UosRestaurantMenuResponse getUosRestaurantMenu(UosRestaurantInput input) { // 학식 조회 UosRestaurant findUosRestaurant = uosRestaurantRepository.findByCrawlingDateAndRestaurantNameAndMealType(input.getDate(), input.getRestaurantName(), input.getMealType()) .orElseThrow(() -> new UosRestaurantMenuException(UosRestaurantMenuException.NOT_FOUND_MENU)); // 조회수 증가 findUosRestaurant.increaseView(); return UosRestaurantMenuResponse.of(findUosRestaurant); } TO BE @Transactional public UosRestaurantMenuResponse getUosRestaurantMenu(UosRestaurantInput input) { // 캐시에서 학식 조회 Optional<CacheUosRestaurant> cacheUosRestaurant = cacheUosRestaurantRepository .findById(CacheUosRestaurant.createId(input)); // 캐시에 학식이 존재하면 if(cacheUosRestaurant.isPresent()) { // 조회수 증가 cacheUosRestaurant.get().increaseView(); CacheUosRestaurant saveCacheRestaurant = cacheUosRestaurantRepository.save(cacheUosRestaurant.get()); return UosRestaurantMenuResponse.of(saveCacheRestaurant); } // 학식 조회 UosRestaurant findUosRestaurant = uosRestaurantRepository.findByCrawlingDateAndRestaurantNameAndMealType(input.getDate(), input.getRestaurantName(), input.getMealType()) .orElseThrow(() -> new UosRestaurantMenuException(UosRestaurantMenuException.NOT_FOUND_MENU)); // 조회수 증가 findUosRestaurant.increaseView(); // 캐시에 저장 cacheUosRestaurantRepository.save(CacheUosRestaurant.of(findUosRestaurant)); return UosRestaurantMenuResponse.of(findUosRestaurant); } 캐시에 대한 로직이 추가될 때 위와 같이 하나의 서비스 레이어 메소드에 작성하는 것이 좋은걸까요? 캐시에 대한 테스트 코드를 작성할 때 캐시를 사용할 때와 캐시를 사용하지 않을 때를 상황을 구분하여 작성 하는 것이 맞을까요..?(그것이 좋겠죠....? -> 기존 비지니스 로직 테스트 코드를 수정하는것이 최선일까? 에대한 의문이 들어서 질문 드렸습니다.) 스프링에서 CacheManager를 이용하여 @Cacheable 을 활용하는 방법이 있는데, CrudRepository를 사용하는 방법과 CacheManager를 사용하는 방법 중 어느 것이 더 좋은(?) 방법 인지 말씀주시면 감사하겠습니다. 4. 마지막으로 캐시를 사용할 때 깔끔하게 비지니스 로직을 작성할 수 있는 노하우 말씀주시면 감사하겠습니다.!!! 다소 질문이 난해한데 너그럽게 이해해주시면 감사하겠습니다.ㅠㅠ 좋은 강의 잘 듣고 있습니다. 다음 강의도 기대할께요^^!! 감사합니다.
중형 테스트는 멀티 스레드, 멀티 프로세스를 허용하는 테스트라고 이해했는데 그 설명을 하시면서 "멀티 프로세스와 멀티 스레드를 사용할 수 있는 완화조건이 생기죠. 이는 다시 말해 h2 같은 테스트용 DB를 사용할 수 있게 된다는 의미입니다." 라고 하셔서요! 혹시 h2를 사용하려면 멀티 스레드나 멀티 프로세스 환경에 있어야 한다고 이해해도 되는걸까요? "h2는 멀티 스레드, 멀티 프로세스 환경에 있어야 하기 때문에 h2를 사용하는 순간 중형 테스트로 보면 된다"로 이해하면 될까요??
구글링을 해서 폐쇄망에 react 프로젝트를 어찌어찌 설치를 했습니다. 정상적으로 실행도 되고 프로그래밍도 가능합니다. 근데, 중간에 패키지를 추가하려고 인터넷이 가능한 곳에서 패키지를 추가해서 옮겨보려 했으나 잘 안되더군요. 폐쇄망에서 패키지 추가는 어떻게 해야 하는지 도움 부탁드립니다.
안녕하세요 강사님 현재 회사에서 진행중인 프로젝트에서 ControllerTestSupport를 생성하여 controllerTest시 스프링부트를 띄우는 횟수를 줄이기 위해 사용하였는데 테스트 속도가 줄어들거나 하지 않고 거의 똑같더라구요,, 그래도 스프링부트를 띄우는걸 최소화 하는게 나을까요? ControllerTestSupport 사용해야하는 더 나은 이점이 있는지 궁금합니다!
안녕하세요! 강의 너무 잘듣고있습니다. 소스코드 내에서 UuidHolder 와 User 의 관련 의존성 방향에 대해 질문이 있어 글 남깁니다 public static User from(UserCreate userCreate, UuidHolder uuidHolder) { return User.builder() .email(userCreate.getEmail()) .nickname(userCreate.getNickname()) .address(userCreate.getAddress()) .status(UserStatus.PENDING) .certificationCode(uuidHolder.random()) .build(); } 위 코드에서 Parameter값을 통해 User 객체가 UuidHolder 라는 클래스에 의존하게 되는데요. UuidHolder 의 경우 common 패키지의 application layer에 존재하는 것으로 알고 있습니다. Domain layer의 User객체가 application layer의 객체에 의존하는 것이 parameter형태로는 허용이 되는지 질문드립니다.
안녕하세요, 강의 잘보고 필요할떄마다 계속 보고 있습니다. 현재 강의에선 도메인 객체와 영속성 객체(Entity class)를 분리해서 사용하여서 문제가 없을 것 같은데, 보통 Entity class 에서 ID를 자동으로 생성하는 경우가 대부분이어서, 생성자를 생성 할 때, ID를 포함하지 않는 생성자를 많이 만드는 것으로 알고 있습니다. 이 때, 테스트코드는 어떻게 작성해야할까요? 소형 테스트를 작성한다면 H2 같읕 데이터베이스에서 객체를 가져오지 않는 한 아이디가 존재하지 않을텐데 아이디로 비교하지 말고 다른 값들만 비교해야할까요? 그렇다면 로직내에 findById() 같은 메서드로 객체를 불러와서 처리하는 경우가 있다면 어떻게 하시는지 궁금합니다. 객체의 아이디가 없으므로 로직내에서 아이디를 가져오거나 비교하는 경우가 있다면, NPE가 발생하여 테스트가 제대로 진행이 되지 않는 경우가 많을 것 같습니다. 다들 보통 어떻게 하시는지 궁금하여 질문 남깁니다 감사합니다
의존성 조언 두번째 방법에서 문제점이 UserService.login은 여전히 의존성이 감춰져있다. 위와 마찬가지로 Userservice.login을 테스트할 때 clock에 의존하고 있는지 알 수 없다. 이거였는데 세번 째 방법도 UserService를 사용하는 메서드는 login(user)만 남겨서 user가 Clock에 의존하는지 모르지 않나요? 아까 맥도날드 예제에서는 인터페이스를 사용하면 일을 시킨다고 설명하셨는데 이것도 인터페이스를 사용해서 일을 시키는거니 이렇게 구현하면 내부에서 clockHolder를 사용해도 외부에서 몰라도 되나요?
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "PRODUCTS" not found (this database is empty); SQL statement: select product0_.name as name1_0_0_, product0_.discount_policy as discount2_0_0_, product0_.id as id3_0_0_, product0_.price as price4_0_0_ from products product0_ where product0_.name=? [42104-214] 이런 에러가 나옵니다.. application.properties 에서 jpa 관련 설정해준건 없고 따라하기만 했을 뿐인데 저런 에러가 뜨네요.. 제가 알기론 jpa 의 ddl-auto 가 create 라서 저절로 생성해주는걸로 알고있는데 ,, 왜 저런 에러가 뜨는지 그리고 강사님이랑 같은 코드를 입력했는데 발생하는지 궁금하네요
안녕하세요. 강의에 매우 깊은 감명을 받고 토이 프로젝트에 말씀해주신 부분들 적용해보고 있습니다. 강의에서 가이드 주신대로 유효성 검증에 대한 테스트도 mock 라이브러리 없이 순수 자바 코드로 해보고 있습니다. 기존 코드에서는 입력값 유효성 검증을 위해 @Valid 어노테이션을 사용했는데요. 스프링부트를 실행시키지 않으니 @Valid 어노테이션이 정상작동을 안해서 테스트를 작성하는데 어려움을 겪고 있습니다....ㅎㅎ 그래서 유효성 검증을 하는 코드를 직접 작성해야 하나 고민하다가 질문을 남기게 되었습니다. 강사님께서는 유효성 검증에 대한 테스트 코드를 작성하는데 어떤 방법을 사용하시는지 궁금합니다. (@Valid는 자바 표준 스펙으로 알고 있어서 스프링을 실행시키지 않아도 방법을 잘 찾아보면 가능할 것 같다 라고도 생각하고 있습니다.)
안녕하세요. 우빈님! 먼저 우빈님의 테스트 강의가 너무나 재미있어, 테스트에 대한 막연한 지식을 구체화하고 테스트 작성 열망을 크게 키울 수 있어 서 감사하다는 말씀을 드리고 싶습니다! 다름이 아니라, Controller 테스트를 위해 @WebMvcTest 를 통해 Service와 Repository를 Mocking하여 단위 테스트의 형식으로 작성 한다는 것을 배웠습니다. 이렇게 배운 것을 사이드 프로젝트에 적용해보며, 의문점이 생겼는데 능력 부족으로 인해 의문이 해결되지 않아 질문을 드리려 합니다. 상황 강의에서 작성한 컨트롤러 테스트 중 일부인 OrderControllerTest의 테스트 메서드는 다음과 같습니다. @DisplayName("신규 주문을 등록한다.") @Test void createOrder() throws Exception { // given OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001")) .build(); // when & then mockMvc.perform( post("/api/v1/orders/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value("200")) .andExpect(jsonPath("$.status").value("OK")) .andExpect(jsonPath("$.message").value("OK")); } 또한, 해당 메서드의 실행 로그를 보면 MockHttpServletResponse 이 다음과 같다는 것을 볼 수 있었습니다. MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"application/json"] Content type = application/json Body = {"code":200,"status":"OK","message":"OK","data":null} Forwarded URL = null Redirected URL = null Cookies = [] 이후, 저의 사이드 프로젝트의 컨트롤러 테스트를 위와 동일한 방식으로 작성하였지만, 아래와 같이 MockHttpServletResponse의 Body가 빈 채로 응답 이 되어 테스트가 실패하게 되는 문제가 발생했습니다. MockHttpServletResponse: Status = 200 Error message = null Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"] Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = [] 이때, given()을 통해 Service에 대한 행위를 Stubbing해주니 정상적으로 테스트가 성공 하는 것을 확인했습니다. 문제 파악을 위해 조사한 결과 추측되는 차이점은 다음과 같습니다. 강사님의 프로젝트에서 Controller의 응답 타입은 커스텀 응답 객체인 ApiResponse 를 사용합니다. 저의 사이드 프로젝트에서 Controller의 응답 타입은 HttpEntity를 상속하는 ResponseEntity 를 사용합니다. 질문 Q1. 강사님의 코드를 보면, 아래와 같이 createOrder 메서드에 대한 Stubbing 없이도 정상적으로MockHttpServletResponse의 Body가 응답되어 테스트가 성공합니다. // given ... given(orderService.createOrder(any())) .willReturn(OrderResponse.builder() ... // 생략 .build() ); // 없어도 테스트는 성공한다. 저는 given() 절에 @MockBean을 통해 Mock 객체로 설정한 OrderService가 어떤 행위를 해야할지 Stubbing 해주어야 하는 것으로 이해하고 있었는데, 어떻게 Stubbing 없이 Body가 정상적으로 채워져 테스트가 성공한 것인지 궁금 합니다. Q2. Q1과 연관하여 강사님의 코드에서는 ApiResponse 라는 커스텀한 응답 객체를 컨트롤러 메서드의 응답으로 사용하는데, 제 사이드 프로젝트에서의 응답 타입은 ResponseEntity 를 사용하고 있습니다. 이 차이 때문에 발생하는 문제인지 궁금합니다. Q3. 이번 의문점을 통해 컨트롤러 테스트에서 메서드의 행위에 대한 기댓값을 Stubbing하여 검증하는 것이 일종의 답정너(?)와 같은 테스트를 작성하는 것은 아닐까? 라는 생각과 함께, 컨트롤러 테스트 방식에 많은 고민을 해야 하겠다는 다짐을 하게 되었습니다. 이에 대한 우빈님의 생각은 어떠하신지 궁금합니다. 질문이 수준이 다소 떨어지지만, 이 의문점을 해결하고 싶은 마음에 장황하게 나열할 수 밖에 없었음을 양해 부탁드립니다. 답변 기다리겠습니다. 감사합니다!
안녕하세요 강의 잘 보고 있습니다. 다름이 아니라 이렇게 H2등을 쓰지않고 테스트 할 경우, 많은 테스트 데이터들을 셋업하는 것이 힘들것 같은데 보통 어떻게 처리하시나요? h2등을 사용한다고 하면 강의에서 알려주신 것 처럼 보통 .sql을 실행시켜서 사용할텐데, 이렇게 소형테스트들에 테스트데이터를 java code로 반복문 돌면서 생성하자니 좀 번거러운 것 같은데 혹시 다른 방법이 있을런지 혹은 어떻게 하시는지 궁금하여 여쭤봅니다. 환절기가 다가오는데 더위 조심, 감기 조심 하시길 바랍니다. 감사합니다.
안녕하세요. 먼저, 강의 잘 듣고 있습니다. 강의 따라하던 중 아래 이슈가 발생해서 문의 드립니다. Q1. @NotNull 지정 시, Not-null fields must be initialized 라고 경고창이 뜨며.. Q2. 테스트를 진행하면 아래와 같이 메세지 부분에 기대했던 값 외에.. 추가 적인 스트링이 들어가서 테스트 성공이 되지 않는 것 같습니다. Body = {"code":400,"status":"BAD_REQUEST","message":"Field error in object 'productCreateRequest' on field 'type': rejected value [null]; codes [NotNull.productCreateRequest.type,NotNull.type,NotNull.sample.cafekiosk.spring.domain.product.ProductType,NotNull]; arguments [ org.springframework.context.support .DefaultMessageSourceResolvable: codes [productCreateRequest.type,type]; arguments []; default message [type]]; default message [상품 타입은 필수입니다.]","data":null} 그 외에 @NotBlank, @Postivie 도 유사한 에러가 발생하고 있습니다. Expected :상품 가격은 양수여야 합니다. Actual :Field error in object 'productCreateRequest' on field 'price': rejected value [0]; codes [Positive.productCreateRequest.price,Positive.price, Positive.int ,Positive]; arguments [ org.springframework.context.support .DefaultMessageSourceResolvable: codes ... Q2 의 경우 하여.. 우선은 아래와 같이 문자열을 포함하는지로 수정해서 테스트는 넘어갔습니다. .andExpect(jsonPath("$.message", "상품 가격은 양수여야 합니다.").exists()) Q1 내용은 어떻게 수정하면 된느지 궁금합니다. 확인 부탁드립니다~
강의를 굉장히 감명 깊게 보고 Spring 기반으로 되어 있는 레거시 프로젝트에 도메인을 분리하고 유닛 테스트를 넣는 작업을 하고 있습니다. 여기에서 가장 답답한 게 단순한 유닛 테스트임에도 빌드 타임을 그대로 기다려야 한다는 점인데요. 비록 빌드 캐시 덕에 아주 오래 걸리는 건 아니긴 해도 몇십 ms면 끝날 테스트를 몇 초씩 기다려야 하는 점이 많이 답답합니다. 시간 상으로는 인메모리 db를 띄우는 것과 별 차이 없는 것 아닌가? 하는 생각이 들 정도예요. (물론 테스트 코드를 작성하기 쉬운 구조가 좋은 코드 구조라는 명제에는 이견이 없기 때문에 별 차이가 없다 하더라도 계속 진행할 예정입니다.) 선생님께서도 담당하시던 시스템에 유닛 테스트를 붙이는 작업을 하셨다고 들었는데 이러한 부분은 어떻게 극복하셨는지 궁금합니다.
querydsl설정은 각종 블로그 보고 설정 적용을 한상태입니다. 일단 엔티티 구조가 Transaction 엔티티에서 Accout 엔티티를 @MantToOne 다대일 단방향 매핑중이고, 입출금 내역 조회부분에서 jpql 부분을 그대로 챗 gpt 에 querydsl 기준 동적쿼리 코드를 돌려보면 이런 코드를 알려줍니다 우선 여기서 질문이 WithdrawAccount, DepositAccount라는 클래스는 없고 Account를 참조 하는데 QWithdrawAccount, QDepositAccount Q클래스가 필요한 이유를 모르겠습니다. 저 코드를 해당 코드와 같이 바꿨습니다 그리고 테스트 코드를 돌려보면 transaction.withdrawAccount is not a root path 라는 에러가 나는데 root path 관련 질문글 찾아보면 " 이를 해결하려면 from 절에서 해당 객체를 다시 선언해주면 됩니다. 즉, querydsl에서는 join을 할 때 alias를 줘서 해당 객체를 다시 선언해주는 방식으로 작성해야 해결할 수 있습니다. " 이런 답변이있는데 변경 된 코드에서 innerjoin의 오른쪽 파라미터에 별칭을 잘못 줘서 에러가 나는거 같긴 한데 QTransaction.transaction.withdrawaccount 와 같은 별칭을 부여하면 안되는 걸까요,,?