블로그
전체 52025. 03. 30.
0
인프런 워밍업 스터디 클럽 3기 백엔드-code 4주 차 마지막 발자국
이 글은 박우빈님의 Practical-Testing 강의 를 참조하여 작성한 글 입니다.또 다시 일주일이 지나 어느새 마지막 발자국만을 남겨두었다.반신반의하며 시작했지만, 한 달 동안 무려 2개의 강의를 완강하고 미션까지 모두 수행한 내 자신이 참 대견하다! 히히 😊벌써 1분기가 끝난게 믿기지 않지만, 그래도 이것저것 공부하며 알차게 살았더니 이번 1분기는 아쉽지 않게 보내줄 수 있을 것 같다. 특히 테스트 코드와 관련해 많은 지식을 얻을 수 있었다.강의 내용이 알찼던 것은 물론이고, 미션을 수행하며 쌓은 지식을 정리하고 다른 사람의 코드를 읽는 과정에서도 많은 배움이 있었다.무엇보다 우빈님께 직접 코드 리뷰를 받으면서 내 코드의 개선점을 확인하고, 작성 과정에서 궁금했던 부분을 직접 물어볼 수 있었던 경험이 정말 값졌다.테스트 코드를 작성할 때마다 내가 올바르게 작성하고 있는 지에 대한 의심이 항상 존재하였는데, 코드 리뷰를 통해 테스트 코드 작성에 대한 자신감이 한층 더 생긴 것 같다!ㅋㅋㅋ또한 리뷰를 통해 많은 고민 및 궁금증을 해결할 수 있었고, 가독성 및 유지보수 하기 좋은 테스트 코드 작성법 및 테스트 작성의 중요성을 한층 더 깨닫게 된 것 같다!다음에 스터디 클럽이 또 열린다면,,무조건 참여하길 바란다👍🏻학습 내용 요약 Mock을 마주하는 자세@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이위 링크에 미션 수행하면서 학습한 내용을 정리해 두었으니 참고 바란다! 더나은 테스트를 작성하기 위한 구체적 조언완벽하게 제어하기테스트 코드를 작성할 때 모든 조건들은 완벽히 제어가 가능해야 한다.LocalDateTime.now()와 같이 제어할 수 없는 값은 최대한 지양하자! 테스트 간 독립성을 보장하자공유 변수 사용x 한 눈에 들어오는 Test Fixture 구성하기Test Fixturegiven절에서 생성했던 모든 객체들을 의미테스트를 위해 원하는 상태로 고정시킨 일련의 객체BeforeEach, BeforeAll, AfterEach, AfterAll셋업에 유치한 이런 공통의 픽스처들은 테스트와 결합도 생기게 만듦픽스처들을 수정하거나 하는 경우에 모든 테스트에 공통으로 영향을 주기 때문에 지양하는 것이 좋음사용하는 기준:각 테스트 입장에서 봤을 때 아예 몰라도 테스트 내용을 이해하는 데 문제가 없을 때만 사용하기수정해도 모든 테스트에 영향을 주지 않는 경우에만 사용하기테스트 시 sql로 given 객체 생성하지 말자!given절이 파편화되어 뭘 테스트 해야하는지 파악하기 어려워짐프로젝트가 커질수록 데이터 구조, 필드 등 변경이 발생하면 sql문 관리가 어려워짐 Test Fixture 클렌징deleteAllInBatch테이블 전체를 bulk성으로 날릴 수 있는 좋은 메서드순서를 잘 고려를 해야함(중간테이블 먼저 삭제해주어야 함)deleteAllselect후 delete하기 때문에 테이블에 있는 데이터 수 만큼 쿼리가 실행됨순서 고려x 테스트 수행도 비용이다. 환경 통합하기service와 repository부분을 하나의 추상클래스(e,g,.IntegrationTestSupport)를 상속받게 함으로 서버 실행 횟수를 줄이기! Q. 테스트에서만 필요한 메서드가 생겼는데 프로덕션 코드에서는 필요 없다면? 무엇을 테스트하고 있는지를 명확히 인지하기현재 동작하고 있는 프로덕션 코드를 테스트 한다면 테스트에서는 필요한데 프로젝트에서는 필요 없는 그런 메소드들이 나올 수가 있다.이런 경우 만들어도 된다. 하지만 최대한 지양 하자. getter, 기본 생성자, 생성자 빌더, 사이즈 이런 것들 어떤 객체가 마땅히 가져도 될 마땅히 가져도 되는 행위라고 생각이 되면서 미래에도 충분히 사용될 수 있는 성격의 메소드일 것 같다. Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구 API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원할하게 함 기본적으로 AsciiDoc을 사용하여 문서 작성 장점테스트를 통과해야 문서가 만들어짐(신뢰도 높음)프로덕션 코드에 비침투적단점코드 양이 많다설정이 어렵다 미션Day 18 - @BeforeEach, given절, when절에 항목 배치나의 코드: https://inf.run/fK9MY 🧐 고려한 부분남아있는 데이터가 테스트에 영향을 끼치지 않도록 각 테스트 수행 전 @BeforeEach를 통해 데이터를 초기화 시켜주었다.모든 댓글 테스트에서 필요한 사용자와 게시물은 테스트의 기본 전제 조건이므로, 사용자 생성 및 게시물 생성 메서드를 별도로 분리하여 중복을 제거하였다. 우빈님의 코드 리뷰 (Feat. Day 11 - 스터디 카페 이용권 선택 시스템 테스트 코드 작성하기 )나의 코드: https://inf.run/xPthb Q. 나의 질문: @CsvSource내 enum(passType) 문자열 직접 작성으로 인한 유지보수 비용 증가 문제A. 우빈님 답변: passType이 변경되면, 이를 테스트하고 있는 부분이 같이 영향을 받게 되고변경한 사람은 테스트 수행 시점에 테스트가 깨진 것을 보고 영향 범위를 인지할 수 있다.따라서 테스트 코드가 없어서 영향도를 인지하지 못하는 것보다 나은 것이라 생각한다!물론 테스트 코드를 수정해야 하는 비용은 들겠지만, 테스트 코드는 원래 프로덕션 코드와 같이 상생하는 코드이므로,프로덕션 코드를 팔로업하면서 같이 변경되는 것이 더 자연스럽다!@ParameterizedTest @CsvSource({ "HOURLY, HOURLY, true", "HOURLY, WEEKLY, false" }) @DisplayName("이용권 내 이용권 타입이 비교하는 타입과 같은지 비교할 수 있다.") void isSamePassType(StudyCafePassType passType, StudyCafePassType expectedPassType, boolean expectedResult) { // given StudyCafeSeatPass pass = StudyCafeSeatPass.of(passType, 2, 4000, 0.0); // when boolean result = pass.isSamePassType(expectedPassType); // then assertThat(result).isEqualTo(expectedResult); } Q. 나의 질문: 일급컬렉션 내 테스트만을 위한 메서드 추가 없이 테스트 한 방법A. 우빈님 답변: 기본적으로 프로덕션에 없는 코드를 테스트 코드만을 위해 추가하는 것은 '지양'하는 것이 맞다.하지만 해당 기능이 단순하고, 미리에도 충분히 활용될 수 있다면 아주 보수적으로 추가해도 좋다고 생각한다.e,g,. 단순히 일급컬렉션 내부의 원소 개수를 반환하는 size(), 내부 항복이 존재하는지 확인할 수 있는 isEmpty() 등 해당 테스트에서 검증하고 싶은 것은 전체 개수가 아닌 타입별 개수이므로, 타입별 개수를 반환하는 메서드를 추가하기에는 애매해 보인다.따라서 어쩔 수 없이 아래 코드 처럼 작성하는 것이 맞으나, findPassBy()와 결합도가 생기는 것을 감안하면 될 것 같다고 하셨다.하지만 findPassBy() 단위 테스트는 필수!!@DisplayName("파일을 읽어 이용권 목록을 가져올 수 있다.") @Test void getSeatPasses() { // given SeatPassFileReader seatPassFileReader = new SeatPassFileReader(); // when StudyCafeSeatPasses seatPasses = seatPassFileReader.getSeatPasses(); // then assertAll( () -> assertThat(seatPasses.findPassBy(StudyCafePassType.HOURLY)).hasSize(6), () -> assertThat(seatPasses.findPassBy(StudyCafePassType.WEEKLY)).hasSize(5), () -> assertThat(seatPasses.findPassBy(StudyCafePassType.FIXED)).hasSize(2)); } 아래 테스트는 변수명 잘 지었다고 받은 칭찬 리뷰이다😆 마무리한 달동안 정말 많은 것을 배웠다,,,희희단순히 일방적으로 인풋만 넣는게 아니라,매일 강사님 그리고 스터디원분들과 소통하면서 함께 학습하니 더욱 단기간에 성장할 수 있었던 것 같다ㅎㅎㅎ미션도 너무너무 재밌었고, 그에 대한 우빈님의 피드백까지 받으니 정말이지 알차고 소중한 경험이었다.다음에 워밍업 스터디 클럽이 또 하게 된다면, 주변에 적극적으로 홍보해야겠다!!스터디를 기획 및 운영하신 모든분들, 강사님, 그리고 스터디에 참여한 스터디 분들 모두 고생 많으셨습니다☺
백엔드
・
클린코드
・
테스트코드
・
발자국
・
워밍업스터디클럽
2025. 03. 25.
0
레이어드 아키텍처 특징 및 테스트 작성법 알아보기
이 글은 박우빈님의 Practical-Testing 강의 를 참조하여 작성한 글입니다. 레이어드 아키텍처(Layered Architecture)란?소프트웨어 시스템을 계층(layer)으로 나누어 구성하는 설계 방식으로, 각 계층이 특정한 역할과 책임을 갖도록 설계하는 것을 의미한다.이렇게 각 계층들을 관심사를 기준으로 분리함으로써 계층의 응집도를 높이고 결합도를 낮출 수 있다.-> 유지보수성이 올라감! 레이어드 아키텍처의 주요 특징 계층 분리 Layered Architecture에서는 보통 3개의 Layer로 구성되어 있다.Presentation Layer (UI 계층): 사용자의 요청 및 응답을 처리하며 상호작용 한다.Business Layer (서비스 계층): 비즈니스 로직을 수행하는 책임을 지닌다.Persistence Layer (비즈니스 계층): DB에 접근하여 상호작용(데이터 CRUD) 한다. 독립성상위 계층은 하위 계층에 의존하지만,하위 계층은 상위 계층에 대한 지식이나 정보를 가지지 않아야 한다.e.g, Presentation Layer에서는 하위 계층인 Business Layer를 의존한다.이때 Business Layer는 상위 계층인 Presentation Layer에서 넘어온 데이터로 비즈니스 로직을 처리할 뿐이며,Presentation Layer 에 대해 알고 있지 않다!유연성 특정 계층의 구현을 변경하더라도 다른 계층에는 영향을 미치지 않도록 설계할 수 있다. 테스트 용이성계층별로 테스트가 가능하므로, 각 계층의 단위 테스트를 독립적으로 수행할 수 있다. 각 계층 테스트 코드 작성법 Presentation LayerAPI의 요청-응답 흐름과 응답 형식(HTTP 상태 코드와 JSON 응답 데이터)을 검증하는 테스트를 작성한다.@WebMvcTest 를 통해 테스트 하고자 하는 컨트롤러를 등록해준다.해당 컨트롤러를 테스트 하기위해 필요한 Business 계층(Service 클래스)를 @MockBean을 통해 주입하며, MockMvc 또한 의존성 주입해준다.이 때 json과의 소통이 필요하므로 객체를 json, json을 객채로 변환할 수 있게끔 ObjectMapper 또한 주입해주자.@WebMvcTest(controllers = OrderController.class) class OrderControllerTest { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @MockBean OrderService orderService; @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")); } @DisplayName("신규 주문을 등록할 때 상품번호는 1개 이상이어야 한다.") @Test void createOrderWithEmptyProductNumbers() throws Exception { // given OrderCreateRequest request = OrderCreateRequest.builder() .build(); // when // then mockMvc.perform( post("/api/v1/orders/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value("400")) .andExpect(jsonPath("$.status").value("BAD_REQUEST")) .andExpect(jsonPath("$.message").value("상품 번호 리스트는 필수입니다.")) .andExpect(jsonPath("$.data").isEmpty()); } } Business Layer작성한 비지니스 로직이 의도한 대로 작동하는지 검증하는 테스트를 작성한다.@SpringBootTest 을 통해 모든 Bean 의존성 주입함으로써 통합 테스트가 가능하다.해피케이스 뿐만 아니라 예외 테스트, 경계값 테스트 등 여러 케이스에 대해 테스트를 작성하자.@Transactional vs sql을 이용한 데이터 삭제실제 코드에서는 @Transactional을 사용하지 않는데, 단순히 롤백만을 위해 테스트코드에서 @Transactional을 사용하면, 실제 작동 방식과 다르게 작동할 수 있다.따라서 테스트 코드 작성시 Transactional의 부작용에 대해 인지하고 사용할 것!!@SpringBootTest class ProductServiceTest { @Autowired private ProductService productService; @Autowired private ProductRepository productRepository; @AfterEach void tearDown() { productRepository.deleteAllInBatch(); } @DisplayName("신규 상품을 등록한다. 상품번호는 가장 최근 상품의 상품번호에서 1 증가한 값이다.") @Test void createProduct() { //given Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); productRepository.save(product1); ProductCreateRequest request = ProductCreateRequest.builder() .type(HANDMADE) .sellingStatus(SELLING) .name("카푸치노") .price(5000) .build(); //when ProductResponse productResponse = productService.createProduct(request); //then assertThat(productResponse) .extracting("productNumber", "type", "sellingStatus", "price", "name") .contains("002", HANDMADE, SELLING, 5000, "카푸치노"); List products = productRepository.findAll(); assertThat(products).hasSize(2) .extracting("productNumber", "type", "sellingStatus", "price", "name") .containsExactlyInAnyOrder( tuple("001", HANDMADE, SELLING, 4000, "아메리카노"), tuple("002", HANDMADE, SELLING, 5000, "카푸치노") ); } } Persistence Layer 데이터에 접근하는 역할로 데이터 CRUD와 연관된 메서드들을 테스트 한다. @DataJpaTest JPA와 관련된 의존성들만 주입해준다 -> @SpringBootTest보다 가볍다.어노테이션 내부에 @Transactional이 포함되어 있어 테스트 후 데이터가 롤백된다.@DataJpaTest // @DataJpaTest안에 @Transactional이 걸려있어서 자동으로 rollback이 됨 class ProductRepositoryTest { @Autowired private ProductRepository productRepository; @DisplayName("원하는 판매상태를 가진 상품들을 조회한다.") @Test void findAllBySellingStatusIn() { //given Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); Product product2 = createProduct("002", HANDMADE, HOLD, "카페라떼", 4500); Product product3 = createProduct("003", HANDMADE, STOP_SELLING, "팥빙수", 7000); productRepository.saveAll(List.of(product1, product2, product3)); //when List products = productRepository.findAllBySellingStatusIn(List.of(SELLING, HOLD)); //then assertThat(products).hasSize(2) .extracting("productNumber", "name", "sellingStatus") .containsExactlyInAnyOrder( tuple("001", "아메리카노", SELLING), tuple("002", "카페라떼", HOLD) ); } }Referencehttps://www.inflearn.com/course/practical-testing-%EC%8B%A4%EC%9A%A9%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C/dashboard
백엔드
・
레이어드아키텍쳐
・
워밍업클럽
・
테스트코드
2025. 03. 22.
0
인프런 워밍업 스터디 클럽 3기 백엔드-code 3주 차 발자국
이 글은 박우빈님의 Practical-Testing 강의 를 참조하여 작성한 글입니다.어느덧 수료까지 일주일만을 앞두고 있다. 벌써 75%나 진행되었다니,,,이때까지 강의 안 밀리고 시간 안에 미션 제출한 내 자신 칭찬한다👍🏻 (아직 강의 섹션3개, 미션 2개 남음)남은 한 주도 열심히 해서 수료해야지!! 이번 주는 테스트 코드가 필요한 이유 및 레이어드 아키텍쳐 내에서 각 레이어드별 테스트 코드 작성하는 법에 대해 배웠다.강의를 듣고 테스트 코드를 작성해야 하는 이유에 대해서 완전 설득이 되었다.기존에는 테스트 코드 작성하는 것이 너무 귀찮고 시간이 오래 걸린다는 이유에서 꺼려했지만, 점점 리팩토링 또는 기능을 추가할 때마다 수동으로 테스트 하는게 더욱 귀찮았다. 또한 수동으로 테스트를 했어도 항상 찝찝함이 존재했다.그래서 우빈님 강의를 들으면서 엄청난 공감을 느꼈고, 강의를 열심히 학습해서,,,많이 배워야겠다는 다짐을 다시 하게된 것 같다.다음으로 readable-code 강의에서 만든 스터디 카페 이용권 프로그램에 대해 스스로 테스트 코드를 작성해 보는 시간을 가졌다.이번에는 열심히 작성한 후 리뷰 신청도 하였다.테스트 코드 관련해서는 리뷰를 한번도 받아본 적이 없기도 하고, 코드 작성 중 궁금한 점이 생겨 리뷰 신청을 하는 용기를 내보았다..ㅋㅋㅋ다음 주 중간점검 때 리뷰를 해주실 예정인데 기대된다!!!학습 내용 요약 테스트는 왜 필요할까?빠른 피드백리팩토링, 신규 기능 추가 등 변화가 생기는 매순간마다 테스트 코드를 통해, 기존 코드가 정상 동작하는 지 빠르게 피드백을 받을 수 있다.만약 테스트 코드를 작성하지 않는다면?변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 한다.변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 한다.자동화자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있다.(인간이 수동으로 테스트를 하게 된다면 실수할 확률이 매우 높음)안정성빠르게 변화하는 소프트웨어에서 테스트 코드를 통해 100%는 아니지만 안정성을 보장할 수 있다. 단위테스트단위테스트란?작은 코드 단위를 독립적으로 검증하는 테스트통합테스트에 비해 준비해야 할 코드가 적으며, 검증 속도가 빠르고 안정적이다.단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없다는 단점이 존재한다.JUnit5: 단위 테스트를 위한 테스트 프레임 워크AssertJ: 테스트 코드 작성을 원활하게 돕는 테스트 라이브러리 (풍부한 API, 메서드 체이닝 지원) TDD: Test Driven Development선 기능 구현, 후 테스트 작성테스트 누락 가능성 존재해피 케이스만 검증할 가능성 존재잘못된 구현을 늦게 발견할 가능성 존재선 테스트 작성, 후 기능 구현 (TDD)복잡도가 낮은, 테스트 가능한 코드로 구현할 수 있게 한다(유연하며 유지보수 쉬운 코드)쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해줌구현에 대한 빠른 피드백을 받을 수 있음 -> 과감한 리팩토링 가능해짐 Spring / JPA 훑어보기 & 기본 엔티티 설계라이브러리내 코드가 주체가 돼서 필요한 기능이 있다면 외부에서 끌어와서 사용하게 되는데, 이 때 외부에 있는 것들을 라이브러리라 한다.따라서 라이브러리는 내 코드가 주체가 되는 환경이고 능동적인 특징을 지닌다.프레임워크이미 갖춰진 동작할 수 있는 그런 환경들이 구성이 돼 있고, 내 코드가 이 프레임 안에 들어가는 수동적인 역할을 한다.따라서 프레임워크에서는 내 코드가 수동적인 존재가 된다.ORM 객체 지향 프로그래밍(OOP)과 관계형 데이터베이스(RDB) 간의 구조적 차이를 해소하기 위해 사용되는 기술이다.데이터베이스에 CRUD하는 작업을 객체 기반으로 처리함으로써 개발자들이 기본적인 쿼리 작성하는 단순 작업을 줄이고 비지니스 로직에 집중할 수 있게 해준다.JPAJava진영의 ORM 기술 표준인터페이스이며, 여러 구현체가 있지만 주로 Hibernate를 많이 사용한다. Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA제공한다. Persistence Layer 테스트데이터에 접근하는 역할로 데이터 CRUD와 연관된 메서드들이 위치해 있다.비지니스 로직이 포함돼서는 안됨!@DataJpaTestJPA와 관련된 의존성들만 주입해준다 -> @SpringBootTest보다 가볍다.어노테이션 내부에 @Transactional이 포함되어 있어 테스트 후 데이터가 롤백된다. Business Layer 테스트비지니스 로직에 관련된 메서드들이 위치해 있다.@Transactional vs sql을 이용한 데이터 삭제실제 코드에서는 @Transactional을 사용하지 않는데, 단순히 롤백만을 위해 테스트코드에서 @Transactional을 사용하면, 실제 작동 방식과 다르게 작동할 수 있다.따라서 테스트 코드 작성시 Transactional의 부작용에 대해 인지하고 사용할 것!!리포지토리 테스트와 로직이 많이 없는 얇은 서비스 테스트는 결이 비슷하다왜냐하면 리포지토리에 대한 결이 그대로 오기 때문이다.하지만서비스가 더 기능이 추가될수록 발전을 하기 때문에, 동일한 테스트라고 생각이 되더라도 작성을 하는 게 좋다! (검증이 추가될 수도 있기 때문)클래스 상단에 @Transactional(readOnly = true) 로 표현하고 실제 트랜잭션이 사용되는 메서드에 @Transactional로 표현해주기! 미션Day 11 - 스터디 카페 이용권 선택 시스템 테스트 코드 작성하기나의 코드: https://github.com/Jiihyun/readable-code/pull/3🤔 고민사항 1. StudyCafeSeatPassTest 클래스테스트 케이스 반복을 줄이기 위해 @CsvSource를 사용해 보았다.이로 인해 데이터만 추가하면되니 테스트 케이스를 쉽게 확장할 수 있다는 장점을 느꼈다.하지만 StudyCafePassType을 직접 문자열로 작성했다보니, 후에 passType이 수정될 경우 테스트 코드 내 passType을 직접 수정해야 하기 때문에 유지보수 비용이 증가할 것 같다는 생각이 들기도 했다..그래서 현업에서는 어떤 방식으로 테스트를 하지? 하는 궁금증이 생겼던 것 같다.두 방법에 대해 트레이드오프가 존재하는데 어떤 걸 더 중요시하게 여겨 테스트하는지 궁금하다!2. FileReaderTest 클래스파일을 잘 읽어와 데이터를 의도한 대로 파싱하였는지 테스트하고 싶었다.하지만 반환 타입이 일급컬렉션으로 되어 있고,컬렉션 내의 데이터를 확인하는 메서드는 프로그램에서 사용되지 않기 때문에 존재하지 않았다.테스트를 공부하면서 배운 것 중 하나는 테스트만을 위한 메서드는 최대한 자제해야 한다고 했다.그래서 오로지 테스트를 위해 컬렉션의 크기 등을 확인할 수 있는 메서드를 추가하고 싶지 않았고, 그래서 일급 컬렉션의 메서드를 직접 호출하여 테스트를 진행했다.그치만 이렇게 짜여진 테스트도 본 적이 없는데....이런 방식으로 테스트를 해도 괜찮은지 궁금하다 ㅋㅎㅎㅋ마무리앞으로 이제 일주일 남았다..!! 벌써...?다음 주 학습 내용에는 평소에 궁금했던 내용들에 대해 다루기 때문에 매우 기대가 된다.배워본 적 없는 내용이라 시간을 많이 투자해야 할 것 하지만,,,열심히 학습해서 내 것으로 만들어야지!
백엔드
・
클린코드
・
테스트코드
・
발자국
・
워밍업스터디클럽
2025. 03. 16.
0
인프런 워밍업 스터디 클럽 3기 백엔드-code 2주 차 발자국
이 글은 박우빈님의 readable-code강의 를 참조하여 작성한 글입니다.정신없이 진도표를 따라 강의를 수강하고 미션을 진행하였더니 일주일이 순식간에 지나가버렸다.처음에는 할 만하다고 느꼈었는데,,,강의 내용을 습득하여 스스로의 힘으로 코드에 적용까지 하려니 우빈님이 당부하신 대로 쉽지 않았던 나날들이었던 것 같다,,🥹특히 이번 스터디카페 리팩토링 미션에 뻥 안치고 10시간 이상은 투자한 것 같은데,,,ㅋㅋㅋㅋ(미션 당일 + 자고 읽어나서 맑은 정신으로 한 번 더 도전)이후에 리팩토링 강의를 수강하며 내가 전혀 고려하지 못했던 부분을 리팩토링하신 것을 보고 놀랐다. 뿐만 아니라, 리팩토링을 뚝딱 해내시는 모습에서도 감탄했다.원래 누군가가 뭔가를 쉽게 해내는 것처럼 보이면 진짜 고수라는 말이 딱 맞는 것 같다...!강의 속에서 리팩토링 하시는 모습은 굉장히 쉬워보였는데...혼자서 해내려니 정말 막막했다 ㅋㅋㅋㅋ그래도 나도 경험을 더 쌓고 나면, 지금보다 더 짧은 시간 안에 더 객체지향적으로 리팩토링을 해낼 수 있겠지!!이를 위해 다른 분들이 리팩토링 하신 코드도 많이 읽으면서 여러 번 리팩토링 해봐야겠다. 학습 내용 요약 주석의 양면성주석의사 결정의 히스토리 를 도저히 코드로 표현할 수 없을 때, 주석으로 상세하게 설명하자주석이 많다 == 비지니스 요구사항을 코드에 잘못 녹였다 는 의미가 성립됨주석을 작성할 때, 자주 변하는 정보는 최대한 지양해서 작성하자 (그렇지 않으면, 주석도 신경써서 계속 업데이트 해줘야 하는 단점 존재)변수와 메서드의 나열 순서상태 변경 > 불리언 등 판별 >= 조회 메서드 순으로 메서드 순서 나열 패키지 나누기패키지: 문맥으로써의 정보를 제공할 수 있음대규모 패키지 변경은 팀원과의 합의를 이룬 후 하자본인만 사용하는 부분이면 괜찮지만, 여러 사람과 공통으로 사용하는 클래스들의 패키지를 한번에 변경하면 충돌이 발생할 수 있음! 은탄환은 없다클린코드가 은탄환은 아니다 (클린코드가 무조건적인 정답은 아니다)요구사항이 변경될 일이 없는 코드는 절차지향적인 코드가 오히려 정답일 수 있음 (e.g., 체스는 500년동안 규칙이 변하지 않았음)실무는 지속가능한 소프트웨어의 품질 vs 기술부채를 안고 가는 빠른 결과물 사이의 줄다리기무조건적으로 클린 코드를 추구하기보다는 주어진 기간을 최대한 맞추게끔 결과물을 내놓고, 이후 미래에 잘 고치도록 할 수 있는 코드 센스가 필요 (e.g., 주석으로 리팩토링 할 부분 남겨 놓기 등) 미션 Day 4 미션 피드백기존 코드 (return 타입 - boolean)public 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; } 내가 리팩토링 한 코드 중 일부 (1)public class Order { private long id; private List items; private Customer customer; public Order(final List items, final Customer customer) { validateOrder(items, customer); this.items = items; this.customer = customer; } private void validateOrder(final List items, final Customer customer) { if (doesNotHaveItems(items)) { throw new RuntimeException("주문 항목이 없습니다."); } if (doesNotHaveCustomerInfo(customer)) { throw new RuntimeException("사용자 정보가 없습니다."); } } public boolean doesNotHaveItems(final List items) { return items.isEmpty(); } public boolean doesNotHaveCustomerInfo(final Customer customer) { return customer == null; } } => boolean을 return하고 있는 기존 메서드에 대한 리팩토링으로 예외를 던지는 것으로 변경하는 것은 좋을 수도, 나쁠 수도 있다.(자칫하면 오버엔지니어링이 될 수 있음)📌 예외를 던지는 것은 비싸기 때문에, 신중하게 메서드의 사용현황을 파악 후 상황에 맞게 리팩토링 할 것!! 내가 리팩토링 한 코드 중 일부 (2)public class Item { private static final int MINIMUM_VALUE = 1; private long id; private String name; private int price; public Item(final long id, final String name, final int price) { validatePositivePrice(price); this.id = id; this.name = name; this.price = price; } private void validatePositivePrice(final int price) { if (price 고민이었던 부분: Order 클래스 내에 Item 클래스를 새로 만들어, 이를 리스트 형태로 Order에 의존성 주입하도록 코드 리팩토링을 진행하였다. 이 때 Item 객체를 생성할 때 마다 금액이 양수인지 검증하고 있는데, Order 클래스에서 전체 총 금액이 양수인지 검증을 다시 한번 해주는 게 좋을지 고민이 됐었다.=> 우빈님 답변: 이미Item 객체를 생성할 때 금액의 유효성을 보장하고 있으니, Order 클래스는 굳이 하지 않아도 될 것 같다.물론 비지니스 로직이 복잡해지면, 오히려 필요하다고 느끼는 순간이 올 수 있으니, 그 때 추가해도 늦지 않다! Day 7 미션미션 한 줄 소개: 스터디 카페 이용권 선택 시스템 리팩토링 하기 리팩토링 한 부분 코드 중복 제거 및 메서드 추출StudyCafePassMachine의 의존성 config에서 주입: StudyCafePassMachine은 필요한 의존성을 외부에서 주입받기만 하고, 내부에서 어떻게 사용하는지는 외부에 노출하지 않을 수 있다!일급 컬렉션 활용: 일급 컬렉션으로 분리함으로써, 원래는 private 메서드라 테스트하지 못했던 로직도 테스트 가능해짐!무분별한 getter 사용이 아닌, 객체에 메세지 보내기public boolean isSamePassTypeWith(final StudyCafePassType studyCafePassType) { return passType == studyCafePassType; }passType를 비교해야 하는 곳에서 passType를 게터를 통해 비교해주는 게 아닌, isSamePassTypeWith 메서드와 같이 객체에 메세지를 전달하자!스터디 카페 이용권 인터페이스 적용: 이부분은 리팩토링을 잘 하였는지 감이 안온다..시도에 의의를 두자 ㅎ헤ㅔㅎpublic interface StudyCafePassHandler { boolean isAppliable(final StudyCafePassType studyCafePassType); StudyCafePasses findCandidateStudyCafePasses(final StudyCafePasses studyCafePasses); } ========= # StudyCafePassMachine private void processUserSelection(final StudyCafePassType studyCafePassType) { final StudyCafePasses availablePasses = getAvailablePasses(studyCafePassType); outputHandler.showPassListForSelection(availablePasses); final StudyCafePass selectedPass = inputHandler.getSelectPass(availablePasses); if (studyCafePassType == StudyCafePassType.FIXED) { checkLockerPass(selectedPass); return; } outputHandler.showPassOrderSummary(selectedPass, null); }Hourly, Weekly, Fixed라는 3종류의 카페 이용권이 존재하기 때문에, 인터페이스를 정의하여 if문 사용을 자제하고, 상황에 맞는 이용권을 가져왔다. 리팩토링 놓친 부분FileHandler: 데이터를 어디로부터 어떻게 가져올 것인가에만 초점이 맞춰져 있음-> File관련 로직이 들어나면 FileHandler 가 방대해질 것개선 방향: provider를 통해 어떤 데이터를 필요로 하는가에 초점을 맞출 것SeatPassProviderLockerPassProvider domain영역에 view관련 로직 침투public enum StudyCafePassType { HOURLY("1", "시간 단위 이용권"), WEEKLY("2", "주 단위 이용권"), FIXED("3", "1인 고정석"); private final String command; private final String description; StudyCafePassType(final String command, final String description) { this.command = command; this.description = description; } public static StudyCafePassType from(final String userInput) { return Arrays.stream(StudyCafePassType.values()) .filter(studyCafePassType -> studyCafePassType.command.equals(userInput)) .findAny() .orElseThrow(() -> new AppException("잘못된 입력입니다.")); } } PassType은 중요한 도메인 모델인데, Input과 관련된 의미를 지닌 command가 침투되었다. passType을 선택하는 command가 변경된다면, 단순히 입력 방식을 바꿨을 뿐인데 도메인 모델을 수정해야 하는 좋지 않은 상황이 발생한다.StudyCafePassOrder 도메인 추출스터디 카페 좌석 이용권 + 사물함 이용권을 합친 Order 도메인을 새로 추출할 수 있다.이로 인해 FIxedPassType에만 적용되는 사물함 로직 분기문을 간단하게 처리할 수 있었다!StudyCafePass 내 LOCKER_TYPES 상수 선언public enum StudyCafePassType { HOURLY("시간 단위 이용권"), WEEKLY("주 단위 이용권"), FIXED("1인 고정석"); private static final Set LOCKER_TYPES = Set.of(FIXED); private final String description; StudyCafePassType(String description) { this.description = description; } public boolean isLockerType() { return LOCKER_TYPES.contains(this); } public boolean isNotLockerType() { return !isLockerType(); } }전혀 생각지도 못했지만, 알아두면 참 좋은 객체에 메세지를 보내는 방법에 대해 배웠다.LOCKER_TYPES를 StudyCafePassType enum내에 적용하여 처리할 수 있다니도메인 지식이 부족해서 그랬나, 나는 생각지도 못했던 방법이다.나는 상위 도메인에서 if문을 통해 매번 확인해주었는데, 우빈님이 하신 방법이 더 책임 분리가 잘되어있고 객체에 메세지를 보내는 좋은 방법인 것 같다!!또한 Locker type이 늘어나도, set에 내용만 추가해주면 된다는 점에서 유지보수도 훨씬 쉬울 것 같다! 마무리강의를 굉장히 빠른 시간 안에 완강했다!!!그렇지만 강의 내용을 완전히 내 것으로 만들었다기엔 부족하다,,,,강의 볼 때는,,'이럴 때 조합, 일급 컬렉션, VO 등을 적용 하는구나~!' 를 배우면서, 앞으로 스스로 잘 판단해 낼 줄 알았는데,,,혼자서 해 보려니까 너무나도 막막했다.그래도 강의 내에서 주신 미션을 통해 내가 어느 부분이 부족한지 파악할 수 있었던 것 같다. 무엇보다도 너무 재밌었다,,,시간 가는 줄 모르고 했던 것 같다.이제 강의는 끝이 났지만 지뢰찾기랑 스터디카페 코드에 대해 복습할 것이다. 리팩토링 적용하기 어려웠던 부분을 반복 작성해 보면서 체득시킬 생각이다. 이렇게 반복하다보면 우빈님의 사고법을 체득할 수 있겠지요...?다음 주부터는 테스트 코드에 대해 배우는데,,평소 테스트 코드에 대해 공부를 많이 하지 않았어서 새로 배우는 양이 어마어마 할 것 같다.강의 내용을 잘 습득할 수 있도록 메타인지 열심히 해야겠다!나 자신 아자아자 화이팅이다💪🏻 [출처]readable-code : https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard
백엔드
・
클린코드
・
테스트코드
・
발자국
・
워밍업스터디클럽
2025. 03. 09.
0
인프런 워밍업 스터디 클럽 3기 백엔드-code 1주 차 발자국
이 블로그 글은 박우빈님의 readable-code강의 를 참조하여 작성한 글입니다. 강의 수강 및 워밍업 스터디 참여 계기나는 평소 가독성 좋은 코드, 유지보수 하기 좋은 코드에 대해 관심을 갖고 있었다.그 이유는 지속적으로 변화하는 요구사항에 유연하게 대응하며 코드를 작성하기 위해서다.우리는 개발자로서 새로운 기능 개발하는 시간 보다 기존 코드를 유지보수하는 시간이 더 많을 것이다. 또한, 코드를 읽는 시간이 작성하는 시간보다 훨씬 길다.따라서, 읽기 쉬운 코드 작성 방법을 배우고자 이번 강의를 수강하게 되었다.이때 강력한 동기부여 장치로 여러 동료들과 함께 성장할 수 있는 스터디 또한 참여하게 되었다.학습 내용 요약추상: 중요한 정보는 가려내어 남기고, 덜 중요한 정보는 생략하여 버린다-> 구체에서 조각하듯이 정보를 덜어내고 정말 중요한 것만, 엑기스만 뽑아서 남겼을 때를 추상화했다고 표현함 적절한 추상화: 해당 도메인의 문맥 안에서, 정말 중요한 핵심 개념만 남겨서 표현하는 것적절한 추상화를 위한 방법좋은 이름 짓기단수, 복수 구분이름 줄이지 않기은어, 방어 사용 금지추상화 레벨 동등하게 유지매직넘버 상수화의미를 갖고 있으나, 상수로 추출되지 않은 숫자 문자열 등은 상수화를 통해 이름 부여뇌 메모리 적게 쓰기인지적 경계성 추구: 읽는 사람으로 하여금, 읽는 사람 뇌 메모리에 정보를 최대한 적게 올리도록 코드를 작성해보자!중첩 반복문 사용 자제 -> early return 활용하려 else 사용 지양하기사용할 변수는 가깝게 선언하기부정 연산자 피하기부정어구를 쓰지 않아도 되는 상황인지 체크부정의 의미를 담은 다른 단어가 존재하는지 고민하기 or 부정어구로 메서드 명 구성부정 연산자(!)는 가독성이 떨어진다!! 객체지향 SOLID 5가지 원칙SRP: 하나의 클래스는 하나의 책임만 가져야 한다.OCP: 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.LSP: 부모 클래스 인스턴스를 자식 클래스의 인스턴스로 치환이 가능해야 한다.ISP: 사용하지 않는 인터페이스에 의존하면 안 된다.DIP: 구체화가 아닌 추상화에 의존해라.보다 자세한 설명은 아래 링크에 정리해두었으니 참고 바랍니다!https://jiihyunn.tistory.com/12 일주일 간의 학습 내용 회고계속할 점1차로 강의 보면서 노트 적극 활용하기!!이후 강사님이 하신 것중 생각나는 대로 리팩토링 따라해본 후, 기억이 나지 않으면 강의 다시 보면서 학습할 것학습한 내용 중 중요한 부분 나만의 언어로 블로그에 정리하기강의 진도표에 맞춰서 학습할 것. 밀리지 않기!느낀 점우테코 프리코스를 진행하면서 학습해왔던 가독성 좋은 코드 작성법을 강의를 통해 다시 한번 탄탄히 다질 수 있었다. 강의를 들으며 기존에 알고 있었던 내용을 복습하는 동시에, 두루뭉실하게 알고 있었던 부분도 확실히 예시를 통해 익힐 수 있어 더욱 의미 있는 시간이 되었다. 하나의 클래스에 100줄이 넘는 방대한 코드가 처음에는 절대 리팩토링할 수 없을 것처럼 느껴졌지만, 강사님께서 몇 차례 다듬으신 후 객체지향적으로 변하는 과정을 보며 큰 신선함을 느꼈다. 코드가 점점 명료하고 유연하게 개선되는 모습을 보면서 객체지향 설계의 강력함을 다시금 체감할 수 있었다. 리팩토링을 고려해봤을 때 조합, 다형성 이용 등 사용하는 것은 전혀 생각지도 못했는데, 강의를 통해 활용법을 배워서 재밌었다. 앞으로 혼자 지뢰찾기 게임 리팩토링을 다시 해보면서 강사님의 생각을 읽을 수 있도록 많이 복습해야겠다! 미션 회고[DAY 2]평소 추상에 대해 깊게 생각해보지 않았는데, 미션을 통해 추상이 무엇인지, 코드에는 추상화라는 걸 어떻게 적용시킬 수 있는지 좀 더 명확하게 알게 되었다.[DAY 4]미션 PR: https://github.com/Jiihyun/readable-code/pull/1 🤔 고민사항Item 객체 생성할 때 마다 금액이 양수인지 검증을 해주었는데,Order 클래스에서 총 가격이 양수인지 검증을 다시 한번 해줘야 할지 엄청 고민이 되었다. [출처]readable-code : https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard
백엔드