안녕하세요 선생님! 동시성과 병렬성 관련해 기초가 약해 해당 강의를 들으면서 OS 부족한 부분을 많이 알게되어 도움이 많이되고 있습니다. 다름이 아니라 해당 강좌 범위는 벗어나지만.. 구글링해도 명확하게 해결되지 않아 질문 드립니다. 동시성을 구현하는 방법으로 멀티쓰레드 나 코루틴 방법으로 구현을 할 수있고 멀티쓰레드 방법에서 쓰레드 전환 간 context switch가 많기 때문에 오버헤드도 많아 Coroutine 방법을 많이 쓰고 또한 유저가 이벤트루프(스케쥴링)을 app에서 관리한다는 측면에서 디버그가 유용하다 정도로 이해하고 있습니다. 많은 블로그에서 멀티쓰레드보다 코루틴을 소개하면서 동시성 구현시 코루틴을 더 상위(혹은 좋은) 방법으로 서술이 많이 되어 있더라구요. I/O작업이 빈번할 경우 코루틴이 더 유리하다는 이해가 됩니다. 싱글스레드 + async 싱글스레드 멀티스레드 + async 멀티스레드 2개의 구현방법이 레이어가 다르다(thread와 task)보니 4가지 경우의 수를 적용할 수 있는데 실제 동시성을 적용해야되는 상황이면 어떻게 판단하고 최적의 구현방법(4가지 중 선택)을 적용할 수 있을까요? (파이썬 스택을 쓰고 있으며 GIL도 이해하고 있는 상태입니다.) 다시한번 해당 강의와 조금 벗어난 질문드려 죄송합니다. ( _ _ )
강의에서 코틀린을 간간히 언급을 해주시는데, 실무에서는 코틀린을 사용중이신건지 궁금해서 글을 남기게 되었습니다! 코틀린을 사용하신다면, 어떤 상황에서는 자바를 사용하고, 어떤 상황에서는 코틀린을 사용하면 좋을지 추천해주시면 좋을 것 같아서 질문을 남겨봅니다! 강의 너무 잘보고 있습니다 감사합니다!
안녕하세요! 수업과 최대한 유사하게 코드를 작성해왔는데, 테스트가 무시되는 상황이 발생하여 질문 올립니다. 에러메세지대로 @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(으)로 완료된 프로세스
안녕하세요. 우빈님! 캐시를 처음 적용해 보아서 캐시를 포함한 비지니스 로직을 어떻게 작성하는 것이 좋을까? 에 대한 고민이 있어 글 남깁니다...! 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. 마지막으로 캐시를 사용할 때 깔끔하게 비지니스 로직을 작성할 수 있는 노하우 말씀주시면 감사하겠습니다.!!! 다소 질문이 난해한데 너그럽게 이해해주시면 감사하겠습니다.ㅠㅠ 좋은 강의 잘 듣고 있습니다. 다음 강의도 기대할께요^^!! 감사합니다.
안녕하세요 강사님 현재 회사에서 진행중인 프로젝트에서 ControllerTestSupport를 생성하여 controllerTest시 스프링부트를 띄우는 횟수를 줄이기 위해 사용하였는데 테스트 속도가 줄어들거나 하지 않고 거의 똑같더라구요,, 그래도 스프링부트를 띄우는걸 최소화 하는게 나을까요? ControllerTestSupport 사용해야하는 더 나은 이점이 있는지 궁금합니다!
하고 싶은 것 일부 주문이 서비스 유형: 픽업임에도 배달메뉴코드로 생성되어 이를 찾고 싶습니다. (배달메뉴와 픽업메뉴 코드는 서로 다름) 엑셀 vookup을 사용해서 상품코드를 배달 메뉴 테이블에서 검색하고, 검색되지 않으면 오주문으로 판단하는 코드를 구현했습니다. 같은 프로그램을 파이썬을 통해 구현해보고 싶은데, 다른 dataframe에 없는 값을 찾아 행을 추가하는 과정이 어려워 조언을 구하고 싶습니다. 파이썬 코드 df_pickup = 픽업 주문내역 데이터 프레임 delivery_menu_code = df_delivery_menu["상품코드"] df_misorder = pd.DataFrame() # 픽업 주문내역에서 상품코드를 꺼내 해당 상품코드가 배달 메뉴 코드에 포함되어 있는지 확인 # 만약에 코드가 없으면 해당 row를 df_misorder에 추가 for idx, row in df_pickup.iterrows(): misorder = df_delivery_menu[delivery_menu_code.str.contains(row["상품코드"], na = False)] if len(misorder) != 0: df_misorder.concat(row) else: pass # 어떻게 추가해도 계속 0개로 나와서, 도와주시면 감사드리겠습니다! len(df_misorder)
안녕하세요. 우빈님! 먼저 우빈님의 테스트 강의가 너무나 재미있어, 테스트에 대한 막연한 지식을 구체화하고 테스트 작성 열망을 크게 키울 수 있어 서 감사하다는 말씀을 드리고 싶습니다! 다름이 아니라, 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하여 검증하는 것이 일종의 답정너(?)와 같은 테스트를 작성하는 것은 아닐까? 라는 생각과 함께, 컨트롤러 테스트 방식에 많은 고민을 해야 하겠다는 다짐을 하게 되었습니다. 이에 대한 우빈님의 생각은 어떠하신지 궁금합니다. 질문이 수준이 다소 떨어지지만, 이 의문점을 해결하고 싶은 마음에 장황하게 나열할 수 밖에 없었음을 양해 부탁드립니다. 답변 기다리겠습니다. 감사합니다!
중괄호 등을 통해서 코드를 작성할 때 중괄호가 [] 이렇게 두개 자동으로 생성되는데요. 이외에도 '' 등 두개씩 자동으로 만들어줘서 코드를 작성하는데는 편리한데 'LEE' 등 코드를 쓰고 EE에서 '를 넘어 가기 위해 방향키를 눌러줘야 하는 불편함이 있습니다. 혹시 빠르게 작성하기 위해 단투키 같은게 별도로 있을지 문의드립니다
안녕하세요. 먼저, 강의 잘 듣고 있습니다. 강의 따라하던 중 아래 이슈가 발생해서 문의 드립니다. 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 내용은 어떻게 수정하면 된느지 궁금합니다. 확인 부탁드립니다~
안녕하세요. 우빈님 질문이 있습니다! AOP, Interceptor... 와 같은 공통 관심사를 테스트 할 때는 보통 어떤 전략 을 갖고 테스트 코드를 작성하나요? 예를 들어 아래와 같은 요구사항이 있다고 가정합니다. 22시 이후에는 모든 요청에 대해서 예외를 발생시킨다. 저라면 모든 Controller에 check22hours() 라는 로직을 AOP를 사용하여 적용할 것 같은데요. 이때 순수하게 check22hours() 라는 단위 테스트만 작성하면 되는 것 일까요? 아니면 AOP 자체가 의도한대로 동작하는지 테스트 코드를 작성하는 것이 바람직한 것 인가요? 감사합니다.
안녕하세요. 테스트 환경 통합 강의를 보다가 궁금한 사항이 있어서 질문드립니다. 강의 내용처럼 통합 테스트를 수행할 때 여러번 Spring 서버를 띄우는 것을 효과적으로 개선하기 위해 TestSupport 추상클래스를 상속받아 Repository/Service 계층 테스트시에 통합된 환경을 구축하는게 더 좋은 것은 이해했는데요. Controller 테스트의 경우, 강의 내용처럼 따로 스프링을 띄우지 않고 @WebMvcTest로만 테스트코드를 작성하는 경우라면 공통 추상 클래스를 구현하는게 비효율적일수도 있을 것 같아서 궁금증이 생겼습니다. @WebMvcTest(Controllers="Controller.class") 형식으로 컨트롤러 클래스들을 명시해 줘야 하는데, 클래스가 수십개로 많아질수록 매번 추가해야 하고, Controllers에 많은 클래스를 넣어야 하고(패키지 단위로 지정한다든가 등의 방식은 없는것 같더라구요), 각 클래스에서 사용하는 MockBean이 많아질수록 필드가 많아져서 본문이 길어 보기 힘들수도 있을 것 같아서요. @SpringBootTest처럼 서버를 띄우는 비용이 발생하지 않으므로, 각각의 컨트롤러 테스트마다 명시적으로 @WebMvcTest를 사용하고, 해당 클래스에서 사용할 Mockbean 또한 명시적으로 지정하는 방식도 괜찮을까요? 실무적인 관점으로 볼 때, 제 생각대로 Controller 클래스의 테스트는 통합하지 않고 각각 구현하는 건 어떨지 궁금합니다.
우빈님 커뮤니티나 유튜브에서 잘보고 있습니다.ㅎㅎ TDD관련해서 질문이있는데, 프로덕션 코드 이전에 테스트 케이스를 먼저 작성할때 성공/실패 케이스에 대해서 미리 작성하고 "리팩토링"단계에서 앞서 작성한 케이스를 성공하도록 프로덕션 코드를 구현하는 순으로 하나요? 아니면, 성공 케이스만 작성하고 리팩토링 단계에서 프로덕션 코드를 구현하고, 이후 실패 케이스를 작성하고 프로덕션 코드를 수정하는 순으로 하나요?!
안녕하세요 저는 강사님 강의로 공부하고 파이썬으로 코테를 준비하고 있습니다. 저와 같은 상황에 계신분들과 공유하고 싶어 글을 올립니다. 파이썬 풀이에서 부족한 부분 알려주시면 수정하겠습니다.~ import sys sys.setrecursionlimit(10 ** 6) N, M = map(int, sys.stdin.readline().split()) MAX = 1000 + 10 graph = [[False for in range(MAX)] for in range(MAX)] visited = [False for in range(MAX)] for in range(M): x, y = map(int, sys.stdin.readline().split()) graph[x][y] = True graph[y][x] = True def dfs(idx): visited[idx] = True for j in range(1, N + 1): if not visited[j] and graph[idx][j]: dfs(j) cnt = 0 for i in range(1, N + 1): if not visited[i]: dfs(i) cnt += 1 print(cnt)