박준형
@bak38394306
수강평 작성수
-
평균평점
-
블로그
전체 9#카테고리
- 백엔드
#태그
- 워밍업클럽4기
- 백엔드
- 4주차발자국
- Day18미션
- 맥엔드
- Day16미션
- 3주차발자국
- 2주차발자국
- 1주차발자국
- Day4미션
- Day2미션
![[워밍업 클럽 4기 백엔드] 4주차 발자국](https://cdn.inflearn.com/public/files/blogs/7db66200-3bb5-44e3-9b12-1d89c5493293/4-backend.png?w=260)
2025. 06. 22.
1
[워밍업 클럽 4기 백엔드] 4주차 발자국
4주차에 배운 것들📌Test Double"Test Double"은 테스트 중에 실제 객체를 대체하기 위해 사용하는 객체를 통칭하는 개념으로 다섯 가지 유형이 존재Dummy : 아무 것도 하지 않는 깡통 객체Fake : 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체ex. FakeRepository Stub : 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체 그 외에는 응답하지 않는다.상태 검증(State Verification) : 메서드 호출 후 Stub이 상태가 어떻게 바뀌었는지 검증Mock : 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체행위 검증(Behavior Verification) : 메서드에 파라미터를 넘겨줬을 때, 어떤 값을 반환할지 행위를 검증Spy : Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체 일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있다. 📌 나는 Classicist 일까 Mockist 일까"Classicist VS. Mockist" 강의를 듣기 전까지 Mockist였다. "Mocking을 통해서 단위 테스트로 기능을 테스트했으면 통합 테스트에서는 안해도 될 것 같은데?"처럼 단순하게 생각했다. 그런데 코치님께서 Classicist를 지향하시는 이유를 듣고난 후 다시 생각해보았다. 어쩔 수 없이 Mocking을 해야하는 상황을 제외하고, 각 기능들이 의존 관계를 가질 때는 어떤 예외가 발생할지 모르기 때문에 최대한 프로덕션 환경과 비슷하게 테스트를 하는 것이 좋다고 생각되었다. 그래서 앞으로는 Classicist 방식으로 테스트 하는 것을 지향해야겠다. 📌 그렇다면 Mocking은 언제?💡 Mocking이 유용한 상황은 대표적으로 2가지가 있다.외부 시스템에 의존하는 기능외부 시스템 같이 오류가 발생해도 우리가 통제할 수 없다.테스트할 때마다 비용이 발생하는 기능 ex) 메일 전송 ❓ 만약 한 객체에 특정 기능은 Mocking을 하고싶고, 실제 동작도 테스트하고 싶다면?이럴 땐 Spy 객체를 활용할 수 있다! Mocking을 원하는 특정 기능만 Stubbing을 하고, 나머지는 원본 객체처럼 실제 동작을 테스트할 수 있다.🔗 Mock 관련 어노테이션 정리 📌 Mockito도 BDD 스타일로Mockito.when(mailSendClient.sendEmail(anyString(), anyString())) .thenReturn(true); BDDMockito.given(mailSendClient.sendEmail(anyString(), anyString())) .willReturn(true);기존 Mockito에서는 stubbing을 위한 기능의 메서드명이 when 인데, stubbing 과정은 given 절에서 해야하는 작업이라 메서드명이 부자연스러웠다. 그럴땐 Mockito를 한 번 감싼 BDDMockito를 사용하면 BDD 스타일에 더 자연스럽게 코드를 작성할 수 있다. BDDMockito에서 given 은 Mockito의 when 과 동일한 기능을 한다. 📌한 문단에 한 주제@Test @DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.") void containsStockTypeEx() { // given ProductType[] productTypes = ProductType.values(); for (ProductType productType : productTypes) { if(productType == ProductType.HANDMADE) { // 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(); } } }각 타입들을 모두 검증할 수 있도록 하나의 테스트로 구성하는 것은 단점이 있다.한 문단에 두 가지 이상의 내용이 포함된다.반복문 자체가 여러 논리 구조가 들어가게 하고, 보는 사람의 사고를 복잡하게 한다.💡이럴 경우에는 케이스를 나눠서 테스트를 작성하는 것이 좋고, 이 방식은 지양하자! 📌테스트 환경의 독립성을 보장하자다른 API를 끌고와서 테스트 간 결합도가 생길 수 있다.// given Stock stock1 = Stock.create("001", 2); Stock stock2 = Stock.create("002", 2); stock1.deductQuantity(3); // 예외 : 차감할 재고 상품이 없습니다.테스트 주제와 맞지 않은 given 절에서 예외가 발생하고 있음왜 실패했는지 유추하기 어려운 상황이 발생💡 독립성을 보장하는 방법given 절에서 순수 builder 또는 생성자 기반으로 데이터를 생성팩토리 메서드나 메서드 기반은 지양어떠한 의도를 가지고 필요한 인자만 받거나 내부에 검증 로직이 포함되어 있을 수 있기 때문 📌Test Fixture 구성하기✅Test Fixture : given 절에서 생성한 객체들Fixture : 고정물, 고정되어 있는 물체테스트를 위해 원하는 상태로 고정시킨 일련의 객체💡 공통된 Fixture들을 만들어야 할 때문서로서의 역할을 위해서 given 절에서 생성하는 것이 좋다!@BeforeEach void setUp() { // before method }@BeforeEach 를 사용공유 변수 같이 테스트 간 독립성을 훼손(테스트 간 결합도가 생김)상위 setUp 메서드에서 어떤 데이터를 생성하는지 확인해야 하며 파편화를 유발문서로서의 역할을 할 수 없음 만약 각 테스트 입장에서 봤을 때 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는 경우나 해당 공유 변수를 수정해도 모든 테스트에 영향을 주지 않는 경우는 사용해도 괜찮다.data.sql을 사용하여 테스트 데이터 생성파편화가 발생 (given 절에 데이터가 없으므로 어떤 데이터로 테스트를 하는지 확인하기 불편함)테스트가 커질 수록 또 다른 관리 포인트가 됨builder를 private 메서드로 구성파라미터에 해당 테스트에서 필요한 데이터만 받도록 구성테스트 데이터용 builder를 한 곳에 모아서 사용자바 특성 상 필요한 파라미터가 달라 새로운 builder가 계속 생겨 오히려 복잡도가 상승🚨Test Fixture 클렌징 시 유의 사항deleteAllInBatch()테이블 전체를 벌크성으로 빠르게 삭제할 수 있음외래키 제약 조건로 인해 순서를 잘 고려해야함여러 트랜잭션 경계가 참여하는 @Transactional 의 롤백 기능을 사용하기 어려워 해당 방법을 사용해야 함 deleteAll()해당 테이블을 조회하여 엔티티를 통해 delete(엔티티) 방식으로 데이터를 삭제데이터 개수만큼 쿼리가 발생 → 병목지점 유발자신과 외래키 제약 조건으로 연관된 데이터도 조회하여 삭제 📌@ParameterizedTest여러가지 값에 대한 테스트를 하나의 테스트 코드에 녹이기 위한 방법// Case 1 @ParameterizedTest @CsvSource({"HANDMADE, false", "BOTTLE, true", "BAKERY, true"}) @DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.") void containsStockType4(ProductType productType, boolean expected) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isEqualTo(expected); } // Case 2 private static Stream provideProductTypesForCheckingStockType() { return Stream.of( Arguments.of(ProductType.HANDMADE, false), Arguments.of(ProductType.BOTTLE, true), Arguments.of(ProductType.BAKERY, true) ); } @ParameterizedTest @MethodSource("provideProductTypesForCheckingStockType") @DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.") void containsStockType5(ProductType productType, boolean expected) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isEqualTo(expected); } 📌@DynamicTest환경을 설정하고, 환경에 변화를 중간 중간 검증을 하는 시나리오를 테스트하고싶을 때 사용@TestFactory @DisplayName("재고 차감 시나리오") 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("차감할 재고 상품이 없습니다."); }) ); }💡 List.of() 에 정의된 순서대로 테스트가 동작하며 이전 테스트의 형태를 그대로 유지하기 때문에 테스트 간 결합이 생겨 시나리오를 구성하여 테스트가 가능하다. 📌 테스트 환경 통합하기테스트 환경 통합전에는 전체 테스트(gradle → Tasks → verification → test) 수행 시 총 6번의 스프링 컨텍스트가 로딩되었다. 테스트가 많아질 수록 이는 더 많은 시간을 소모하게 되고, 테스트를 하지 않게 할 수 있다.@ActiveProfiles("test") @SpringBootTest public abstract class IntegrationTestSupport { @MockitoBean protected MailSendClient mailSendClient; } // 통합이 필요한 클래스에서 상속하여 환경 통합 class OrderServiceTest extends IntegrationTestSupportMocking을 하고 있는 클래스를 통합하려면 상위 클래스에 해당 객체를 옮겨주어야 한다. Mock객체가 없는 클래스와 있는 클래스는 다른 환경이라고 인식하여 제대로 통합이 이루어지지 않기 때문이다.이럴 경우 Mock이 필요없는 클래스에도 객체가 들어가는데 이게 싫다면, 테스트를 두 가지 환경으로 분리하여 Mock이 필요한 클래스, 필요하지 않은 클래스로 나누고 필요한 클래스의 상위 클래스에 Mock 객체를 몰아 넣는 형식으로 구성할 수 있다.@WebMvcTest(controllers = { OrderController.class, ProductController.class }) public abstract class ControllerTestSupport { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; @MockitoBean protected OrderService orderService; @MockitoBean protected ProductService productService; } class ProductControllerTest extends ControllerTestSupportController 테스트 환경 통합도 동일하게 진행하면 된다.✅환경을 통합한 후에는 총 2번의 스프링 컨텍스트가 로딩되어 테스트 수행 시간을 단축할 수 있었다. 📌Q. private 메서드의 테스트는 어떻게 하나요?private 메서드는 테스트할 필요가 없다!만약 private 메서드를 테스트 하고 싶은 때에는 “객체를 분리할 시점인가?”에 대해서 생각해봐야 한다.public 메서드(공개 API)를 가지고 있다는 것은 이를 의존하는 테스트코드, 컨트롤러 즉 클라이언트 입장에서는 공개 메서드만 알면 된다. 외부로 노출되지 않는 private 메서드에 대해서는 알 필요가 없다는 것이다. public 메서드의 테스트를 수행하면서 private 메서드의 테스트도 저절로 수행되기 때문이다.해당 private 메서드의 테스트는 꼭 필요하다고 생각되면 객체에 역할에 맞게 분리가 되지 않았을 수 있다. 그러므로 객체를 분리하여 다른 클래스로 만든 뒤 테스트를 구성하는 것이 바람직하다. 📌Q. 테스트에서만 필요한 메서드가 생겼는데 프로덕션 코드에서는 필요 없다면?만들어도 된다! 하지만 보수적으로 접근해야 한다.@Test @DisplayName("신규 상품을 등록한다.") void createProduct() throws Exception { // given ProductCreateRequest request = ProductCreateRequest.builder() .type(ProductType.HANDMADE) .sellingStatus(ProductSellingStatus.SELLING) .name("아메리카노") .price(4000) .build(); }해당 request 객체는 프로덕션 코드에서는 필요가 없다.(@RequestBody 로 외부로부터 값을 받기 때문) 하지만 테스트를 하기 위해서는 꼭 필요하다.객체로서 가져도 되는 기능(빌더, 생성자, 사이즈)이면서 미래에도 충분히 사용이 될 수 있는 기능이라면 만들어도 된다. 📌 학습 테스트?잘 모르는 기능, 라이브러리, 프레임워크를 학습하기 위해 작성하는 테스트여러 테스트 케이스를 스스로 정의하고 검증하는 과정을 통해 보다 구체적인 동작과 기능을 학습할 수 있다.관련 문서만 읽는 것보다 훨씬 재미있게 학습할 수 있다.💡사용하려는 라이브러리를 테스트 코드를 작성하면서 학습하면 팀 내에서도 공유하는 자료로도 활용할 수 있다. 📌 Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원활하게 한다.기본적으로 AsciiDoc을 사용하여 문서를 작성한다.👍장점테스트를 통과해야 문서가 만들어진다. (신뢰도가 높다.)프로덕션 코드에 비침투적이다.👎단점코드 양이 많다.설정이 어렵다.지금까지 API 문서를 위해 Swagger를 사용했는데, Swagger는 어노테이션으로 선언만 해주면 해당 API가 검증되지 않아도, 해당 API의 명세가 생성된다. 그에 반해 Spring REST Docs는 테스트 코드가 통과해야 명세가 생성되므로 안정성을 보장하는 면에서 더 이점이 있다고 생각한다. 그렇지만 명세를 위한 코드의 양이 너무 많고, 작성하기 번거로웠다. 이 문제를 해결하기 위해 자동화할 수 있는 방법을 찾아봐야겠다.미션을 통해 배운 것들📌 Day 16각 레이어가 어떤 특징들을 가지는지 명확하게 알 수 있었고, 적합한 테스트 방법과 Classicist인지 Mockist인지 생각할 수 있는 기회를 준 좋은 미션이였습니다!해당 미션을 수행할 때까지는 Business Layer에서 통합 테스트를 수행 시 Persistence Layer의 기능은 단위 테스트로 검증했으니 Mocking하여 비즈니스 로직만 테스트하는 것이 좋다고 생각했는데, 이후 "Classicist VS. Mockist" 강의를 듣고 해당 부분에 대한 제 생각을 다시 정리할 수 있었습니다. 📌 Day 18다른 강의에서 Mock 관련 기능들을 간단히 배웠는데, 그 때까지는 Mocking을 한다는 것이 정확히 뭔지 잘 이해하지 못했습니다. 그리고 비슷한 어노테이션과 서로 다른 주입법들로 인해서 복잡하고 이해하기 힘들었습니다.이번 강의를 통해 "Mocking을 왜 해야하는지"와 각 어노테이션들을 사용하는 방법과 상황에 대해서 완벽하게 이해할 수 있었습니다. 미션을 통해 같은 Mock 객체를 생성할 때도 @Mock 을 사용해야할 때, @MockitoBean 을 사용해야할 때를 명확하게 알 수 있었고, setUp절에 배치하면 좋은 Fixture 들을 구분하는 방법도 이해할 수 있었습니다.
백엔드
・
워밍업클럽4기
・
백엔드
・
4주차발자국
![[워밍업 클럽 4기 백엔드] 완주 확인용 수료증 제출](https://cdn.inflearn.com/public/files/blogs/0ce377f1-5381-4784-8a4c-7c67c48c6501/4-backend.png?w=260)
2025. 06. 19.
0
[워밍업 클럽 4기 백엔드] 완주 확인용 수료증 제출
![[워밍업 클럽 4기 백엔드] Day 18 미션](https://cdn.inflearn.com/public/files/blogs/7da52638-d039-4b76-b699-f9b38664461d/4-backend.png?w=260)
2025. 06. 18.
0
[워밍업 클럽 4기 백엔드] Day 18 미션
1. Mock 관련 어노테이션 정리하기📌@MockMockito에서 제공하는 어노테이션실제 객체를 생성하지 않고, 가짜 객체를 생성하여 해당 객체의 행위에 대한 반환값 조작 및 메서드 호출을 확인할 수 있다.만약 메서드를 stubbing 하지 않으면 기본값이 반환된다.(String : "", int : 0)단위 테스트를 수행할 때 적합하다.객체 생성 비용이 많이 발생하거나, 기능 수행 비용이 많이 발생하는 경우(외부 API 호출, 메일 전송 등)에 사용하면 테스트의 효용성을 높일 수 있다.📌@MockitoBeanSpring Boot가 제공해주던 @MockBean 이 deprecated되고, Spring 프레임워크에서 기능이 같은 @MockitoBean 을 제공해준다.Spring Context에 가짜 객체를 등록한다.@SpringBootTest 와 같이 사용되어야 한다.Spring DI 로 의존성을 주입해준다.통합 테스트를 수행할 때 적합하다.@Mock 과 기능은 동일하다.📌@SpyMockito에서 제공하는 어노테이션실제 객체를 감싸서 Spy 객체를 만든다.stubbing 하지 않은 메서드는 실제 객체의 메서드의 기능이 작동된다.특정 기능의 반환값을 조작하고 싶다면 stubbing을 꼭 해줘야 한다.일부 동작만 stubbing하고, 나머지는 실제 동작을 테스트하고 싶을 때 유용하다.단위 테스트를 수행할 때 적합하다.📌 @MockitoSpyBeanSpring Boot가 제공해주던 @SpyBean 이 deprecated되고, Spring 프레임워크에서 기능이 같은 @MockitoSpyBean 을 제공해준다.Spring Context에 등록된 기존 빈을 감싸서 Spy 객체로 만든다.@SpringBootTest 와 같이 사용되어야 한다.Spring DI 로 의존성을 주입해준다.통합 테스트를 수행할 때 적합하다.@Spy 와 기능은 동일하다.📌 @InjectMocksMockito에서 제공하는 어노테이션테스트 할 대상 객체를 생성하고, 이 객체에 필요한 의존성 (@Mock 과 @Spy 로 생성된 객체)을 자동으로 주입해준다.2. 각 항목을 적절한 위치에 배치하기@BeforeEach void setUp() { 1-1, 2-1, 3-1. 사용자 생성에 필요한 내용 준비 1-3, 2-3, 3-5. 게시물 생성에 필요한 내용 준비 1-2, 2-2, 3-2. 사용자 생성 1-4, 2-4, 3-6. 게시물 생성 } @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-3. 사용자2 생성에 필요한 내용 준비 3-4. 사용자2 생성 3-7. 사용자1의 댓글 생성에 필요한 내용 준비 3-8. 사용자1의 댓글 생성 // when // then 3-9. 사용자2가 사용자1의 댓글 수정 시도 검증 }💡고려 사항댓글 관련 기능을 테스트하므로 이와 관련 없는 사용자, 게시물 생성을 테스트 코드에서 분리하고, 독립성에도 문제가 없기 때문에 공유 변수로 설정댓글은 테스트의 주체이므로 각 메서드 내부에서 생성하여 테스트 간 독립성을 보장cannotUpdateCommentWhenUserIsNotWriter() 사용자2 데이터는 해당 메서드에서만 사용하기 때문에 분리할 필요가 없다고 판단 사용자1과 다른 사용자2가 있다는 것을 명시적으로 테스트 코드에 보여주어야 무엇을 테스트 하는지 이해를 도울 수 있다고 판단3-9는 예외를 검증하는 코드이므로 assertThatThrownBy() 을 사용할 것으로 생각되어 when, then 절에 배치 출처 : Practical Testing: 실용적인 테스트 가이드
백엔드
・
워밍업클럽4기
・
백엔드
・
Day18미션
![[워밍업 클럽 4기 백엔드] Day 16 미션](https://cdn.inflearn.com/public/files/blogs/30839175-5078-4a65-a38e-7cf30d1cdce2/4-backend.png?w=260)
2025. 06. 16.
0
[워밍업 클럽 4기 백엔드] Day 16 미션
📌 Persistence Layer✅특징Data Access의 역할비즈니스 가공 로직이 포함되어서는 안 된다.Data에 대한 CRUD에만 집중한 레이어이기 때문 작성한 쿼리가 우리의 의도대로 동작하는지 확인하는 단위 테스트의 성격을 가진다. 해당 쿼리가 미래에 어떠한 구현체로 변형될지 모르기 때문에 테스트 코드로 보장해주어야 한다.💡 테스트 방법실제 데이터베이스와 연동하여 작성한 쿼리가 의도대로 동작(저장, 조회, 수정, 삭제 등)하는지 테스트한다.@ActiveProfiles 를 사용하여 실제 애플리케이션 환경과 테스트 환경을 분리하는 것이 좋다.@DataJpaTest 를 사용할 경우 내부에 @Transactional 이 포함되어 있어 테스트 종료 후 롤백된다.@SpringBootTest 를 사용할 경우 @Transactional 을 선언하거나, 클랜징 작업(deleteAllInBatch)을 수행해주어야 한다.📌Business Layer✅특징비즈니스 로직을 구현하는 역할Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.트랜잭션을 보장해야 한다.트랜잭션 : "쪼갤 수 없는 업무 처리의 최소 단위"로 원자성, 일관성, 격리성, 지속성 일명 ACID를 보장해야함 작성한 비즈니스 로직이 의도대로 동작하는지 확인하는 통합 테스트의 성격을 가진다. 💡테스트 방법Business Layer는 Persistence Layer를 의존하므로 이 둘을 통합적으로 테스트한다. Persistence Layer의 테스트를 수행했다면, Persistence Layer는 Mocking하여 비즈니스 로직만 테스트하는 것도 좋다고 생각한다.경계값이 주어져도 기능이 정상적으로 동작하는지 테스트해야 한다. 해피케이스(예상한 반환값)와 예외케이스(예상한 예외 종류)에 관해서도 해당 계층에서 테스트해야 한다.@SpringBootTest 와 @Transactional 을 사용한다면 서비스 클래스에 @Transactional 이 빠지지 않았는지 유의해야한다.📌Presentation Layer✅ 특징외부 세계의 요청을 가장 먼저 받는 계층으로파라미터에 대한 최소한의 검증을 수행넘어온 값들의 검증(validation)이 중요하고, 비즈니스 로직은 필요없음Business Layer에서 비즈니스 로직을 전개하기 전에 값들이 유효한지 검증 💡 테스트 방법Business Layer와 Persistence Layer는 Mocking을 사용하여 정상 동작을 하는 것으로 간주하고 테스트한다.@MockitoBean 을 사용하여 서비스 클래스를 Mocking요청 파라미터에 대한 유효성 검증(필수값, 값의 형식 등) Validation이 정상적으로 작동하는지 테스트한다.@WebMvcTest 을 사용하여 Controller, Filter, Interceptor 와 같은 웹 계층과 관련된 빈만 로드하여 빠르게 테스트할 수 있다.MockMvc를 사용하여 서버를 실행하지 않고 요청을 보내고 응답값을 테스트할 수 있다.MockMvc : Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크로 애플리케이션을 띄우지 않고도 HTTP 요청/응답 흐름을 테스트할 수 있다. 출처 : Practical Testing: 실용적인 테스트 가이드
백엔드
・
워밍업클럽4기
・
맥엔드
・
Day16미션
![[워밍업 클럽 4기 백엔드] 3주차 발자국](https://cdn.inflearn.com/public/files/blogs/0d39181b-bd6c-4d9d-b3d8-b79bf6b902d8/4-backend.png?w=260)
2025. 06. 14.
1
[워밍업 클럽 4기 백엔드] 3주차 발자국
3주차에 배운 것들📌 Layered Architecture(레이어드 아키텍처) 테스트💡관심사를 분리하여 계층을 나누고, 책임을 나누어 유지보수에 용이하게 하는 방법테스트 하기 복잡해 보이지만, 무엇을 어떻게 테스트 할 지 알아보자!📌 Persistence LayerData Access의 역할비즈니스 가공 로직이 포함되어서는 안 된다.Data에 대한 CRUD에만 집중한 레이어이기 때문@Test @DisplayName("상품번호 리스트로 상품들을 조회한다.") void findAllByProductNumberIn() { // 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.findAllByProductNumberIn(List.of("001", "002")); // then assertThat(products).hasSize(2) .extracting("productNumber", "name", "sellingStatus") .containsExactlyInAnyOrder( tuple("001", "아메리카노", SELLING), tuple("002", "카페라떼", HOLD) ); }✅특징작성한 쿼리가 우리의 의도대로 동작하는지 확인하는 단위 테스트의 성격을 가진다. 해당 쿼리가 미래에 어떠한 구현체로 변형될지 모르기 때문에 테스트 코드로 보장해주어야 한다. 📌 Business Layer비즈니스 로직을 구현하는 역할Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.트랜잭션을 보장해야 한다.트랜잭션 : "쪼갤 수 없는 업무 처리의 최소 단위"로 원자성, 일관성, 격리성, 지속성 일명 ACID를 보장해야함@Test @DisplayName("주문번호 리스트를 받아 주문을 생성한다.") void createOrderTest() { // given Product product1 = createProduct(HANDMADE, "001", 1000); Product product2 = createProduct(HANDMADE, "002", 3000); Product product3 = createProduct(HANDMADE, "003", 5000); productRepository.saveAll(List.of(product1, product2, product3)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001", "002")) .build(); // when LocalDateTime registeredDateTime = LocalDateTime.now(); OrderResponse orderResponse = orderService.createOrder(request.toServiceRequest(), registeredDateTime); // then assertThat(orderResponse.getId()).isNotNull(); assertThat(orderResponse) .extracting("registeredDateTime", "totalPrice") .contains(registeredDateTime, 4000); assertThat(orderResponse.getProducts()).hasSize(2) .extracting("productNumber", "price") .containsExactlyInAnyOrder( tuple("001", 1000), tuple("002", 3000) ); }✅특징작성한 비즈니스 로직이 의도대로 동작하는지 확인하는 통합 테스트의 성격을 가진다. Business Layer는 Persistence Layer를 의존하므로 이 둘을 통합적으로 테스트한다.📌Presentation Layer외부 세계의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증을 수행넘어온 값들의 검증(validation)이 중요 → 비즈니스 로직은 필요없음Business Layer에서 비즈니스 로직을 전개하기 전에 값들이 유효한지 검증 class ProductControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @MockitoBean private ProductService productService; @Test @DisplayName("신규 상품을 등록한다.") void createProduct() throws Exception { // given ProductCreateRequest request = ProductCreateRequest.builder() .type(ProductType.HANDMADE) .sellingStatus(ProductSellingStatus.SELLING) .name("아메리카노") .price(4000) .build(); // when // then // body 에 값을 넣어서 직렬화와 역직렬화가 발생 mockMvc.perform( post("/api/v1/products/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ) .andDo(print()) .andExpect(status().isOk()); } }MockMvc : Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크objectMapper : Java 객체를 JSON 문자열로 변환(직렬화), JSON 문자열을 Java 객체로 변환(역직렬화)을 수행content(objectMapper.writeValueAsString(request)) -> 직렬화controller의 @RequestBody 에 의해 역직렬화 수행✅ 특징Business Layer와 Persistence Layer는 Mocking을 사용하여 정상 동작을 하는 것으로 간주하고 테스트한다.@MockitoBean 을 사용하여 ProductService를 Mocking애플리케이션을 띄우지 않고도 HTTP 요청/응답 흐름을 테스트할 수 있다.Validation이 정상적으로 작동하는지 테스트할 수 있다.📌 @DataJpaTest vs @SpringBootTest가장 큰 차이점은 @Transactional 유무의 차이이다. @DataJpaTest 는 트랜잭션을 지원해서 테스트 종료 후 롤백을 실행하는 반면, @SpringBootTest 는 트랜잭션을 지원하지 않아 테스트 간에 간섭을 받지 않기 위해서는 클랜징 작업(deleteAllInBatch)이 필요하다. 추가적으로 @DataJpaTest 는 @Entity, @Repository만 Bean으로 등록하기 때문에 Persistence Layer 테스트를 할 때만 사용하는 것이 바람직하다. ❓ 그렇다면 @SpringBootTest 과 @Transactional 를 같이 사용하면 되는거 아닐까?둘이 같이 사용하게 되면 테스트에서 롤백을 해줘서 편하지만 주의해서 사용해야한다. 만약 service(Business Layer)에서 @Transactional 을 빼먹어도, 테스트에서 사용한 트랜잭션으로 인해 테스트는 정상 통과되고, 기능에 문제가 없다고 판단할 경우가 생길 수도 있기 때문이다.그렇기 때문에 편리함을 위해 사용해도 되지만 service에 @Transactional 이 빠지지 않았는지, 실수할 수도 있다는 인지를 하면서 사용하는 것이 좋다.📌 Validationorg.springframework.boot:spring-boot-starter-validation 을 사용하여 클라이언트의 요청 값에 대한 검증을 할 수 있다.@Getter @NoArgsConstructor public class ProductCreateRequest { @NotNull(message = "상품 타입은 필수입니다.") private ProductType type; @NotNull(message = "상품 판매 상태는 필수입니다.") private ProductSellingStatus sellingStatus; @NotBlank(message = "상품 이름은 필수입니다.") private String name; @Positive(message = "상품 가격은 양수여야 합니다.") private int price; } . . . @PostMapping("/api/v1/products/new") public ApiResponse createProduct( @Valid @RequestBody ProductCreateRequest request ) { return ApiResponse.ok(productService.createProduct(request)); }💡 클라이언트의 요청을 받는 시점에 @Valid 가 적용된 객체를 검증할 수 있다.String 검증@NotNull : 빈 문자열(””), 공백 문자열 통과(” “)@NotEmpty : 공백 문자열 통과(” “)@NotBlank : 둘 다 통과할 수 없음 → 문자가 필수로 있어야 함❓ 상품 이름은 20자 제한이 주어진다면controller에서 valid를 사용해서 해야하는지 고민유효한 문자열이라면 가져야하는 합당한 검증과 도메인 정책에 맞는 특수한 형태의 검증을 구분할 줄 아는 시야를 길러야 한다. ✅ 20자 제한은 서비스 계층, 생성자로 생성을 할 때 검증을 할 수 있다.미션을 통해 배운 것들📌 Day 11스터디카페 프로젝트의 테스트 코드를 작성하면서 많은 고민들을 통해 배울 수 있었다. 테스트를 통해서 기능을 검증하는 것을 넘어서, '이 테스트를 왜 해야 하는지'에 초점을 두고 이번 미션을 진행했다. '안다고 생각하는 것'과 '아는 것' 의 차이는 정말 크구나 다시 한번 느낄 수 있었다. 강의를 보면서 강사님의 코드를 따라 칠때는 "생각보다 쉬운데?"라는 생각을 했다. 하지만 내 스스로 어떤 테스트를 해야 적합한지 생각하고, 이해하기 쉬운 DisplayName을 생각하는 것도 어렵게 느껴졌다. 실제로 "사물함을 구매할 수 없다" vs "사물함을 사용할 수 없다" 이 두 문장을 가지고 20분은 넘게 고민도 하고, 열심히 짜둔 테스트 코드가 적합하지 않은 것 같아서 싹 지우고 다시 작성하기도 했다.테스트를 직접 작성하면서 "나는 지금까지 무책임한 개발을 하고 있었구나"라는 생각이 들었다. 실무에서의 테스트는 이 예제보다 더 복잡하고, 생각할 것도 많을텐데 개발을 하면서 테스트 코드 작성 경험을 하지 못한 것은 참 아쉽다는 생각도 들었다. 그래도 이번 미션을 통해 그 감을 조금 익힐 수 있었고, 부족한 부분과 보완해야 할 점을 알 수 있는 좋은 기회가 되었다. 출처: Readable Code: 읽기 좋은 코드를 작성하는 사고법
백엔드
・
워밍업클럽4기
・
백엔드
・
3주차발자국
![[워밍업 클럽 4기 백엔드] 2주차 발자국](https://cdn.inflearn.com/public/files/blogs/5d36a0e5-96b0-47f6-9223-528bd1c1255c/4-backend.png?w=260)
2025. 06. 08.
1
[워밍업 클럽 4기 백엔드] 2주차 발자국
2주차에 배운 것들 📌자동화된 테스트를 만들자지금까지 개발을 하면서 테스트 코드를 만들어서 검증을 한적이 한번도 없었다. 강의에서도 언급된 콘솔을 통한 검증 방법을 통해 원하는 값이 잘 나오는지 직접 눈으로 확인했다.@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와 AssertJ를 사용하여 자동 테스트를 만들어보자.단위 테스트 : 작은 코드 단위(클래스 or 메서드)를 독립적으로 검증하는 테스트로 검증 속도가 빠르고 안정적이다. @Test void add() { CafeKiosk cafeKiosk = new CafeKiosk(); cafeKiosk.add(new Americano()); assertThat(cafeKiosk.getBeverages()).hasSize(1); assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노"); }이렇게 테스트 코드를 구성하게 되면 테스트 최종 단계에서 사람의 개입 없이 검증할 수 있고, 기능이 정상적으로 동작했을 때의 결과를 통해 기능을 검증하므로 어떤 것을 검증하는지 명확하다.📌 테스트하기 어려운 영역을 분리하자public Order createOrder() { LocalTime currentTime = LocalDateTime.now().toLocalTime(); if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) { throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요."); } return new Order(beverages); }이 코드는 Order 객체를 생성하는 코드이다. 코드 내부에서 현재 시간을 기준으로 주문 시간이 아닌 경우 예외를 던지도록 해두었다. 이 코드를 단위 테스트를 통해 검증할 수 있을까?그럴 수 없다. 테스트 코드를 실행하는 시점에 따라서 결과가 계속 달라지기 때문에 테스트의 정합성을 보장할 수 없기 때문이다. 그렇다면 어떻게 테스트를 해야할까?💡시간을 매개변수를 통해서 외부에서 받으면 된다!public Order createOrder(LocalDateTime currentDateTime) { LocalTime currentTime = currentDateTime.toLocalTime(); if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) { throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요."); } return new Order(currentDateTime, beverages); }@Test void createOrderWithCurrentTime() { CafeKiosk cafeKiosk = new CafeKiosk(); Americano americano = new Americano(); cafeKiosk.add(americano); // 시작 시간의 경계값 Order order = cafeKiosk.createOrder(LocalDateTime.of(2025, 5, 28, 10, 0)); assertThat(order.getBeverages()).hasSize(1); assertThat(order.getBeverages().get(0).getName()).isEqualTo("아메리카노"); }이 방식을 통해 시간을 외부에서 입력 받으면 테스트를 실행하는 시점과 관계 없이 우리가 원하는 값으로 테스트를 진행할 수 있게 된다. 게다가 영업 시작 시간(10시), 영업 종료 시간(22시)과 같은 경계값 테스트도 수행할 수 있어 유연한 테스트 코드를 작성할 수 있게 된다.경계값 : 범위(이상, 이하, 초과, 미만), 구간, 날짜 등물건 5개 이상 구매 시 할인 적용물건을 5개로 설정하고, 할인이 적용되는지 확인물건을 4개로 설정하고, 할인이 적용되지 않는지 확인그렇다면 구현 단계에서 이런 부분까지 신경쓸 수 있을까? TDD를 통해 기능 구현 전 테스트를 먼저 작성하여 테스트가 어려운 영역을 인지할 수 있고, 테스트에 유연한 코드를 작성할 수 있다!📌 TDD를 통해 테스트에 유연한 코드를 작성하자TDD란? 프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론🔄TDD 사이클실패하는 테스트를 작성하여 빨간불 보기 -> 테스트가 아직 성공하지 않았다고 명시적으로 표현주먹구구식으로 엉터리 구현을 해서라도 테스트를 통과하는 기능 구현하기 -> '기능 구현'에 초점을 두고 코드 작성초록불을 유지하면서 구현 코드 개선하기 -> 기능에 대한 검증은 끝났으므로 그 틀안에서 '읽기 좋은 코드'에 초점을 두고 리팩토링 🚨선 기능 구현, 후 테스트 작성의 문제점테스트 코드 자체를 까먹고 작성하지 않을 수 있음특정 테스트 케이스(해피 케이스)만 검증할 가능성잘못된 구현을 다소 늦게 발견할 가능성 💡 TDD는 빠른 피드백을 받을 수 있다!테스트에 유연하며 유지보수가 쉬운 코드로 구현할 수 있게 한다.위에서 언급한 테스트하기 어려운 영역을 미리 생각하여 구현 단계에서 해당 영역을 의식할 수 있음발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백이 가능하다.과감한 리팩토링이 가능하다.초록불을 보았으면, 기능에 대한 검증이 된 테스트 코드로 인해 리팩토링 중 잘못된 구현을 해도 리팩토링이 잘못되고 있다는 것을 빠르게 알 수 있음📌 테스트는 문서다!내가 작성한 코드와 문서가 다른 팀원에게는 어떻게 비춰질지 생각하면서 작성하는 것이 중요하다!프로덕션 기능을 설명한다.다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완한다.어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서, 모두의 자산으로 공유할 수 있다. 그렇다면 이 문서를 어떻게 잘 작성할 수 있을까❓ 💡@DisplayName 을 섬세하게 작성하자음료 1개 추가 테스트 ❌ -> ~'테스트' 를 지양음료를 1개 추가할 수 있다. ⭕ -> 명사의 나열보다는 문장으로 구성음료를 1개 추가하면 주문 목록에 담긴다. ⭕ -> 테스트 행위에 대한 결과까지 기술하면 다른 팀원 또는 미래의 내가 봤을 때 이해하기 쉬워짐 특정 시간 이전에 주문을 생성하면 실패한다. ❌ -> 보는 사람 입장에서 '특정 시간'이 언제인지 불분명함영업 시작 시간 이전에는 주문을 생성할 수 없다. ⭕ -> 도메인 용어인 '영업 시작 시간'을 사용하여 한층 추상화된 내용을 담아 테스트를 진행할 명확한 시간대를 알 수 있게됨메서드 자체의 관점보다 도메인 정책에 관점을 두고 작성테스트 현상을 중점으로 기술하기 보다, "어떤 행동이 어떤 결과를 만든다" 식의 표현으로 작성 💡 BDD 스타일로 작성하자BDD(Behavior Driven Development)는 TDD에서 파생된 개발 방법으로 함수 단위의 테스트에 집중하기보다, 시나리오에 기반한 테스트케이스 자체에 집중하여 테스트하는 방법을 말한다. 이 방법은 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준을 권장한다.Given : 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등) → 어떤 환경에서When : 시나리오 행동 진행 → 어떤 행동을 진행했을 때Then : 시나리오 진행에 대한 결과 명시, 검증 → 어떤 상태 변화가 일어난다'음료를 1개 추가하면 주문 목록에 담긴다' 테스트를 BDD 스타일로 구성해보자@Test @DisplayName("음료 1개 추가하면 주문 목록에 담긴다.") void add() { // given CafeKiosk cafeKiosk = new CafeKiosk(); Americano americano = new Americano(); // when cafeKiosk.add(americano); // then assertThat(cafeKiosk.getBeverages()).hasSize(1); assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노"); }✅ 이 방법을 통해 @DisplayName 을 명확하게 작성할 수 있게 된다!미션을 통해 배운 것들📌 Day 7여유롭게 리팩토링을 진행하기 위해서 진도를 미리 나간 후 3일 동안 리팩토링을 진행했다. 처음 시작했을 때 어떤 것부터 리팩토링을 해야할지 막막했었다. 그리고 처음부터 '완벽한 리팩토링'을 해야한다고 생각했기 때문에 고려할 요소들을 한번에 적용하려고 했다. 1시간 동안 어떻게 할지 생각만 했고, 생각만 하고 있으면 아무것도 안되기에 한 가지 요소만 집중해서 차근 차근 바꿔나갔다.한 가지 요소에만 집중하니까 확실히 수월하게 리팩토링을 진행할 수 있었다. 한 가지 요소를 해결하니 전에는 보이지 않던 문제들이 보이기 시작했고, 다른 요소들을 적용할 수 있는 방법들이 떠올랐다. 이를 통해 한 번에 해결하려는 욕심을 버리고, 단계적으로 나아가는 방법에 대해서 깨닫게 되었다.그리고 강의를 통해 이론을 배우고, 코드를 따라 치는 것만 해서는 내용을 제대로 '체득'하는 것이 어렵다는 것을 알게 되었다. 강의를 수강하고, 이제부터는 읽기 좋은 코드를 작성할 수 있을 것 같다는 자신감이 생겼는데 직접 코드에 적용시키려니 잘 되지 않았다. '읽기 좋은 코드를 작성하는 능력'은 스스로 생각하고, 많은 시행착오를 거쳐야 비로소 얻을 수 있는 것이구나 깨닫게 되었다.📌 중간 점검중간 점검에는 다른 사람들의 코드와 고민했던 점을 볼 수 있어서 좋았다. 같은 코드, 같은 언어, 같은 기능을 리팩토링 하는데도 접근 방식과 구현 방법이 다르고, 내가 생각하지 못한 것들을 보고 많이 배울 수 있었다. 그 중에서 코치님께서 좋은 접근 방법이라고 하신 것들은 따로 작성해두었다.그리고 직접 해주신 코드 리뷰 덕분에 내가 작성한 코드가 다른 사람의 관점에서는 어떻게 보이는지 알 수 있었다. 잘 한 부분과 부족한 부분을 알려주셔서 잘 한 부분은 더 발전시키고, 부족한 부분은 보완하는 과정을 통해 더 성장할 수 있는 좋은 기회였다고 생각한다. 출처: Readable Code: 읽기 좋은 코드를 작성하는 사고법
백엔드
・
워밍업클럽4기
・
백엔드
・
2주차발자국
![[워밍업 클럽 4기 백엔드] 1주차 발자국](https://cdn.inflearn.com/public/main/blog/default_thumbnail.png?w=260)
2025. 06. 01.
1
[워밍업 클럽 4기 백엔드] 1주차 발자국
1주차에 배운 것들📌 추상에 대해서강의를 듣기 전까지는 '추상'에 대해서 사전적으로만 알고 있었다. 사실 사전적 정의를 알았던 것이지 "객체 지향에 추상을 어떻게 적용하는 거지?"라는 의문과 구체와 추상의 경계가 모호했다. 하지만 강의를 통해 나는 이미 많은 구체들을 추상화를 시키며 개발했다는 것을 알게 되었다. 결국 추상화를 잘 하기 위해서는 이론을 공부하듯 암기하는 것이 아니라 고민을 통해 익숙해져야 하는 것 같았다. 📌 알고 있지만 무시했던 것들프로젝트를 하다보면 당장의 기술 구현을 위해 변수와 메서드의 이름을 지금 당장만 이해할 정도로 짓고 넘어갔다. 게다가 한 메서드에 모든 코드가 들어가 있어 매우 복잡했다. 메서드를 분리하고, 이름을 잘 지어야 나중에 코드를 봐도 잘 이해할 수 있다는 것은 알고 있었지만, 해커톤과 공모전 같이 제출이 끝난 후에는 리팩토링 하지 않을 개발만 하다보니 현재를 위한 개발만 하게 되었다. 결국 사용자를 모집하고, 실제 운영하는 서비스를 개발할 때도 그 습관으로 개발을 하니 리팩토링을 해야 할 때가 와서야 후회하게 되었다. 강의를 통해서 이름을 짓는 방식과 메서드를 분리하는 경계를 어느정도 익혔으니 이제부터라도 미래를 위한 개발을 해야겠다고 생각했다. 📌 객체의 책임을 다하게 하자어느 글에서 'getter를 사용하지 말자'라는 문장을 본적이 있는데 그 때는 "getter가 없으면 객체가 가진 값을 어떻게 검증하지?"라는 생각을 하고 그냥 무시해버렸다. 그때는 객체를 단순히 값을 가진 덩어리로 취급을 했었던 것이다. 객체 지향 언어를 사용해 개발을 하지만 언어의 강점을 너무 무시하며 개발을 하고 있었다. 객체에게 검증을 위임하는 메서드를 만들고, 그 메서드를 통해 의미를 만드는 것이 강력한 기능을 한다는 것을 알게 되었다. 📌 SOLID에 대해서SOLID를 의식하면서 개발을 하지 못했는데 강의를 통해 코드를 직접 쳐보면서 감을 잡을 수 있었다. 특히 단일 책임 원칙에 대해서는 너무 자주 위반한 것을 깨닫게 되어 이 부분을 더 신경쓰며 개발을 해야겠다. 📌 Enum 활용법열거형에 인터페이스를 구현하면 열거형 상수에 메서드를 재정의하여 활용할 수 있는 방법을 알게 되었다.미션을 통해 배운 것들📌 Day 2 미션현실 세계에 있는 추상과 구체들을 생각하고, 그것을 글로 옮기는 작업을 통해서 '추상화'하는 방법에 대해서 감을 잡을 수 있었다. 그리고 추상이 무엇인지 잘 와닿지 않았는데, 이 과정 덕분에 '추상'이란 단어를 이해할 수 있게 되었고, 개발에서 추상의 중요성을 알게 되었다. 📌 Day 4 미션주어진 코드를 리팩토링 하면서 강의에서 배운 '사고의 depth 줄이기', '중첩 분기문을 제거한 early return', '부정어구 사용', '객체에 검증 위임'과 같은 내용을 녹이려고 노력한 결과 이전 코드보다 확실히 깔끔해지고 읽기 쉬운 코드로 리팩토링할 수 있었다. SOLID에 대해 정리하는 과정에서 각 원칙이 가진 의미와 목표하는 바를 명확하게 이해할 수 있었고, 이를 암기하는 것이 아닌 이해하는게 중요하다는 것을 깨닫게 되었다. 출처: Readable Code: 읽기 좋은 코드를 작성하는 사고법
백엔드
・
워밍업클럽4기
・
백엔드
・
1주차발자국
![[워밍업 클럽 4기 백엔드] Day 4 미션](https://cdn.inflearn.com/public/main/blog/default_thumbnail.png?w=260)
2025. 05. 29.
0
[워밍업 클럽 4기 백엔드] Day 4 미션
1. 읽기 좋은 코드로 리팩토링 해봅시다.😫 Beforepublic 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; }😄 Afterpublic boolean validateOrder(Order order) { if(order.isEmptyItems()) { log.info("주문 항목이 없습니다."); return false; } if(order.isInvalidTotalPrice()) { log.info("올바르지 않은 총 가격입니다."); return false; } if (order.hasNotCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } return true; }✅ 불필요한 중첩 분기문 제거 & early return사고의 흐름을 복잡하게 하는 중첩 분기문을 제거하여 사고의 depth를 줄임early return을 활용하여 각 분기문에 대한 맥락만 파악할 수 있도록 함✅ 부정 연산자보단 부정어구 사용!order.hasCustomerInfo() 같이 가독성이 떨어지는 부정 연산자 보단, order.hasNotCustomerInfo() 처럼 문장 자체에 부정의 의미를 담는 방식을 채택하여 이해를 도움✅ getter를 통한 검증보단 해당 객체에 검증 위임order 객체가 가진 값을 직접 꺼내와서 검증하는 것보다, 객체에게 메시지를 보내어 검증을 요구기존 getter를 통해 값을 가져와서 하는 검증보다, 메서드명에서 무엇을 검증하려고 하는지 보여줄 수 있어 기능에 대한 이해를 빠르게 할 수 있음 2. SOLID에 대하여 자신만의 언어로 정리해 봅시다.📌Single Responsibility Principle(단일 책임 원칙)하나의 클래스가 하나의 책임만 가지도록 설계하자!객체간의 책임을 명확하게 나누어 설계해야 한다.객체의 관심사를 분리하여 하나의 관심사만 가지도록 하여 명확한 책임을 부여한다.우리가 설계한 객체가 하나의 책임만을 가지고 있는지 점검하며 개발한다. 책임을 제대로 나누지 못하면 리팩토링 과정에서 엉뚱한 클래스를 수정 해야하는 상황이 발생한다. 📌Open-Closed Principle(개방-폐쇄 원칙)확장에는 열려 있고, 수정에는 닫혀 있어야 한다!요구 사항이 생겨도 기존 코드를 과도하게 변경하지 않고 확장할 수 있어야 한다.인터페이스를 활용하여 확장 가능성이 있는 기능은 추상화를 잘해야 한다. 📌Liskov Substitution Principle(리스코프 치환 원칙)자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.상속 구조로 객체를 설계할 때, 자식은 @Override 를 통해 부모의 기능을 훼손해서는 안된다.항상 부모의 자리에 자식이 위치했을 때도 생각하며 자식을 설계해야 한다.만약 자식 클래스의 기능을 사용할 때 타입 체크를 해야 하는 상황이라면 설계를 다시 하자. 📌Interface Segregation Principle(인터페이스 분리 원칙)클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다!인테페이스를 잘게 쪼개어 꼭 필요한 기능만 구현할 수 있도록 설계한다.모든 기능을 추상화한 인터페이스로 인해, 구현 클래스에서 구현을 하기 힘들거나 예외 던지기 같은 상황을 유발하면 안된다. 📌Dependency Inversion Principle(의존성 역전 원칙)상위 수준의 모듈은 하위 수준의 모듈에의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다!저수준 모듈 : 구체에 가까운 모듈고수준 모듈 : 추상화 레벨이 높은 모듈추상화에만 의존하여 런타임 시점에 어떤 구현체가 들어와도 추상화된 기능에만 의존할 수 있도록 한다.출처: Readable Code: 읽기 좋은 코드를 작성하는 사고법
백엔드
・
워밍업클럽4기
・
백엔드
・
Day4미션
![[워밍업 클럽 4기 백엔드] Day 2 미션](https://cdn.inflearn.com/public/main/blog/default_thumbnail.png?w=260)
2025. 05. 27.
0
[워밍업 클럽 4기 백엔드] Day 2 미션
📌 "추상과 구체" 강의를 듣고, 생각나는 추상과 구체의 예시가 있다면 한번 3~5문장 정도로 적어봅시다."치킨을 먹는다"밀가루 반죽에 버무린 닭고기를 뜨거운 기름에 넣는다.이것이 "바사삭" 소리가 날 때까지 기름 속에 넣어둔다.스며든 기름을 털어내고 그릇에 잘 담는다.치아를 이용하여 잘게 쪼개 목구멍을 통해 넘긴다.강의를 듣기 전까지 추상이 무엇인지 잘 와닿지 않았는데, 덕분에 '추상'이란 단어를 이해할 수 있게 되었고, 개발에서 추상의 중요성을 알게 되었다!
백엔드
・
워밍업클럽4기
・
백엔드
・
Day2미션




