블로그
전체 82025. 06. 19.
0
워밍업 클럽 4기 BE - 4주차 발자국
4주차 미션 회고레이어 아키텍처에서 각 계층별로 테스트 코드를 작성하는 실용적인 방법을 체계적으로 익힐 수 있었다. 특히 Controller, Service, Repository 계층에서 어떤 부분을 중점적으로 테스트해야 하는지, 그리고 각 계층 간의 의존성을 어떻게 효과적으로 격리할 수 있는지에 대한 명확한 가이드라인을 얻었다. 또한 @Mock, @MockBean, @Spy 등 여러 Mock 애노테이션들의 차이점과 사용 시점을 명확히 구분할 수 있게 되면서, 상황에 맞는 적절한 Mock 전략을 선택할 수 있는 능력을 키울 수 있었다. BDD 스타일의 테스트에서는 Given-When-Then 각 단계를 명확히 구분하여 작성할 수 있게 되었다. 4주차 강의 회고테스트 코드를 작성하면서 그동안 애매하게 느꼈던 부분들이 많이 해소되었다. 기본적인 Mock 사용법부터 Spring REST Docs까지 학습하면서 실무에서 바로 적용할 수 있는 실용적인 지식을 쌓을 수 있었고, 테스트 코드에 대한 막연한 두려움도 자연스럽게 사라졌다.길다면 길고 짧다면 짧은 4주라는 시간이 빠르게 지나갔지만, 다행히 강의와 미션들을 무사히 완주했다. 클린코드와 테스트코드는 모든 개발자의 기본 소양이라고 생각하는데, 이번 기회를 통해 그 기반을 탄탄히 다지게 되어 앞으로 더 발전할 수 있는 든든한 토대를 마련한 느낌이다. 4주차 학습 내용 요약테스트 더블테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체Mokito 주요 애노테이션✅@InjectMocks 주의사항@InjectMocks은 Mockito 테스트 영역에서 사용되며, @Mock이나 @Spy로 생성된 Mockito 객체만 주입 대상으로 인식한다.따라서 @InjectMocks은 스프링 컨텍스트와는 무관하게 동작하며, @MockBean이나 @SpyBean 같은 스프링 테스트 애노테이션으로 생성된 객체를 주입받을 수 없다.@InjectMocks이 적용된 객체는 필드 주입, 생성자 주입, setter 주입 등을 통해 @Mock 또는 @Spy 객체가 자동으로 주입된다.✅@SpyBean 주의사항@SpyBean은 스프링 컨텍스트에 실제로 등록된 빈을 감싸는 프록시 객체를 생성하여 사용한다.따라서 @SpyBean의 대상이 인터페이스인 경우에는 스프링 컨텍스트에 해당 인터페이스를 구현한 실제 구현체 빈이 반드시 존재해야 한다.BDDMockito// Mockito Mockito.when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString())) .thenReturn(true); // BDDMockito BDDMockito.given(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString())) .willReturn(true); BDD 스타일로 Mockito를 사용할 수 있도록 지원해주는 라이브러리이다.실제 Mockito를 상속해서 구현한 객체이므로 모든 기능은 Mockito와 동일하다.더 나은 테스트 작성법✅하나의 테스트에서 하나의 검증만 수행하기(한 문단에 한 주제)for (ProductType productType : productTypes) { if (productType == ProductType.HANDMADE) { ... } if (productType == ProductType.BAKERY) { ... } } void containsStockType() { ProductType givenType = ProductType.HANDMADE; boolean result = ProductType.containsStock(givenType); assertThat(result).isFalse(); } void containsStockType2() { ProductType givenType = ProductType.BAKERY; boolean result = ProductType.containsStock(givenType); assertThat(result).isTrue(); } 분기문, 반복문 등의 논리 구조를 지양해야 한다.DisplayName을 한 문장으로 구성할 수 있는지 판단해보자. ✅제어 가능한 테스트가 가능하도록 코드 작성하기public Order createOrder() { LocalDateTime currentDateTime = LocalDateTime.now(); LocalDate currentDate = currentDateTime.toLocalDate(); if (currentTime.isBefore(...)) throw new IllegalArumentException(...) } public Order createOrder(LocalDateTime currentDateTime) { LocalDate currentDate = currentDateTime.toLocalDate(); if (currentTime.isBefore(...)) throw new IllegalArumentException(...) } ✅테스트 간 독립성 보장하기@TestInstance(TestInstance.Lifecycle.PER_CLASS) class SharedStateTest { private int count = 0; @Test void testIncrementOnce() { count++; assertThat(count).isEqualTo(1); // ✅ 통과 } @Test void testIncrementTwice() { count++; count++; assertThat(count).isEqualTo(2); // ❌ 실패 (이전 테스트에서 count 증가됨) } } 테스트가 외부 조건, 순서, 공유 상태 등에 영향을 받지 않고 항상 동일한 조건에서 수행되어야 한다. ✅한 눈에 들어오는 Test Fixture@DisplayName("신규 상품을 등록한다. 상품번호는 가장 최근 상품의 상품번호에서 1 증가한 값이다.") @Test void createProduct() { // Given Product product = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); productRepository.save(product); // When ProductResponse productResponse = productService.createProduct(request); // Then ... } // 픽스처 생성 메서드 // **createProduct 테스트에서 상품번호 외의 값은 고려할 대상이 아니다.** private Product createProduct(String productNumber) { return Product.builder() .productNumber(productNumber) .type(HANDMADE) .sellingStatus(SELLING) .name("아메리카노") .price(4000) .build(); } beforeAll, setUp (@beforeEach) 사용 지양하기 (사용하고 싶다면 아래 2가지 질문해보기) 각 테스트 입장에서 봤을 때, 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는가?수정해도 모든 테스트에 영향을 주지 않는가?data.sql과 같은 공통 로직으로 테스트 픽스처 구성하지 않기픽스처 생성 로직은 같은 테스트 클래스에서 관리하기픽스처 생성 메서드에서는 정말 필요한 값만 파라미터로 받기 테스트는 하나의 이해하기 쉬운 문서처럼 작성되어야 하며, given 절에서 테스트의 픽스처를 구성하는 것이 가독성과 유지보수 측면에서 유리하다. ✅Spring Data JPA 사용 환경에서 Test Fixture 클렌징 시 주의사항✅@DaynamicTestclass OrderStatusDynamicTest { @TestFactory Collection testOrderStatusTransitionsWithDifferentLogic() { return List.of( DynamicTest.dynamicTest("CREATED → PAID: 결제 처리 및 영수증 생성", () -> { // Given Order order = new Order(OrderStatus.CREATED); // When order.processPayment(); // 내부에서 status 변경 + 영수증 생성 // Then assertEquals(OrderStatus.PAID, order.getStatus()); assertNotNull(order.getReceipt()); }), DynamicTest.dynamicTest("PAID → SHIPPED: 송장 발급 및 상태 변경", () -> { // Given Order order = new Order(OrderStatus.PAID); // When order.prepareShipment(); // 송장 생성 + 상태 변경 // Then assertEquals(OrderStatus.SHIPPED, order.getStatus()); assertNotNull(order.getInvoiceNumber()); }), DynamicTest.dynamicTest("SHIPPED → DELIVERED: 배송 완료 처리", () -> { // Given Order order = new Order(OrderStatus.SHIPPED); // When order.markAsDelivered(); // Then assertEquals(OrderStatus.DELIVERED, order.getStatus()); assertTrue(order.isDeliveredTimeRecorded()); }) ); } } 전체 테스트 수행 시간 줄이기✅스프링 컨텍스트가 다시 로딩되는 케이스스프링 테스트 프레임워크는 컨텍스트 로딩 비용을 줄이기 위해 컨텍스트 캐시를 사용한다.동일한 설정(Class, 프로파일, MockBean 등)이면 컨텍스트를 재사용한다.하지만 위와 같은 조건이 달라지면 캐시가 무효화되고 컨텍스트 재로딩이 발생한다. ✅해결 방법Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구이다.API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원활하게 한다.기본적으로 AsciiDoc을 사용하여 문서를 작성한다.테스트 코드를 통과해야 문서가 만들어진다.프로덕션 코드에 비침투적이다. (Swagger는 침투적이다.) ✅빌드 설정 plugins { id "org.asciidoctor.jvm.convert" version "3.3.2" } configurations { asciidoctorExt } dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' (3) testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' (4) } // 테스트 실행 시 생성되는 REST Docs 스니펫 파일들이 저장될 디렉토리를 정의 ext { snippetsDir = file('build/generated-snippets') } // 테스트 태스크가 snippetsDir에 출력 파일을 생성한다고 Gradle에 알림 test { outputs.dir snippetsDir } asciidoctor { // 스니펫 파일들을 입력으로 사용 inputs.dir snippetsDir // AsciiDoc 확장 기능 활성화 configurations 'asciidoctorExt' // 모든 하위 폴더의 index.adoc 파일만 처리 sources { include("**/index.adoc") } // 소스 파일의 위치를 기준으로 상대 경로 해석 baseDirFollowsSourceDir() // AsciiDoc 생성 전에 반드시 테스트 실행 dependsOn test } // Spring Boot JAR 파일에 생성된 문서를 포함 bootJar { dependsOn asciidoctor // 생성된 HTML 문서 파일들(현재 예시에서는 index.adoc)을 가져옴 from ("${asciidoctor.outputDir}") { into 'static/docs' } } test → 스니펫 파일 생성 (build/generated-snippets)asciidoctor → AsciiDoc을 HTML로 변환bootJar → 생성된 HTML을 JAR에 포함 ✅스니펫 조합 템플릿 작성ifndef::snippets[] :snippets: ../../build/generated-snippets endif::[] = CafeKiosk REST API 문서 :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 2 :sectlinks: [[Product-API]] == Product API // 유지보수 편의를 위해 각각의 adoc 파일을 분리하여 index.adoc 파일에 합칠 수 있다. include::api/product/product.adoc[]기본 경로 : src/docs/asciidoc/index.adocgenerated-snippets에 생성된 코드 조각들을 하나의 완성된 문서로 조합하는 템플릿 역할을 한다.AsciiDoctor는 index.adoc을 처리하여 완성된 HTML 문서를 생성한다.여러 스니펫(파일 조각)을 어떤 형식으로 배치할지 결정한다. ✅커스텀 스니펫 템플릿 작성src/test/resources/org/springframework/restdocs/templates/asciidoctor/ ├── request-fields.snippet ├── response-fields.snippet ├── path-parameters.snippet ├── request-parameters.snippet ├── http-request.snippet ├── http-response.snippet ├── curl-request.snippet └── ...기본 경로 : src/test/resources/org/springframework/restdocs/templates/asciidoctor/==== Request Fields |=== |Path|Type|Optional|Description {{#fields}} |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} |{{#tableCellContent}}{{#optional}}O{{/optional}}{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |===request-fields.snippet 오바라이드 예시Spring REST Docs는 기본 스니펫 템플릿이 내장되어 있다.기본 경로에 파일을 생성하면 기본 스니펫 템플릿을 오버라이드할 수 있다.각각의 스니펫을 어떤 형식으로 보여줄지 결정한다. ✅사용 예시 mockMvc.perform(post("/api/v1/products/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ) .andDo(print()) .andExpect(status().isOk()) // restdoc config .andDo(document("product-create", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), // request requestFields( fieldWithPath("type").type(JsonFieldType.STRING).description("상품 타입"), fieldWithPath("sellingStatus").type(JsonFieldType.STRING).description("상품 판매상태").optional(), fieldWithPath("name").type(JsonFieldType.STRING).description("상품 이름"), fieldWithPath("price").type(JsonFieldType.NUMBER).description("상품 가격") ), // response responseFields( fieldWithPath("code").type(JsonFieldType.NUMBER).description("코드"), fieldWithPath("status").type(JsonFieldType.STRING).description("상태"), fieldWithPath("message").type(JsonFieldType.STRING).description("메시지"), fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("상품 ID"), fieldWithPath("data.productNumber").type(JsonFieldType.STRING).description("상품 번호"), fieldWithPath("data.type").type(JsonFieldType.STRING).description("상품 타입"), fieldWithPath("data.sellingStatus").type(JsonFieldType.STRING).description("상품 판매상태"), fieldWithPath("data.name").type(JsonFieldType.STRING).description("상품 이름"), fieldWithPath("data.price").type(JsonFieldType.NUMBER).description("상품 가격") ) ) );requestResponse
2025. 06. 19.
0
워밍업 클럽 4기 BE - Day18 미션
1. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.@MockBean, @SpyBean은 스프링 부트 테스트에서 사용되는 애노테이션이다.@Mock테스트 대상 클래스에 주입할 가짜(Mock) 객체를 생성함직접 컨트롤 가능하지만 스프링 컨텍스트에는 등록되지 않음@MockBean 스프링 컨텍스트에 Mock 객체로 등록함기존 실제 빈을 대체함 (통합 테스트에서 사용)@Spy원본 객체의 일부 메서드만 Stub하고, 나머지는 실제 메서드를 호출함@SpyBean스프링 컨텍스트에 등록되는 Spy 객체를 생성일부 메서드만 Stub하고 나머지는 실제 메서드 사용@InjectMocks@Mock 또는 @Spy로 생성한 객체를 주입받는 대상 클래스에 붙임필드, 생성자, setter로 의존성 주입을 자동 수행 2. 아래 3개의 테스트가 있습니다. 내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요? (@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)AS-IS@BeforeEach void setUp() { ❓ } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { 1-1. 사용자 생성에 필요한 내용 준비 1-2. 사용자 생성 1-3. 게시물 생성에 필요한 내용 준비 1-4. 게시물 생성 1-5. 댓글 생성에 필요한 내용 준비 1-6. 댓글 생성 // given ❓ // when ❓ // then 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { 2-1. 사용자 생성에 필요한 내용 준비 2-2. 사용자 생성 2-3. 게시물 생성에 필요한 내용 준비 2-4. 게시물 생성 2-5. 댓글 생성에 필요한 내용 준비 2-6. 댓글 생성 2-7. 댓글 수정 // given ❓ // when ❓ // then 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { 3-1. 사용자1 생성에 필요한 내용 준비 3-2. 사용자1 생성 3-3. 사용자2 생성에 필요한 내용 준비 3-4. 사용자2 생성 3-5. 사용자1의 게시물 생성에 필요한 내용 준비 3-6. 사용자1의 게시물 생성 3-7. 사용자1의 댓글 생성에 필요한 내용 준비 3-8. 사용자1의 댓글 생성 3-9. 사용자2가 사용자1의 댓글 수정 시도 // given ❓ // when ❓ // then 검증 } TO-BE@BeforeEach void setUp() { 1-1, 2-1. 사용자 생성에 필요한 내용 준비 1-2, 2-2. 사용자 생성 1-3, 2-3. 게시물 생성에 필요한 내용 준비 1-4, 2-4. 게시물 생성 } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { // given 1-5. 댓글 생성에 필요한 내용 준비 // when 1-6. 댓글 생성 // then 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { // given 2-5. 댓글 생성에 필요한 내용 준비 2-6. 댓글 생성 // when 2-7. 댓글 수정 // then 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { // given 3-1. 사용자1 생성에 필요한 내용 준비 3-2. 사용자1 생성 3-3. 사용자2 생성에 필요한 내용 준비 3-4. 사용자2 생성 3-5. 사용자1의 게시물 생성에 필요한 내용 준비 3-6. 사용자1의 게시물 생성 3-7. 사용자1의 댓글 생성에 필요한 내용 준비 3-8. 사용자1의 댓글 생성 // when 3-9. 사용자2가 사용자1의 댓글 수정 시도 // then 검증 }
2025. 06. 17.
0
워밍업 클럽 4기 BE - Day16 미션
Layered Architecture 구조의 레이어 별 특징Presentation Layer사용자와 직접 상호작용하는 계층이다.요청을 수신하고, 처리 결과를 응답한다.스프링은 MVC 아키텍처를 사용하여 해당 계층을 구현한다.Business Layer실제 서비스의 핵심 비지니스 로직을 처리하는 계층이다. 트랜잭션 처리를 담당한다.Persistence Layer데이터를 조회 및 저장하는 계층이다.보통 데이터베이스에 접근하여 여러 작업을 수행한다. Layered Architecture 구조의 레이어 별 테스트 방법 (with Spring)Presentation LayerSpring의 @WebMvcTest 기능을 통해 해당 계층의 빈만 컨테이너에 등록하여 테스트를 진행한다.해당 계층과의 의존 관계(불필요한 관심사)는 모킹 처리하여 계층 테스트의 독립성을 보장해야 한다.Business Layer핵심 로직이 Persistence Layer와 강하게 연관되어 있다면, 두 계층을 묶어서 통합 테스트로 진행한다.그렇지 않다면, Mockito를 이용해 의존하는 Repository나 외부 서비스들을 Mock 처리한다.Persistence LayerJPA를 사용하고 있다면, @DataJpaTest 기능을 통해 해당 계층의 빈만 컨테이너에 등록하여 테스트를 진행한다.저장소의 CRUD 동작을 확인하는것이 목적이기에 프로덕션이 아닌 테스트 전용 저장소를 사용해야 한다.(보통 인메모리 데이터베이스를 활용한다.)
2025. 06. 15.
1
워밍업 클럽 4기 BE - 3주차 발자국
3주차 미션 회고토이 프로젝트에서 직접 테스트 코드를 작성하면서, 예상보다 다양한 부분에서 고민할 지점들이 생겼다. 테스트 설명을 일관되게 유지하는 방법부터, 하나의 테스트 단위를 어떤 크기로 설정할지까지 여러 가지를 고민하게 됐다. 직접 테스트를 작성하지 않았다면 떠올리지 못했을 다양한 관점들을 생각해볼 수 있는 좋은 기회였다. 3주차 강의 회고스프링을 활용해 간단한 서비스를 직접 만들어보고, 각 기능에 맞는 테스트 코드를 작성해 나가는 과정을 통해 많은 것을 배울 수 있었다. 테스트 코드 작성뿐만 아니라, 잠시 잊고 지냈던 스프링과 JPA 관련 트러블슈팅부터 아키텍처에 대한 관점, 패키지 구조에 대한 인사이트까지 얻을 수 있었던 유의미한 시간이었다. 3주차 학습 내용 요약Persistence LayerData Access의 역할 비지니스 가공 로직이 포함되어서는 안된다. Data에 대한 CRUD에만 집중한 레이어Business Layer비지니스 로직을 구현하는 역할Persistence Layer와의 상호작용을 통해 비지니스 로직을 전개시킨다. 트랜잭션을 보장해야 한다. Presentation Layer외부 세계의 요청을 가장 먼저 받는 계층 파라미터에 대한 최소한의 검증을 수행한다.주요 인사이트 1섹션명 : Business Layer 테스트 (2)타임블록 : 11:10 ~ 12:00내용 : @DataJpaTest와 @SpringBootTest주요 인사이트 2섹션명 : Business Layer 테스트 (3)타임블록 : 31:00 ~ 32:00내용 : 여러 계층에서 생기는 중복 벨리데이션에 대한 고찰주요 인사이트 3섹션명 : Business Layer 테스트 (3)타임블록 : 46:00 ~ 47:00내용 : 테스트 코드의 @Transaction 사용 시 주의사항
2025. 06. 08.
1
워밍업 클럽 4기 BE - 2주차 발자국
2주차 미션 회고배운 내용을 토대로 실제 코드에 적용하면서 정말 많은 것을 배우고 느꼈다. 리팩토링에는 정답이 없지만 오답은 있을 수 있다는 것을 깨달았고, 앞으로 개발할 때 클린코드의 진정한 의미에 대해 생각하면서 코드를 작성해야겠다. 2주차 강의 회고클린코드 강의를 마무리하고 본격적인 테스트 코드 강의에 들어가기 전 워밍업 느낌의 1~5장 챕터라고 생각됐다.테스트의 개념부터 기본적인 테스트 코드 작성법까지 어찌보면 가장 중요한 챕터일지도 모른다는 생각이 들었다.최대한 꼼꼼히 공부하면서 첫 시작부터 기반을 다진다는 마음가짐으로 강의를 들었다. 2주차 학습 내용 요약테스트가 필요한 이유테스트 코드를 작성하지 않는다면,변화가 생기는 매순간마다 발생할 수 있는 모든 케이스를 고려해야 한다.변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 한다.빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다.잘못된 테스트 코드를 작성한다면,프로덕션 코드의 안정성을 제공하기 힘들어진다.테스트 코드 자체가 유지보수하기 어려운 짐이 된다.잘못된 검증이 이루어질 가능성이 생긴다.올바른 테스트 코드를 작성한다면,자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있다.소프트웨어의 빠른 변화를 지원한다.팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.가까이 보면 느리지만 멀리 보면 가장 빠르다.소프트웨어 테스트 종류✅기능 테스트단위 테스트: 하나의 함수나 메서드 단위 동작 검증 (로직 중심) 통합 테스트: 여러 모듈이 함께 동작하는지 확인 (인터페이스, 데이터 흐름 검증) 시스템 테스트: 전체 시스템이 요구사항대로 동작하는지 확인 E2E 테스트: 사용자 흐름을 처음부터 끝까지 시뮬레이션하여 기능 동작 여부 확인 회귀 테스트: 기존 기능이 변경 없이 잘 동작하는지 반복 확인 유효성 검사 테스트: 입력값 유효성, 필수 값 누락 등 사용자의 잘못된 입력 처리 검증 조건부 분기 테스트: if/else, switch 등 분기문이 정상적으로 작동하는지 확인 ✅비기능 테스트동시성 테스트: 여러 스레드/사용자가 동시에 접근할 때의 일관성, 충돌 여부 확인 성능 테스트: 시스템이 응답 속도나 처리량 요구사항을 충족하는지 확인 부하 테스트: 많은 사용자나 트래픽이 몰릴 때 성능 저하 없이 동작하는지 확인 스트레스 테스트: 시스템 한계를 넘는 부하에서도 장애 없이 복구 가능한지 확인단위 테스트 시 검증해야 할 주요 케이스정상 케이스기대한 입력값이 주어졌을 때 올바른 결과가 나오는지 확인 ex) input = 5 → output = 25경계 값 테스트허용되는 최소/최대값 근처의 입력을 검증 ex) min = 1, max = 100 → 0, 1, 100, 101예외/에러 케이스잘못된 입력이 들어왔을 때 예외가 잘 처리되는지 확인 ex) input = null → IllegalArgumentException 발생 빈 값/Null 처리null, 빈 리스트, 빈 문자열 등 비정상적이지만 흔한 값 검증 ex) "", null, [] 입력 처리 확인 중복/경합 조건동일 입력을 여러 번 넣었을 때 문제가 없는지 확인 ex) 중복 ID 처리, 중복 요청 무시 등 상태 기반 테스트특정 상태 변경 전/후 동작이 예상대로 수행되는지 검증 ex) 로그인 전/후 메뉴 접근 가능 여부 등 시간 관련 테스트시간에 따라 달라지는 로직의 동작 확인 ex) 쿠폰 유효 기간, 예약 처리 등 성능 경계 테스트데이터량이 많을 때 정상 처리 여부 ex) 리스트 10,000건 처리 가능 여부 등테스트하기 어려운 영역✅관측할 때마다 다른 값에 의존하는 코드 (현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력 등)public class Order { public String generateOrderId() { return "ORD-" + UUID.randomUUID(); // 매번 다른 값 } public boolean isTodayOrder(LocalDate orderDate) { return orderDate.equals(LocalDate.now()); // 현재 날짜 의존 } } ✅외부 세계에 영향을 주는 코드 (표준 출력, 메시지 발송, 데이터베이스 기록 등)public class Notifier { public void sendNotification(String message) { System.out.println("Sending message: " + message); // 표준 출력 } } public class UserRepository { public void save(User user) { // 실제 DB에 저장하는 코드 (예: JDBC, JPA) entityManager.persist(user); // 외부 세계에 영속성 부여 } } TDD (Test Driven Development)테스트를 먼저 작성하고, 그 테스트를 통과하는 최소한의 코드를 구현한 후, 리팩토링하는 개발 방식이다.// Step 1 (Red): 실패하는 테스트 작성 @Test void add_shouldReturnSum() { Calculator calc = new Calculator(); assertEquals(5, calc.add(2, 3)); // 아직 add 메서드 없음 → 실패 } // Step 2 (Green): 테스트 통과하는 최소한의 코드 작성 public int add(int a, int b) { return a + b; } // Step 3 (Refactor): 코드 리팩토링 (필요시 구조 개선 등) 🔴Red: 실패하는 테스트 작성 (아직 기능이 없으므로 실패)✅Green: 테스트를 통과할 최소한의 코드 작성🧹Refactor: 중복 제거 및 코드 정리 (기능은 그대로)BDD (Behavior Driven Development)사용자 관점의 시나리오(행동)를 중심으로 기능을 정의하고 테스트하는 접근 방식이다.Feature: 로그인 기능 Scenario: 올바른 아이디와 비밀번호 입력 시 로그인 성공 **Given** 사용자가 로그인 페이지에 접속했을 때 **When** 아이디와 비밀번호를 올바르게 입력하면 **Then** 로그인에 성공하고 홈 화면으로 이동한다 Given: 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등)When: 시나리오 행동 진행Then: 시나리오에 대한 결과 명시, 검증테스트 제대로 설명하기- 음료 1개 추가 **테스트** (bad) → ~테스트 문장 지양 - 음료를 1개 **추가할 수 있다.** (not bad) → 명사의 나열보단 문장으로 - 음료를 1개 **추가하면 주문 목록에 담긴다.** (good) → 테스트 행위에 대한 결과까지 - 특정 시간 이전에 주문을 생성하면 **실패한다.** (bad) -> 테스트 현상 중점 기술 - **영업 시간** 이전에는 주문을 생성할 수 없다. (good) -> 도메인 용어 사용~테스트 문장 지양하기명사의 나열보단 문장으로 기술하기테스트 행위에 대한 결과까지 기술하기도메인 용어를 사용하여 한층 추상화된 내용 담기테스트의 현상을 중점으로 기술하지 말기
2025. 06. 01.
1
워밍업 클럽 4기 BE - 1주차 발자국
1주차 미션 회고강의에서 배운 내용을 내 머릿속에 있는 단어들로 재해석하니 개념들이 확실히 잡히는 느낌이었다.AS-IS 코드에 챕터 별 내용을 하나씩 적용해가면서 리팩토링을 하니 어느새 클린 코드가 되어 있어서 재밌는 경험이었다. 1주차 강의 회고생각보다 긴 강의 시간에 조금 지치기도 했지만 실습 위주의 커리큘럼으로 집중해서 진행할 수 있었다.요즘 업무가 바쁘다는 핑계로 코드를 작성할 때 이런 개념들을 잠시 등한시 했던 나를 반성하게 된다.클린 코드 핸드북을 목표로 핵심만 정리해서 추후 코드 작성 시 매번 참고하는 자료가 되도록 만들어야겠다. 1주차 학습 내용 요약추상화추상(抽象) : 중요한 정보는 가려내어 남기고, 덜 중요한 정보는 생략하여 버린다.추상화는 복잡한 데이터와 복잡한 로직을 단순화하여 이해가 쉽도록 돕는다.잘못된 추상화가 야기하는 사이드 이펙트는 생각보다 크다.적절한 추상화는 해당 도메인의 문맥 안에서, 정말 중요한 핵심 개념만 남겨서 표현하는 것이다.이름 짓기단수와 복수를 구분하기이름 줄이지 않기 (관용어는 예외)은어/방언 사용하지 않기비슷한 상황에서 자주 사용하는 단어, 개념 습득하기메서드 추상화void 서점에서_책을_샀다() { 우빈이는 산책을 하다 은행에 가서 얼마 인출했다. 서점 가는 길에 아이스크림을 하나 사먹었다. 남은 돈으로 서점에 가서 보고싶은 책을 고르고, 책을 구매했다. }void 산책하면서_돈쓰기() { Money 은행에서_현금_인출(); Balance 아이스크림_사먹기(Money); Book 서점에서_책_구입하기(Balance); }잘 쓰여진 코드라면, 한 메서드의 주제는 반드시 하나다.추상화 레벨public static void main(String[] args) { showGameStartComments(); **// 10 (추상화 레벨)** initializeGame(); **// 10** showBoard(); **// 10** if(gameStatus == 1) { **// 5 (갑자기 너무 구체적인 내용)** ... } }하나의 세계 안에서는, 추상화 레벨이 동등해야 한다.코드 주변과 동등한 추상화 레벨을 갖는게 중요하다.매직 넘버, 매직 스트링private static String[][] board = new String[8][10];public static final int BOARD_ROW_SIZE = 8; public static final int BOARD_COL_SIZE = 10; private static String[][] board = new String[BOARD_ROW_SIZE][BOARD_COL_SIZE];의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등을 의미한다.상수 추출로 이름을 짓고 의미를 부여함으로써 가독성, 유지보수성이 높아진다.메서드 선언부// bad String createDailyShopKey(String shopId, String localDateString); // good String createDailyShopKey(String shopId, LocalDate sellingDate); 추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름으로 짓는다.파라미터의 타입, 개수, 순서를 통해 의미를 전달한다.적절한 타입의 반환값으로 리턴한다. (void 대신 반환할 만한 값이 있는지 고민해보기)Early returnif(a > 3) { doSomething1(); } else if(a 1) { doSomething2(); } else { doSomething3(); } void extracted() { if(a > 3) { doSomething1(); return; } if(a 1) { doSomething2(); return; } doSomething3(); } 중첩문 줄이기for(int i=0; i=10 && j추상화를 통한 사고 과정의 depth를 줄이는 것이 중요하다.중첩 구조로 표현하는 것이 사고하는 데 더 도움이 된다고 판단한다면, 그대로 놔두는 것이 더 나은 선택일 수 있다.사용할 변수는 최대한 가깝게 선언하기int i = 10; // 코드 20줄 ... int j = i + 30; // 해당 코드에서 i의 값을 다시 기억해내야 한다.int i = 10; int j = i + 30;부정어를 대하는 자세부정 연산자(!)는 가독성이 떨어진다.부정어구를 쓰지 않아도 되는 상황인지 체크한다.부정의 의미를 담은 다른 단어가 존재하는지 고민해본다.Null을 대하는 자세항상 NullPointException을 방지하는 방향으로 경각심을 가진다.메서드 설계 시 return null 사용을 지양한다. (Optional 사용 고려)✅Optionalpublic T orElse(T other) { return value != null ? value : other; } public T orElseGet(Supplier supplier) { return value != null ? value : supplier.get(); } // ================================================================================ // Optional value = Optional.of("Hello"); // ⚠️ expensiveOperation() 항상 호출됨 String result = value.orElse(expensiveOperation()); // ✅ 값이 비어있을 때만 expensiveOperation() 호출됨 String result = value.orElseGet(() -> expensiveOperation()); 꼭 필요한 상황에서 반환 타입에 사용한다.Optional을 파라미터로 받지 않도록 한다.Optional을 반환받았다면 최대한 빠르게 해소한다.상속 대신 조합 사용하기public abstract class Cell { protected boolean isOpened; protected boolean isFlagged; public abstract String getSign(); ... } public class EmptyCell extends Cell { ... @Override public String getSign() { if (isOpened) { return EMPTY_SIGN; } if (isFlagged) { return FLAG_SIGN; } return UNCHECKED_SIGN; } }public class CellState { private boolean isFlagged; private boolean isOpened; public static CellState initialize() { return new CellState(false, false); } public boolean isOpened() { return isOpened; } public boolean isFlagged() { return isFlagged; } } public class EmptyCell { private final CellState cellState = CellState.initialize(); }SRP 분리CellState는 "상태 관리"만 담당하고, EmptyCell은 "표현 방식"만 담당함 재사용성 증가동일한 CellState 클래스를 다른 셀 유형(MineCell, NumberCell)에서도 재사용 가능테스트 용이성상태 로직(isOpened, isFlagged)을 독립적으로 테스트할 수 있음유연성 증가EmptyCell에 새로운 동작을 추가할 때 상속보다 덜 제한적 (확장 쉬움)상속 제한 회피Java는 단일 상속만 가능 → 조합을 쓰면 다른 기능도 쉽게 조합 가능불변성 강화CellState를 final로 구성하거나 setter 없이 만들어 불변 객체로 설계 가능의존성 주입 가능테스트나 상태 공유 목적일 때 CellState를 외부에서 주입 받을 수 있음 (mock 주입 등)Value Objectclass Address { private String 시도; private String 시군구; private String 도로명; // 생성자 외에 상태 변경 메서드 없음 (불변성) public Address(String 시도, String 시군구, String 도로명) { // 유효성 검증 if (시도== null || 시도.trim().isEmpty()) { throw new IllegalArgumentException(fieldName + "는 비어 있을 수 없습니다."); } if(...) this.시도 = 시도; this.시군구 = 시군구; this.도로명 = 도로명; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Address)) return false; // 동등성 검증 Address that = (Address) o; return Objects.equals(시도, that.시도) && Objects.equals(시군구, that.시군구) && Objects.equals(도로명, that.도로명); } @Override public int hashCode() { return Objects.hash(시도, 시군구, 도로명); } }도메인의 어떤 개념을 추상화하여 표현한 값 객체값으로 취급하기 위해서, 불변성, 동등성, 유효성 검증 등을 보장해야 한다.✅vs Entityclass UserAccount { private String userId; // 식별자 private String 이름; private String 생년월일; }식별자가 같으면 동등한 객체로 취급한다.VO는 식별자 없이, 내부의 모든 값이 다 같아야 동등한 객체로 취급한다.일급 컬렉션class CreditCards { private final List cards; public List findValidCards() { return this.cards.stream() .filter(CreditCard::isValid) .toList(); } }컬렉션을 포장하면서, 컬렉션만을 유일하게 필드로 가지는 객체이다.컬렉션을 추상화여 의미를 담을 수 있고, 가공 로직의 보금자리가 생긴다.컬렉션 반환 시 외부 조작을 피하기 위해 새로운 컬렉션으로 만들어서 반환해야 한다.Enum의 특성과 활용public enum UserAction { OPEN("셀 열기"), FLAG("깃발 꽂기"), UNKNOWN("알 수 없음"); private final String description; UserAction(String description) { this.description = description; } }Enum은 상수의 집합이며, 상수와 관련된 로직을 담을 수 있는 공간이다.특정 도메인 개념에 대해 그 종류와 기능을 명시적으로 표현해줄 수 있다.변경이 잦은 개념은 Enum 보다 DB로 관리하는 것이 나을 수 있다.다형성 활용하기private String decideCellSignFrom(CellSnapshot snapshot) { CellSnapshotStatus status = snapshot.getStatus(); if (status == CellSnapshotStatus.EMPTY) { return EMPTY_SIGN; } if (status == CellSnapshotStatus.FLAG) { return FLAG_SIGN; } if (status == CellSnapshotStatus.LAND_MINE) { return LAND_MINE_SIGN; } if (status == CellSnapshotStatus.NUMBER) { return String.valueOf(snapshot.getNearbyLandMineCount()); } if (status == CellSnapshotStatus.UNCHECKED) { return UNCHECKED_SIGN; } throw new IllegalArgumentException("확인할 수 없는 셀입니다."); }private String decideCellSignFrom(CellSnapshot snapshot) { List cellSignProviders = List.of( new EmptyCellSignProvicer(), new FlagCellSignProvider(), ... ); return cellSignProviders.stream() .filter(provider -> provider.supports(snapshot)) .findFirst() .map(provider -> provider.provide(snapshot)) .orElseThrow(() -> new IllegalArgumentException("확인할 수 없는 셀입니다.")); }public interface CellSignProvidable { boolean supports(CellSnapshot cellSnapshot); String provide(CellSnapshot cellSnapshot); }public class EmptyCellSignProvicer implements CellSignProvidable { private static final String EMPTY_SIGN = "■"; @Override public boolean supports(CellSnapshot cellSnapshot) { return cellSnapshot.getStatus() == CellSnapshotStatus.EMPTY; } @Override public String provice(CellSnapshot cellSnapshot) { return EMPTY_SIGN; } }
2025. 05. 30.
0
워밍업 클럽 4기 BE - Day4 미션
1. 아래 코드와 설명을 보고, [섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링해 봅시다.AS-ISpublic boolean validateOrder(Order order) { if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } else { if (order.getTotalPrice() > 0) { if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } else if (!(order.getTotalPrice() > 0)) { log.info("올바르지 않은 총 가격입니다."); return false; } } return true; } TO-BEpublic boolean validateOrder(Order order) { // NullPointException 처리 if(order == null) { log.info("주문 정보가 없습니다."); return false; } if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } if (order.getTotalPrice() > 0) { // 부정구 제거 if (order.hasCustomerInfo()) { return true; } log.info("사용자 정보가 없습니다."); return false; } log.info("올바르지 않은 총 가격입니다."); return false; }NullPointException 예외 처리 추가부정 연산자 제거Early return으로 중첩문 최소화 2. SOLID에 대하여 자기만의 언어로 정리해 봅시다.SRP: 비서는 회의 스케줄만 관리하고, 식사 예약은 총무팀이 한다.OCP: 게임 콘솔은 본체를 수정하지 않고 새 게임 카드만 꽂아도 작동한다.LSP: 정직원이 회사 시스템에 로그인할 수 있다면, 계약직 직원도 똑같이 로그인할 수 있어야 한다.ISP: 카메라 기능만 필요한 사람은 여러 기능이 있는 스마트폰을 사용할 필요가 없다.DIP: 테니스 라켓은 특정 브랜드 공에만 맞게 만들지 않고 어떤 공이든 칠 수 있도록 표준 규격으로 만든다.
2025. 05. 27.
0
워밍업 클럽 4기 BE - Day2 미션
1. 강의에서 안내하는 것처럼, 프로젝트를 개인 계정으로 fork하고 강의를 수강해 주세요.https://github.com/mkms1104/readable-code "추상과 구체" 강의를 듣고, 생각나는 추상과 구체의 예시가 있다면 한번 3~5문장 정도로 적어봅시다.ex) 코딩 공부는 계획적으로 해야한다. (추상)알고리즘과 CS로 기초 지식을 쌓는다. 특정 언어를 선택해서 공부한다.아키텍처와 설계를 배운다.프로젝트를 완성한다.
백엔드