블로그
전체 52024. 11. 06.
0
[인프런 워밍업 스터디 클럽 2기 백엔드(클린코드, 테스트코드)] 후기
강의 후기길다면 길고 짧다면 짧은 인프런 워밍업 클럽이 종료되었다.내가 워밍업 클럽을 신청했던 이유는 Readable Code 강의를 되게 감명깊게 들었는데, 마침 우빈님께서 해당 부캠의 코치로 참여하신다 들었고, 테스트 코드 강의도 들어보고 싶어서 신청하게 되었다.그리고 강의를 들으면서 좋은 내용들을 많이 들을 수 있었고, 지금은 실무에서 어떻게 적용해볼 수 있을까 생각하면서 내 스스로 노력하려고 한다. 내가 노력해보려는 것은SOLID 원칙을 아직까지 철저하게 준수하진 못하지만 SRP 같이 단일 책임 원칙을 적용하여 객체의 책임을 작게 나누려고 있다. 이렇게 작게 나눠 보니까 수정이 생겼을 때 범위를 최소화 할 수 있었다.추상적으로 접근하기, 네이밍 잘 짓기 등 구체적으로 적기 보단 추상적인 메시지를 만들려고 하는데, 때로는 직관적인 메서드 명을 짓는게 나은거 같기도 한 생각이 든다. 이 부분은 계속 숙련해서 나만의 기준을 정해야겠다.테스트 코드를 작성할 때 레드&그린 리팩토링을 적용해보는 것. 나같은 경우엔 비즈니스 코드를 우다닥 짜고, 시간 나면 테스트 코드를 통해 검증하는데 레드&그린 리팩토링을 적용하면서 하면 계속 검증을 하면서 코드를 짜니 안정도가 더 올라가는 기분이었다.등등 위에 기술한거 외에도 많은 부분을 적용해보려고 한다. 좋은 강의를 제공해준 우빈님과 인프런에 감사를 표한다.앞으로 반복 수강하여 더 내 지식으로 체화할 수 있도록 해야겠다 😀오프라인 수료식 간단 후기워밍업 클럽을 진행하면서 미션과 발자국(후기)을 열심히 남기려고 노력하다 종종 까먹긴했었는데, 운영진의 친절 덕분에 감사하게도 모든 미션을 수행하고 기분 좋게 오프라인 수료식을 다녀올 수 있었다!먼저 이렇게 입구에서 귀여운 인프런 캐릭터와 반겨주고 있었다.이것은 SNS 팔로우를 맺어 받은 스티커인데, 이것 또한 너무 귀여웠다 ㅎㅎ이후에 각각 파트에 맞게 자리를 앉아 인프런에서 제공해주는 피자를 먹으며, 다양한 사람들과 네트워킹 활동을 가질 수 있었다. 신기하게도 같이 앉으셨던 분들이 나랑 비슷한 직종의 회사를 다니셨는데, 동일하게 이 직종으론 이직 안 한다고 선언하셔서 재밌었다 ㅎㅎ그리고 대망의 나의 두 번째 온라인 스승님이신 우빈님을 영접할 수 있었고, 내가 궁금했던 것과 다른 분들이 하신 질문들의 답변을 들으면서 아주 좋은 귀동냥 시간이 되었다.이번 워밍업 클럽을 통해서 우빈님의 좋은 강의들을 들을 수 있었던 거 같다.우빈님의 말씀 중에서 돈 받고 일하는 프로라면 주어진 시간 안에 일정 수준 이상의 품질을 보장하는 산출물을 만들어야 한다고 말씀 하셨다. 물론 나도 아직까지 일정을 못 지킨적은 없었는데 시간이 부족하면 품질 쪽은 일단 넘겼던 적이 있는데 강의에서 말씀했던 조상 코드를 내 스스로 만들고, 내가 다시 후손이 되어 내가 왜 이렇게 짰을까 싶은게 더러 있었다. 우빈님이 주신 피드백 처럼 숙련도를 올릴 수 있도록 노력해봐야겠다.다시 한 번 좋은 자리를 마련해준 인프런 팀과 좋은 말씀 주신 우빈님께 감사를 표하며, 글을 마무리 하겠다 끝~ 😀
백엔드
2024. 10. 27.
0
인프런 워밍업 클럽 백엔드 4주차 후기
후기 드디어 인프런 워밍업 클럽의 마지막 발자국을 쓰게 되었다.길다면 길고 짧다면 짧은 4주동안 두 개의 강의를 들으면서 많은 것을 느낄 수 있던 시간이었다.Readable Code에서는 추상을 통해서 메시지를 잘 만들고, 책임과 역할을 잘 분배하여 읽기 좋은 코드를 만들 수 있도록 노력해야 겠음을 느꼈고, Practical Test에서는 좋은 테스트 코드를 만들어서 사람이 노가다 하는 비중을 최대한 줄이면서, 안정적인 코드를만들어야겠음을 느꼈다.개발이라는 것은 나혼자만이 아닌 팀 단위로 움직이는 것이기 때문에 이를 팀 레벨로 이끌고 싶은 욕구가 생겼다. 그러기 위해선 내가 그를 증명할 수 있는 탄탄하고 좋은 실력을 갖추도록 노력해야겠다. 이번 인프러너의 좋은 강의들을 재반복하면서 나의 지식으로 습득하고, 이를 다른 사람들에게 전수할 수 있는 그런 사람이 되도록 해야겠다.즐거운 기회를 마련해준 우빈님과 인프런에게 감사를 표하며 글을 마치겠다! ㅎㅎ 내용 정리Presentation Layer 테스트(1)외부 세계의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증을 수행한다Mock (가짜, 대역) 객체테스트 시 의존관계 주입이 방해될 때, 가짜를 집어넣어 처리함MockMvcMock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크 @Transactional(readOnly = true)CRUD에서 CUD가 동작 안 함오직 R만 작동JPA는 기본적으로 1차 캐시에 스냅샷을 저장하고, 트랜잭션 커밋 플러시 하는 시점에 변경 감지가 작동하여 업데이트 쿼리를 발생시킴근데 reaOnly = true를 열면 CUD 스냅샷 저장, 변경감지를 안 하여 성능 향상 효과가 있음CQRSCommand / QueryRead 작업이 거의 80%정도 주를 이룰 때가 많음그래서 command와 query 작업을 분리하여 서로 연관되지 않게끔 분리하는 CQRS 패턴을 활용조회 하는 서비스만 만들면 @Transactional(readOnly=true)를 써서 관리할 수 있음DB 엔드포인트도 구분하여 쓰기는 마스터, 읽기는 슬레이브로 보내버릴 수 있음어노테이션 보고 마스터나 슬레이브로 구분해줄 수 잇다함클래스 단위에은 readOnly를, 변경 메서드엔 @Transactional을 달자아님 객체 단위로 나눠서 관리Presentation Layer 테스트(2)Validation을 활욜String@NotBlank: 전부 다 허용 안됨@NotNull: “”, “ “는 통과됨@NotEmpty: “ “ 공백은 통과 “” 빈문자열 실패정책을 validation에서 거르는게 맞을까? 라는 고민을 해야함EX) 상품 이름은 20자 → 이런건 서비스 레이어나 프러턱트 코드 같은 안쪽에서 해도 됨상위 레이어는 하위 레이어를 알아도 되지만하위 레이어는 상위 레이어를 모르게 하는게 좋음Layered Architecture 단점DB랑 연결하기 위해 JPA 도메인 객체를 만들었는데, 너무 강결합 되는 구조가 됨깊어질 수록 바꾸기가 어려워짐 Hexagonal Architecture포트와 어댑터 형태포트를 통해 외부와 통신을 함도메인 정책이 가장 안 쪽에 있음모노레포라면 레이어드 아키텍쳐도 괜찮지만점점 커질 거 같으면 헥사고날을 고려해보자QueryDSLJPA랑 함께 많이 쓰이는 동적 쿼리 빌더타입체크를 지원해주서 컴파일단에서 체크해 안전해짐 섹션7: Mock을 마주하는 자세Mockito로 Stubbing 하기이메일 등 외부 네트워크에 전송하는 서비스 로직에는 Transactional 안 걸어두는게 좋음트랜잭션으로 DB 조회할때 커넥션을 가지고 있는데, 외부 소통 하면서 갖고 있으면 다른데서 못 가져감Test DoubleStunt Double: 스턴트 배우를 쓰는 것을 차용 → 대역을 사용Dummy: 아무 것도 하지 않는 깡통 객체Fake: 단순한 형태로 동일한 기능을 수행하나, 프로덕션에서 쓰기에는 부족한 객체 (ex. FakeRepository - 메모리 Map)Stub: 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체. 그 외에는 응답하지 안흠Spy: Stub 이면서 호출된 내용을 기록하여 보여줄 수 있는 객체. 일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있다.Mock: 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체Stub과 Mock이 좀 헷갈림Stub은 상태 검증 (State Verfication)Mock은 행위 검증(Behavior Verication): 메서드가 무엇을 했을 때(행위) 어떤 값을 돌려주는 느낌~Mocks Aren't Stubspublic interface MailService { void send(Message msg); } public class MailServiceStub implements MailService { private List messages = new ArrayList(); public void send(Message msg) { messages.add(msg); } public int numberSent() { return messages.size(); } } // Stub 검증 class OrderStateTest { @Test void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); MailServiceStub mailer = new MailServiceStub(); order.setMailer(mailer); order.fill(wraehouse); assertEquals(1, mailer.numberSent()); // 상태에 대한 검증 } } // Mock 검증 class OrderInteractionTester { @Test void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); Mock warehouse = mock(Warehouse.class); Mock mailer = mock(MailService.class); order.setMailer((MailService) mailer.proxy()); mailer.expects(once()).method("send"); // 메서드가 한 번 불림. 행위 확인 warehouse.expects(once()).method("hasInventory") .withAnyArguments() .will(returnValue(false)); order.fill((Warehouse) warehouse.proxy()); } } @Mock, @Spy, @InjectMocks순수 mockito로 검증하기BDDMockitoClassicist VS MockistMockist: 모든 걸 Mocking 처리해서 하자Classicist: 모킹만 해서 실 프로덕션 환경을 다 커버하긴 어렵다필요한 경우엔 실제를 쓰다가 필요하면 Mockito를 쓰자강사님 (Classcisit)Presentaion은 외부에서 오는 값만 검증하고, 하위 레벨은 Mocking외부 시스템은 우리에게 제어권이 없기에 이런 경우 (외부 계를 나누자)비용이 조금 더 들더라도 실제 객체를 갖고와 테스트를 하는게 안전하지 않을까~키워드 정리Bean이 들어가면 스프링 환경에서이니, 단위 테스트로만 Mocking하고 싶으면 Bean이 안 들어간걸 쓰자섹션8: 더 나은 테스트를 작성하기 위한 구체적 조언한 문단에 한 주제!글쓰기에서 요지는 한 문단엔 한 주제만을 넣음테스트도 문서로써의 기능을 함테스트 코드를 글 쓰기의 관점에서 하나의 테스트도 하나의 문단으로 보고, 그에 맞게 처리하자void containsStockTypeEx() { // given ProductType[] productTypes = ProductType.values(); for (ProductType productType: productTypes) { if (productType == ProductType.HANNDMADE) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isFalse(); } if (productType == ProductType.BAKERY || productType == ProductType.BOTTLE) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isTrue(); } } 위와 같이 if 같은 분기문이 있으면 두 가지 이상의 케이스를 구분하겠단 의미반복문의 경우에도 테스트 코드를 읽는 사람이 생각을 해야함DisplayName을 한 문장으로 만들게끔 해서 하나의 케이스만 처리하게끔 하자완벽하게 제어하기아래와 같이 시간, 랜덤값 같이 내가 제어할 수 없는 값은 외부에서 주입할 수 있도록 해야함public Order createOrder() { LocalDateTime currentDateTime = LocalDateTime.now(); final LocalTime currentTime = currentDateTime.toLocalTime(); if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) { throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요."); } return new Order(LocalDateTime.now(), beverages); } // 외부에서 주입 public Order createOrder(LocalDateTime currentDateTime) { final LocalTime currentTime = currentDateTime.toLocalTime(); if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) { throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요."); } return new Order(LocalDateTime.now(), beverages); } 그렇다고 LocalDateTime registeredDateTime = LocalDateTime.now() 테스트 코드에서 현재 시각을 주는 메서드를 막 쓰지는 말자 (환경마다 다를 수 있으니)시각을 LocalDateTime.of() 같은 시각을 지정해줄 수 있도록 원칙을 만드는 것도 좋음테스트 환경의 독립성을 보장하자@DisplayName("재고가 부족한 상품으로 주문을 생성하려는 경우 예외가 발생한다.") @Test void createOrderWithNoStock() { // given final 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); stock1.deductQuantity(1); // todo stockRepository.saveAll(List.of(stock1, stock2)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001", "001", "002", "003")) .build(); // when // then assertThatThrownBy(() -> orderService.createOrder(request.toServiceRequest(), registeredDateTime)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("재고가 부족한 상품이 있습니다."); } deductQuantity 부분으로 인해 논리적으로 한 번 더 생각해서 테스트를 짜게 된다테스트는 when, then에 집중해야는데, given도 보면서 혼란스러워짐given은 순수 생성자 기반의 값을 주는게 좋음 or Builder팩토리 메서드도 지양하는게 좋음 (팩토리 메서드 내에 의도를 가지고 뭔가 로직이 있을 수 있기에)테스트 간 독립성을 보장하자class StockTest { private static final Stock stock = Stock.create("001", 1); @DisplayName("재고의 수량이 제공된 수량보다 작은지 확인한다.") @Test void isQuantityLessThanEx() { // given int quantity = 2; // when boolean result = stock.isQuantityLessThan(quantity); // then assertThat(result).isTrue(); } @DisplayName("재고를 주어진 개수만큼 차감할 수 있다.") @Test void deductQuantityEx() { // given int quantity = 1; // when stock.deductQuanaity(quantity); // then assertThat(stock.getQuanaity()).isZero(); } } 두 가지 테스트가 static 변수 같은 공유 자원을 활용하고 있음공유 자원의 값이 변경되면 다른 테스트 결과에 영향이 생김테스트 수행 순서는 랜덤하기에 독립적으로 항상 올바를 테스트가 되도록 해줘야함DynamicTest를 쓰면 지정해줄 수 있나봄한 눈에 들어오는 Test Fixture 구성하기Fixture: 고정물, 고정되어 있는 물체테스트를 위해 원하는 상태로 고정시킨 일련의 객체활용 어노테이션@BeforeAll@BeforeEach@AfterAll@AfterEach픽스처를 통해 공통의 테스트 객체를 만들면 하나의 테스트를 바꿀 때 모두 영향이 끼치기에 지양하는 것이 좋음테스트클래스가 엄청 길어진 경우 given 절을 보는데 문맥을 기억하기 어려워짐 (→ 문서 파악이 어려워짐)사용해도 괜찮을 때각 테스트 입장에서 봤을 때 : 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는 경우에수정해도 모든 테스트에 영향을 주지 않는가data.sql에서 given 데이터를 만들 수 있겠지만, 데이터 파편화가 될 가능성이 큼 (비추)테이블도 많아지고, 사이즈가 커질 수록 관리가 어려워짐생성 메서드를 만들 때 테스트에 필요한 파라미터만 받아서 쓰자name 필드가 필요없으면 파라미터에서 빼버림테스트 패키지 전체에서 사용하는 추상 클래스를 만들어 픽스처 빌더들을 모아 쓸 순 있겠지만 NO 추천파라미터가 엄청 많아질 때 마다 내가 필요한 빌더들이 생기고, 관리가 더 안 될 수도 있음코틀린을 사용하면 롬복도 필요없고, 빌더도 필요없음. 기본값을 지정해줄 수 있음Text Fixture 클렌징deleteAll: select 해서 건건이 where id 조회해서 지움연관 관계 걸려있는 것도 찾아서 같이 지어줌전체 찾아서 순회해서 지워주고 있음삭제도 일종의 비용이기 때문에 테스트에서 오래걸리면 좀 그럴 수 있음@Transactional public void deleteAll() { Iterator var2 = this.findAll().iterator(); while(var2.hasNext()) { T element = (Object)var2.next(); this.delete(element); } } deleteAllInBatch전체 삭제 쿼리를 만들어서 지어줌 → 벌크성으로 지워줌@Transactional public void deleteAllInBatch() { Query query = this.entityManager.createQuery(this.getDeleteAllQueryString()); this.applyQueryHints(query); query.executeUpdate(); } private String getDeleteAllQueryString() { return QueryUtils.getQueryString("delete from %s x", this.entityInformation.getEntityName()); } @Transactional을 쓰면 되는데, 사이드 이펙트를 잘 고려해서 진행해야함@ParameterizedTest반복되는 given에 대해서 위 어노테이션을 활용해서 반복해서 할당할 수 있다.@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.") @CsvSource({ "HANDMADE, false", "BOTTLE, true", "BAKERY, true" }) @ParameterizedTest void containsStockType4(ProductType productType, boolean expected) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isEqualTo(expected); } private static Stream provideProductTypesForCheckingStockType() { return Stream.of( Arguments.of(ProductType.HANDMADE, false), Arguments.of(ProductType.BOTTLE, true), Arguments.of(ProductType.BAKERY, true) ); } @DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.") @MethodSource("provideProductTypesForCheckingStockType") @ParameterizedTest void containsStockType5(ProductType productType, boolean expected) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isEqualTo(expected); } @DynamicTest공통의 값으로 단계 별로 테스트를 돌리고 싶으면 사용한다@DisplayName("") @TestFactory Collection dynamicTest() { return List.of( DynamicTest.dynamicTest("", () -> {}), DynamicTest.dynamicTest("", () -> {}) ); } @DisplayName("재고 차감 시나리오") @TestFactory Collection stockDeductionDynamicTest() { // given Stock stock = Stock.create("001", 1); return List.of( DynamicTest.dynamicTest("재고를 주어진 개수만큼 차감할 수 있다.", () -> { // given int quantity = 1; // when stock.deductQuantity(quantity); // then assertThat(stock.getQuantity()).isZero(); }), DynamicTest.dynamicTest("재고보다 많은 수의 수량으로 차감 시도하는 경우 예외가 발생한다", () -> { // given int quantity = 1; // when // then assertThatThrownBy(() -> stock.deductQuantity(quantity)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("차감할 재고 수량이 없습니다."); }) ); } 테스트 수행도 비용이다. 환경 통합하기테스트를 작성하는 이유는 사람이 수동으로 돌리는 비용보다 기계에 맡겨서 피드백을 빨리 받게 하기 위함테스트의 속도가 빨라야 유의미하기에 비용관리를 해야함서버 띄우는 경우가 많아지면 서버가 오래 걸리게 됨이런 비용을 관리해야함@MockBean 의 경우에도 서버를 새로 띄어야됨@WebMvcTest(controllers = { OrderController.class, ProductController.class }) public abstract class ControllerTestSupport { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; @MockBean protected OrderService orderService; @MockBean // Mock 객체 만들어줌 protected ProductService productService; } @ActiveProfiles("test") @SpringBootTest public abstract class IntegrationTestSupport { @MockBean protected MailSendClient mailSendClient; } Q. private 메서드의 테스트는 어떻게 하나요?하려고 해서도 안 되고 할 필요가 없다.클라이언트(외부)는 공개된 API만 알면 되기에, private을 알 필요 없다만약 private 메서드를 단독으로 빼서 테스트하고 싶다면, 객체를 분리할 시점인가를 고민해봐야 한다필요하다면 새로운 객체를 만들어 책임을 위임하면 된다Q. 테스트에서만 필요한 메서드가 생겼는데 프로덕션 코드에서는 필요 없다면?만들어도 됨. 하지만 보수적으로 접근하기테스트에서 조회성을 위해 만드는 거 정돈 괜찮지만, 테스트에서만 사용되는 메서드를 막 만드는 것은 지양해야함getter, 생성자, 생성자 빌더, 사이즈 등 객체가 마땅히 가져도 되는 행위라 생각되고 미래에 충분히 사용되는 것들은 만들어도 괜찮다~ 학습 테스트잘 모르는 기능, 라이브러리, 프레임워크를 학습하기 위해 작성하는 테스트여러 테스트 케이스를 스스로 정의하고 검증하는 과정을 통해 보다 구체적인 동작과 기능을 학습할 수 있다.관련 문서만 읽는 것보다 훨씬 재미있게 학습할 수 있다.Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원할하게 한다기본적으로 AsciiDoc을 사용하여 문서를 작성한다REST Docs VS SWAGGERREST DOCS장점테스트를 통과해야 문서가 만들어진다 (신뢰도가 높음)프로덕션 코드에 비침투적임단점코드 양이 많다.설정이 어렵다SWAGGER장점적용이 쉬움문서에서 바로 API 호출을 수행해볼 수 있다.단점프로덕션 코드에 침투적이다테스트와 무관하기 때문에 신뢰도가 떨어질 수 있음
백엔드
2024. 10. 20.
0
인프런 워밍업 클럽 백엔드 3주차 후기
3주차 후기 Readable Code 강의를 재밌게 수강하고, 드디어 고대하던 강의 중 하나인 테스트 코드 강의를 들었다.나는 새로 기능을 만들어 볼 때마다 JUnit으로 테스트 코드를 돌려보려고 한다. 근데 아직 경험이 많이 부족하고 시간이 부족하다는 핑계로 미루게 된다. 그러다 보니 자연스레 서버를 켜서 직접 API를 호출해서 테스트를 하는 비효율적인 방법을 선택하고 있다.이전에 인프런 CTO의 이동욱 님의 어느 글에서 안 좋은 방식이라 했던걸 내가 하고 있던 것이다..! ㅎㅎ이를 지양하기 위해 이번 테스트 강의를 열심히 들었고, 들을 수록 이래서 테스트 코드를 작성해야 함을 배우게 되었다.강의를 마저 다 들어서 얼른 회사에서도 다 적용해볼 수 있도록 해야겠다. 섹션2: 테스트는 왜 필요할까테스트는 왜 필요할까?테스트는 사실.. 귀찮은 작업실무에서는 짧은 시간에 기능을 구현해서 QA(Quality Assuarance)해야는데 시간이 부족함근데 왜 테스트 코드를 짜야할까?기존 프로덕션 코드가 있었는데, 기능을 계속 추가하면서 테스트를 하게 됨근데 아래와 같이 기존 코드를 일부 참고하는데가 생기면서 새로 검증을 해야됨프로덕션 코드가 계속 커지면 테스트하는데만 시간이 다 소진되어버림또한 사람이 하게 되면 실수&누락이 발생하게 될 수 있음 테스트 코드를 작성하지 않으면변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 함변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 함빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다올바른 테스트 코드는자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있음소프트웨어의 빠른 변화를 지원한다.팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.가까이 보면 느리지만, 멀리 보면 가장 빠르다우리가 테스트 코드로 얻을 수 있는 점빠른 피드백: 코드가 실패하면 바로 알 수 있음자동화: 기계가 자동으로 해줘서 사람이 탈 일이 없어짐안정감: 그로 인해 안정감이 늚💡 물론 테스트 코드를 어렵게 짜면 더 힘들어 질 수도 있다. 잘 짜는 것이 중요! 테스트는 귀찮지만 해야한다! 섹션3: 단위 테스트 수동테스트 VS 자동화된 테스트아래와 같이 print를 활용해서 수동으로 확인하는게 과연 맞을까?class CafeKioskTest { @Test void add() { CafeKiosk cafeKiosk = new CafeKiosk(); cafeKiosk.add(new Americano()); System.out.println(">>> 담긴 음료 수 : " + cafeKiosk.getBeverages().size()); System.out.println(">>> 담긴 음료 : " + cafeKiosk.getBeverages().get(0).getName()); } } JUnit5로 테스트하기단위 테스트(Unit test)작은 코드 단위(클래스 or 메서드)를 독립적으로 검증하는 테스트독립적: 외부 네트워크 같은 환경에 의존하는게 아닌 딱 코드 단위를 테스트검증 속도가 빠르고, 안정적임JUnit5단위 테스트를 위한 테스트 프레임워크XUnit - Kent Beck 창시SUnit(Smalltalk), JUnit(Java), NUnit(.NET) 등AssertJ테스트 코드 작성을 원할하게 돕는 테스트 라이브러리풍부한 API, 메서드 체이닝 지원테스트 케이스 세분화하기해피 케이스: 요구 사항이 그대로 만족하는 케이스 (해피해피)예외 케이스: 예외가 발생하는 경우도 생각해야함두 가지를 고려하기 위해 경계값 테스트를 잘 만들어야 함범위(이상, 이하, 초과, 미만), 구간, 날짜 등테스트하기 어려운 영역을 분리하기추가 요구사항가게 운영 시간(10:00~22:00) 외에는 주문을 생성할 수 없다.public Order createOrder() { LocalDateTime currentDateTime = LocalDateTime.now(); final LocalTime currentTime = currentDateTime.toLocalTime(); if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) { throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요."); } return new Order(LocalDateTime.now(), beverages); } 현재 코드와 같이 시간에 대한 값을 프로덕션 코드에 넣어두면 테스트할 때마다 환경이 달라져 일관성 있는 테스트를 하기 어려워진다이처럼 테스트하기 어려운 영역을 구분하고 분리해야함public Order createOrder(LocalDateTime currentDateTime) { final LocalTime currentTime = currentDateTime.toLocalTime(); if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) { throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요."); } return new Order(LocalDateTime.now(), beverages); } 다음과 같이 파라미터로 외부에서 주입받게 해두면 테스트하기 좋아짐 테스트하기 어려운 영역관측할 때마다 다른 값에 의존하는 코드현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력 등외부 세계에 영향을 주는 코드표준 출력(로그), 메시지 발송, 데이터베이스에 기록하기 등 순수 함수(pure functions)같은 입력에는 항상 같은 결과외부 세상과 단절된 형태테스트하기 쉬운 코드 섹션4: TDD Test Drive DevelopmentTDD: Test Driven Development프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론테스트 작성 → 기능 구현레드 그린 리팩토링레드: 실프하는 테스트 작성 (실패를 봐라)그린: 테스트 통과, 최소한의 코딩 (일단은 통과 시켜~)리팩토링: 구현 코드 개선, 테스트 통과 유지핵심 가치: 피드백내가 작성하는 코드에 대해서 자주, 빠르게 피드백을 받을 수 있음선 기능 구현, 후 테스트 작성의 문제점 (우리가 일반적으로 했던)테스트 자체의 누락 가능성특정 테스트(해피 케이스) 케이스만 검증할 가능성잘못된 구현을 다소 늦게 발견할 가능성선 테스트 작성, 후 기능 구현 (TDD 방식)복잡도가 낮은(유연하며 유지보수가 쉬운) 테스트 가능한 코드로 구현할 수 있게 한다.쉽게 발견하기 어려운 엣지(Edge) 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백을 받을 수 있다.과감한 리팩토링이 가능해진다.클라이언트 관점에서의 피드백을 주는 Test Driven 섹션5: 테스트는 []다테스트는 []다.테스트는 문서다문서?프로덕션 기능을 설명하는 테스트 코드 문서다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서, 모두의 자산으로 공유할 수 있음DisplayName을 섬세하게명사의 나열보다 문장으로 작성A이면 B이다A이면 B가 아니고 C다테스트 행위에 대한 결과까지 기술하면 더 좋음 💡음료 1개 추가 테스트 → ~테스트 지양하기음료를 1개 추가할 수 있다.음료를 1개 추가하면 주문 목록에 담긴다 (결과 기술)도메인 용어를 사용하여 한층 추상화된 내용을 담자메서드 자체의 관점보다 도메인 정책 관점으로 작성테스트의 현상을 중점으로 기술하지 말자 💡특정 시간 이전에 주문을 생성하면 실패한다. (AS-IS)영업 시작 시간 이전에는 주문을 생성할 수 없다. (TO-BE)BDD(Behavior Driver Development) 스타일로 작성하기TDD에서 파생된 개발 방법함수 단위의 테스트에 집중하기보다, 시나리오에 기반한 테스트케이스(TC) 자체에 집중하여 테스트한다.개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준(레벨)을 권장Given / When / ThenGiven: 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등) → 어떤 환경에서When: 시나리오 행동 진행 → 어떤 행동을 진행했을 때Then: 시나리오 진행에 대한 결과 명시, 검증 → 어떤 상태 변화가 일어난다→ DisplayName에 명확하게 작성할 수 있음intelliJ → Live Templates → Java 단축키 등록패키지 풀 네임까지 적어야 자동 import 됨@org.junit.jupiter.api.DisplayName("") @org.junit.jupiter.api.Test void $METHOD_NAME$() { // given $END$ // when // then } 키워드 정리Spock: groovy 기반 BDD 프레임워크언어가 사고를 제한한다한 번 언어로 규정해 놓으면 우리의 사고도 그에 맞춰 제한이 됨명확하지 표현 못한 테스트 자체가 허들이 되고 사고를 제한할 수 있음문서로써 테스트도 중요 섹션6: Spring & JPA 기반 테스트Layered Architecture보통 아래와 같이 계층을 나눔 → 관심사의 분리를 위해 (책임을 나누고 유지보수성 높이기)테스트하기 어려운 부분을 분리해서 해당 부분만 집중하는 것은 동일 통합 테스트(Integration test)여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트일반적으로 작은 범위의 단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없음풍부한 단위 테스트 & 큰 기능 단위를 검증하는 통합 테스트Spring/JPA 훑어보기 & 기본 엔티티 설계Library VS Famework라이브러리는 내 코드가 주체가 됨필요한 기능이 있으면 외부에서 라이브러리를 끌어와서 도구로써 사용프레임워크는 이미 동작하게끔 환경이 구성이 되어있음내 코드는 수동적으로 프레임워크에 꽂혀서 이용이 됨Spring FameworkIoC(Inversion Of Control)제어의 역전객체의 생명 주기 제어를 프레임워크가 맡아줌DI(Dependency Injection)의존성 주입특정 객체를 바로 주입해주는게 아니라 인터페이스를 주입하여 약한 결합을 해주도록 활용A는 B객체를 주입 받지만, 어떻게 받는지 관심없고 단지 주입만 받아 사용AOP(Aspect Oriented Programming)관점 지향 프로그래밍비즈니스 흐름과 관계없는 부분을 관점을 한 데로 모아 활용프록시 활용트랜잭션, 로깅 등에 활용ORM: Object-Relational-Mapping기존 RDB 패러다임 불일치를 가운데에서 맞춰주게끔 해줌객체 지향 팰러다임과 관계형 DB 패러다임의 불일치이전에는 개발자가 객체의 데이터를 한땀한땀 매핑하여 DB에 저장 및 조회(CRUD)ORM을 사용함으로써 개발자는 단순 작업을 줄이고, 비즈니스 로직에 집중할 수 있음JPA(Java Persistence API)자바 진영의 ORM 기술 표준인터페이스이고, 여러 구현체가 있지만 보통 Hibernate를 많이 사용함반복적인 CRUD SQL을 생성 및 실행해주고, 여러 부가 기능들을 제공함편리하지만 쿼리를 직접 작성하지 않기 때문에, 어떤 식으로 쿼리가 만들어지고 실행되는지 명확하게 이해하고 있어야 함!Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA 제공QueryDSL과 조합하여 많이 사용함 (타입체크, 동적쿼리)@Entity, @ID, @Column@ManyToOne, @OneToMany, @OneToOne, @ManyToManyManyToMany는 일대다-다대일 관계로 풀어서 사용하길 권장!
백엔드
2024. 10. 17.
0
인프런 워밍업 클럽 백엔드 2주차 후기
2주차 후기 강의를 듣고 발자국 남기는걸 까먹어서 부랴부랴 남긴다 ㅎㅎ..😅1주차에 이어서 남은 리팩토링 강의를 들으면서 강사님께서 남겨주신 리팩토링 연습문제를 풀어보았다.인프런의 스타강사 영한님의 명언 "백문이 불여일타"를 기억하기에 모든 강의를 열심히 따라 치다가, 이번엔 스스로 문제풀이를 해보려고 하니 생각보다 어려웠다.정리한 강의 내용을 쥐어짜며, 어떻게든 내 나름대로 추상화와 코드정리를 해본 후 강사님과 비교해보았다.나와 비슷하게 하신 점을 볼 때는 뿌듯했고, 내가 미처 생각치 못한 방식으로 리팩터링을 하실 때는 감탄을 금치 못했다.강의 너머 우빈님께 정말 많은 것을 배워볼 수 있는 기회였다.그리고 정말 코드에선 추상이라는 개념은 중요한 것 같다. 동등한 추상 레벨을 맞추거나, 메시지 이름을 통해서 "아 이거는 이런 역할을 하겠구나" 의미를 파악할 수 있는 점들이 중요함을 느꼈다. 하지만 또 모든 것을 추상해버리려는 오버 엔지니어링도 주의해야겠다. 이런 경각심을 배우며 실전에서도 조심하며 해볼 수 있도록 해야겠다.이제 남은 테스트 코드 강의를 들으며 더 정진해야겠다! 아래는 강의를 정리한 내용섹션6 - 리팩토링 연습문제: 스터디 카페 이용권 선택 시스템사용자는 시간권, 주단위 이용권, 1인 고정석 중 선택할 수 있음시간권: 2, 4, 6, 8, 10, 12시간주권: 1, 2, 3, 4, 12주고정석: 4주, 12주추가금액을 내면 사물함도 사용할 수 있는데, 고정석인 경우만 선택 가능함선택한 이용권, 사물함 여부에 따른 최종 금액을 계산해준다.오픈 이벤트로 2주권 이상 10%, 12주권 15% 할인을 진행중임리팩토링 포인트추상화 레벨객체로 묶어볼만한 것은 없는지객체지향 패러다임에 맞게 객체들이 상호 협력하고 있는지SRP: 책임에 따라 응집도 있게 객체가 잘 나뉘어져 있는지DIP: 의존관계 역전을 적용할만한 곳은 없는지일급 컬렉션리팩토링(2) - 객체의 책임과 응집도IO 통합일급 컬렉션display()의 책임Order 객체리팩토링(3) - 관점의 차이로 달라지는 추상화FileHandler를 바라보는 관점헥사고날 아키텍처 - 포트와 어댑터포트: 인터페이스: 규격만 맞으면 꽂을 수 있는 플러그 같은 스펙어댑터: 포트에 맞는 구현체섹션7 - 기억하면 좋은 조언들1. 능동적 읽기복잡하거나 엉망인 코드를 읽고 이해하려 할 때, 리팩토링하면서 읽기공백으로 단락 구분하기메서드와 객체로 추상화 해보기주석으로 이해한 내용 표기하며 읽기우리에게는 언제든 돌아갈 수 있는 git reset —hard가 있다.핵심 목표는 우리의 도메인 지식을 늘리는 것. 그리고 이전 작성자의 의도를 파악하는 것2. 오버 엔지니어링필요한 적정 수준보다 더 높은 수준의 엔지니어링을 하는 것EX) 구현체가 하나인 인터페이스인터페이스 형태가 아키텍처 이해에 도움을 주거나, 근시일 내에 구현체가 추가될 가능성이 높다면 OK구현체를 수정할 때마다 인터페이스도 수정해야 함코드 탐색에 영향을 줌. 애플리케이션이 비대해 짐EX) 너무 이른 추상화정보가 숨겨지기 때문에 복잡도가 높아진다.후대 개발자들이 선대의 의도를 파악하기가 어렵다.3. 은탄환은 없다만능 해결사 같은 기술은 없다 클린 코드도 은탄환이 아니다실무: 2가지 사이의 줄다리기지속 가능한 소프트웨어의 품질 VS 기술 부채를 안고 가는 빠른 결과물대부분의 회사는 돈을 벌고 성장해야 하고, 시장에서 빠르게 살아남는 것이 목표임이런 경우에도, 클린 코드를 추구하지 말라는 것이 아니라, 미래 시점에 잘 고치도록 할 수 있는 코드 센스가 필요함결국은, 클린 코드의 사고법을 기반으로 결정하는 것 기술부채: 당장 직면한 문제를 적정한 기술을 도입하지 못하고 단순하게 해결하고 미래에 미뤄두는 것모든 기술과 방법론은 적정 기술의 범위 내에서 사용되어야 한다.EX) 당장 급하게 배포 나가야 하는데, 동료에게 style 관련된 리뷰를 주고 고치도록 강요하는 사람도구라는 것은, 일단 그것을 한계까지 사용할 줄 아는 사람이 그것을 사용하지 말아야 할 때도 아는 법이다.적정 수준을 알기 위해, 때로는 극단적으로 시도해보자 (오버엔지니어링도 해보면서 적정 수준이 무엇일지 겪어보기)
백엔드
2024. 10. 06.
0
인프런 워밍업 클럽 백엔드 1주차 후기
내가 워밍업 클럽을 신청한 이유우선 나는 백엔드 개발자로 재직한지 갓 1년이 된 몹시 주니어인 개발자이다.아직 1년 차인 내가 왈가왈부할 실력도 아니고, 다른 회사의 환경을 경험을 한 건 아니지만, 우리 회사의 환경은 뭐랄까.. 내가 공부한 내용과는 괴리가 컸다. Spring Boot를 공부했지만 Spring legacy 환경을 쓰고 있고, JPA를 공부했지만 오직 Mybatis로 구성된 환경이다. (지금 계속 개선되고 있지만 배포도 아직 FTP로 많이 한다...ㅠ)내가 겪은 어려움은 Mybatis를 사용하면서 VO 개념도 쓰고 있긴 하지만, 대부분의 반환 타입이나 매개 변수로 단순한 Map 타입의 객체를 받아서 이를 컨트롤러나 서비스 객체에서 get() 또는 put() 메서드를 활용해서 단순히 절차지향적으로만 쓰는 느낌이 강했다. '오브젝트'와 '객체 지향 사실과 오해' 같은 객체지향 관련한 명저를 읽으면서 객체지향 찍먹을 해봤기에 아직 초급 단계도 못 벗어난 내가 봐도 이거는 자바를 잘 쓰고 있는 느낌을 못 받았었다. 이에 회사 코드를 리팩토링을 시도했지만, 나도 결국 한낱 초보자라 결국 선배들과 비슷한 코드를 작성하며 흘러가고 있는 것이 나에게 큰 고민거리였다.이렇게 리팩토링에 대한 갈망만 가진 채 회사를 다니던 중 우빈님의 강의 'Readable Code'를 발견하게 되었고, 소개 영상과 목차를 통해 강의를 수강하기로 마음 먹었다. 또한 운이 좋게도 이번 워밍업 클럽 과정에 속한 것을 발견하게 되었고, 나와 같은 고민을 가진 분들과 소통하고 싶어 신청하게 되어 열심히 들으며 많은 가르침을 얻고 있는 중이다. 추상의 중요성먼저 강의 시작의 asis의 지뢰찾기 코드를 한 번 보고 충격 받았다. "우리 회사 코드랑 흡사하구나?" 모든 비즈니스 로직이 한 메서드에 들어 가 있는 "조상" 코드를 보며 이것을 어떻게 읽기 좋게 바꿀 것인지가 몹시 궁금해졌다. 그리고 추상이란 행위가 프로그래밍에 있어서 많이 중요함을 느꼈다. 어떤 식으로 이름을 지어줘야 후손이 쉽게 읽을까에 대한 고민을 하게 되었고, 당장에 i, temp 같은 땜빵용 변수명, 메서드명들을 찾아 코드를 읽어보며 의미있게 변경해봤다. 이것만 해보아도 코드 흐름을 상당히 편히 읽히게 할 수 있겠구나 느꼈다. 객체 지향의 중요성객체 지향에 대해서 많은 것을 배울 수 있었다. 특히 일급 컬렉션에 대해서는 이번 개발에 바로 적용해서 써먹어 보았다.이번에 개발할 때 어떤 리스트 컬렉션을 만들어서 데이터를 순회해서 가공했어야 했는데, 이를 일급 컬렉션 객체를 만들어 해당 객체 안에서 추상화된 메시지만 던져 수행시키니 훨씬 응집도가 높아졌고, 호출하는 쪽은 내부 로직은 신경 안 써도 된다는 점에서 이런 것들이 객체 지향의 초석이구나를 느낄 수 있었다. 나의 인프런 온라인 스승님 "김영한" 님의 강의 명언 중 하나인 "백문이불여일타", 보지만 말고 쳐보는 것이 중요함은 이런 것임을 느꼈고 앞으로 강의에서 배운 점은 바로바로 써먹어볼까 한다 미션 수행Day 4의 미션은 코드를 리팩토링 하는 것이었는데, 강의에서 너무 잘 알려주셔서 어렵지 않게 풀어볼 수 있었다.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; }위와 같이 indent가 깊고, if-else로 되어있다보니 이전의 문맥(컨텍스트)를 머릿속에 계속 담아야 하는 불편함이 있었다.또한 order의 변수들을 getter로 꺼내서 호출하는 쪽에서 내용을 판단하는 아주 무례한(?) 행동을 하고 있었다.우빈님 강의에서 언급했듯이 뇌 메모리를 적게 쓰는 방향으로 리팩토링 하도록 노력하였다.public boolean validateOrder(Order order) { // 조기 종료 if (order.hasNotItems()) { log.info("주문 항목이 없습니다."); return false; } // 올바르지 않은 총 가격이면 조기 종료 if (order.isTotalPriceNotValid()) { log.info("올바르지 않은 총 가격입니다."); return false; } // 사용자 정보 없으면 조기 종료 if (order.hasNotCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } // 여기까지 오면 정상 종료 return true; }다음과 같이 수정하였는데, early return을 통해 유효하지 않으면 빨리 종료 시키도록 하여 if-else를 없앴고, order에게 메시지를 보내 묻는 형태로 바꾸어서 더는 getter로 꺼내 쓰지 않도록 해보았다. 후기1주일 만에 강의를 통해 정말 많은 것을 느낄 수 있었다. 이를 통해 내가 아직 더 성장할 여지가 많다는 것도 느꼈고, 공부를 통해 회사 코드를 개선해보면서 후대에 이로움만 남겨줘야 겠다는 강한 동기도 얻었다. 다음 주차도 열심히 해봐야겠다.아자아자 화이팅!
백엔드