블로그
전체 32024. 10. 20.
0
발자국 3주차 [인프런 워밍업 클럽 2기 백엔드(클린코드/테스트)]
이번 주 강의섹션 2소프트웨어를 유지보수하기 위한 테스트코드의 필요성 섹션 3단위테스트란 독립적으로 클래스나 메서드를 테스트하는 것해피 케이스, 예외 케이스, 경곗값 케이스를 테스트한다외부와 통신하거나 랜덤성을 갖는 코드는 단위테스트가 어렵다 섹션 4 TDD 는 프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현과정을 주도하도록 하는 방법론Red - Green - Refactor 과정을 통해 메시지부터 작성하고 코드를 구현피드백을 받으며 프로그램을 작성할 수 있는 효과를 가진테스트하기 쉬운 코드를 작성하도록 만들어 코드의 분리를 미리 고려하여 구현을 작성할 수 있고 유지보수에 유리섹션 5테스트코드는 어플리케이션을 설명하는 문서의 역할을 할 수 있다 @DisplayName - 비개발자도 이해할 수 있는 내용으로 작성BDD 기반 테스트코드 given : 주어진 데이터when : 수행할 행동then : 예측된 결과
2024. 10. 13.
0
발자국 2주차 [인프런 워밍업 클럽 2기 백엔드(클린코드/테스트)]
이번 주 강의 섹션6섹션 5에 이어서 지뢰게임 도메인 계속 리펙터링 중주석주석은 추상화를 해칠 수 있는 위험요소가 있다주석도 버전 관리의 대상이다. 업데이트되지 않은 주석은 클라이언트를 혼란스럽게 할 수 있다추상화 가능한 부분을 주석으로 대체하고 있지 않은 지 고민해보자패키지패키지 이름도 문맥에 대한 정보를 가질 수 있다"Level" 패키지의 클래스는 "BegineerLevel" 이 아닌 "Begineer" 라고 써도 의미가 전달된다패키지는 적정 수준으로 분리해야 된다 너무 많으면 관리가 어렵고 너무 적으면 응집도가 낮아진다 버그 잡기, 알고리즘 개선게임 종료조건 버그 수정게임 보드의 크기가 커지면 stackoverflowerror 발생하는 부분을 알고리즘 변경으로 개선 섹션7스터디카페 이용권 판매라는 새로운 도메인에서 리펙토링 실습을 계속하였고 강의를 보기 전에스스로 리펙토링을 진행하면서 강의를 볼 때 중점적으로 볼 부분을 정리했다 섹션 7의 리펙토링 내용중복 제거, 메서드 추출객체에 메시지 보내기IO 통합일급 컬렉션display 로직 이관 - display 는 다양한 플랫폼에서 이뤄질 수 있으므로 그 동작을 객체가 하는 것보다 입출력을 담당하는 곳에서 하는 게 적당하다passOrder 개념 추출 -중요한 도메인 로직을 입출력에서 담당하지 말고 별도의 도메인 객체로 추상화하자 FileReader 인터페이스를 Provider 인터페이스를 도입하여 분리 (이 부분이 정말 대단하다!) 섹션8능동적 읽기난해한 코드를 적극적으로 리펙토링하면서 읽는 습관을 가져보자오버 엔지니어링필요한 적정 수준보다 더 높은 엔지니어링클린코드에 대한 모든 원칙, 조언은 꼭 필요한 상황에서만 사용하자은탄환은 없다클린 코드는 은탄환이 아니다결과물의 완성도가 중요한지 빨리 결과를 내는 게 중요한 지 나의 목표를 잘 생각하자적정 수준을 항상 지키자, 적정 수준을 알기 위해선 많이 사용하고 과하게 사용해보자섹션9마무리추상과 구체를 넘나드는 사고를 하자우리는 전지전능한 신이 아니기 때문에 한번에 모든 구조를 파악하고 설계할 수 없다 이상한 고정관념에 빠지지 말자 강의 회고섹션 7이 지금까지 배운 것을 적용할 수 있는 기회였어서 많이 기억에 남는다강의와 중간점검에서 멘토님이 하신 것과 똑같이 변경한 부분도 있었지만 멘토님이 인터페이스의 책임을 생각해서다시 리펙토링하는 것을 보고 아직 부족함을 깨닫고 더 많이 공부해야겠다고 생각이 들었다 이번 주 미션[섹션 7. 리팩토링 연습]의 "연습 프로젝트 소개" 강의를 보고, '스터디 카페 이용권 선택 시스템' 프로젝트에서 지금까지 배운 내용을 기반으로 리팩토링을 진행해 봅시다.오늘 1차 리팩토링을 마치고, 다음날 자고 일어나서 다시 한번 내가 리팩토링한 코드를 살펴봅니다.자고 일어나서 뇌가 맑아지면 새로운 시야가 열릴 때가 많거든요.만약 추가로 수정하고 싶은 부분이 보인다면, 2차 리팩토링을 진행합니다. 일단 결과적으로 StudyCafePssMachine.run 의 중복되는 코드를 리펙터링 하는 것이 가장 중요한 목표인 것 같았다 public void run() { try { outputHandler.showWelcomeMessage(); outputHandler.showAnnouncement(); outputHandler.askPassTypeSelection(); StudyCafePassType studyCafePassType = inputHandler.getPassTypeSelectingUserAction(); if (studyCafePassType == StudyCafePassType.HOURLY) { StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); List hourlyPasses = studyCafePasses.stream() .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.HOURLY) .toList(); outputHandler.showPassListForSelection(hourlyPasses); StudyCafePass selectedPass = inputHandler.getSelectPass(hourlyPasses); outputHandler.showPassOrderSummary(selectedPass, null); } else if (studyCafePassType == StudyCafePassType.WEEKLY) { StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); List weeklyPasses = studyCafePasses.stream() .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.WEEKLY) .toList(); outputHandler.showPassListForSelection(weeklyPasses); StudyCafePass selectedPass = inputHandler.getSelectPass(weeklyPasses); outputHandler.showPassOrderSummary(selectedPass, null); } else if (studyCafePassType == StudyCafePassType.FIXED) { StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); List fixedPasses = studyCafePasses.stream() .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.FIXED) .toList(); outputHandler.showPassListForSelection(fixedPasses); StudyCafePass selectedPass = inputHandler.getSelectPass(fixedPasses); List lockerPasses = studyCafeFileHandler.readLockerPasses(); StudyCafeLockerPass lockerPass = lockerPasses.stream() .filter(option -> option.getPassType() == selectedPass.getPassType() && option.getDuration() == selectedPass.getDuration() ) .findFirst() .orElse(null); boolean lockerSelection = false; if (lockerPass != null) { outputHandler.askLockerPass(lockerPass); lockerSelection = inputHandler.getLockerSelection(); } if (lockerSelection) { outputHandler.showPassOrderSummary(selectedPass, lockerPass); } else { outputHandler.showPassOrderSummary(selectedPass, null); } } } catch (AppException e) { outputHandler.showSimpleMessage(e.getMessage()); } catch (Exception e) { outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다."); } }딱 보기에도 난잡한 조건문과 중복 로직이 보인다 이를 적절하게 공통 로직으로 묶고 추상화하려고 시도했다 public void run() { try { outputHandler.showAnnouncementMessageAtFirst(); StudyCafePassType passTypeSelectingUserAction = inputHandler.getPassTypeSelectingUserAction(); List selectablePassesForUserSelection = studyCafeInitHandler.getSelectablePassesForUserSelection( passTypeSelectingUserAction); outputHandler.showPassListForUserSelection(selectablePassesForUserSelection); StudyCafePass selectedPassForUser = inputHandler.getSelectPass(selectablePassesForUserSelection); StudyCafeLockerPass selectableLockerPassForUserSelection = studyCafeInitHandler.getSelectableLockerPassForUserSelection( selectedPassForUser); boolean isLockerPurchased = isLockerPurchasedIfExists(selectableLockerPassForUserSelection); if (isLockerPurchased) { outputHandler.showPassOrderSummaryIfLockerPass(selectedPassForUser, selectableLockerPassForUserSelection); } outputHandler.showPassOrderSummary(selectedPassForUser); } catch (AppException e) { outputHandler.showSimpleMessage(e.getMessage()); } catch (Exception e) { outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다."); } } 최종 코드는 다음과 같고 내가 시도해본 것은 다음과 같다StudyCafeFileHandler 에서 csv 파일을 불러오는 경로를 매직 스트링으로 변경StudyCafeFileHandler 를 구현하는 상위 인터페이스 initHandler 를 생성강의와 중간점검 피드백을 보면 다형성은 좋았는데 추상화의 목적을 잘못 설정했다묻지 말고 시켜라 원칙과 비슷한 결이라고 생각되는데 데이터를 가져오는 방법을 추상화하는 것보다는필요한 데이터 요구하는 메시지를 추상화하는 게 응집도 면에 더 낫다 인터페이스를 추상 클래스로 변경했다 그 이유는 pass 저장된 csv 파일을 읽는 로직을 public 으로 제공하지 않고 protected 로 사용하고 사용자가 사용가능한 pass 의 결과만 클라이언트가 이용하도록 하고 싶었다 이렇게 하고 싶었던 것은 내가 FileHandler 를 로직이 시작될 때 필요한 데이터를 초기화하는 역할로 생각했었는데너무 많이 추상화시킨 것 같다. 강의를 보고 많은 공부가 되었다 일부 메시지를 수정했다 studyCafeInitHandler.getSelectableLockerPassForUserSelection 같이 명확하게 하려고 했다null 처리를 하고 싶어서 Optional 을 이용하여 lockerPass 가 null 이면 isLockerPurchased 를 불리언으로 해서오버로딩된 showPassOrderSummary 를 쓰도록 했다 근데 멘토님을 보니까 lockerPass 자체를 Optional 로 사용하셨다 조금 더 생각해볼 걸 그랬다미션을 수행하면서 InputHandler 와 OutputHandler 가 따로 있는 게 코드를 읽을 때 난잡하다고 느껴졌는데 멘토님은 이를 하나의 클래스로 합치셔서 인상적이었다 습관적으로 입력 클래스와 출력 클래스를 분리했는데 꼭 그럴 필요는 없다는 걸 배웠다
2024. 10. 06.
0
발자국 1주차 [인프런 워밍업 클럽 2기 백엔드(클린코드/테스트)]
1주차 강의 섹션2-추상✅추상은 컴퓨터과학을 발전시킨 핵심적인 개념이다 내가 알아야 하는 것과 몰라도 되는 것을 구분하자추상과 구체를 구분하자. 구체에서 의미 있는 특징을 뽑아 추상화하자 의미를 드러낼 수 있는 변수명, 메서드명을 사용하자메서드마다 추상화 수준을 동일하게 유지하도록 노력하자매직 넘버, 매직 스트링(상수) 를 사용해보자 섹션3-논리, 사고의 흐름✅사고의 깊이를 줄이고, 처리해야 할 정보를 줄이자 코드를 작성할 때에도 똑같다Early return 을 통해 조건문에서 기억해야 할 정보량을 줄이자반복문 등에서 indent 를 줄여보자공백도 의미가 있다이중 부정을 바꿀 수 있는 지 고민하자사용자의 데이터는 불신하고 예외 처리를 하자 섹션4-객체 지향 패러다임✅책임과 관심사가 명확하게 구분된 객체들이 서로 소통하는 프로그램을 만들자관심사를 분할하여 응집도를 높이고 높은 추상화 수준을 사용할 수 있다객체를 존중하자객체 지향 SOLID 5원칙 : SRP, OCP, LSP, ISP, DIPSingle Responsibility Principle(SRP) : 하나의 클래스는 하나의 책임을 가져야 한다Open-Closed Principle(OCP) : 확장에는 열려 있고 변경에는 닫혀 있어야 한다Liskov Substitution Principle(LSP) : 하위 타입으로 대체해도 문제 없어야 한다Interface Segregation Principle(ISP) : 최소한의 인터페이스에 의존하자Dependency Inversion Principle(DIP) : 구체보다는 추상에 의존하도록 설계하자 섹션5-객체 지향 적용하기Value Object 까지 수강함 회고 강사님께서 레거시 코드를 리펙터링하는 식으로 수업이 진행되었다수업 코드가 있는 레포지토리를 fork 해서 진행하는 만큼 강의 진도를 커밋해서 구분해두면나중에 복습하기 좋을 것 같아 강의별로 한 내용을 커밋 메시지로 요약하고 푸쉬해두었다각 수업마다 무엇을 했는지 알아보기도 쉽고 잔디도 쌓여서 뿌듯하다그런데 약간 계획표 진도에 못 미치는 부분이 있으니 더욱 시간을 투자하려고 한다 1주차 미션 Day2 미션"추상과 구체" 강의를 듣고, 생각나는 추상과 구체의 예시가 있다면 한번 3~5문장 정도로 적어봅시다. 일상 생활, 자연 현상, 혹은 알고 있는 개발 지식 등 어느 것이든 상관 없습니다. 추상에서 구체로, 또는 구체에서 추상으로 방향은 상관 없으나, 어떤 것이 추상이고 어떤 것이 구체 레벨인지 잘 드러나게 작성해 보아요 :) 나의 답변 : 운영 체제의 주요 기능 중 하나는 컴퓨터의 CPU , 메모리 입출력장치등의 하드웨어 기능을 추상화하여 사용자가 편하게 사용할 수 있도록 하는 것이다 예를 들어 하드 디스크에 데이터를 읽고 쓰는 데 필요한 기계적인 지식을 알 필요 없이 파일 시스템이라는 체계적이고 직관적인 개념을 통해 읽고 쓰는 개념을 추상화하여 하드웨어의 종류에 관계 없이 동일한 작업을 수행할 수 있다 추상이라는 것이 자바에서 인터페이스에만 사용하는 줄 알았는데 컴퓨터과학 전반에서 매우 중요한 개념이라는 걸 배웠다그래서 컴퓨터과학에서 대표적인 추상을 이용한 사례로 운영체제에 대한 내용이 있길레 작성해보았다추상에 대해 깊이 생각해볼 수 있어서 좋았다 Day4 미션 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 boolean validateOrder(Order order){ if(isEmpty(order){ log.info("주문 항목이 없습니다."); return false; } if(isInvalidTotalPrice(order){ log.info("올바르지 않은 총 가격입니다."); return false; } if(isInvalidUser(order)){ log.info("사용자 정보가 없습니다."); } return true; } private boolean isEmpty(Order order){ return order.getItems().size() ==0; } private boolean isInvalidTotalPrice(Order order){ return order.getTotalPrice() 강의해서 메서드의 관심사를 분리하고 early-return 을 사용해 가독성을 높이고 정보량을 줄이는 방법 그리고부정어를 대체하는 방법을 배웠다코드가 딱 그런 내용을 리펙터링하는 것이라 생각해서 그에 맞게 고쳐보았다다른분들 코드와 비슷한 것 같다 SOLID에 대하여 자기만의 언어로 정리해 봅시다.나의 답변:SRP : 단일 책임의 원칙 한 클래스는 하나의 책임만 가져야 한다 변경 사유가 여러 책임이 되어선 안된다OCP: 개방 폐쇄 원칙 확장에는 열러 있지만 변경에는 닫혀 있어야 한다 클라이언트 코드가 변경에 영향받으면 안된다LSP : 리스코프 치환 원칙 객체는 하위 타입의 인스턴스로 바꿀 수 있어야 한다 구현 차원에서의 문제 - 인터페이스 규약과 다르게 구현하면 객체를 신뢰할 수 없음ISP: 인터페이스 분리 원칙 범용 인터페이스보다는 여러 개가 낫다DIP: 의존관계 역전 원칙 구체에 의존하지 말자 추상에 의존해라객체지향 프로그래밍 5원칙에 대해 생각해보는 미션이 나왔는데 평소에 내가 이해하고 있는 대로 내용을 적었다길게 작성하신 분들도 계셨는데 나의 언어로 표현하면 이정도가 끝이라 공부가 더 필요할 것 같다