묻고 답해요
141만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트
inteface UserRepsotiroy 를 service 패키지로 뺀 이유에대해서 궁금합니다.
service 레이어는 repository 보다 상위 레이어에 있습니다.일반적으로 service 레이어는 repository 를 주입받아서 사용됩니다.OOP 원칙에서도 상위레이어는 하위레이어를 알아도 괜찮은것으로 알고있습니다. 반대로 하위레이어는 상위레이어를 알아서는 안됩니다.'외부 연동을 다루는 방법' 강의 1:53 ~ 2:04 에상위 레이어인 Service 가 infraStructure 에 의존하는 모습을 보이기때문에 라고 하셨습니다. 이말을 해석하면, 앞으로 변경될 코드에서는 UserService 는 interface 인 UserRepository 를 의존하게됩니다. 그러나 자세히 들어가면 service 레이어가 UserRepository 에 의존하는것이 아닌 의존성역전을 통해 그의 Impl 을 받게됨으로 약결합이 되게됩니다. 그러므로, service 가 interface 인 UserRepository 에 의존하는건 아니기때문에 service 패키지로 해당 interface 를 옮긴것인지 궁금합니다.
-
미해결Practical Testing: 실용적인 테스트 가이드
ProductNumber 계산 로직
안녕하십니까! 먼저 항상 좋은 강의 감사드립니다.특히 이번 강의에서는 Transaction에서 readOnly 옵션을 사용하므로써 JPA 최적화를 통해 성능 향상을 이점을 얻을 수 있다와 함께,CQRS 측면을 말씀해 주셔서 좋았습니다!저의 경우는 아직 master/slave DB 구조를 다뤄본적이 없어서 몰랐는데, 이렇게 readOnly 옵션을 사용할 경우 DB endpoint를 구분하여 명시하므로써 master/slave DB 구조를 사용할 수 있다는 측면도 알게되어서 좋았습니다.본론으로 넘어가서, Q1. ProductNumber를 계산하는 로직을 DB에서 최신값을 읽어온 후에 ,그 값을 기반으로 다음 productNumber 값을 계산하는 로직을 저의 경우는 동시성 이슈를 고려하여 , 1.일단 SaveAndFlush()로 Id를 할당받고 2. 그 Id 값으로 만들자 라는 로직을 사용하였습니다.(IDENTITY 전략이므로 테이블당 1부터 Id가 할당되기 때문에 가능)이러한 저의 로직에 문제가 있는지 피드백 주시면 정말 감사하겠습니다.특히 calculateProductNumber() 메소드를 저의 경우는 도메인 주도 전략에 기반하여 서비스의 private Method로 정의하지 않고 , Entity 안에 정의하여 사용하는 편인데, 이러한 스타일에 대해서도 문제가 없을지 여쭤보고 싶습니다.Q2.두번째로 저의 경우는 createProduct() 메소드 테스트를 1.처음 생성일의자리 productNumber로 생성십의자리 productNumber로 생성백의자리 productNumber로 생성1000개를 넘어가면 예외발생으로 구분하여 테스트 하였습니다.그런데 이 테스트를 한꺼번에 돌렸을 땐 직전 테스트로 이미 저장된 Product에 의해 다음 테스트에 영향을 미쳐 일부 테스트가 실패하는 상황이 발생하였습니다.이상황을 어떻게 해결할 수 있을지 질문 드리고 싶고(sleep() 으로 간격을 주어야 할까요,,)또한 이 원인이 테스트를 한꺼번에 실행시킬 경우,각 테스트 메소드가 각 쓰레드로 동시에 실행되기 때문인지여쭤보고 싶습니다! 감사합니다.
-
미해결실전! 스프링부트 상품-주문 API 개발로 알아보는 TDD
POJO 상품 등록 기능 구현하기 강의 질문
안녕하세요.POJO 상품 등록 기능 구현하기 강의의3:58 에서 throw new UnsupportedOperationException("Unsupported addProduct");가 제안으로 바로 뜬 이유를 여쭙니다..!
-
미해결Practical Testing: 실용적인 테스트 가이드
주문한 상품 개수만큼 Stock을 감소시키는 로직
안녕하십니까! 저의 경우는 해당 로직을 stockRepository.findByProductNumberIn()이 아니라,일일이 재고와 관련된 ProductNumber에 대하여 stockRepository.findByProductNumber()로조회하여 decreaseQuantity()를 수행하였습니다. 그렇게 하여도, 동일 Stock의 경우 EntityManager에서 조회해오기 때문에 중복된 쿼리가 나가지 않아 어느정도의 성능 감소를 막을 수 있다고 판단하였고, (물론 In절로 한꺼번에 가져오는것 보단 성능이 덜 나올 것 같습니다)그대신에 , 로직이 직관적이라는 장점이 있을 것 같아서 위와 같이 해당 로직을 작성하였습니다. 이에 대해서는 어떻게 생각하시는지 의견 남겨주시면 감사하겠습니다!
-
미해결Practical Testing: 실용적인 테스트 가이드
요청값으로 넘어온 productNumber의 Product가 재고와 관련있는 상품인지 확인하는 로직 질문
안녕하세요! 강의에서는 해당 로직을 ProductType이 BOTTLE이나 BAKERY인 경우에 한하여 true가 되도록 containsStockType() 메소드를 작성하여 판별 해주셨습니다!혹시 이 경우를 stockRepository.existsByProductNumber() 를 호출하여,재고가 있는 상품들만 필터링 해내는 로직으로 작성하게 되면,주문 상품수가 많을 경우 쿼리가 여러번 나가기 때문에 비효율적이어서,요구사항에 따른 검증 로직을 작성하셨는지 여쭙고 싶습니다!
-
미해결Practical Testing: 실용적인 테스트 가이드
@MockBean, @Mock
@MockBean, @Mock 두 개 사용이 계속 헷갈려서 질문 드립니다. 스프링 서버를 띄워서 테스트 할 때 @MockBean을 사용하는 걸로 알려주셨는데 @MockBean도 결국 Mock 객체를 사용하는 거니 @MockBean 대신 @Mock을 써서 테스트를 해도 될 것 같은데 왜 @SpringBootTest나 @WebMvcTest 어노테이션과 @MockBean을 같이 사용해서 테스트 하는 지 궁금합니다.Controller 테스트를 할 때 @WebMvcTest를 사용하기 위해서 @MockBean을 사용하는 걸까요? @WebMvcTest와 @MockBean이 짝궁(?)이어서 이렇게 사용하는걸까요?
-
해결됨부트캠프에서 알려주지 않는 것들 (리액트) 1편
공유소스 실행시 에러 문의 드립니다~
안녕하세요 다름이 아니라 공유해 주신 소스를 기반으로 코드 리포지토리: https://github.com/things-not-learn-from-bootcamp/form-practice/tree/use-reducer-fancy-typing 의 소스를 실행 시켜 보았는데요 위 와 같은 에러가 계속 발생해서요 아무리 구글 검색을 해도 해결이 안되어 문의 드립니다.또 한가지 더 문의 부탁드립니다 위 동일한 레파지토리 소스를 실행시켰을 빌드해서 실행시켰을 때 위 처럼 빨간줄이 생겨서요 좀처럼 이해가 안되어서 같이 문의 드립니다.바쁘신데 죄송하면 문의 부탁드립니다.감사합니다.
-
미해결Practical Testing: 실용적인 테스트 가이드
WebMvcTest에서의 when
Controller 테스트를 하실 때어떤 경우에서는 when으로 값을 세팅해주고어떤 경우에는 사용을 안하시는데기준이 있으실까요
-
미해결실전! 스프링부트 상품-주문 API 개발로 알아보는 TDD
steps 클래스를 사용하는 이유
안녕하세요!강의 잘 듣고 있습니다! steps 클래스를 따로 만드는 이유는 뭐라고 할 수 있을까요?? 감사합니다.
-
미해결Practical Testing: 실용적인 테스트 가이드
강의 코드 질문 드립니다.
안녕하세요. 강의 코드는 혹시 제공 되나요?
-
미해결Practical Testing: 실용적인 테스트 가이드
OrderRepository에 @Repository
안녕하세요JPARepository에는 @Repository 를 안 붙여도정상 작동하는 걸로 알고 있는데따로 붙이신 이유가 있으실까요?
-
해결됨부트캠프에서 알려주지 않는 것들 (리액트) 1편
[섹션 3. Form 컴포넌트로 배우는 실전 기술] 강좌의 소스를 부탁드립니다.
안녕하세요 저는 리액트만 4년차 개발자인데요.. 기존에 리액트 강좌를 무지 많이 수강했음에도 아직도 많이 부족하다 싶어서 강사님의 강좌를 듣게 되었는데 많은 도움이 되고 있어서 너무 감사합니다. 참고로 아래와 같은 과정을 수상신청을 아래와 같이 했습니다. 부트캠프에서 알려주지 않는 것들 (리액트 렌더링 최적화 편) 2편부트캠프에서 알려주지 않는 것들 (리액트) 1편클론코딩에서 알려주지 않는 것들 (시스템 이론과 DB) 1편실전 연습으로 익히는 고급 타입스크립트 기술 다만 부탁이 있어서요 다름이 아니라 좀더 세심하게 코드를 보구 싶어서요혹시 강좌중 부트캠프에서 알려주지 않는 것들 (리액트) 1편 에서 섹션 3. Form 컴포넌트로 배우는 실전 기술 강좌의 소스를 과정별로 브랜치로 구분한 레파지토리는 없는지 해서요..강좌별로 들으면서 분석하면 많은 도움이 될거 같아서 문의 드립니다.정 안되면 최종 소스라도 부탁드립니다. 위에 강좌 신청을 좋게 봐 주셔서 부탁드립니다.공개가 힘드시면 메일이라도 부탁드리면 안될런지요? manhattansky73@gmail.com 입니다.감사합니다.
-
미해결실전! 스프링부트 상품-주문 API 개발로 알아보는 TDD
랜덤 포트를 사용하는 이유
안녕하세요! 너무 좋은 강의 잘 보고 있습니다. 감사합니다! 강의를 듣다가 궁금한 것이 생겼는데,굳이 ApiTest 라는 클래스를 따로 만들어서 랜덤 포트로 설정해준 이유는 무엇인가요?? 감사합니다.
-
해결됨쥬쥬와 함께 하루만에 끝내는 스프링 테스트
JaCoCo 레포트 파일이 안생깁니다.
안녕하세요. 강의 잘 듣고 있습니다.노션에 있는 그대로 코드를 작성했는데 아무리 테스트가 끝나도jacoco 안에 파일이 안생깁니다. 그래서 terminal 에서 ./gradlew jacocoTestReport 를 했는데BUILD SUCCESSFUL 은 나오는데 파일이 안생기는데 이유를 모르겠습니다!
-
미해결Practical Testing: 실용적인 테스트 가이드
코틀린 관련 질문!
강의에서 코틀린을 간간히 언급을 해주시는데, 실무에서는 코틀린을 사용중이신건지 궁금해서 글을 남기게 되었습니다! 코틀린을 사용하신다면, 어떤 상황에서는 자바를 사용하고, 어떤 상황에서는 코틀린을 사용하면 좋을지 추천해주시면 좋을 것 같아서 질문을 남겨봅니다! 강의 너무 잘보고 있습니다 감사합니다!
-
미해결Practical Testing: 실용적인 테스트 가이드
테스트가 무시되는 상황이 일어납니다.
안녕하세요!수업과 최대한 유사하게 코드를 작성해왔는데,테스트가 무시되는 상황이 발생하여 질문 올립니다. 에러메세지대로 @SpringBootConfiguration도 추가해보고,구글링도 해봤는데 해결이 되지 않아 질문 올립니다 죄송합니다. 테스트가 무시되었습니다.java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test at org.springframework.util.Assert.state(Assert.java:76) at org.springframework.boot.test.context.SpringBootTestContextBootstrapper.getOrFindConfigurationClasses(SpringBootTestContextBootstrapper.java:237) at org.springframework.boot.test.context.SpringBootTestContextBootstrapper.processMergedContextConfiguration(SpringBootTestContextBootstrapper.java:152) at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:393) at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildDefaultMergedContextConfiguration(AbstractTestContextBootstrapper.java:309) at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:262) at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildTestContext(AbstractTestContextBootstrapper.java:107) at org.springframework.boot.test.context.SpringBootTestContextBootstrapper.buildTestContext(SpringBootTestContextBootstrapper.java:102) at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:137) at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:122) at org.junit.jupiter.engine.execution.ExtensionValuesStore.lambda$getOrComputeIfAbsent$4(ExtensionValuesStore.java:86) at org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.computeValue(ExtensionValuesStore.java:223) at org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.get(ExtensionValuesStore.java:211) at org.junit.jupiter.engine.execution.ExtensionValuesStore$StoredValue.evaluate(ExtensionValuesStore.java:191) at org.junit.jupiter.engine.execution.ExtensionValuesStore$StoredValue.access$100(ExtensionValuesStore.java:171) at org.junit.jupiter.engine.execution.ExtensionValuesStore.getOrComputeIfAbsent(ExtensionValuesStore.java:89) at org.junit.jupiter.engine.execution.ExtensionValuesStore.getOrComputeIfAbsent(ExtensionValuesStore.java:93) at org.junit.jupiter.engine.execution.NamespaceAwareStore.getOrComputeIfAbsent(NamespaceAwareStore.java:61) at org.springframework.test.context.junit.jupiter.SpringExtension.getTestContextManager(SpringExtension.java:294) at org.springframework.test.context.junit.jupiter.SpringExtension.beforeAll(SpringExtension.java:113) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeBeforeAllCallbacks$10(ClassBasedTestDescriptor.java:381) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeBeforeAllCallbacks(ClassBasedTestDescriptor.java:381) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:205) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:80) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:148) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57) at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)종료 코드 255(으)로 완료된 프로세스
-
해결됨쥬쥬와 함께 하루만에 끝내는 스프링 테스트
localstack 이 deprecated 되서 이렇게 수정했습니다.
안녕하세요. 강의 잘 듣고 있습니다.localstack 이 deprecated 가 되어서 조금 수정이 필요해서 고쳐보았습니다. 노션에도 의존성에 버전을 작성해야 동작을 해야 하더라구요. testImplementation "org.testcontainers:localstack:1.19.0" DockerImageName imageName = DockerImageName.parse("localstack/localstack:0.11.2"); aws = (new LocalStackContainer(imageName)) .withServices(LocalStackContainer.Service.S3) .withStartupTimeout(Duration.ofSeconds(600)); aws.start(); 전 이렇게 수정해서 고쳤습니다!
-
미해결Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트
안녕하세요. 강의 수강 후, 토이프로젝트에 적용해보다가 Fake Repository에 관해 궁금한 것이 생겨 질문드립니다!
안녕하세요. 우선 강의 너무 잘 들었습니다.설계를 개선하며 소형테스트를 풍부하게 작성할 수 있다는 것을 보고 이를 체화하려고 공부중인데요,강의에서 배운것을 적용하다가 막힌 곳이 있어서 질문드립니다.우선, 저는 강의와 유사하게 Repository를 추상화해서 프로덕션(~RepositoryImpl)과 테스트 환경(Fake~Repository)을 다르게 가져가고 있고, 막힌 부분은 아래와 같습니다.프로덕션에서 groupBy를 사용하고, Dto로 바로 조회하는 방식으로 프로덕션 Repository에 코드를 작성한 부분이 있는데,이를 Fake Repository에서 작성하려니까 막막하더라구요왜 FakeRepository에 작성하려 하냐면, 제가 이해하기로는 FakeRepository를 구현해야 Service 레이어를 테스트할 때, 소형테스트로 작성할 수 있다고 이해를 했기 때문입니다. 막힌 부분에서 여러 고민해보다가, 다양한 조건절과 그룹핑이 들어간 쿼리는 어떻게 Fake Repository에 작성해서 테스트할 수 있을까? 하는 궁금증이 생겼습니다. 그래서 질문입니다.Fake Repository를 사용해서 일부 Service 레이어의 로직을 테스트할 수 있지만, 좀 더 복잡한 쿼리들은 Fake로 구현(stream, filter등을 사용해서)하기 어려우니, Fake로 구현하지 않고 중형 테스트로 테스트한다.그리고 위의 기준이 소형테스트로 테스트할 지, 중형테스트로 테스트할 지 고려할 기준이 된다..?저는 위와같이 생각이 이어지는데, 혹시 옳은 생각일까요? 아님 열심히 Fake로 구현을 해봐야하는 것일까요?그것도 아니라면 다른 방법이 있을까요? 강사님은 복잡한 쿼리들을 어떻게 하시는지 궁금합니다..!열심히 강의해주셨는데, 막상 적용해보려니 쉽지 않아서 민망합니다..답변 미제공 강의지만, 호옥시나 하는 마음에 질문 남깁니다.🥹좋은 강의 감사드립니다
-
미해결Practical Testing: 실용적인 테스트 가이드
캐시를 포함한 Service Layer 테스트 가이드
안녕하세요. 우빈님!캐시를 처음 적용해 보아서 캐시를 포함한 비지니스 로직을 어떻게 작성하는 것이 좋을까? 에 대한 고민이 있어 글 남깁니다...! 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. 마지막으로 캐시를 사용할 때 깔끔하게 비지니스 로직을 작성할 수 있는 노하우 말씀주시면 감사하겠습니다.!!!다소 질문이 난해한데 너그럽게 이해해주시면 감사하겠습니다.ㅠㅠ좋은 강의 잘 듣고 있습니다. 다음 강의도 기대할께요^^!!감사합니다.
-
미해결Practical Testing: 실용적인 테스트 가이드
@Transactional을 붙였을 때"만" Stock 감소 검증에 성공합니다
각 테스트 케이스들을 검증하고, 이에 대해 값을 비워주기 위해 두 가지 방식을 소개해주셨습니다.1) tearDown()을 사용해서 after each로 모두 날려주는 방식과2) 클래스 레벨에서 테스트 클래스에 @Transactional을 붙이는 방식 강의와 동일하게 "001" : 1000 : 2개, "002" : 3000 : 2개, "003" : 5000 가정 하에, "001", "001", "002", "003" 순으로 주문했다 하였습니다. 하지만, 문제는1)을 사용하였을 때는 아래와 같은 에러가 발생하구요, 2)을 사용했을 때는 테스트 케이스가 정상 검증 됩니다.[Extracted: productNumbeㅏr, quantity] Expecting actual: [("001", 2), ("002", 2)] to contain exactly in any order: [("001", 0), ("002", 1)] elements not found: [("001", 0), ("002", 1)] and elements not expected: [("001", 2), ("002", 2)] 올려주신 깃허브 코드 5-7 또한 참고해보았으며, 코드 복붙까지 시도했는데 AfterEach 방식에서만 검증 실패가 등장하네요// 보여주신 디버깅 방식 참조하여 디버깅도 해보았는데, 제 눈에는,,, 이상이 없었습니다... 미천한 디버깅 실력에 부끄러움만 앞서지만, 왜 트랜잭션 어노테이션에서만 검증 성공인지 의아합니다.혹시 답변 가능하시다면 부탁드립니다,,! 코드 첨부합니다.OrderServicepackage sample.cafekiosk.api.service.order; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import sample.cafekiosk.api.controller.order.OrderCreateRequest; import sample.cafekiosk.domain.order.Order; import sample.cafekiosk.domain.product.Product; import sample.cafekiosk.domain.product.ProductRepository; import sample.cafekiosk.domain.product.ProductType; import sample.cafekiosk.domain.stock.Stock; import sample.cafekiosk.domain.stock.StockRepository; import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class OrderService { // Product를 가져와야 하므로 의존 private final ProductRepository productRepository; private final OrderRepository orderRepository; private final StockRepository stockRepository; public OrderResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) { List<String> productNumbers = request.getProductNumbers(); List<Product> products = findProductsBy(productNumbers); deductStockQuantities(products); Order order = Order.create(products, registeredDateTime); Order savedOrder = orderRepository.save(order); return OrderResponse.of(savedOrder); } private void deductStockQuantities(List<Product> products) { List<String> stockProductNumbers = extractStockProductNumbers(products); Map<String, Stock> stockMap = createStockMapBy(stockProductNumbers); Map<String, Long> productCountingMap = createCountingMapBy(stockProductNumbers); for (String stockProductNumber : new HashSet<>(stockProductNumbers)) { Stock stock = stockMap.get(stockProductNumber); int quantity = productCountingMap.get(stockProductNumber).intValue(); if (stock.isQuantityLessThan(quantity)) { throw new IllegalArgumentException("재고가 부족한 상품이 있습니다."); } stock.deductQuantity(quantity); } } private List<Product> findProductsBy(List<String> productNumbers) { List<Product> products = productRepository.findAllByProductNumberIn(productNumbers); Map<String, Product> productMap = products.stream() .collect(Collectors.toMap(Product::getProductNumber, p -> p)); return productNumbers.stream() .map(productMap::get) .collect(Collectors.toList()); } private static List<String> extractStockProductNumbers(List<Product> products) { return products.stream() .filter(product -> ProductType.containsStockType(product.getType())) .map(Product::getProductNumber) .collect(Collectors.toList()); } private Map<String, Stock> createStockMapBy(List<String> stockProductNumbers) { List<Stock> stocks = stockRepository.findAllByProductNumberIn(stockProductNumbers); return stocks.stream() .collect(Collectors.toMap(Stock::getProductNumber, s -> s)); } private static Map<String, Long> createCountingMapBy(List<String> stockProductNumbers) { return stockProductNumbers.stream() .collect(Collectors.groupingBy(p -> p, Collectors.counting())); } } OrderServiceTestpackage sample.cafekiosk.api.service.order; import jakarta.transaction.Transactional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.web.bind.annotation.RestController; import sample.cafekiosk.api.controller.order.OrderCreateRequest; import sample.cafekiosk.domain.product.Product; import sample.cafekiosk.domain.product.ProductRepository; import sample.cafekiosk.domain.product.ProductSellingStatus; import sample.cafekiosk.domain.product.ProductType; import sample.cafekiosk.domain.stock.Stock; import sample.cafekiosk.domain.stock.StockRepository; import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; import static org.junit.jupiter.api.Assertions.*; import static sample.cafekiosk.domain.product.ProductSellingStatus.SELLING; import static sample.cafekiosk.domain.product.ProductType.*; @ActiveProfiles("test") @SpringBootTest //@Transactional class OrderServiceTest { @Autowired private OrderService orderService; @Autowired private ProductRepository productRepository; @Autowired private OrderProductRepository orderProductRepository; @Autowired private OrderRepository orderRepository; @Autowired private StockRepository stockRepository; @AfterEach void tearDown() { orderProductRepository.deleteAllInBatch(); productRepository.deleteAllInBatch(); orderRepository.deleteAllInBatch(); stockRepository.deleteAllInBatch(); } @DisplayName("재고와 관련된 상품이 포함되어 있는 주문번호 리스트를 받아 주문을 생성한다.") @Test void createOrderWithStock() { // given LocalDateTime registeredDateTime = LocalDateTime.now(); Product product1 = createProduct(BOTTLE, "001", 1000); Product product2 = createProduct(BAKERY, "002", 3000); Product product3 = createProduct(HANDMADE, "003", 5000); productRepository.saveAll(List.of(product1, product2, product3)); Stock stock1 = Stock.create("001", 2); Stock stock2 = Stock.create("002", 2); stockRepository.saveAll(List.of(stock1, stock2)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001", "001", "002", "003")) .build(); // when OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime); // then assertThat(orderResponse.getId()).isNotNull(); assertThat(orderResponse) .extracting("registeredDateTime", "totalPrice") .contains(registeredDateTime, 10000); assertThat(orderResponse.getProducts()).hasSize(4) .extracting("productNumber", "price") .containsExactlyInAnyOrder( tuple("001", 1000), tuple("001", 1000), tuple("002", 3000), tuple("003", 5000) ); List<Stock> stocks = stockRepository.findAll(); assertThat(stocks).hasSize(2) .extracting("productNumber", "quantity") .containsExactlyInAnyOrder( tuple("001", 0), tuple("002", 1) ); } @DisplayName("주문 번호리스트를 받아 주문을 생성한다.") @Test void createOrder() { // GIVEN LocalDateTime n = LocalDateTime.now(); Product product1 = createProduct(HANDMADE, "001", 1000); Product product2 = createProduct(HANDMADE, "002", 2000); Product product3 = createProduct(HANDMADE, "003", 3000); productRepository.saveAll(List.of(product1, product2, product3)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001","002")) .build(); // WHEN OrderResponse orderResponse = orderService.createOrder(request,n ); // THEN assertThat(orderResponse.getId()).isNotNull(); assertThat(orderResponse) .extracting("registeredDateTime","totalPrice") .containsExactlyInAnyOrder(n,3000); assertThat(orderResponse.getProducts()).hasSize(2) .extracting("productNumber", "price") .containsExactlyInAnyOrder( tuple("001", 1000), tuple("002", 2000) ); } @DisplayName("중복되는 상품번호 리스트로 주문을 생성할 수 있다.") @Test void createOrderWiuthDuplicatedProductNumbers() { // GIVEN LocalDateTime n = LocalDateTime.now(); Product product1 = createProduct(HANDMADE, "001", 1000); Product product2 = createProduct(HANDMADE, "002", 2000); Product product3 = createProduct(HANDMADE, "003", 3000); productRepository.saveAll(List.of(product1, product2, product3)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001","001")) .build(); // WHEN OrderResponse orderResponse = orderService.createOrder(request,n ); // THEN assertThat(orderResponse.getId()).isNotNull(); assertThat(orderResponse) .extracting("registeredDateTime","totalPrice") .containsExactlyInAnyOrder(n,2000); assertThat(orderResponse.getProducts()).hasSize(2) .extracting("productNumber", "price") .containsExactlyInAnyOrder( tuple("001", 1000), tuple("001", 1000) ); } private Product createProduct(ProductType type, String productNumber,int price) { return Product.builder() .productNumber(productNumber) .type(type) .sellingStatus(SELLING) .name("아메리카노") .price(price) .build(); } }Stockpackage sample.cafekiosk.domain.stock; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import sample.cafekiosk.domain.BaseEntity; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Stock extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; private String productNumber; private int quantity; @Builder public Stock(String productNumber, int quantity) { this.productNumber = productNumber; this.quantity = quantity; } public static Stock create(String s, int i) { return Stock.builder().productNumber(s).quantity(i).build(); } public boolean isQuantityLessThan(int quantity) { return this.quantity < quantity; } public void deductQuantity(int quantity) { if(this.quantity < quantity){ throw new IllegalArgumentException("주문 수량이 재고보다 많습니다."); } this.quantity-=quantity; } }