블로그
전체 6#카테고리
- 백엔드
#태그
- 테스트코드
- 인프런워밍업클럽
- 테스트
- Layered-Architecture
- 테스트필요성
- 테스트작성법
- 클린코드
- 읽기좋은코드
2024. 10. 27.
0
워밍업 클럽 2기 BE 클린코드&테스트 - 회고 4회
Practical Testing : 실용적인 테스트 가이드 - 회고 4회 이 글은 박우빈님의 강의를 참조하여 작성한 글입니다. 벌써 4주차가 되었습니다. 인프런 워밍업 클럽의 마지막 회고입니다. 다른 것도 한다는 핑계로 시간을 많이 투자하지 못한 거 같아 아쉽기도 하지만 최대한 잘 녹여서 작성하겠습니다. 테스트 작성에 대한 많은 팁을 알게 되어 좋았지만 테스트를 바라보는 여러 관점을 알게 되고 이에 대한 주관이 나름 생긴 거 같아 좋은 시간이었습니다. 학습 목차Spring & JPA 기반 테스트: Presentation Layer(제외) Mock을 마주하는 자세더 나은 테스트를 작성하기 위한 구체적 조언학습 테스트(제외) Mock을 마주하는 자세Mock에 대한 찬반 의견이 있다. 물론 현재 상황(구현 기간, 서비스 특성 등)에 따라 얼마나 Mock 처리의 빈도가 달라질 것이다.기본적인 Mock에 대한 의견은 Classicist와 Mockist 로 나뉜다. Classicist VS Mockist말그대로 Mock을 하면 안 된다와 해야 한다로 나뉜다. Classicist는 여러 객체(모듈)이 상호작용하기에 A와 B에 대한 통합 테스트를 구현하는 경우에 A, B 둘 다 실제 객체를 사용해야 한다고 Mockist는 이미 A, B 각 객체에 대한 단위 테스트가 이뤄졌기에 A, B에 대한 통합 테스트를 구현할 때 A 또는 B 하나를 Mock 처리해서 하는 것이 비용과 시간에 대해 더 합리적이라고 주장한다. 더 나은 테스트를 작성하기 위한 구체적 조언완벽하게 제어하기// 시간에 따라 주문 여부가 정해지는 로직 void createOrderWithCurrentTime(){ CafeKiosk cafeKiosk = new CafeKiosk(); Americano americano = new Americano(); cafeKiosk.add(americano); Order order = cafeKiosk.createOrder(LocalDateTime.of(2023,1,17,10,0)); assertThat(order.getBeverage()).hasSize(1); assertThat(order.getBeverage().get(0).getName()).isEqualTo("아메리카노"); } // 시간, 외부 API 등 변할 수 있는 요소는 외부로 빼서 가져오기 // 예로 시간을 파라미터로 가져오는 게 아니라면 // LocalDateTime.now()가 내부 로직에 있다면 테스트를 실행하는 // 시간마다 성공 여부가 다를 것이다. 따라서 제어가 어려운 것은 // 외부로 내보내 완벽하게 제어하자. public Order createOrder(LocalDateTime currentTime){ // 위 시간 인자를 통해 지정한 시간 내인지 확인하는 로직 // 가능하면 주문 생성, 아니면 예외 발생 } 테스트 환경의 독립성을 보장하자(테스트 하나에는 하나만 검증)void createOrderWithNoStock(){ //given LocalDateTime registeredDateTime = LocalDateTime.now(); Product product1 = createProduct(BOTTLE, "001", 1000); Product product2 = createProduct(BAKERY, "002", 2000); Product product3 = createProduct(HANDMADE, "003", 3000); Stock stock1 = Stock.create("001", 2); Stock stock2 = Stock.create("002", 2); stock1.deductQuantity(3); // 행위1 stockRepository.saveAll(List.of(stock1, stock2)); OrderCreateServiceRequest request = OrderCreateServiceRequest.builder() .productNumber(List.of("001","001","002", "003")); .build(); // when/then (행위2) assertThatThrownBy( () -> orderService.createOrder(request, registerDateTime)) .isInstanceOf(IlligalArgumentException.class) .hasMessages("재고가 부족한 상품이 있습니다")); 밑줄 친 코드를 보자. 테스트는 하나의 행위만 검증해야 한다. 위 상황에서는 현재 재고보다 더 많은 상품을 주문했을 때, 예외가 발생하는 지 검증하는 테스트이다. given 절에 밑줄 코드를 보면 어떠한 행위가 또 들어가 있다. 즉, 상황을 준비만 해야 하는 given 절에서 어떠한 행위가 일어났다. 위 테스트 코드를 이해하려면 밑줄 친 코드(메서드)의 행위를 알기 위한 논리적 사고가 발생하여 독립적이지 못한 테스트이다. } 한 눈에 들어오는 Test Fixture 구성하기테스트를 작성하다 보면 공통적인 given절로 중복되는 코드가 발생한다. 보통, 코드의 중복을 줄이기 위해 @BeforeEach, @BeforeAll, @Afrer... 등이나 data.sql 파일로 손 쉽게 데이터의 삽입이 이뤄진다. (삭제는 테스트 간 결합도를 낮추기 위해 사용하므로 @BeforeEach 혹은 @Transactional을 사용하자) 비즈니스 로직 개발 관점에서는 당연히 코드의 중복을 줄이는 것이 합리적이며 유지보수성이 좋다고 생각하지만 테스트에서는 위와 같은 방법을 지향해야 한다. 테스트는 말 그대로 테스트고 위와 같은 방법을 사용하면 모든 테스트가 @BeforeEach, data.sql 등에 강결합하여 의존성이 커진다. 위 설정 과정이 달라졌을 때, 어떤 결과가 나올지 모른다. 따라서 테스트 코드가 길어지더라도 given 절에서 설정하는 것이 올바르며 한 눈에 파악하기 쉽다.(data.sql로 given을 구성하면 테스트를 볼때마다 data.sql로 이동하기에 파악하기 어렵다.[파편화]) ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ 1. 각 테스트 입장에서 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는가? 2. 수정해도 모든 테스트에 영향을 주지 않는가? 위 질문에 해당하지 않는다면 @BeforeEach와 같은 기능을 사용해도 괜찮다. 위 내용을 떠나서라도 @BeforeEach을 사용하더라도 전달하고자 하는 도메인 개념이나 로직 등 관련 내용이 충분히 전달되면 괜찮다. 핵심은 도메인이다. Test Fixtuer 클렌징 @BeforeEach void beforEach(){ // 1 orderProductRepository.deleteAllInBatch(); orderRepository.deleteAllInBatch(); productRepository.deleteAllInBatch(); // 2 orderProductRepository.deleteAll(); orderProductRepository.deleteAll(); orderProductRepository.deleteAll(); } 1번은 해당 테이블 데이터 전체를 삭제하겠다는 의미이다. 2번은 해당 테이블 데이터 전체를 조회하고 1씩 삭제하겠다는 의미이다. 데이터가 많을수록 성능 차이가 커지므로 1번을 사용하자. 대신 참조키, 무결성 관계를 고려해서 순서에 맞게만 사용하면 된다. ex) orderRepository.deleteAllInBatch(); orderProductRepository.deleteAllInBatch(); productRepository.deleteAllInBatch(); 위 예시처럼 실행하면 orderProduct가 order를 참조하기 있기 때문에 테스트가 실패한다. [ order orderProduct ] ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ **추가로 @Transactional, @BeforeEach 중 어떤 것을 사용할 지는 상황에 맞게 선택하면 된다. 테스트 간편화 기능 @ParameterizedTest : 다양한 인자를 통해서 테스트의 완성도를 높이거나 코드를 줄일 수 있음 @CsvSource, @MethodSource 등 제공하는 값을 통해서 메서드 인자로 넘기면 해당 수만큼 실행한다. @DynamicTest @Test 대신 @TestFactory를 사용해야 하고 Collection을 기준으로 연쇄적인 기능을 가진 객체를 사용하면 된다.(List, stream 등) 연쇄적인 테스트가 필요하다면 해라. 따로 따로 하는 것보다 연쇄적으로 하는 것이 좀 더 직관적일 수 있다. 예로, 해피 케이스랑 예외 케이스를 따로 테스트를 작성해서 검증할 수 있다. 하지만 해당 어노테이션을 사용하면 하나의 테스트에서 연쇄적으로 검증할 수 있다. '성공 -> 예외' 한 묶음으로. 하지만 given-when-then 절이 중첩 if 문 같이 사용되어 가독성이 떨어질 수 있으니 적절하게 사용하자. 추가로 연쇄적인 시나리오를 위해서도 사용 가능하다. 재고 차감을 예시로 보면 재고가 1인 경우에 감소하고 0인 경우 감소하는 경우를 연속으로 확인할 수 있어 좀 더 가독성이 높다. private 메서드는 테스트 해야 할까? 외부 관점(컨트롤러, 외부 호출자 등)에서는 공개된 API만 사용한다. private 메서드를 직접적으로 호출하는 경우는 없으며 공개 API(public 메서드)를 호출한다. 또한 공개 API를 호출하면서 내부에 있는 private 메서드도 호출한다. 즉, 공개 API가 검증이 되면 내부에 있는 기능 또한 검증되므로 굳이 private 메서드를 검증할 필요가 없다. 결국 private 메서드는 테스트 할 필요가 없다. 그럼에도 이런 생각이 든다면 ‘**객체를 분리할 시점인가?‘**라는 질문을 해라.위와 같은 생각이 든다면 객체의 책임이 복합적일 수도 있다는 신호라고 한다.(사실, 이러한 경험이 없어 위와 같은 느낌이 아직은 모호하게 다가온다.) 개인 의견기존에는 고려하지 못한 부분에 대해 많이 알게 되어 테스트 코드에 대한 주관이 조금은 생겼다. 물론 아직도 명확하게 답을 할 수 있는 것들이 적지만 충분히 의미 있는 시간이었다.위에서도 말했듯이 절대적인 것은 없기에 자신이 처한 상황에 따라 되게 유동적으로 테스트 코드의 중요성과 작성법 등 변한다. 상황에 맞게 적용할 수 있게 여러 방법을 시도해 보는 것도 좋은 거 같다.번외로 가장 인상 깊었던 부분은 클린 코드 강의의 목수 이야기다. 대게 새로운 것을 통해 기존의 방식을 변경하는데 어려움을 겪어 거부하기도 하고. 기존에 사용하는 방식만 안 다면 더 쉽고 좋은 방법이 있더라도 이를 적용할 생각조차 하지 않는다는 내용이다.새로운 기술을 학습하고 적용하는 것은 현실을 고려했을 때 어렵긴 하지만 새로운 기술에 대한 학습과 적용을 두려워하지 않는 습관을 기르는 것이 좋다는 것을 상기시켜줘서 좋았다. 학습적인 측면을 넘어 개발자로서 필요한 자세나 습관에 대한 내용도 담겨 있어 좋은 선생님이라 생각합니다.
백엔드
・
테스트코드
・
인프런워밍업클럽
2024. 10. 27.
0
워밍업 클럽 2기 BE 클린코드&테스트 : 미션 - Day 18
워밍업 클럽 2기 BE 클린코드&테스트 : 미션 - Day 18 이 글은 박우빈님의 강의를 참조하여 작성한 글입니다. 미션 - Day 18 미션 내용1. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다. 2. 아래 3개의 테스트가 있습니다. 내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요? (@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치) ✔ 게시판 게시물에 달리는 댓글을 담당하는 Service Test✔ 댓글을 달기 위해서는 게시물과 사용자가 필요하다. ✔ 게시물을 올리기 위해서는 사용자가 필요하다. 미션1테스트를 통해 검증하는 과정에서 여러 객체(모듈)이 상호 작용하고 객체 간 의존성이 클수록 필요한 데이터를 준비하는데만 오랜 시간을 걸린다. 배보다 배꼽이 큰 경우를 막기 위해 검증하지 않는 객체(모듈)은 Mock 처리하여 검증하고자 하는 것에만 더 집중할 수 있다. 우리는 아래와 같은 어노테이션을 통해 이러한 이점을 얻을 수 있다. @Mock : 실제 객체 대신에 가짜(Mock) 객체를 생성한다.@MockBean : Spring 테스트 환경에서 스프링 컨텍스트에 등록된 Bean을 Mock 객체로 대체한다.@Spy : 실제 객체를 생성하되, 특정 메서드에 대해 행위와 결과를 지정할 수 있다.@SpyBean : Spring 테스트 환경에서 스프링 컨텍스트에 등록된 Bean을 Spy 객체로 대체한다.@InjectMocks : 의존성 주입을 자동으로 수행한다. 테스트 대상 클래스의 인스턴스를 생성하고, 해당 클래스가 의존하는 객체들을 @Mock 또는 @Spy로 주입합니다. Bean의 유무 차이(@Mock, @MockBean / @Spy, @SpyBean)기능에 대한 차이는 없다. 다만 스프링 컨텍스트를 사용한다면 싱글톤 방식으로 각 인스턴스가 관리된다. 대게 단위 테스트 환경에서는 의존성 주입을 위해 @Mock, @Spy를 사용하고 스프링 환경에서 테스트하고 싶을 때는 @MockBean, @SpyBean을 사용하면 된다. @injectMocks vs @Autowired여기서도 Mock, Spy를 통해 의존성을 주입할 때는 @injectMocks을 사용하면 된다. @Mock UserRepository userRepository; @InjectMocks UserService userService; // userRepository가 주입됨스프링 컨테이너에서 빈을 가져올 때는 다음과 같이 사용하면 된다.@MockBean UserRepository userRepository; @Autowired UserService userService; // userRepository가 주입됨 미션2✔ 게시판 게시물에 달리는 댓글을 담당하는 Service Test✔ 댓글을 달기 위해서는 게시물과 사용자가 필요하다. ✔ 게시물을 올리기 위해서는 사용자가 필요하다. @BeforeEach void setUp() { 사용자 생성에 필요한 내용 준비 사용자 생성 게시물 생성에 필요한 내용 준비 게시물 생성 } @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-7. 사용자1의 댓글 생성에 필요한 내용 준비 3-8. 사용자1의 댓글 생성 3-3. 사용자2 생성에 필요한 내용 준비 3-4. 사용자2 생성 // when 3-9. 사용자2가 사용자1의 댓글 수정 시도 // then 검증 }[작성 이유]@BeforeEach 구조 이유해당 테스트에 핵심 도메인은 댓글이다. 댓글을 달기 위한 사용자와 게시물을 부수적인 요소이며 댓글을 생성하기 위해서는 사용자와 사용자가 생성한 게시물이 필수라는 조건이 있다. 해당 도메인 로직이 변경되더라도 이는 전제 조건이므로 setUp 메서드에서 구현해도 의미를 담기 충분하다고 판단했고 덤으로 중복도 방지할 수 있다고 생각한다. 테스트 별 given, when 절 구조 이유각 테스트 별 when 절에는 하나의 행위만 있다. 테스트는 하나의 행위에 대한 검증을 해야 한다. 만약 테스트에 2개 이상의 행위를 검증하게 되는 경우 검증하고자 하는 것이 모호해지고 실제로 테스트에 성공하더라도 2개 이상의 행위가 복합적으로 동작했기에 정확한 결과를 예측하기 힘들다.
백엔드
・
테스트코드
2024. 10. 21.
0
워밍업 클럽 2기 BE 클린코드&테스트 : 미션 - Day 15
이 글은 박우빈님의 강의를 참조하여 작성한 글입니다. 미션 - Day 15 미션 내용Layered Architecture 구조의 레이어별 테스트 작성법을 알아보았습니다. 레이어별로 1) 어떤 특징이 있고, 2) 어떻게 테스트를 하면 좋을지, 자기만의 언어로 다시 한번 정리해 볼까요? 1. 계층별 특칭 Layered Architecture : Presentation Layer Business Layer Persistence Layer Layered Architecture 는 3가지 계층으로 구성된다.웹 클라이언트와 연결된 Controller 부분에 해당하는 Presentation Layer, 서비스의 비즈니스 로직을 처리하는 Business Layer, DAO에 해당하는 Persistence Layer가 있다. 이렇게 계층을 나누는 이유는 관심사의 분리이다. 관심사, 책임을 나누기에 테스트의 작성을 편리하게 하고 신뢰성을 높이고 유지보수 또한 수월하다. Presentation Layer외부 세계의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증을 수행한다. Business Layer비즈니스 로직을 구현하는 역할Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.트랜잭션을 보장해야 한다. Persistence LayerData Access의 역할비즈니스 가공 로직이 포함되어서는 안 된다. Data에 대한 CRUD에만 집중한 레이어 2. 어떻게 테스트를 하면 좋을지, 자기만의 언어로 다시 한번 정리해 볼까요? 하나의 모듈을 기준으로 독립적으로 진행되는 단위 테스트와 둘 이상의 여려 모듈이 협력하여 기능을 통합적으로 검증하는 통합 테스트가 있다.각 계층에 대한 테스트는 다음과 같이 진행하면 좋을 거 같다. Persistance Layer -> Spring과 통합한 단독 계층 테스트(Spring, Jpa 등 활용한 DB 접근), 단위 테스트 성격 Business Layer -> Persistence를 포함한 통합 테스트 Presentation Layer -> 이외 계층을 Mocking한 단독 계층 테스트, 단위 테스트 성격 강사님의 의견과 유사한 생각을 가지고 있다. 위와 같은 생각은 다음과 같은 이유 때문이다. 이유Persistence Layer는 DAO 관련 계층이다. 컨트롤러나 서비스 등 이외 계층과 협력할 필요가 없다. 그렇기에 스프링, JPA를 활용한 테스트를 진행할 수 밖에 없다. A와 B 기능이 각각 있을 때, 정상적으로 동작할 수 있다. 하지만 A + B 와 같이 사용된다면 실제 결과는 어떻게 나올지 모른다. 이러한 여러 모듈이 복잡하게 상호작용할 수록 이를 예측하는 건 더 어렵다. 이처럼 Business Layer 와 Persistence Layer를 통합한 테스트를 진행하면 더 높은 신뢰성을 보장할 수 있다.또한 Persistence Layer에 대한 테스트를 이미 작성했다면 굳이 Persistence Layer를 Mocking하지 않는 것이 더 비용적으로 합리적일 수 있다. Presentation Layer는 외부 세계와 연결된 계층이다. 외부 세계로 받은 정보에 대한 검증이 필요하다.도메인의 성격, 서비스에 대한 내용과는 무관하다. 따라서 굳이 도메인 관련 로직을 검증할 필요는 없다고 생각한다.물론 각 모듈이 상호작용할 때와 각각 작용할 때는 다른 결과값이 나올 수 있기에 3가지 계층에 대한 검증을 한 번에 하는 것이 더 신뢰성을 보장할 것이다.그렇지만 비용 또한 고려해야 하므로 수동 테스트로 마무리하거나 비용이 나오더라도 꼭 검증해야할 정도로 중요하다면 그때 모든 계층에 대한 통합 테스트를 작성하여 신뢰성을 보장하면 된다고 생각한다.
백엔드
・
테스트
・
Layered-Architecture
2024. 10. 20.
0
워밍업 클럽 2기 BE 클린코드&테스트 - 회고 3회
이 글은 박우빈님의 강의를 참조하여 작성한 글입니다. 벌써 3주차가 되었습니다. 읽기 좋은 코드 관련 강의가 마무리되고 실용적인 테스트 강의를 시작했습니다. 부족한 부분도 있겠지만 열심히 달려온 과정을 적겠습니다. 강의 목적테스트 코드의 중요성과 작성해야 하는 이유테스트 코드를 작성하는 방법 테스트의 필요성테스트는 기능에 대한 부가적인 요소이다. 실제로는 기능을 구현하기 바쁘다. 그런데도 왜 테스트의 중요성이 강조될까?테스트를 작성하면 위와 같은 단점이 있다. 반대로 테스트를 작성하지 않는다면 어떻게 될까? 커버할 수 없는 영역 발생경험과 감에 의존 → 인간이기에 이럼.늦은 피드백 → 수동으로 테스트 해야 함.유지보수 어려움 → 확장, 수정이 일어난다면 어디까지 영향을 미칠지 모름소프트웨어 신뢰성 낮음 → 언제 어디서 버그가 터질지 불안함 테스트 코드를 작성하지 않는다면변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 한다.변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 한다.빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다.테스트 코드가 병목이 된다면프로덕션 코드의 안정성을 제공하기 힘들어진다.테스트 코드 자체가 유지보수하기 어려운, 새로운 짐이 된다.잘못된 검증이 이루어질 가능성이 생긴다. 테스트 코드를 작성하지 않는다면변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 한다.변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 한다.빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다.테스트 코드가 병목이 된다면프로덕션 코드의 안정성을 제공하기 힘들어진다.테스트 코드 자체가 유지보수하기 어려운, 새로운 짐이 된다.잘못된 검증이 이루어질 가능성이 생긴다. 그럼 올바른 테스트 코드를 서비스에 적용시킨다면 어떤 결과를 가져올까?올바른 테스트 코드는자동화 테스트로 비교적 빠른 시간 안에 버그 발견, 수동 테스트에 드는 비용을 크게 절약소프트웨어의 빠른 변화를 지원한다.팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.가까이 보면 느리지만, 멀리 보면 가장 빠르다. 테스트 케이스 세분화하기 테스트에는 해피 케이스와 예외 케이스가 있다. 우리는 요구사항대로 기능을 구현할 때, 이외 여러 경우를 고려해야 한다 예를 들어 커피 주문하기에 대한 기능을 구현할 때는 커피의 개수가 1 이상이여야 한다는 숨겨진 조건이 있다.이처럼 숨겨진 조건을 고려하여 작성할 때 도움이 되는 것은 경계값 테스트이다. 경계값 : 범위(이상, 이하, 초과, 미만), 구간, 날짜 등경계값을 기준으로 테스트를 고려해야 기능(도메인)에 대한 명확한 인지에 도움이 되며 예외 상황도 쉽게 파악할 수 있다. 테스트하기 어려운 영역을 분리하기테스트 코드는 작성하기 쉬운 부분과 어려운 부분이 존재한다. 예를 들어 오전 10시부터 오후 2시까지만 주문이 가능한 조건이 있다 가정하자. order(List items){ LocalDateTime now = LocalDateTime.now(); if(now 위 메서드에 대한 테스트를 진행하면 어떻게 될까?테스트를 실행하는 시간에 따라 성공 여부가 달라질 것이다.그렇다면 이처럼 테스트하기 어려운 상황이 생기면 어떻게 해야 할까?바로 테스트하기 어려운 부분을 외부로 분리해야 한다. order(List items, LocalDateTime time){ if(time위 코드처럼 시간이란 테스트하기 어려운 영역을 외부로 분리하여 파라미터로 받는다. 이렇게 코드를 작성하면 테스트하는 시간마다 성공 여부가 달라지지 않는다.테스트라는 기능을 온전히 수행할 수 있을 것이다. 이처럼 우리는 테스트하기 어려운 부분이 있다면 이를 외부 세계로 분리하여 테스트하기 쉽게 만들어야 한다.그럼 이런 어려운 부분은 무엇이 있을까? 어려운 영역관측할 때마다 다른 값에 의존하는 코드현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력 등외부 세계에 영향을 주는 코드표준 출력, 메시지 발송, 데이터베이스에 기록하기 등쉬운 영역 → 순수함수(pure fuction)같은 입력에는 항상 같은 결과외부 세상과 단절된 형태테스트하기 쉬운 코드이러한 영역은 위와 같이 있지만 직접 경험하면서 어떤 부분을 분리하는 것이 더 좋을 지에 대한 고민을 해야 시야를 기를 수 있다. TDD:Test Driven Development TDD는 위 구조를 통해 테스트 코드를 작성하는 것이다. 구현 → 테스트 순이 아닌테스트 → 구현 순으로 진행하는 방법이다.왜 TDD가 좋을까? 어떤 가치를 지니지?가장 큰 가치 중 하나는 빠른 피드백이다. 선 기능 구현, 후 테스트 작성테스트 자체의 누락 가능성특정 테스트 케이스만 검증할 가능성(해피 케이스)잘못된 구현을 다소 늦게 발견할 가능성테스트를 먼저 작성한다면 테스트에 대한 고려에 대한 시야를 가질 수 있다. 선 테스트 작성, 후 기능 구현복잡도 낮은 테스트 가능한 코드로 구현할 수 있게 한다.유연하며 유지보수가 쉽다.쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백을 받을 수 있다.과감한 리팩토링이 가능하다. TDD : 관점의 변화지금까지 기능 구현과 테스트의 관계를 봤을 때, 우리는 아래와 같이 기능과 테스트가 상호작용하는 구조를 추구해야 한다.기존에는 테스트는 구현부의 검증을 위한 보조 수단이었다면 TDD를 이용하면 테스트와 상호 작용하며 발전하는 구현부를 가질 수 있다.클라이언트 관점에서의 피드백을 주는 Test Driven. 테스트는 문서다. 문서란?프로덕션 기능을 설명하는 테스트 코드 문서다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서, 모두의 자산으로 공유할 수 있다.우리는 테스트 코드를 통해 해당 기능이 어떻게 동작하는 지 파악할 수 있다. 해피 케이스와 예외 케이스를 통해 코드(도메인 지식)를 인지하는데 도움을 얻을 수 있고 기존의 테스트를 통해 잘못된 부분이나 놓친 부분을 파악할 수 있다.우리는 항상 팀으로 일한다. 팀원에게 어떻게 보일 지 항상 고민하면서 작성하는 습관을 기르자.DisplayName을 섬세하게@DisplayName("음료 1개 추가 테스트") // 1 @DisplayName("음료 1개를 추가하면 주문 목록에 담긴다.") // 2 우리는 신규 입사자다. 서비스의 테스트 코드를 봤을 때 1, 2 중 어떤 것이 테스트 코드의 담긴 의미를 더 많이 전달해주는가?바로 2번이다. 왤까? 명사의 나열보다 문장으로 표현하기우리는 좀 더 명확한 의미를 파악할 수 있다. 정보의 생략의 없기 때문이다.테스트 행위에 대한 결과까지 기술하기이 또한 해당 기능에 대한 모든 정보를 알기 위해 행위에 대한 결과까지 기술하자.도메인 용어를 사용하여 한층 추상화된 내용을 담기(메서드 자체의 관점보다 도메인 정책 관점으로)테스트의 현상을 중점으로 기술하지 말 것 특정 시간 이전에 주문을 생성하면 실패한다. // 1 영업 시작 시간 이전에 주문을 생성하면 생성할 수 없다. // 21, 2 중 당연히 2가 더 추상화된 내용을 전달해준다. 우리가 도메인 지식을 이해하는데 더욱 도움을 준다.또한 ‘실패한다’와 ‘생성할 수 없다’를 보자. 실패한다는 것은 도메인에 대한 정보가 아니다. 단순히 테스트의 성공, 실패라는 결과에 의존한 것이다. 우리는 도메인의 정보를 전달할 수 있게 주의해야 한다. BDD 스타일로 작성하기given - when - then → ‘테스트의 준비 - 행위 - 결과’ 를 의미한다.명확하게 표시해 줌으로써 테스트 코드를 좀 더 이해하기 쉽다. Test의 양면성과 바라봐야 할 시각(개인 정리)이처럼 테스트에 대한 이점과 작성법이 있다. 하지만 테스트 또한 비용이다. 테스트가 오히려 기능 구현보다 비용이 비쌀 수도 있고 그렇기에 이를 단순 테스트의 용도로 바라보거나 기능 구현에 편향된 모습을 보이며 테스트 작성이 오히려 불필요하다는 반론도 많다. 하지만 우리는 지금 당장이 중요한 것이 아니다. 서비스가 운영된다면 종료되기 전까지 성장할 것이다. 또한 사용자가 많아지고 서비스의 규모가 커질수록 우리 코드는 더 많은 상호 협력을 요구하기에 결합도가 커지기 마련이다.이에 따라 장애가 발생할 확률도 높다. 테스트를 작성할 때, 항상 고민하는 습관이 있어야 장애의 발생을 예방할 수 있다. 테스트를 단순 검증의 역할로만 바라보지 말고 문서의 역할까지 크게 바라보자. 그럼에도 비용은 무시할 수 없다. 비용과 비용에서 오는 이점을 고려하여 우리는 테스트를 다루는 것도 필요한 요소라고 생각한다.
백엔드
・
테스트
・
테스트필요성
・
테스트작성법
2024. 10. 14.
0
워밍업 클럽 2기 BE 클린코드&테스트 - 회고 2회
해당 회고는 박우빈님의 'Readable Code : 읽기 좋은 코드를 작성하는 사고법' 강의를 참조하여 작성했습니다. 2주차 회고인데 까먹고 이제야 씁니다 . . . 남은 기간에는 좀 더 열심히 참여해서 많이 얻어 가겠습니다! 기억하면 좋은 조언들능동적 읽기학습 내용을 빠르게 체득할 수 있는 방법은 직접 적용해 보는 것이다. 실습을 통해 이론으로 얻은 지식만의 부족함을 채울 수 있으며 결과를 직접 확인 가능하여 크게 와닿는다. 이처럼 클린 코드에 대한 견문과 견해도 직접 적용해 봐야 시야를 기를 수 있다.모든 것을 한 번에 보기는 어려우니 직접 하나하나 리팩토링 하면서 이해하기리팩토링을 통해 도메인 지식을 늘리고 작성자의 의도를 파악하도록 노력하기리팩토링을 무서워할 필요가 없다. git reset을 통해 언제든지 복구 가능하니 잘못된 코드를 작성하더라도 괜찮다. 결국 직접 시도해야지만 성장할 수 있다. 오버 엔지니어링모든 것에는 오버 엔지니어링이 생길 수 있다. 보여준 예시처럼 체스 게임을 구현했을 때, 너무 매달리지 않아도 된다. 체스란 게임은 몇 백년 간 변하지 않았다. 체스란 게임만 봤을 때, 끝없는 리팩토링과 효율성을 고려하는 것이 옳을까?보통 서비스는 발전하기에 우리는 추후 수월한 유지보수를 하기 위해 클린 코드를 고려해야 한다. 만약 단발성이나 지속 가능성이 없다면 적절한 타협점을 찾으면 된다.구현체가 하나인 인터페이스인터페이스 형태가 아키텍처 이해에 도움을 주거나, 근시일 내에 구현체가 추가될 가능성이 높다면 인터페이스를 적용할 의미가 있다.하지만 구현체를 수정할 때마다 인터페이스도 수정해야 한다.인터페이스이므로 코드 탐색에 영향을 주고 필요 이상으로 애플리케이션이 비대해질 수 있다.너무 이른 추상화정보가 숨겨지기 때문에 복잡도가 높아진다.후대 개발자들이 선대의 의도를 파악하기 어렵다.이처럼 클린 코드는 절대적인 법칙이 아니다. 우리는 필요에 의해 적용해야 한다. 이러한 견문은 경험에 따라 달라지니 부족하다면 열심히 적용해보자. 은탄환은 없다 클린 코드는 은탄환이 아니다. 실무 상황에서는 금전적 이익이 필수적이다. 그렇기에 고민에 빠진다.지속 가능한 소프트웨어 품질 VS 기술 부채를 안고 가는 빠른 결과물대부분의 회사는 돈을 벌고 성장해야 하고, 시장에서 빠르게 살아남는 것이 목표이다.이런 경우에도, 클린 코드를 추구하지 말라는 것이 아니라, 미래 시점에 잘 고치도록 할 수 있는 코드 센스가 필요하다. 결국은 클린 코드의 사고법을 기반으로 결정된다.이처럼 우리는 적정선을 찾을 수 있는 능력이 필요하다. 급하게 만들어 품질이 낮은 결과물에 대해서는 주석을 통해 추후 개선사항을 남기듯 상황에 맞는 합의점을 찾아야 한다.하지만 이는 클린 코드에 대한 이해와 경험이 충분해야 찾기 쉽다. 적정 수준을 알기 위해 극단적으로 사용해보는 것이 도움이 될 것이다. 예시로 망치만 쓰는 초보 목수는 모든 작업에 망치만 사용할 것이다. 망치밖에 쓸 줄 모르기 때문이다.망치와 톱을 다룰 줄 아는 숙력자 목수는 상황에 적절한 도구를 사용할 것이다. 나무를 자르는데 당연히 톱을 사용한다. 클린 코드도 마찬가지이다. 상황에 대한 판단이 가능한 숙력자가 되기 위해 극단적으로 숙력될 때까지 사용하자. 개선할 점실제로 리팩토링한 것과 강사님이 리팩토링한 부분과 차이를 보면서 보는 관점이 다르다는 것을 체감할 수 있었습니다. 제가 리팩토링하면서 익숙치 않아 옳은 리팩토링인가에 대한 의문은 있었습니다. 그래도 리팩토링에 대한 이유를 계속 생각하면서 하니 비교했을 때, 다시 개선해야 할 부분에 대해 인지할 수 있어 좋았습니다. 다른 말로는 오늘 리팩토링한 것을 내일 보라는 얘기를 해주셨는데 확실히 다음날 보니 생각치 못했던 부분에 대한 새로운 시야가 보여 마음에 와닿았습니다.많이 부족하지만 계속 연습해서 저만의 견해가 담긴 클린 코드를 작성할 수 있도록 노력하겠습니다.
백엔드
・
클린코드
・
읽기좋은코드
2024. 10. 06.
0
워밍업 클럽 2기 BE 클린코드&테스트 - 회고 1회
해당 회고는 박우빈님의 'Readable Code : 읽기 좋은 코드를 작성하는 사고법' 강의를 참조하여 작성했습니다. 범위는 섹션 1 ~ 4 입니다.회고와 같은 정리는 가끔 귀찮기도 했지만 결과물에 대한 정리와 점검할 수 있는 시간을 준다고 하니 좋은 마음으로 기록하는 습관을 기르겠습니다! 미션 public boolean validateOrder(Order order) { if(notExistItemsFrom(order)){ // 주문에 상품 목록이 존재하면 log.info("주문 항목이 없습니다."); return false; } if (isNotVaildTotalPrice(order)) { // 총액이 유효한 값인지? log.info("올바르지 않은 총 가격입니다."); return false; } if (hasNotCustomerInfoFrom(order)) { // log.info("사용자 정보가 없습니다."); return false; } log.info("주문 항목이 없습니다."); return true; } private static boolean hasNotCustomerInfoFrom(Order order) { return !order.hasCustomerInfo(); } private static boolean isNotVaildTotalPrice(Order order) { return order.getTotalPrice() 주문에 대한 검증 로직1. 주문 안에 상품이 존재하는가? 없다면 잘못된 주문으로 판별2. 상품의 가격이 1원 이상이라면 정상 처리 / 아니라면 잘못된 주문으로 판별3. 주문에 대한 사용자 정보가 있어야 한다. 없다면 잘못된 로직이다. 적용할 수 있는 읽기 좋은 코드 방식1. 사고의 depth 줄이기2. 부정구 지양하기3. 최대한 추상적으로 접근하기 ( getter로 직접 접근하기 X)4. early return 사용하기 각 조건을 모두 통과해야 유효한 주문이라는 결과를 도출해야 한다. 따라서 비즈니스 로직을 지키며 위 리팩토링 기준을 모두 적용하려 했으나 'not'과 같은 부정어를 사용했다. 이에 대한 이유if 조건 안에 && 처리를 하여 긍정구로 표현하려 했으나 조건이 길어지면 오히려 기억해야 할 정보가 많을 거 같아 이와 같이 부정구를 사용하지만 depth와 if문마다 하나의 조건만 담기 위해 이와 같이 리팩토링했다. 인상적인 부분 테스트 코드와 클린 코드에 대한 중요성은 들어봤지만 기능 구현조차도 쉽지 않다 보니 평소 읽기 좋은 코드에 대해 접해볼 시도조차 없었습니다. 강의 도입 섹션에서는 읽기 좋은 코드의 중요성에 대해 설명하시는데 클린 코드에 관한 새로운 관점(?)도 알 수 있어 좋았고 읽기 좋은 코드 작성에 대한 이유와 실제 적용을 통해 진행해서 더 마음에 와닿았습니다. 학습 정리강의 내용과 제가 따로 정리한 부분이 섞여 있으니 참고해주세요.방대한 강의 내용과 강의에서 제공한 모든 내용 제시를 피하기 위해서 인상 깊었던 섹션 1,2 위주로 정리하겠습니다. 1. 클린 코드와 추상 관계 왜 클린 코드를 추구하는 것일까?결국, 클린 코드는 읽기 쉬운 코드이며 적절히 추상화가 이뤄진 코드이다. 이러한 코드는 유지 보수에 들어가는 시간과 비용을 절약해준다.극단적으로 확장 가능성이 없거나 그 순간에 개인만이 서비스에 대한 코드에 접근하고 작성한다면 보편적인 클린 코드 대신 본인이 알아보기 쉬운 형태의 코드로 작성하는 것이 더 합리적일 수도 있다.하지만 이와 같은 경우보다는 거의 없기에 클린 코드를 추구하는 것이다. 프로그램의 정의는 무엇일까?다양한 의미가 있겠지만 다음과 같이 정의할 수 있다.‘프로그램 = 데이터 + 코드’데이터는 어떠한 정보 자체를 의미하고 코드는 어떠한 논리적 행위를 의미하며 데이터와 데이터 간, 데이터와 코드 간 등 여러 관계에서 논리적 행위가 일어날 수 있다. 그럼 데이터와 코드는 실제 우리가 작성하는 코드에서는 어떤 것을 의미할까?데이터는 객체, 클래스가 가지는 값이 있으며 객체가 어떤 행위를 수행하는 의미를 가진 메서드 선언부를 나타낼 수 있다.(반환값, 파라미터, 메서드명 등)코드는 메서드의 내부 행위 값이 있고 크게는 코드 간 여러 복합적인 메서드 간 상호 작용으로도 볼 수 있다.위 코드에 대한 예시를 보면 calculateChangeMoney 메서드 선언부와 Person 필드 등이 데이터가 볼 수 있고 calculateChangeMoney, donamteMoney 등 메서드에 내부 로직을 코드로 볼 수 있다. 도대체 읽기 좋은 코드에 대한 객관적인 기준이 뭐야?읽기 좋다는 것은 바로 추상과 구체에 의해 나타난다.하나의 예시를 보자. 💡나는 누군가 쳐다본다. 일반적인 ‘보다’ 라는 느낌과는 다르다. 주위에 있으면 한순간도 놓치지 않고 어떤 표정을 짓는지, 무엇을 하는지 바라보게 된다.이 사람이 슬퍼하기 보단 웃고 행복했으면 좋겠고 이 사람을 생각하면 가슴이 두근두근 뛴다. 슬퍼하면 나도 슬프고 행복해 보이면 괜히 나도 행복해진다.이 사람이 좀 더 행복할 수 있다면 내가 좀 더 손해를 봐도 좋다. 손해를 보면 기분이 나쁘거나 우울한 것과 같이 부정적인 생각이 들어야 하는데 오히려 손해를 보는 것이 더 좋다.이러한 내용은 무엇을 나타낼까? 하나의 단어로 표현할 수 있다.‘사랑’ 이란 단어로 표현 가능하다. 여기서 ‘사랑’은 위 내용에 대한 추상이며 위 내용은 ‘사랑’이란 단어에 대한 구체이다.2가지 문장 중 어떤 것이 읽기 편하고 합리적이라 생각하는 가? 💡1번너 요즘 사랑하는 사람이 있어?2번너 요즘 어떤 사람을 보면 가슴이 콩닥콩닥 뛰고 그 사람만 쳐다 보고 싶고 다 해주고 싶고 손해를 봐도 전혀 아쉽지 않고 행복하길 바라고 감정을 나누고 싶은 사람이 있어?보편적으로 1번이 읽기 편하고 합리적일 것이다.여기서 추상과 구체의 관계를 알 수 있으며 적절한 추상화는 문장을 이해하는 데 적은 비용과 시간이 든다. 쉽게 이해할 수 있기 때문이다.이러한 추상과 구체는 우리가 작성하는 코드에도 적용되고 있다. 결론적절한 추상화는 복잡한 데이터와 복잡한 로직을 단순화하여 이해하기 쉽도록 돕는다. = 읽기가 좋다.우리는 추상과 구체의 관계를 알 수 있으며 추상이 어떤 역할을 하는 지도 알 수 있었다. 추상화를 하면 우리는 복잡한 것을 쉽게 이해할 수 있으니 무조건 추상화를 해야 할까?무분별한 추상은 좋지 않다. 과한 추상은 구체를 유추하지 못할 수 있다. 지속적으로 언급한 ‘적절한 추상화’만이 읽기 좋은 효과를 가져올 수 있다.어떠한 경우가 추상으로부터 구체를 유추하지 못할까? 💡나는 밤이 좋아졌잘싸나는 친구랑 샤우팅 갔어 위 3가지 예시에 구체적인 의미를 알 수 있는가?1번은 먹는 밤인지 시간에 따라 나타나는 밤인지 알 수 없다.2번은 ‘졌지만 잘 싸웠다’라는 의미지만 과하게 줄인 탓에 알 수 없었다.3번은 샤우팅이라는 단어의 의미를 알 수 없었다. 소리 지르는 행위에 대한 미미한 유추만 가능하다.위 예시에 대한 문제점이다.추상화 과정에서 중요한 정보를 부각시키지 못했다.상대적으로 덜 중요한 정보를 남기고 중요한 정보는 제거했다.해석자가 동일하게 공유하는 문맥이 없다.중요한 정보의 기준이 다를 수 있다.도메인 영역 별 추상화 기준이 다를 수 있다.즉, ‘잘못된 추상화’는 오히려 추상화 안 한 것보다 못할 수 있다. 야기하는 side-effect는 생각보다 정말로 크다.‘적절한 추상화’는 해당 도메인 문맥 안에서, 정말 중요한 핵심 개념만 남겨서 표현하는 것이다.이름 짓기로 추상화 하기메서드와 추상화 한 문단의 주제는 반드시 하나다.잘 쓰여진 글이라면, 한 문단의 주제는 반드시 하나다. 0개도 2개도 아닌, 무조건 1개이다.실제로 국어/영어 시험에서도 주제가 1개가 아니라면 답이 오직 1개라고 정의하기 힘들다.문장과 문장에 대한 주제는 마치 코드에서의 메서드 선언부와 메서드 로직과 같은 역할을 한다.메서드 이름으로 구체적인 내용을 추상화한 것이다. 다른 예시를 보자.메서드의 로직 내부에서는 2가지 이상의 일을 하고 있다.메서드에서 수행하는 일산책하기, 은행가서 현금 인출하기, 음식점에서 밥 먹기, 책 구매하기오른쪽 메서드를 보자. 더 큰 맥락 안에서 포괄적인 의미를 담았다.즉, 잘 쓰여진 코드라면 하나의 메서드의 주제는 반드시 하나이다.메서드 선언부반환타입메서드 시그니처에 납득이 가는, 적절한 타입의 반환값 돌려주기→ 반환 타입이 boolean인데, 이게 이 메서드에서 무엇을 의미하는거지?void 대신 충분히 반환할 만한 값이 있는지 고민하기→ 반환값이 있다면 테스트도 용이해 진다. 결과값이 없다면 상태와 행위 중, 행위밖에 검증할 수 없다.메서드명추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름파라미터와 연결지어 더 풍부한 의미를 전달할 수 있다.파라미터타입, 개수, 순서를 통해 의미를 전달외부 세계와 소통하는 창String createDailyShopKey(String shopId, String localDateString){ return String.format("%s_%s", shopId, localDateString); } String createDailyShopKey(String shopId, LocalDate sellingDate){ return String.format("%s_%s", shopId, sellingDate.toString()); } 위 두 메서드 중 어떤 것이 더 잘 추상화를 했을까?//S34_2024-06-01 String shopId = "S34"; LocalDate today = LocalDate.of(2024, 6, 1); //1 String dailyShopKey = createDailyShopKey(shopId, today.toString()); //2 String dailyShopKey = createDailyShopKey(shopId, today); 정답은 2번이다. 이유가 뭘까?이유날짜를 String 값으로 나타낼 때, 다양한 형식이 있다. 2024-06-01, 2024.06.01 등 어떤 형식의 문자열로 넘길지 고민하게 된다.따라서 LocalDate 자체로 넘기면 날짜의 의미를 가진 LodatDate 구체화된 타입을 넘기면 된다라는 명확함을 인지할 수 있다.sellingDate와 localDateString의 의미를 보면 메서드 행위에 대한 의미를 보면 sellingDate가 더 많은 정보를 제공하므로 파악하기 쉽다.localDateString에 비해 sellingDate는 판매날짜의 의미를 담고 있다. 추상화 레벨 서점에 위와 같이 진열대에 책이 나열되어 있다. 여기는 여러 책이 진열되어 있는 공간이며 책의 제목이란 추상을 통해 책의 내용을 유추할 수 있다. 그런데 책의 구조인 제목, 목차, 내용 순이 아닌 단순 서류 뭉치로 이루어진 책 하나가 있다.이상하지 않는가? 서류 뭉치를 본 순간에 책인지, 그냥 단순 서류 뭉치인지 알 수 없다. 저 서류 뭉치의 존재에 대해 의문이 들고 이해하는데 비용이 발생한다.void method(){ ....... T t = extracted(p); .... } T extracted(P p){ ...something.... } 우리는 메서드 구현부를 확인할 때, 위처럼 extracted 메서드와 같이 추상화된 내부 메서드를 본다. 추상화된 메서드명의 주제에 대해 더 궁금하면 해당 메서드 내부에 들어가 확인할 것이다.이는 외부 세계와 내부 세계로 나뉠 수 있으며 method 내부에 메서드 선언부로만 표현된 extracted(p)가 경계가 된다.이는 외부 세계는 추상화 레벨이 높고, 내부 세계는 추상화 레벨이 낮음을 의미한다. 당연히 메서드 구현부에는 메서드 선언부에 대한 구체적인 내용이 있기 때문이다.하나의 세계에서는 추상화 레벨이 동등해야 한다.우리는 진열된 책을 예시로 봤다. 코드로 직접 봐보자.public static void main(String[] args) { showGameStart(); 10 initializeGame(); 10 showBoard(); 10 .... if (gameStatus == 1) { 5 System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); break; } ..... 10 } 10, 5는 예시로 추상화 단계를 수로 표현했다. 10이라는 추상화 단계를 가다가 ‘gameStatus == 1’ 이라는 구체에 가까운 5라는 추상화 단계를 만나며 코드를 이해하는데 혼란을 줄 수 있으며 추가적인 논리가 더 필요로 하다.그렇기에 추상화 단계를 맞춰주는 것은 중요하다. 진행할 점강의의 양이 매우 방대하여 깔끔하게 정리하기 쉽지 않은 거 같다. 추상과 구체의 관계에 대해 학습했으니적절한 추상화를 통해 내용을 정리해서 상대방에게 이해하기 쉬운 글을 제공할 수 있게 연습해봐야겠다.
백엔드
・
읽기좋은코드
・
클린코드