블로그
전체 4#카테고리
- 백엔드
2025. 03. 28.
0
워밍업 클럽 3기 BE 클린코드&테스트 - 3주차 발자국
회고3주차 발자국을 4주차 작성 이후에 쓰게 됐습니다. 제가 놓치고 작성하지 못한터라 아쉽게만 여기고 있었는데 기회를 주셔서 감사합니다.이번 3주차 수업은 실습에 높은 비중을 두고 진행됐습니다. 우빈님의 잘 정리된 프레젠테이션과 함께하는 이론 수업도 좋지만 실습은 실습만의 매력이 있습니다. 가령 이론 수업에서 배웠던 것들이 실제로 어떤 식으로 활용되는지 좀 더 구체적으로 알 수 있어서 좋았습니다. 이론으로 시작하는 추상적인 내용들이 실습을 통해 바로바로 구체적으로 변환되는 과정은 정말 유익하네요.특히 테스트 코드에 대해선 정말 감동이었습니다. 예전에 테스트 코드를 작성을 시도해본 적이 있었습니다만, 적은 리퍼런스, 어려운 테스트 프레임워크 앞에서 좌절한 경험이 있었습니다. 우빈님의 강의를 들으며 다시 시도해보니, 이렇게 쉬웠던 일인가 싶을 정도로 테스트 코드에 자신감이 붙고 있습니다. 강의 내용 요약레이어드 아키텍처와 테스트통합 테스트여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트일반적으로 작은 범위의 단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없다보통 풍부한 단위 테스트 & 시나리오 단위 통합 테스트로 진행 Library vs Framework라이브러리란?내 코드가 주체가 된다다른 기능이 필요할 때 외부에서 가져오는 코드를 라이브러리라고 한다프레임워크이미 프레임이 있다이미 동작할 수 있는 환경이 구성되어 있고 내 코드가 환경에 추가된다Spring의 특징IoC (Inversion of Control)스프링만의 개념은 아니다(다른 프레임워크들에서도 통용되고 있다)DI (Dependency Injection)AOP (Aspect Oriented Programming)JPA (Java Persistence API)ORM의 일종이다Java 진영의 ORM 기술 표준인터페이스여러 구현체가 있고 대표적으로 Hibernate가 있다반복적인 CRUD 쿼리를 생성 및 실행해주고 여러 부가 기능들을 제공한다편리하지만 쿼리를 직접 작성하지 않기 때문에 SQL의 내부 원리를 이해하고 있어야 한다Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA 제공QueryDSL과 조합하여 많이 사용한다 (타입체크, 동적쿼리)ORM객체 지향 패러다임과 관계형 DB 패러다임의 불일치 해소이전에는 개발자가 객체의 데이터를 각각 매핑하여 DB에 저장 및 조회ORM을 사용함으로써 개발자는 단순 작업이 줄고 비즈니스 로직 작성에 집중하게 된다 Persistence LayerData Access 의 역할비즈니스 가공 로직이 포함되어서는 안된다Data에 대한 CRUD에만 집중한 레이어Business Layer비즈니스 로직을 구현하는 역할Persistence Layer와의 상호 작용을 통해 비즈니스 로직 전개트랜잭션을 보장해야 한다Presentation Layer외부 세계의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증을 수행한다 미션Day 11미션 설명[Readable Code] 강의의 두 프로젝트(지뢰찾기, 스터디카페) 중 하나를 골라, 단위 테스트를 작성해 봅시다. 조건은 아래와 같습니다.각 프로젝트 모두 강의 중에 작성한 tobe패키지 코드를 기준으로 함 (lesson 6-4가 가장 마지막 버전)3개 이상의 서로 다른 클래스 & 총 7개 이상의 테스트 작성 (시간이 된다면 더 많이 작성해보면 좋겠죠?)단, 같은 인터페이스를 구현하고 있는 구현체들은 1개 클래스로 간주한다.무엇을 테스트하고자 했는지를 잘 나타낸 @DisplayName 작성하기BDD(given/when/then) 스타일 따르기 (주석으로 표기)나의 답class StudyCafePassTypeTest { @DisplayName("시간 단위 이용권은 라커 타입이 아니다.") @Test void HourlyPassTypeIsNotLockerType() { // given StudyCafePassType passType = StudyCafePassType.HOURLY; // when boolean isLockerType = passType.isLockerType(); boolean isNotLockerType = passType.isNotLockerType(); // then assertThat(isLockerType).isFalse(); assertThat(isNotLockerType).isTrue(); } @DisplayName("주 단위 이용권은 라커 타입이 아니다.") @Test void WeeklyPassTypeIsNotLockerType() { // given StudyCafePassType passType = StudyCafePassType.WEEKLY; // when boolean isLockerType = passType.isLockerType(); boolean isNotLockerType = passType.isNotLockerType(); // then assertThat(isLockerType).isFalse(); assertThat(isNotLockerType).isTrue(); } @DisplayName("1인 고정석은 라커 타입이다.") @Test void FixedPassTypeIsLockerType() { // given StudyCafePassType passType = StudyCafePassType.FIXED; // when boolean result1 = passType.isLockerType(); boolean result2 = passType.isNotLockerType(); // then assertThat(result1).isTrue(); assertThat(result2).isFalse(); } }class StudyCafeSeatPassTest { @DisplayName("인스턴스 생성 시 주입했던 passType, duration, price을 그대로 반환한다.") @Test void get() { // given StudyCafePassType passType1 = StudyCafePassType.HOURLY; int duration1 = 2; int price1 = 6500; double discountRate1 = 0.0; StudyCafeSeatPass studyCafeSeatPass = StudyCafeSeatPass.of(passType1, duration1, price1, discountRate1); // when StudyCafePassType passType2 = studyCafeSeatPass.getPassType(); int duration2 = studyCafeSeatPass.getDuration(); int price2 = studyCafeSeatPass.getPrice(); // then assertThat(passType1).isEqualTo(passType2); assertThat(duration1).isEqualTo(duration2); assertThat(price1).isEqualTo(price2); } @DisplayName("이용권 타입과 이용 기간이 좌석 이용권과 같은 라커 이용권일 경우 true를 반환한다.") @Test void isSameDurationType() { // given StudyCafePassType passType = StudyCafePassType.FIXED; int duration = 4; int price = 250000; double discountRate = 0.1; StudyCafeSeatPass studyCafeSeatPass = StudyCafeSeatPass.of(passType, duration, price, discountRate); StudyCafeLockerPass studyCafeLockerPass = StudyCafeLockerPass.of(passType, duration, price); // when boolean isSameDurationType = studyCafeSeatPass.isSameDurationType(studyCafeLockerPass); // then assertThat(isSameDurationType).isTrue(); } @DisplayName("이용권의 할인율이 적용된 가격을 반환한다.") @Test void test() { // given StudyCafePassType passType = StudyCafePassType.FIXED; int duration = 4; int price = 250000; double discountRate = 0.1; int expectedDiscountPrice = (int) (price * discountRate); StudyCafeSeatPass studyCafeSeatPass = StudyCafeSeatPass.of(passType, duration, price, discountRate); // when int discountPrice = studyCafeSeatPass.getDiscountPrice(); // then assertThat(discountPrice).isEqualTo(expectedDiscountPrice); } }class StudyCafeLockerPassTest { @DisplayName("인스턴스 생성 시 주입했던 passType, duration, price을 그대로 반환한다.") @Test void get() { // given StudyCafePassType passType1 = StudyCafePassType.HOURLY; int duration1 = 2; int price1 = 6500; StudyCafeLockerPass studyCafeLockerPass = StudyCafeLockerPass.of(passType1, duration1, price1); // when StudyCafePassType passType2 = studyCafeLockerPass.getPassType(); int duration2 = studyCafeLockerPass.getDuration(); int price2 = studyCafeLockerPass.getPrice(); // then assertThat(passType1).isEqualTo(passType2); assertThat(duration1).isEqualTo(duration2); assertThat(price1).isEqualTo(price2); } }
2025. 03. 28.
0
워밍업 클럽 3기 BE 클린코드&테스트 - 4주차 발자국
회고짧지만 긴 한달 간의 워밍업 클럽도 이제 막을 내립니다. 시작하기 직전까지만 해도 저는 워밍업 클럽 3기가 이렇게 혹독할거라곤 생각하지 못했습니다. 그저 산책 같을 거라 지레짐작했던 그 실체는 혹독한 마라톤이었습니다. 하루에 대략 하나의 섹션, 평균적으론 두시간 정도인 수업과 미션들에 반나절을 할애할 줄 누가 알았을까요.한눈 팔 새도 없이 진도 따라 수업을 따라가다보니 어느덧 끝이 왔습니다. 무척 어렵고, 낯선 내용들이라 배울 때는 머리 속에 애써 넣어도 다 튕겨나가는 기분이라 아쉽기만 한 시간이었네요. 그래도 아쉽지만은 않습니다. 이렇게 좋은 강의를 알게 됐다는 것만 해도 큰 소득이라고 생각합니다. n회차 앞에서는 그 어떤 어려운 강의도 만만할 거라는 믿음을 재차 떠올려봅니다. 강의 내용 요약Test Double영어로는 Stunt Double 이라고도 한다Test Double의 다섯가지 종류Dummy아무 것도 하지 않는 깡통 객체Fake단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체ex. FakeRepository(실제 db와 연결되지 않고 내부 메모리만을 사용하는 임시 리포지토리)Stub테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체정의된 내용 외에는 응답하지 않는다.SpyStub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체일부는 실제 객체처럼 동작시키고 일부만 Stubbing 할 수 있다Mock행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체Sutb 과 Mock의 차이Stub상태 검증StatefulMock행위검증Stateless 더 나은 테스트를 작성하기 위한 구체적 조언한 문단에 한 주제글을 쓸 때, 한 문단은 한 주제만을 가져야 한다테스트는 문서로서의 기능을 한다테스트 코드는 글쓰기의 관점에서 하나하나의 테스트가 한 문단이다 완벽하게 제어하기테스트 환경을 조성할 때, 모든 조건을 완벽하게 제어할 수 있어야 한다 테스트 환경의 독립성을 보장하자테스트하고자 하는 대상 함수 외에 다른 함수를 같이 사용할 경우 테스트의 성패가 다른 함수에 기인할 수 있다생성자, builder와 같은 함수 외에는 다른 함수를 사용하면 안된다정적 팩토리 메소드 또한 지양 대상 (정적 팩토리 메소드 자체에 의미가 부여되어있을 경우)테스트 간 독립성을 보장하자한 테스트는 다른 테스트에 영향을 주지 않아야 한다공유 자원을 사용해선 안된다한눈에 들어오는 Test Fixture 구성하기Test Fixture고정물, 고정되어 있는 물체given 절에서 생성하는 모든 물체들을 의미테스트를 위해 원하는 상태로 고정시킨 일련의 객체Test Fixture는 각 테스트마다 명시함으로써 한 눈에 들어오는 테스트를 작성하자@BeforeEach 나 @BeforeAll 도 가급적 지양한다그렇다면 언제 쓸 수 있는가? : 테스트를 한 눈에 보기에 어려움이 없는 간접적인 요소의 중복 제거Test Fixture 클렌징tearDown() 함수를 만들 경우 deleteAll() 보다는 deleteAllInBatch()를 사용함으로써 비용을 줄일 수 있다deleteAllInBatch)쿼리를 한번만 날린다deleteAll()테이블 내 모든 로우를 조회 후 각 로우마다 delete 쿼리를 날린다@ParameterizedTest@CsvSource, @MethodSource 등과 같이 사용한다하나의 테스트 케이스지만 여러개의 값을 대입해보고 싶을 경우에 사용할 수 있다@DynamicTest시나리오를 기반으로 테스트를 할 때 사용한다하나의 테스트에서 상태를 공유하면서 내부적으로 다양한 하위 테스트를 진행할 수 있다환경 통합하기테스트 수행도 비용이기 때문에 최대한 줄여야 한다매 테스트시마다 스프링 부트를 초기화하는데 드는 비용을 최소화 하는 것이 가능하다@WebMvcTest, @DataJpaTest, @SpringBootTest와 같은, 스프링부트를 초기화하는 어노테이션을 쓸 경우부모 클래스 (ex. IntegrationTestSupport)에 어노테이션을 넣어서 상속하는 방식을 사용한다mockBean의 구성이 다를 경우에도 새롭게 초기화하기 때문에 부모 클래스에 담는다이때 protected를 사용하여 상속받는 클래스에서도 사용이 가능하게끔 설정한다서비스, 리포지토리 레이어는 통합, 컨트롤러는 단위 테스트로 진행할 경우WebMvcTest 따로, @DataJpaTest, @SpringBootTest 따로 총 두번의 초기화가 되게끔 작성하는 방식을 사용private 메서드에 테스트하기private 메서드는 테스트를 하면 안된다private 메서드는 보통 public 함수 테스트 시에 같이 테스트가 된다만약 그럼에도 불구하고 테스트가 필요하다고 느낄 경우해당 클래스가 너무 많은 역할을 가졌다는 신호private 메서드를 따로 분리하여 새로운 클래스에 역할을 위임한 후 테스트한다테스트에서만 사용하는 메서드사용 가능하지만 가급적 지양보수적인 측면에서 접근해야 한다학습 테스트잘 모르는 기능, 라이브러리, 프레임워크를 학습하기 위해 작성하는 테스트여러 테스트 케이스를 스스로 정의하고 검증하는 과정을 통해 보다 구체적인 동작과 기능을 학습할 수 있다관련 문서만 읽는 것보다 훨씬 재미있게 학습할 수 있다Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원활하게 한다기본적으로 AsciiDoc을 사용하여 문서를 작성한다REST Docs vs SwaggerREST Docs장점테스트를 통과해야 문서가 만들어진다신뢰도가 높다프로덕션 코드에 비침투적이다단점코드 양이 많다설정이 어렵다Swagger장점적용이 쉽다문서에서 바로 API 호출을 수행해볼 수 있다단점프로덕션 코드에 침투적이다테스트와 무관하기 때문에 신뢰도가 떨어질 수 있다. 미션Day 16미션 설명Layered Architecture 구조의 레이어별 테스트 작성법을 알아보았습니다.레이어별로 1) 어떤 특징이 있고, 2) 어떻게 테스트를 하면 좋을지, 자기만의 언어로 다시 한번 정리해 볼까요? 나의 답Persistence Layer특징DB 데이터에 직접 접근하는 역할을 가진다데이터 제공 외의 역할을 해선 안된다 (비즈니스 가공 로직은 포함해선 안된다)데이터에 대한 CRUD에 집중한 레이어다테스트 코드 작성법DB 접근을 통해 수행하는 CRUD 작업이 정상적으로 수행됐는지 검증한다DataJpaTest 어노테이션을 활용하여 좀 더 가벼운(더 빠른) 테스트 코드 작성이 가능하다Persistence Layer와 관련된 빈들만을 스프링 컨테이너에 올린다테스트 시 사용하는 DB는 실제 배포 환경에서 사용하는 DB를 일치시켜야 한다두 환경의 DB를 다르게 할 경우, 코드의 안정성을 보장할 수 없다@ActiveProfiles("test") 어노테이션 application.yml 파일에 설정해둔 테스트 환경 전용 설정을 사용할 수 있다.코드 예시@ActiveProfiles("test") @DataJpaTest 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) ); } private Product createProduct(String productNumber, ProductType type, ProductSellingStatus sellingStatus, String name, int price) { return Product.builder() .productNumber(productNumber) .type(type) .sellingStatus(sellingStatus) .name(name) .price(price) .build(); } } application.yml 파일 예시spring: application: name: cafekiosk profiles: default: local datasource: url: jdbc:h2:mem:~/cafeKioskApplication driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: none --- spring: config: activate: on-profile: local jpa: hibernate: ddl-auto: create show-sql: true properties: hibernate: format_sql: true defer-datasource-initialization: true # (2.5~) Hibernate 초기화 이후 data.sql 실행 h2: console: enabled: true --- spring: config: activate: on-profile: test jpa: hibernate: ddl-auto: create show-sql: true properties: hibernate: format_sql: true sql: init: mode: NEVERBusiness Layer특징비즈니스 로직을 구현하는 역할을 가진다Persistence Layer와의 상호 작용을 통해 비즈니스 로직을 전개한다트랜잭션을 보장해야 한다테스트 코드 작성법비즈니스 로직의 정상 동작을 테스트한다.엣지 케이스 위주로 작성한다테스트에 성공하는 특정 값의 범위가 있을 경우, 해당 범위의 극점을 사용함으로써 나머지 값들의 성공을 어느정도 보장할 수 있다해피 케이스 외의 눈에 보이는 예외와 눈에 보이지 않는 예외 케이스에 대한 테스트에 집착해야 한다.Persistence Layer와 함께 통합 테스트로 진행한다tearDown() vs @Transactional각 테스트는 독립성이 보장되어야 한다. 이를 위해 테스트를 수행할때마다 초기화 작업이 진행되어야 하며 이때 두가지 방식을 사용할 수 있다.tearDown()@AfterEach 메소드를 붙인 tearDown() 함수를 설정하여 직접 초기화 로직을 작성한다tearDown이라는 명칭은 단순 관례에 불과하며, 이는 테스트 코드 수행 후 정리하는 함수라는 의미를 가진다@Transactional테스트 클래스 혹은 테스트 메소드에 어노테이션 형식으로 사용할 수 있다테스트가 끝날때마다 트랜잭션의 롤백 기능을 활용하여 초기화 작업을 수행한다실제 코드의 안정성을 보장할 수 없기 때문에 조심스럽게 활용해야 한다실제 코드의 @Transactional 누락 여부와 관계 없이 @Transactional 어노테이션이 작동한다.테스트가 실제 코드의 @Transactional 누락을 잡아낼 수 없다!코드 예시@ActiveProfiles("test") //@Transactional @SpringBootTest class OrderServiceTest { @Autowired private ProductRepository productRepository; @Autowired private OrderRepository orderRepository; @Autowired private OrderProductRepository orderProductRepository; @Autowired private StockRepository stockRepository; @Autowired private OrderService orderService; @AfterEach void tearDown() { orderProductRepository.deleteAllInBatch(); productRepository.deleteAllInBatch(); orderRepository.deleteAllInBatch(); stockRepository.deleteAllInBatch(); } @DisplayName("주문번호 리스트를 받아 주문을 생성한다.") @Test void createOrder() { // given LocalDateTime registeredDateTime = LocalDateTime.now(); 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)); OrderCreateServiceRequest request = OrderCreateServiceRequest.builder() .productNumbers(List.of("001", "002")) .build(); // when OrderResponse orderResponse = orderService.createOrder(request, 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) ); } @DisplayName("재고가 부족한 상품으로 주문을 생성하려는 경우 예외가 발생한다.") @Test void createOrderWithNoStock() { // given 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", 1); Stock stock2 = Stock.create("002", 2); stockRepository.saveAll(List.of(stock1, stock2)); OrderCreateServiceRequest request = OrderCreateServiceRequest.builder() .productNumbers(List.of("001", "001", "002", "003")) .build(); // when // then assertThatThrownBy(() -> orderService.createOrder(request, registeredDateTime)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("재고가 부족한 상품이 있습니다."); } private Product createProduct(ProductType type, String productNumber, int price) { return Product.builder() .type(type) .productNumber(productNumber) .price(price) .sellingStatus(SELLING) .name("메뉴 이름") .build(); } }Presentation Layer특징외부 세계의 요청을 가장 먼저 받는 계층이다파라미터에 대한 최소한의 유효성 검사를 수행한다테스트 코드 작성법비즈니스 로직을 제외한, 외부 요청에 대한 유효성 검사와 응답에 대한 테스트를 진행한다Business Layer 호출은 Mock 객체를 통해 이루어진다MockMvc 인스턴스를 통한 모의 요청을 사용한다이때 json 형식의 body가 필요할 경우 ObjectMapper를 활용한다.코드 예시@WebMvcTest(controllers = OrderController.class) class OrderControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @MockitoBean private OrderService orderService; @DisplayName("신규 주문을 등록한다.") @Test void createOrder() throws Exception { // given OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001")) .build(); // when // then mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/orders/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.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() .productNumbers(List.of()) .build(); // when // then mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/orders/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isBadRequest()) .andExpect(jsonPath("$$.code").value("400")) .andExpect(jsonPath("$$.status").value("BAD_REQUEST")) .andExpect(jsonPath("$$.message").value("상품 번호 리스트는 필수입니다.")); } } // $$ 는 실제 코드에서 $ 로 작성됩니다. Day 18미션 설명 1@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다. 나의 답@Mock행위에 대한 기대를 명세하고, 그에 따라 동작하는 객체를 생성합니다테스트하기 어려운 외부 의존성을 대체하기 위해 사용된다@MockBean@Mock과 같은 기능을 하지만 스프링 컨테이너 아래에서 동작한다대상 객체의 Mock 객체를 대신 컨테이너에 올린다@SpyStub 이면서 호출된 내용을 기록하여 보여줄 수 있는 객체실제 객체처럼 동작하고 일부만 Stubbing 할 수 있다@SpyBean@Spy와 같은 기능을 하지만 스프링 컨테이너 아래에서 동작한다대상 객체의 Spy 객체를 대신 컨테이너에 올린다@InjectMocks@Mock 과 @Spy 로 생성한 Stub 객체를 의존성으로 주입한다스프링이 자동으로 의존성을 주입해주는 @MockBean, @SpyBean 과는 달리 @Mock, @Spy를 사용할 경우 @InjectMocks 를 통해 의존성을 주입해줘야 한다미션 설명 2아래 3개의 테스트를 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요?@BeforeEach void setUp() { ❓ } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { 1-1. 사용자 생성에 필요한 내용 준비 1-2. 사용자 생성 1-3. 게시물 생성에 필요한 내용 준비 1-4. 게시물 생성 1-5. 댓글 생성에 필요한 내용 준비 1-6. 댓글 생성 // given ❓ // when ❓ // then 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { 2-1. 사용자 생성에 필요한 내용 준비 2-2. 사용자 생성 2-3. 게시물 생성에 필요한 내용 준비 2-4. 게시물 생성 2-5. 댓글 생성에 필요한 내용 준비 2-6. 댓글 생성 2-7. 댓글 수정 // given ❓ // when ❓ // then 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { 3-1. 사용자1 생성에 필요한 내용 준비 3-2. 사용자1 생성 3-3. 사용자2 생성에 필요한 내용 준비 3-4. 사용자2 생성 3-5. 사용자1의 게시물 생성에 필요한 내용 준비 3-6. 사용자1의 게시물 생성 3-7. 사용자1의 댓글 생성에 필요한 내용 준비 3-8. 사용자1의 댓글 생성 3-9. 사용자2가 사용자1의 댓글 수정 시도 // given ❓ // when ❓ // then 검증 }나의 답@BeforeEach void setUp() { } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { // given 1-1. 사용자 생성에 필요한 내용 준비 1-2. 사용자 생성 1-3. 게시물 생성에 필요한 내용 준비 1-4. 게시물 생성 1-5. 댓글 생성에 필요한 내용 준비 // when 1-6. 댓글 생성 // then 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { // given 2-1. 사용자 생성에 필요한 내용 준비 2-2. 사용자 생성 2-3. 게시물 생성에 필요한 내용 준비 2-4. 게시물 생성 2-5. 댓글 생성에 필요한 내용 준비 2-6. 댓글 생성 // when 2-7. 댓글 수정 // then 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { // given 3-1. 사용자1 생성에 필요한 내용 준비 3-2. 사용자1 생성 3-3. 사용자2 생성에 필요한 내용 준비 3-4. 사용자2 생성 3-5. 사용자1의 게시물 생성에 필요한 내용 준비 3-6. 사용자1의 게시물 생성 3-7. 사용자1의 댓글 생성에 필요한 내용 준비 3-8. 사용자1의 댓글 생성 // when 3-9. 사용자2가 사용자1의 댓글 수정 시도 // then 검증 }의도테스트하고자 하는 행위 외의 모든 준비 로직을 given에 두었습니다각 테스트의 독립성과 한 눈에 확인할 수 있는 가독성을 확보하기 위해 @BeforeEach는 활용하지 않았습니다
2025. 03. 14.
0
워밍업 클럽 3기 BE 클린코드&테스트 - 2주차 발자국
회고이번 2주차는 클린 코드를 대하는 마음 가짐에 대한 강의가 주를 이루었다. 클린 코드를 접한 후의 나는, 마치 연금술사들이 금을 갈망하는 것과 같은, 절대적인 방향을 찾은 것과 같은 기분에 고취해 있었다. 이번 강의들은 그런 나의 확신이 그릇된 것임을 바로잡아 주는 느낌을 받았다.특히 무엇이든 극단적으로 시도해보라는 말씀은 의미가 큰 것 같다. 나는 그동안 어떤 작업을 하면서 고민을 과도하게 한 나머지 코딩에 걸리는 시간이 과도하게 늘어나는 경향이 있었다. 이를 고칠 수 있는 방법을 찾은 것 같다. 강의 내용 요약주석의 양면성주석에는 두가지 의견이 있다주석은 죄악이다!주석이란 코드로 표현할 수 있는 내용을 대체하는 것코드를 설명하는 주석을 쓸 경우, 코드가 아닌 주석에 의존할 수 있다주석에 의존한 코드를 작성할 경우 적절하지 않은 추상화 레벨을 가진, 낮은 품질의 코드가 발생할 수 있다주석은 필수이다!리팩토링할 때의 가장 큰 난관은히스토리를 전혀 알 수 없는 코드작성 의도를 전혀 파악할 수 없는 코드"의사 결정의 히스토리"를 도저히 코드로 표현할 수 없을 때, 주석으로 상세히 설명주석 작성 팁자주 변하는 정보는 가급적 주석을 작성하지 않는다주석을 남기는 순간 주석에도 버전이 생긴다코드 변경에 따라 주석도 자주 관리하게 된다유지보수성이 낮아진다좋은 주석이란?코드를 통해 최대한 표현했음에도 전달하지 못한 정보가 남았을 때 사용하는 주석 변수와 메서드의 나열 순서변수변수는 사용하는 순서대로 나열한다인지적 경제성을 고려한다메서드공개 메서드끼리도 기준을 가지고 배치하는 것이 좋다중요도 순, 종류별로 그룹화하여 배치한다중요한 것은, 나열 순서로도 의도와 정보를 전달할 수 있다는 것이다 패키지 나누기패키지는 문맥으로써의 정보를 제공한다패키지명으로 명시함으로써 클래스명에서 중복되는 내용을 제거할 수 있다.패키지 분리 시 유의점패키지를 충분히 쪼개지 않을 경우 유지보수성이 하락할 수 있다반대로 패키지를 과도하게 쪼갤 경우 유지보수성이 하락할 수 있다공통으로 사용하는 클래스들의 패키지 분리 혹은 패키지명 변경은 충돌을 야기한다본인만 변경하고 있는 부분이라면 상관 없지만 보통 실무에서는 하나의 프로젝트를 여러 사람이 관리한다.대규모 패키지 변경은 팀원과의 합의를 이룬 시점에 한다패키지 구조는 초기 프로젝트 설정 때 최대한 나눠놓는다초기 프로젝트 설정 이후에는 패키지 변경이 어려워진다 능동적 읽기복잡하고 엉망인 코드를 읽고 이해해야 할 때, 리팩토링을 하며 읽는 것도 좋은 방법이다공백으로 단락 구분하기메서드와 객체로 추상화 해보기이해한 내용을 주석으로 표기하며 읽기리팩토링하다 뭔가 돌이키기 힘들 정도로 잘못 됐다면?git reset --hard 를 통해 원복이 가능하다리팩토링의 핵심 목표도메인 이해도 늘리기작성자의 의도 파악하기 오버 엔지니어링필요한 적정 수준보다 더 높은 수준의 엔지니어링오지 않을 미래를 대비한 필요 이상의 리소스 투자ex.구현체가 하나인 인터페이스구현체를 수정할 때마다 인터페이스도 수정해야 한다코드 탐색에 영향을 준다애플리케이션이 비대해진다필요한 경우인터페이스 형태가 아키텍처 이해에 도움을 줄 경우근시일 내에 구현체가 추가될 가능성이 높을 경우너무 이른 추상화정보가 숨겨지기 때문에 복잡도가 높아진다후대 개발자들이 선대의 의도를 파악하기 어렵다 은탄환은 없다!만능 해결사 같은 기술은 없다체스 게임을 구현하려 할 경우 하드 코딩과 객체 지향 코딩 중 어느 쪽이 적절할까?체스는 500년 동안 변하지 않았다유지보수성이 필요하지 않은 상황에서 객체 지향 코딩을 할 이유가 있는가?클린 코드도 은탄환이 아니다실무에서는 두 가지 사이의 줄다리기를 하게 된다지속 가능한 소프트웨어의 품질 vs 기술 부채를 안고 가는 빠른 결과물회사의 사정에 의해 소프트웨어의 품질을 챙기지 못할 수도 있다다만 때로는 기술 부채를 안고 가더라도 확장 가능한 형태로 결과물을 만들기 위해 노력해야 한다모든 기술과 방법론은 적정 기술의 범위 내에서 사용되어야 한다지금 우리에게 필요한, 적절한 기술이 무엇인지 파악해야 한다오래된, 구식의 기술이라도 때로는 우리 팀에게 적절할 수 있다.도구라는 것은, 일단 그것을 한계까지 사용할 줄 아는 사람이 그것을 사용하지 말아야 할 때도 아는 법적정 수준을 알기 위해, 때로는 극단적으로 시도해보자극단적으로 추상화도 해보고극단적으로 오버 엔지니어링도 해보자항상 모든 상황에서 클린 코드를 추구하는 것이 좋은 자세는 아니다 미션Day 7미션 내용[섹션 7. 리팩토링 연습]의 "연습 프로젝트 소개" 강의를 보고, "스터디 카페 이용권 선택 시스템" 프로젝트에서 지금까지 배운 내용을 기반으로 리팩토링을 진행해 봅시다. 나의 답https://github.com/hagd0520/readable-code/tree/c6a2e747055c1ab00a9a90a87143c1c19bb72602후기지금까지 들어온 강사님의 수업은 쉽지 않았습니다. 거의 처음 접하는 클린 코드의 구체적인 예시를 따라하다보면 정신이 쏙 빠지는 느낌을 받았습니다. 그래도 애써 진도를 따라잡으면서 그래도 나름 클린 코드에 익숙해져있다고 뿌듯함을 느껴왔지만 이번 미션을 통해 완전히 무너졌습니다.중복 몇개를 제거하고 나서, 인터페이스 몇개 분리하고 나니 막상 어떤 식으로 리팩토링을 해야 할지 감이 안 잡히네요. 우선 제출은 했지만 못내 아쉬운 마음입니다. 강의를 다시 들으면서 복습해야겠습니다.
백엔드
2025. 03. 07.
1
워밍업 클럽 3기 BE 클린코드&테스트 - 1주차 발자국
회고저는 그동안 제가 이상적인 코드를 작성하고 있다고 생각했습니다. 나름 많은 고민을 코드에 녹여냈고 가독성을 항상 신경썼습니다. 하지만 이번 강의를 통해서 제가 많은 부분들을 몰랐다는 점에서 놀랐습니다. 섹션 3을 통해 전반적인 안 좋은 습관들을 교정해나갈 기회는 저에게 값집니다. 남은 시간들도 기대가 됩니다. 강의 내용 요약인트로우리는 왜 이 강의를 듣는가우리는 코드를 읽는 시간을 코드를 쓰는 시간보다 더 많이 할애한다우리가 읽어야 하는 코드 :여러 사람이 작성한 코드내가 한시간 전에 작성한 코드읽기 좋은 코드는 더 나은 코드 작성을 위해 필수적이다 코드를 잘 짠다는 것은?읽기 좋은 코드를 작성하는 것"코드는 작성한 순간부터 레거시다."코드의 독자 :미래의 동료미래의 나읽기 어려운 코드는 추후의 모두에게 악영향을 미친다이 강의에서는 읽기 좋은 코드를 위해 어떤 관점으로 어떻게 접근해야 좋을지 이야기한다 추상우리가 클린 코드를 추구하는 이유클린 코드를 추구함으로써 가독성을 확보할 수 있다가독성이 높으면 글이 잘 읽힌다= 이해가 잘된다가독성이 높으면 코드가 잘 읽힌다= 이해가 잘된다= 유지보수하기가 수월하다= 우리의 시간과 자원이 절약된다.클린 코드를 작성하기 위해 우리는 추상화에 집중해야 한다 추상과 구체추상이란?어떤 모습에서 형상을 뽑아내는 것구체적인 정보에서 어떤 이미지를 뽑아내는 것특정한 측면만을 가려내어 포착하는 것특정한 측면 외 나머지는 버린다는 것중요한 정보는 남기고, 덜 중요한 정보는 생략하여 버린다. 추상화 레벨추상화 정도에 따라 레벨이 나뉜다추상화 레벨이 높을 수록 중요한 부분만 남기고 나머지는 제한다. 추상화의 가장 대표적인 행위이름 짓기 이름 짓기이름 짓기프로그래머가 가장 힘들어하는 일이름을 짓는다는 행위는 추상적 사고를 기반으로 한다.추상적 사고표현하고자 하는 구체에서 정말 중요한 핵심 개념만을 추출하여 잘 드러내는 표현우리 도메인의 문맥 만에서 이해되는 용어 이름 짓기 유의 사항단수와 복수 구분하기말미에 '-(e)s'를 붙여 어떤 데이터가 단수인지 복수인지를 명확히 하는 것만으로도 읽는 이에게 중요한 정보를 같이 전달할 수 있다.이름 줄이지 않기줄임말이라는 것은 가독성을 제물로 바쳐 효율성을 확보하는 것보통 이름을 줄임으로써 얻는 것보다 잃는 것이 많아 자제하는 것이 좋다다만 관용어처럼 많은 사람들이 자주 사용하는 줄임말이 있다.이런 줄임말이 이해될 수 있는 바탕은 문맥에 있다. 은어/방언 사용하지 않기특정 집단에서만 이해될 수 있는 은어 사용 금지기준 : 새로운 사람이 팀에 합류했을 때 이 용어를 단번에 이해할 수 있는가?도메인 용어 사용하기이 경우 도메인 용어를 먼저 정의하는 과정 (ex.도메인 용어 사전)이 선행되어야 할 수 있다이상적인 표현을 좋은 코드들을 통해 습득하기비슷한 상황에서 자주 사용하는 단어, 개념 습득하기ex. pool, candidate, threshold 등 메서드와 추상화한 문단의 주제는 반드시 하나다잘 쓰여진 코드 또한 하나의 주제만을 가진다생략할 정보와 의미를 정하고 드러낼 정보를 구분해야 한다.메서드 선언부메서드명추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름파라미터와 연결 지어 더 풍부한 의미를 전달할 수도 있다.파라미터파라미터의 타입, 개수, 순서를 통해 의미를 전달파라미터는 외부 세계와 소통하는 창반환 타입메서드 시그니처에 납득이 가는, 적절한 타입의 반환값 돌려주기메서드의 반환 타입만 보고도 바로 이해가 되어야 한다. void 대신 충분히 반환할만한 값이 있는지 고민해보기void로 충분할 경우도 있지만 가급적 반환값 사용하기반환값을 둘 경우 테스트도 용이해진다. 추상화 레벨하나의 세계 안에서는, 추상화 레벨이 동등해야 한다. 매직 넘버, 매직 스트링상수를 추출한다는 것의 의미이름을 추출한다는 것은 그 자체로 추상화상수도 이와 같다매직 넘버, 매직 스트링이란?의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등상수 추출로 이름을 짓고 의미를 부여함으로써 다음의 이점들을 확보할 수 있다.가독성유지보수성 뇌 메모리 적게 쓰기멀티 태스킹은 곧 저글링을 하는 것과 다름이 없다 (책 '도둑맞은 집중력' 발췌)사람은 한번에 하나의 일에만 집중할 수 있다.또한 하던 일을 다른 일로 전환할 경우 그에 따른 전환 비용이 발생한다.우리가 읽기 가장 좋은 코드는 한번에 읽히는 코드이다.이를 위해 아래 세가지 요소는 지양해야 한다.이해하려면 기억해야 하는 정보낮은 추상화 레벨불필요한 정보 Early return조건문을 사용할 때 else if 나 else를 사용할 경우사용자는 else의 선행 조건을 파악하기 위해 앞선 if와 else if들을 확인해야 한다.이는 사용자가 이해를 위해 기억력을 할당해야 하는 안 좋은 패턴이다.else if 와 else 는 지양해야 한다.가급적 지양코드가 짧은 등의 이유로 가독성의 문제가 없을 경우는 예외이다.같은 이치로 switch문도 가급적 지양해야 한다. 사고의 depth 줄이기중첩 분기문, 중첩 반복문중첩되는 분기문과 반복문은 함수로 따로 빼는 것이 좋을 수 있다.함수로 분리함으로써 읽는 사람으로 하여금 사고의 깊이를 줄여 가독성을 높여준다.다만 간단한 중첩문, 분기문의 경우 오히려 분리하지 않는게 좋다.사용할 변수는 가까이 선언사용된 변수가 20줄이 넘어가는 이전의 코드에서 선언될 경우읽는 입장에선 해당 변수의 존재를 확인하기 위해 다시 20줄 위로 올라가야 한다.변수 사용부와 선언부를 가까이 하여 가독성을 높이자 공백 라인을 대하는 자세공백 라인도 의미를 가진다.복잡한 로직의 의미 단위를 나누어 읽는 사람에게 추가적인 정보를 제공할 수 있다. 부정어를 대하는 자세부정 연산자의 경우 가독성이 떨어진다독자로 하여금 사고의 반전을 강제한다.부정어 대처법부정어구를 쓰지 않아도 되는 상황인지 체크부정의 의미를 담은 다른 단어가 존재하는지 고민부정어구로 메서드명 구성 해피 케이스와 예외 처리예외를 대하는 자세예외가 발생할 가능성 낮추기어떤 값의 검증이 필요한 부분은 주로 외부 세계와의 접점인 점에 유의하기ex. 사용자 입력, 객체 생성자, 외부 서버의 요청 등의도한 예외와 예상하지 못한 예외를 구분하기사용자에게 보여줄 예외와 개발자가 직접 보고 처리해야 할 예외 구분Null을 대하는 자세항상 NullPointException을 방지하는 방향으로 경각심 가지기메서드 설계 시 return null 자제하기만약 어렵다면 Optional 사용을 고려Optional을 대하는 자세Optional은 비싼 객체꼭 필요한 상황에서만 활용Optional을 파라미터로 받지 않도록 한다이 경우 분기 케이스가 세가지나 된다Null인 경우Null이 아닌 경우Optional 자체가 Null인 경우Optional을 반환 받았다면 최대한 빠르게 해소한다Optional을 해소하는 방법분기문을 만드는 isPresent()-get() 대신 풍부한 Optional의 API 사용ex. orElseGet(), orElseThrow(), ifPresent(), ifPresentOrElse()orElse(), orElseGet(), orElseThrow()의 차이를 숙지해야 한다orElse() : 항상 실행, 확정된 값일 때 사용orElseGet() : null인 경우 실행, 값을 제공하는 동작 정의 추상의 관점으로 바라보는 객체 지향객체란?추상화된 [데이터 + 코드]관심사의 분리특정한 관심사에 따라서 객체를 만들어낼 수 있다.관심사에 따라 기능과 책임을 나눈다나눈 관심사를 바탕으로 어플을 만든다.이를 통해 유지보수성을 높일 수 있다.높은 응집도, 낮은 결합도특정한 관심사끼리 응집도가 높아야 한다관심사 내의 기능들 간의 결합도가 낮아야 한다뜻A를 수정했을 때 B가 큰 영향을 받아선 안된다. 객체 설계하기객체로 추상화하기사용자는 객체의 내부 로직을 알 필요가 없다 공개 메서드 선언부를 통해 외부 세계와 소통하고 나머지 필드나 로직들은 비공개를 함으로써 캡슐화한다.객체의 책임을 나눔으로써 객체 간 협력을 유도한다.객체가 제공하는 것절차 지향에서 잘 보이지 않았던 개념의 가시화관심사를 한 군데에 모음으로써 높은 응집도 확보= 유지보수성 증가객체를 사용하는 입장에선 구체적인 내부 구현을 신경쓰지 않고 높은 추상화 레벨의 도메인 로직을 다룰 수 있다.새로운 객체를 만들 때 주의할 점1개의 관심사로 명확하게 책임이 정의되어 있는 지 확인하기 setter 사용 자제객체 내부에서 외부 세계의 개입 없는 방식을 추구함으로써 의도치 않은 버그를 사전에 방지사용이 필연적일 경우 'set~'이라는 이름 대신 'update~'와 같은 더 명확한 이름을 고려getter 사용 자제setter 와 같은 이치로 직접 꺼내서 사용하기보다 가급적 객체 내에서 해결하게끔 설계getter의 경우 setter와는 달리 필요할 경우 사용해도 된다.필드의 수 최소화불필요한 데이터가 늘수록 복잡도가 올라 유지보수성이 낮아진다.기존의 필드들을 통해 계산할 수 있는 기능들은 메서드를 통해 제공하자.단, 미리 계산하는 것이 성능상의 이점을 가질 경우 필드로 사용 가능 SOLIDSRP단일 책임 원칙 (Single Responsibility Principle)하나의 클래스는 단 한가지의 변경 사유만을 가져야 한다변경 사유 = 책임 = 관심사SRP 원칙을 잘 지킬 경우의 이점 :관심사의 분리높은 응집도낮은 결합도OCP개방-폐쇄 원칙 (Open-Closed Principle)확장에는 열려있고 수정에는 닫혀 있어야 한다.기존 코드의 변경 없이, 시스템의 기능을 확장할 수 있어야 한다.필수 요소 :추상화다형성LSP리스코프 치환 원칙 (Liskov Substitution Principle상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.자식 클래스는 부모 클래스의 책임을 준수하고부모 클래스의 행동을 변경하지 않아야 한다LSP를 위반할 경우의 문제점 :어플리케이션 오동작예상 밖의 예외위 두 문제를 방지하기 위한 불필요한 타입 체크 동반ISP인터페이스 분리 원칙 (Interface Segregation Principle)클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다.인터페이스를 잘게 쪼개자.기능 단위로 인터페이스를 나눠서 사용하자.ISP를 위반할 경우의 문제점 :불필요한 의존성으로 인한 결합도 상승DIP의존성 역전 원칙 (Dependency Inversion Principle)상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다.추상화에 의존해야 한다.의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화(인터페이스, 추상 클래스)에 의존DIP를 잘 지킬 경우 저수준 모듈이 변경되어도 고수준 모듈에는 영향이 가지 않는다. 상속과 조합상속보다는 조합을 사용해야 한다.상속은 시멘트처럼 굳어지는 구조다.상속의 단점수정이 어려움부모와 자식 간의 결합도가 높음부모가 수정될 경우 모든 자식들에게 영향조합과 인터페이스를 활용하여 유연한 구조를 얻을 수 있다.상속을 통한 코드 중복 제거가 주는 이점보다 중복이 생기더라도 유연한 구조 설계가 가능한 조합이 주는 이점이 더 크다. Value Object도메인의 어떤 개념을 추상화하여 표현한 값 객체값으로 취급하기 위해서 아래 세 가지 요소를 보장해야 한다.불변성final 필드 사용setter 금지동등성서로 다른 인스턴스여도(=동일성이 달라도), 내부의 값이 같으면 같은 값 객체로 취급equals() & hashCode() 재정의 필요유효성 검증객체가 생성되는 시점에 값에 대한 유효성 보장VO vs EntityVO와 Entity의 가장 큰 차이점은 식별자 유무이다Entity식별자가 있다식별자만 같으면 다른 필드가 달라도 동등한 객체로 취급식별자가 다르지만 필드가 다를 경우 시간이 지남에 따라 변화한 것으로 취급VO식별자가 없다내부의 모든 값이 다 같아야 동등한 객체로 취급이는 곧, 전체 필드가 식별자 역할을 한다고 볼 수 있다. 일급 컬렉션컬렉션을 포장하면서 컬렉션만을 유일하게 필드로 가지는 객체컬렉션을 다른 객체와 동등한 레벨로 다루기 위해 사용한다단 하나의 컬렉션 필드만을 가진다컬렉션을 추상화하여 의미를 담을 수 있고, 가공 로직의 보금자리가 생긴다.가공 로직에 대한 테스트도 작성할 수 있다.만약 컬렉션을 반환해야 할 경우 새로운 컬렉션을 반환해야 한다.기존의 컬렉션을 변경할 여지를 없앤다. Enum의 특성과 활용Enum은 상수의 집합상수와 관련된 로직을 담을 수 있는 공간상태와 행위를 한 곳에서 관리할 수 있는 추상화된 객체특정 도메인 개념에 대해 그 종류와 기능을 명시적 표현 가능만약 변경이 잦은 개념은 Enum보다 DB로 관리하는 것이 나을 수 있다. 숨겨져 있는 도메인 개념 도출하기도메인 지식은 만드는 것이 아니라 발견하는 것객체 지향은 현실을 100% 반영하는 것이 아닌 흉내내는 것이다.이를 통해 현실 세계에서 쉽게 인지하지 못하는 개념도 도출해서 사용할 수 있다.완벽한 설계라는 것은 불가능하다근시적, 거시적 관점에서 최대한 미래를 예측해야 한다시간이 지나 만약 틀렸다는 것을 인지할 경우를 상정하고 코드를 작성해야 한다.미션Day 2미션 설명"추상과 구체"의 강의를 듣고 생각나는 추상과 구체의 예시가 있다면 한번 3~5문장 정도로 적어봅시다. 일상 생활, 자연 현상, 혹은 알고 있는 개발 지식 등 어느 것이든 상관 없습니다. 추상에서 구체로, 또는 구체에서 추상으로 방향은 상관 없으나, 어떤 것이 추상이고 어떤 것이 구체 레벨인지 잘 드러나게 작성해 보아요:) 나의 답1.추상코드를 꼽는다구체기기에 연결되어 있는 코드를 콘센트의 두 구멍에 맞춰 연결함으로써 전력을 공급한다.2.추상커피를 마신다구체커피가 담긴 컵을 손으로 잡아 고정시킨 상태에서 컵의 각도를 조절하여 커피를 입에 주입한다.3.추상지하철을 탄다구체지하철 역사의 입구를 찾아 들어간 후 전철에 탑승하여 원하는 정거장에서 하차한다. Day 4미션 1 설명1. 아래 코드와 설명을 보고, [섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링해 봅시다.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; }나의 답public static final String NO_ORDER_ITEM = "주문 항목이 없습니다."; public static final String INVALID_TOTAL_PRICE = "올바르지 않은 총 가격입니다."; public static final String NO_USER_INFO = "사용자 정보가 없습니다."; public boolean validateOrder(Order order) throws OrderException { if (order.doesHaveItem()) { throw new OrderException(NO_ORDER_ITEM); } if (order.doesNotHaveValidTotalPrice()) { throw new OrderException(INVALID_TOTAL_PRICE); } if (order.doesNotHaveCustomerInfo()) { throw new OrderException(NO_USER_INFO); } return true; }Orderpublic abstract class Order { public abstract boolean doesHaveItem(); public abstract boolean doesNotHaveValidTotalPrice(); public abstract boolean doesNotHaveCustomerInfo(); }변경 사항if문의 조건들을 하나의 함수로 정의함으로써 조건의 의미를 명확히 했습니다.if문의 조건에 부합하지 않을 경우 바로 결과를 반환하게 했습니다.불필요한 부정 조건을 제거했습니다.별개의 예외 처리 클래스를 생성하여 예외를 명확히 했습니다.관심사를 기준으로 공백을 두었습니다.미션 2 설명SOLID에 대하여 자기만의 언어로 정리해 봅시다. SSRP (단일 책임 원칙)하나의 클래스는 하나의 책임(=관심사)을 가져야 한다.SRP를 지킴으로써 객체들을 관심사 기준으로 분리할 수 있다.높은 응집도와 낮은 결합도를 제공한다.응집도클래스나 모듈 내 요소들이 긴밀하게 연관되어있는 정도결합도한 요소가 변경되었을 때 다른 요소들이 영향을 받는 정도OOCP (개방-폐쇄 원칙)확장에는 열려 있고, 수정에는 닫혀 있어야 한다.기존 코드의 변경 없이도 시스템의 기능을 확장할 수 있어야 한다.추상화와 다형성을 활용함으로써 구현할 수 있다.LLSP (리스코프 치환 원칙)두 클래스가 상속 구조를 가질 때 부모 클래스의 인스턴스를 자식 클래스로 치환하여도 기능 상에 문제가 없어야 한다.자식 클래스는부모 클래스의 책임을 준수해야 한다.부모 클래스의 행동을 변경하지 않아야 한다.LSP를 위반할 경우 아래 문제가 발생할 수 있다.오동작예상 밖의 예외위 두 문제를 방지하기 위한 불필요한 타입 체크 동반IISP (인터페이스 분리 원칙)클라이언트는 자신이 사용하지 않은 인터페이스에 의존하면 안된다.ISP를 위반할 경우불필요한 의존성으로 인해 결합도가 높아진다.특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.필요한 기능 단위로 인터페이스를 나눠서 사용해라.DDIP (의존성 역전 원칙)상위 수준의 모듈은 하위 수준의 모듈에 직접 의존해서는 안된다.추상화(인터페이스, 추상 클래스)에 의존해야 한다.의존성의 순방향고수준 모듈이 저수준 모듈을 직접 참조의존성의 역방향고수준, 저수준 모듈 모두 추상화를 참조DIP를 지킬 경우 저수준 모듈의 변경이 고수준 모듈에 영향을 미치지 않게 된다.