블로그

정예은

[워밍업클럽3기] 클린코드-박우빈 발자국 1주차

학습내용섹션1~4📝미래의 나를 위해, 미래의 자손을 위해이름 짓기는 깔쌈하게 ! 중요키워드만 뽑아서 !중요한 정보만 남기는 추상화 잘 하기 !메서드 생성클린코드를 위해 각 로직별로 추상화를 하여 메서드로 만들어주자!✅“한가지 역할” 을 하는 코드 블럭을 찾고, 메서드로 분리✅그에 맞는 메서드 “이름 “ 지어주기✅<aside> 💡⭐메서드 생성 단축키 =ctrl+alt+m</aside>   학습정리 링크https://www.notion.so/DAY02-1ab010f075ca81ed8b20fd23dead0c76?pvs=4https://www.notion.so/DAY-04-SOLID-1-1ab010f075ca81abbcd8c909d84e74ce?pvs=4👣회고👣이번 주는 SOLID 원칙을 중심으로 코드 리팩토링을 진행하며, 보다 견고하고 유지보수하기 쉬운 구조를 고민하는 시간을 보냈다. 💡 잘한 점✅ 메서드 추출을 통해 가독성을 높이고 코드의 역할을 명확히 함✅ 기존 코드를 무조건 변경하기보다는, 확장 가능성을 고려하면서 구조를 잡아나감✅ 인터페이스와 추상 클래스의 활용을 고민하며 유연한 설계를 연습함 ⚠ 아쉬운 점아직은 강사님이 따라하는 대로 코드를 있는 그대로 따라치기만 하는 과정으로 수업을 들었음하나하나씩 로직과 메서드들을 분석해가며 수업을 들으려니, 30분 수업은 나에게 60분이되어 돌아왔음그만큼 시간을 오래 잡아먹기 때문에 진도 맞추기가 너무 어려웠다..내가 이 로드맵을 참여한게 올바른 선택이긴 할까? 라는 고민도 많이 들었지만, 일단 코드 100번정도 따라쳐보면 대충 흐름이 파악되지 않을까? 생각하며 수업을 듣고 노션에 정리하던 한주였다....  🎯 다음 주 목표단순히 원칙을 따르는 것이 아니라, 상황에 맞는 적용법을 체득하기미션을 해결할 때, "왜 이렇게 설계했는가"를 먼저 고민하고 코드를 작성하는 습관 들이기   📢미션📢Day02추상 : 눈사람을 만든다 구체 :대기중에 떠다니는 먼지가 핵이 되어, 이 핵을 중심으로 수증기가 응결해가며 형성되는 결정체의 집합체를 손으로 뭉친다2덩이로 둥글게 뭉쳐서 몸통과 머리로 붙여준다주변에 굴러다니는 , 자연에서 산출되는, 생물이 아닌 단단한 고체 물질을 눈과 코에 붙여준다  Day04SOLID원칙단일책임원칙클래스는 하나의 책임만 가져야 한다.책임을 인지하고 분리하고 다른 클래스 만들기.메인 도입부에 게임 실행부 넣지 않고 → 지뢰찾는 로직을 담은 클래스를 하나 생성해서 하나의 책임만 갖도록 Minesweeper 개방 폐쇄 원칙기존 코드를 많이 변경하지 않고 확장할 수 있도록 설계하기 추후 유지보수나 조건들이 추가로 생겨날때 당황하지 않도록 너무 상수로만 값이나 데이터 정의 내리지 않기리스코브 치환 원칙자식은 부모를 대체해서 일할 수 있고, 부모는 자식을 대체할 수 없다. 부모 클래스를 사용하는 곳에 자식 클래스를 넣어도 문제가 없어야 함인터페이스 분리 원칙하나의 커다란 인터페이스 사용하는게 아니라, 여러개의 인터페이스로 분리하기 하나의 인터페이스에는 하나의 메서드만 , 관련된 메서드만 넣어야함의존성 역전 원칙구체적인 구현 클래스가 아니라, 인터페이스나 추상 클래스에 의존 하도록 설계  public boolean validateOrder(Order order) { if (isInvalidOrder(order)) { return false; } return true; } private boolean isInvalidOrder(Order order) { if (order.doesNotHaveAnyItem()) { log.info("주문 항목이 없습니다."); return true; } if (order.doesNotHaveCustomerInfo()) { log.info("사용자 정보가 없습니다."); return true; } if (order.hasNegativeTotalPrice()) { log.info("올바르지 않은 총 가격입니다."); return true; } return false; } 👣회고👣미션을 해결하면서 "추상화"의 중요성을 몸소 체감한 한 주였음특히, 눈사람 만들기 예제를 통해 구체적인 행동을 추상화하는 연습을 했고, 이를 코드에도 적용하려 노력했다. 

백엔드워밍업클럽워밍업클럽3기박우빈백엔드백엔드스터디지뢰찾기클린코드리팩토링

[인프런 워밍업 클럽 3기 - BE 클린 코드, 테스트 코드] 1주차 발자국 👣

1주차 발자국 👣 💻 강의 수강👩🏻‍💻 학습 내용 요약잘 읽히는, 이해할 수 있는 코드를 작성하자.추상화는 구체적인 정보들로터 중요한 정보들만 남기는 것이다. 추상화에서 도메인의 문맥이 중요하다.이름을 짓는 행위도 추상화의 일종으로, 단어만으로 읽는 이에게 정보를 쉽게 전달할 수 있어야 한다.메서드의 주제는 반드시 하나여야 한다. 포괄적인 의미를 담아야 하며, 작은 단위의 메서드로 쪼갤 수 있다.상수와 줄 바꿈으로도 가독성을 높일 수 있다.코드를 읽는 이가 너무 많이 고민하지 않게, 기억해야 하는 정보가 많지 않게 읽을 수 있게 하자.Early return, 사고 과정의 depth 줄이기, 부정어 대체하기 등예외가 발생할 가능성을 낮추자. 의도한 예외와 예상하지 못한 예외를 구분하자.Java에서 null로 인한 예외를 주의하자. Optional 사용 고민해 보자.  🤔 학습 내용에 대한 회고강의를 듣고 이해하며 미션을 수행하는데 시간이 빠듯했다.하나하나 기록하며 강의를 듣는 편인데 이러한 방식은 시간이 오래 걸린다. 어떻게 해야 적은 시간 내에 강의 수강 및 이해, 미션 수행이 가능할 지 학습 방법에 대해서 고민해봐야겠다.강의가 너무 재밌다!!!💙 🎯 다음 주 학습 목표효율적인 학습 방법을 알아내서 적용해보자!!  ✉ 미션💭 미션 해결 과정Day 2 미션 우리 가족 귀염둥이와 산책하는 것에 대해서 구체화해보았다. Day 4 미션1번 내용)강의에서 언급한 논리, 사고의 흐름에 따라서 읽기 좋은 코드가 되도록 리팩토링하였다.2번 내용)SOLID 원칙에 대해서 나만의 언어로 작성하였다. 🤔 미션 해결에 대한 간단한 회고미션을 잘 한 건지 모르겠다.. 피드백을 받고 싶다...  📚 출처[강의] Readable Code: 읽기 좋은 코드를 작성하는 사고법 https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard 

백엔드워밍업클럽백엔드clean-code객체지향리팩토링

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 클린코드 (Day5)

진도표 5일차와 연결됩니다우리는 <클린 코드>라는 개념을 배웠습니다. <클린 코드>에 대한 감각을 익히기 위해서는 어떤 코드가 좋은 코드이고, 어떤 코드가 좋지 않은 코드인지 이론적인 배경을 학습하는 것도 중요할 뿐 아니라, 다양한 코드를 읽어 보며 어떤 부분이 읽기 쉬웠는지, 어떤 부분이 읽기 어려웠는지, 읽기 어려운 부분은 어떻게 고치면 좋을지 경험해보는 과정이 필요합니다.이번 과제는 제시된 코드를 읽어보며, 코드를 더 좋은 코드로 고쳐나가는 과정입니다. 구글에 “클린 코드” 혹은 “클린 코드 정리”를 키워드로 검색해보면, 이론적인 배경을 충분히 찾아보실 수 있습니다. 🙂 그러한 내용들을 보며 제시된 코드를 더 좋은 코드로 바꿔보세요! (코드를 바꿀 때 왜 바뀐 코드가 더 좋은 코드인지 다른 사람에게 설명하신다고 생각해보시면 더욱 좋습니다.) [제시된 코드]여러 함수로 나누어도 좋습니다! 🙂여러 클래스로 나누어도 좋습니다! 🙂클린코드오늘이 벌써 5일차의 날이 밝았다. 이번에는 클린코드가 무엇인지, 어떤 코드가 좋은 코드이며, 어떠한 코드가 안 좋은 코드인지 알아보는 시간을 가졌다. 그리고 또한 모든 비즈니스 로직을 가지고 있는 하나의 controller 클래스를 service와 repository 레이어로 분리함으로 '단일책임의 원칙'을 지킬 수 있었다. 하지만 아직 더 궁금하고 공부하고 싶어서, 2개의 유튜브 영상을 시청하였다. 하나의 영상은 코치님이 올리신 영상이고 하나는 토스에서 올린 영상이였는데 먼저 시청을 해보기로 하였다. 영상 url은 아래 참고에 달아두기로 하겠다. 과제 전, 영상 학습 코치님 영상이 영상의 핵심은 왜 엔티티에서 setter 를 지양해야 하는지에 대한 영상이었습니다. 먼저 결론부터 이야기 해보면 setter는 지양하자라는 말이다. 그 이유는 아래와 같다. 📚Setter 지양 이유1. 변경의도가 파악하기 어렵다: 어느 엔티티에 setter가 있을 때 이 setter를 메서드에 묶어서 사용하는것은 함수의 이름으로도 확인이 가능하며, 이 안의 setter들이 함께 처리된다. 또한 코드가 몇 천줄 이상이 되었고 여기서 추가 요구사항이 있을 때 코드를 추적해서 변경해야 하는데 메서드로 묶으면 한 곳만 수정을 해주면 되겠지만 setter를 직접 해주면 어마 무시한 코드의 수정이 일어날 것이다. 즉, 메서드로 묶었을 때 코드의 응집성이 좋아진다.2. 객체 일관성 유지를 할 수 없다.3. 1번과 연관되지만 메서드로 묶지 않고 setter를 직접 사용하면 생산성 차이가 발생한다.위의 내용은 나에게 와 닿는 부분에 대해서 이야기를 했고 자세한 부분을 원하면 영상에 직접 들어가서 확인 바란다. 토스 영상 (feat. 진유림님)토스 SLASH21에서 진유림님이 발표한 영상을 참고해보았다. 이 영상에서는 지뢰코드를 예시로 들고 있다.🙋🏻 지뢰코드란?회사에서 '이거 건들지 않는게 좋을꺼에요.'의 코드들이 있을 것이다. 이런 코드들은 흐름 파악이 어렵고 도메인 맥락 표현이 안되어 있으며 동료에게 물어봐야 알 수 있는 코드를 뜻한다. 이 코드는 개발할 때 병목현상이 발생하고 유지보수할 때 많은 시간이 들고 심하면 기능추가도 되지 않을 뿐더러 성능도 떨어질 수 있다.그래서 클린코드를 도입하여 이런 코드의 유지보수 시간을 단축(코드 파악 단축, 디버깅 시간 단축, 코드리뷰 단축)할 수 있다. 안일한 코드 함정지뢰코드는 하나의 목적인 코드가 흩어져서 확인할려면 스크롤을 왔다갔다 해야하는 코드를 뜻한다. 우리 회사의 몇개의 코드들이 생각나는 부분이였다. 이런 이유가 된 것은 하나의 함수에 여러가지 일을 하기 때문이다. 그래서 세부구현을 모두 읽어야 함수의 역할을 알게 된다. 그래서 단일책임원칙등 이런 클린코드 개념들이 나왔다. 하지만 이 코드는 처음부터 지저분한 코드들은 아니었을 것이다. 즉, 지뢰코드도 그 당시에는 좋은 코드였을 것이다. 그러다가 이런저런 기능들을 무작정 추가가 되버리고 지뢰코드로 변화된 것일 것이다. 📚 결론클린코드는 단순히 짧은 코드가 아니다. 원하는 로직을 빠르게 찾을 수 있는 코드를 의미한다.또한 선언적 프로그래밍으로 가되, 명령형 프로그래밍 기법을 적절히 사용한다.핵심 기능만 보이게 하고 일부 세부기능은 일단 숨기자.블로그 글클린코드에 대해 구글링을 한 결과, 여러 블로그들이 나왔다. 그 중에, 어느 블로그에 정리가 잘 되어 있어서 일부만 발췌해서 정리해보았다. 자세한 글은 직접 가셔서 읽어보는 것을 추천한다. 📚클린코드 정리1. 객체의 생성에도 유의미한 이름을 사용하라2. 함수는 하나의 역할만 해야한다.3. 명령과 조회를 분리하라(Command와 Query의 분리)4. 오류코드 보다는 예외를 활용하자5. 여러 예외가 발생하는 경우 Wrapper 클래스로 감싸자6. 테스트 코드의 작성7. 변경하기 쉬운 클래스 + 클래스 응집도8. 디미터 법칙 🙋🏻 디미터 법칙: 디미터의 법칙은 어떤 모듈이 호출하는 객체의 속사정을 몰라야 한다는 것이다. 그렇기에 객체는 자료를 숨기고 함수를 공개해야 한다. 만약 자료를 그대로 노출하면 내부 구조가 드러나 결합도가 높아지게 된다.과제그럼 과제에서 제공한 코드를 클린코드의 원칙을 최대한 지키며 리팩토링 해보는 과정을 보여주겠다. package me.sungbin; package me.sungbin; import java.util.Scanner; public class Main { public static void main(String[] args) throws Exception { System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int a = scanner.nextInt(); int r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0; for (int i = 0; i < a; i++) { double b = Math.random() * 6; if (b >= 0 && b < 1) { r1++; } else if (b >= 1 && b < 2) { r2++; } else if (b >= 2 && b < 3) { r3++; } else if (b >= 3 && b < 4) { r4++; } else if (b >= 4 && b < 5) { r5++; } else if (b >= 5 && b < 6) { r6++; } } System.out.printf("1은 %d번 나왔습니다.\n", r1); System.out.printf("2은 %d번 나왔습니다.\n", r2); System.out.printf("3은 %d번 나왔습니다.\n", r3); System.out.printf("4은 %d번 나왔습니다.\n", r4); System.out.printf("5은 %d번 나왔습니다.\n", r5); System.out.printf("6은 %d번 나왔습니다.\n", r6); } } Step0. 코드 분석일단 먼저 눈에 고쳐야 할 부분이 보이는데 바로 아래와 같다. 1. 변수명 개선: r1, r2, r3... 이런 부분과 단순 알파벳 하나로 되어 있는 부분을 고쳐야 할 것 같다.변수를 이렇게 작성하여 코딩하면 위의 코드가 나중에 몇 천줄이 되고 유지보수를 할 때 이 변수는 대체 어떤 변수인지 파악이 어렵기 때문이다. 그래서 유의미한 이름으로 작성을 해보는 것이 좋을 것 같다.2. 매직 넘버 제거: 주사위 면 값을 상수로 정의한다. 이유는 나중에 이런 매직넘버가 여러 코드에 흩어져 있다고 가정하자. 그런데 어느날 주사위가 육면체가 아닌 36면체로 변경해야한다는 것이다. 그러면 이 매직넘버를 하나하나 찾아서 바꿔주야 하지만 상수로만 정의하면 상수 값만 바꾸면 되기 때문이다.3. 중복 제거: 주사위를 굴리는 로직과 출력 부분을 함수로 분리. 이것은 개발자면 당연하다고 느끼는 부분일 것이다. 현재 출력하는 부분과 주사위 굴리는 조건문과 증가 연산자 부분이 뭔가 중복되는 것 같다. 그래서 이 부분도 함수로 변경할 예정이다.4. 책임 분리: 주사위 굴리기와 결과 출력을 분리. 이는 위에서 설명한 클린코드의 단일책임의 원칙으로 분리하는 것이 유지보수성에 좋을 것이다. 왜 좋은지는 두 영상과 블로그를 참조하자.5. 배열사용: 주사위 눈금 횟수를 배열로 사용하는 것은 코드 가독성을 높이고 중복을 줄이며 유지 보수를 쉽게 만든다. 그러면 정리해보겠다. 클린 코드 원칙에 따라, 현재 코드는 여러 가지 방법으로 개선될 수 있다. 먼저, 'r1', 'r2', 'r3', 'r4', 'r5', 'r6'와 같은 변수명은 의미를 명확하게 전달하지 않는다. 더 명확한 변수명을 사용할 수 있습니다. 또한, 각각의 조건문은 매직 넘버(magic number)를 사용하고 있는데, 이는 읽는 사람이 코드의 의도를 바로 이해하기 어렵게 만듭니다. 상수를 사용하여 이러한 숫자에 이름을 붙여 가독성을 높일 수 있습니다.다음으로, 주사위의 각 면을 계산하는 부분은 반복되는 구조를 가지고 있으므로 이를 함수로 분리하여 중복을 줄이고 코드의 재사용성을 높일 수 있습니다. 또한, 현재 코드는 주사위를 굴린 후 결과를 출력하는 두 가지 작업을 동시에 수행하고 있습니다. 이를 분리하여 한 함수는 주사위를 굴리는 로직을, 다른 함수는 결과를 출력하는 로직을 담당하게 하는 것이 좋습니다.마지막으로, 주사위의 각 면이 나온 횟수를 저장하는 배열을 사용하면 변수를 줄일 수 있고, for 루프 안에서의 조건문을 간소화할 수 있습니다. 이렇게 코드를 개선하면 유지 관리가 용이해지고 다른 개발자가 이해하기 쉬운 코드가 됩니다. 🙋🏻 매직넘버란? 의미 있는 이름의 상수로 대체될 수 있는 숫자 Step1. 리팩토링 첫 단계위의 부분을 적용한 코드는 아래와 같다.package me.sungbin.step1; import java.util.Scanner; /** * @author : rovert * @packageName : me.sungbin.step1 * @fileName : Main * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class Main { private static final int SIDES_OF_DICE = 6; public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int rolls = scanner.nextInt(); scanner.close(); int[] counts = rollDice(rolls); printResults(counts); } /** * 주사위를 굴리는 로직이 들어가는 메서드 * @param numberOfRolls * @return */ private static int[] rollDice(int numberOfRolls) { int[] counts = new int[SIDES_OF_DICE]; for (int i = 0; i < numberOfRolls; i++) { int result = (int) (Math.random() * SIDES_OF_DICE); counts[result]++; } return counts; } /** * 출력기능을 담당하는 메서드 * @param counts */ private static void printResults(int[] counts) { for (int i = 0; i < counts.length; i++) { System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, counts[i]); } } }SIDES_OF_DIE 상수를 통해 주사위 면의 수를 명확하게 합니다.rollDice 함수는 주사위를 굴리는 작업만 수행하고, 그 결과를 int[] 배열에 저장합니다.printResults 함수는 주사위의 각 면이 나온 횟수를 출력합니다.int[] counts 배열은 0부터 5까지 각 면이 나온 횟수를 저장합니다. 배열의 인덱스는 주사위 면의 숫자에 해당합니다.Scanner 객체를 종료하는 부분이 누락되어 추가하였다.불필요한 Exception 던지는 부분을 제거한다.코드를 구조화함으로 각 부분의 책임이 명확해지고, 재사용성과 유지보수성이 향상된다. Step2. 리팩토링 2단계 (feat. 객체지향)위와 같이 static method로 분리하면 객체지향적이지 않은 것 같다는 생각을 했고, 하나의 파일에 모든 로직이 있게 되는 셈인 것 같았다. 그래서 객체지향적으로 작성을 위해 여러 파일들을 만들어 분리하기로 하였다. 적용한 코드는 아래와 같다. Dice.javapackage me.sungbin.step2; /** * @author : rovert * @packageName : me.sungbin.step2 * @fileName : Dice * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class Dice { private final int sides; public Dice(int sides) { this.sides = sides; } /** * 주사위 면의 숫자 구하기 * @return */ public int roll() { return (int) (Math.random() * sides); } public int getSides() { return sides; } } DiceRollHandler.javapackage me.sungbin.step2; /** * @author : rovert * @packageName : me.sungbin.step2 * @fileName : DiceRollHandler * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class DiceRollHandler { private final Dice dice; private final int[] counts; public DiceRollHandler(Dice dice, int numberOfRolls) { this.dice = dice; this.counts = new int[dice.getSides()]; for (int i = 0; i < numberOfRolls; i++) { this.counts[dice.roll()]++; } } public void rollAll() { for (int i = 0; i < counts.length; i++) { counts[dice.roll()]++; } } public void printResults() { for (int i = 0; i < counts.length; i++) { System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, counts[i]); } } } DiceGame.javapackage me.sungbin.step2; import java.util.Scanner; /** * @author : rovert * @packageName : me.sungbin.step2 * @fileName : Main * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class DiceGame { private static final int SIDES_OF_DICE = 6; public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int rolls = scanner.nextInt(); scanner.close(); Dice dice = new Dice(SIDES_OF_DICE); // 정해진 면의 수를 가진 주사위 객체 생성 DiceRollHandler handler = new DiceRollHandler(dice, rolls); // 주사위를 던지는 이벤트에 대한 핸들러 객체 생성 handler.rollAll(); // 주사위 던지기 이벤트 비즈니스 로직 handler.printResults(); // 출력 } }이 구조에서는 Dice 클래스가 주사위 자체를 나타내고, DiceRollHandler 클래스가 주사위를 굴리고 결과를 추적하는 역할을 한다. DiceGame 클래스의 main 메서드는 게임을 시작하는 진입점이다.Dice 클래스는 주사위의 기능(굴리기)과 속성(면의 수)을 캡슐화한다.DiceRollHandler 클래스는 주사위를 여러 번 굴리는 로직을 책임지고, 각 면이 나온 횟수를 저장합니다.주사위 게임의 진행과 결과 출력은 DiceRollHandler 클래스에서 담당한다.Main클래스로 되어있는 부분을 이름을 변경하였다. 일단 Main이라는 클래스 명은 이 프로젝트가 무엇인지 알기 어렵기 하기 때문이다. 이렇게 클래스를 분리하면 코드의 각 부분이 명확한 책임을 가지게 되어 유지보수가 용이하고, 다른 주사위 게임에 Dice 클래스나 DiceRollHandler 클래스를 재사용할 수 있는 가능성이 생깁니다.Step3. 리팩토링 3단계다음으로 코드를 더 클린하게 만들기 위해 여러 가지 최적화와 리팩토링을 수행해보겠다. 1. 메소드 분리와 책임의 명확화: 각 메서드가 하나의 작업만 수행하도록 만들어 코드의 가독성을 높인다.2. 입출력 분리: 사용자 입력과 출력을 처리하는 별도의 클래스를 만들어 로직과 UI를 분리한다.3. 예외 처리 개선: Scanner를 사용할 때 발생할 수 있는 예외를 적절히 처리합니다. Dice.javapackage me.sungbin.step3; /** * @author : rovert * @packageName : me.sungbin.step3 * @fileName : Dice * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class Dice { private final int sides; public Dice(int sides) { this.sides = sides; } public int roll() { return (int) (Math.random() * sides); } public int getSides() { return sides; } }DiceRollHandler.javapackage me.sungbin.step3; /** * @author : rovert * @packageName : me.sungbin.step3 * @fileName : DiceRollHandler * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class DiceRollHandler { private final Dice dice; private final int[] counts; public DiceRollHandler(Dice dice) { this.dice = dice; this.counts = new int[dice.getSides()]; } public void rollAll(int numberOfRolls) { for (int i = 0; i < numberOfRolls; i++) { counts[dice.roll()]++; } } public int[] getCounts() { return counts; } }InputHandler.javapackage me.sungbin.step3; import java.util.Scanner; /** * @author : rovert * @packageName : me.sungbin.step3 * @fileName : InputHandler * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class InputHandler { public static int getNumberOfRolls() { System.out.print("숫자를 입력하세요 : "); try (Scanner scanner = new Scanner(System.in)) { return scanner.nextInt(); } catch (Exception e) { throw new IllegalArgumentException("유효하지 않은 입력입니다."); } } }OutputHandler.javapackage me.sungbin.step3; /** * @author : rovert * @packageName : me.sungbin.step3 * @fileName : OutputHandler * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class OutputHandler { public static void printResults(int[] counts) { for (int i = 0; i < counts.length; i++) { System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, counts[i]); } } }DiceGame.javapackage me.sungbin.step3; /** * @author : rovert * @packageName : me.sungbin.step3 * @fileName : DiceGame * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class DiceGame { private static final int SIDES_OF_DIE = 6; public static void main(String[] args) { int rolls = InputHandler.getNumberOfRolls(); Dice dice = new Dice(SIDES_OF_DIE); DiceRollHandler roller = new DiceRollHandler(dice); roller.rollAll(rolls); OutputHandler.printResults(roller.getCounts()); } }이러한 리팩토링을 통해 코드의 가독성과 관리 가능성이 향상되었다. 입력과 출력을 담당하는 InputHandler와 OutputHandler 클래스는 각각의 책임을 분명히 하며, DiceGame은 게임의 로직만을 담당하게 된다. 예외 처리를 통해 프로그램의 예외를 처리할 수 있게 하며, 견고해진 프로그램이 될 것 같다. Step4. 한걸음 더! 적용 (feat. 디자인 패턴)코치님이 한걸음 더에서 주사위의 숫자범위가 달라지더라도 코드를 적게 수정할 수 있도록 해보시라고 하셨다. 그래서 한번 고민을 해보았다. 지금도 주사위 최대 수를 상수로 빼두었기에 충분하다고 생각은 들었다. 또한 디자인패턴을 적용을 해봄으로 더 견고한 코드를 작성해보았다. DiceRollStrategy.javapackage me.sungbin.step4; /** * @author : rovert * @packageName : me.sungbin.step4 * @fileName : DiceRollStrategy * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public interface DiceRollStrategy { int roll(); int getSides(); // 면 수를 반환하는 메소드 추가 } Dice.javapackage me.sungbin.step4; /** * @author : rovert * @packageName : me.sungbin.step4 * @fileName : Dice * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class Dice { private final DiceRollStrategy strategy; public Dice(DiceRollStrategy strategy) { this.strategy = strategy; } public int roll() { return strategy.roll(); } // 주사위의 면 수를 반환하는 메소드 추가 public int getSides() { return this.strategy.getSides(); } }  StandardDiceRollStrategy.javapackage me.sungbin.step4; /** * @author : rovert * @packageName : me.sungbin.step4 * @fileName : StandardDiceRollStrategy * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class StandardDiceRollStrategy implements DiceRollStrategy { private final int sides; public StandardDiceRollStrategy(int sides) { this.sides = sides; } @Override public int roll() { return (int) (Math.random() * sides) + 1; } @Override public int getSides() { return this.sides; } } InputHandler.javapackage me.sungbin.step4; import java.util.Scanner; /** * @author : rovert * @packageName : me.sungbin.step3 * @fileName : InputHandler * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class InputHandler { public static int getNumberOfRolls() { System.out.print("숫자를 입력하세요 : "); try (Scanner scanner = new Scanner(System.in)) { return scanner.nextInt(); } catch (Exception e) { throw new IllegalArgumentException("유효하지 않은 입력입니다."); } } } OutputHandler.javapackage me.sungbin.step4; /** * @author : rovert * @packageName : me.sungbin.step3 * @fileName : OutputHandler * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class OutputHandler { public static void printResults(int[] counts) { for (int i = 0; i < counts.length; i++) { System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, counts[i]); } } } DiceRollHandler.javapackage me.sungbin.step4; /** * @author : rovert * @packageName : me.sungbin.step4 * @fileName : DiceRollHandler * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class DiceRollHandler { private final Dice dice; private final int[] counts; public DiceRollHandler(Dice dice) { this.dice = dice; this.counts = new int[dice.getSides()]; // 주사위 면 수에 맞는 크기로 배열 초기화 } public void rollAll(int numberOfRolls) { for (int i = 0; i < numberOfRolls; i++) { int result = dice.roll() - 1; // 0부터 시작하는 배열 인덱스에 맞추기 위해 1을 뺌 counts[result]++; } } public int[] getCounts() { return counts; } } DiceGame.javapackage me.sungbin.step4; /** * @author : rovert * @packageName : me.sungbin.step4 * @fileName : DiceGame * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ public class DiceGame { private static final int SIDES_OF_DIE = 6; // 기본값, 필요에 따라 변경 가능 public static void main(String[] args) { int rolls = InputHandler.getNumberOfRolls(); DiceRollStrategy strategy = new StandardDiceRollStrategy(SIDES_OF_DIE); // 전략 선택 Dice dice = new Dice(strategy); DiceRollHandler roller = new DiceRollHandler(dice); roller.rollAll(rolls); OutputHandler.printResults(roller.getCounts()); } } Step5. 테스트 코드이제 참고한 블로그에 따르면 테스트코드도 반드시 필요하다고 한다. 그럼 마지막으로 테스트 코드를 작성하고 마무리하자. 테스트 코드는 주요 로직들을 테스트하였다. package me.sungbin.step4; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; /** * @author : rovert * @packageName : me.sungbin.step4 * @fileName : DiceRollHandlerTest * @date : 2/23/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/23/24 rovert 최초 생성 */ class DiceRollHandlerTest { @Test @DisplayName("Dice 클래스의 주사위 롤링 기능이 1부터 6 사이의 값을 정확히 생성하는지 검증") public void testDiceRoll() { DiceRollStrategy strategy = new StandardDiceRollStrategy(6); Dice dice = new Dice(strategy); int result = dice.roll(); assertTrue(result >= 1 && result <= 6); } @Test @DisplayName("StandardDiceRollStrategy 클래스를 통해 생성된 주사위 값이 1부터 6 사이인지 확인") public void testStandardDiceRollStrategy() { StandardDiceRollStrategy strategy = new StandardDiceRollStrategy(6); int result = strategy.roll(); assertTrue(result >= 1 && result <= 6); } }회고이렇게 클린코드로 코드를 리팩토링 해보니, 많은 것을 찾아보고 나 자신이 발전한 느낌이 들었다. 이런 부분을 개인적으로도 자주 연습해봐야겠다. 📚 참고https://www.youtube.com/watch?v=5P7OZceQ69Qhttps://www.youtube.com/watch?v=edWbHp_k_9Yhttps://mangkyu.tistory.com/132

백엔드인프런워밍업스터디클럽백엔드클린코드리팩토링

lch9502

워밍업 클럽 3기 BE - 발자국 2주 차

1. 들어가며어쩌다보니 2추 차까지 왔네요? 이번주는 일이 많아서 정신없이 보냈습니다.게다가 클린코드 리팩토링 실습 미션이 있었습니다.아쉽게도 리팩토링을 깔끔하게 마무리 짓지 못해서 코드 리뷰 신청은 못했네요.. 후회할 일이 늘어났습니다... 😢어쨌든 미션 덕분에 지난주 강의를 꽤 여러번 반복해서 들었는데요, 생각보다 더 많은 인사이트를 얻었습니다.그리고 갈 길이 참 멀다는 사실도 알게 되었죠 ㅎ... 2. 학습했던 내용 나만의 키워드로 작성하기섹션 6. 코드 다듬기좋은 주석주석이 많다는 것은 가독성이 좋지 않은 코드주석이 필요한 경우는 히스토리를 알 수 없을 경우변수와 메서드 나열 순서변수는 사용하는 순서대로공개 메서드를 상단에 위치하고, 비공개 메서드 하단에 위치패키지 나누기기능 유지보수하기기능 유지보수하기정렬 단축키, linting, style - sonarlint, editorconfig   섹션 7. 리팩토링 연습Optionalreturn null / Optional 파라미터 사용은 안티패턴객체에 메시지 보내기공개 메서드를 통해 객체끼리 협력하기객체의 책임과 응집도 - IO 통합, 일급컬렉션, display(), Order 추출추상화 관점의 차이 - FileHandler구현에 초점을 맞춘 추상화 VS 도메인 개념에 초점을 맞춘 추상화  섹션 8. 기억하면 좋은 조언들능동적 읽기 오버 엔지니어링구현체가 하나인 인터페이스너무 이른 추상화은탄환은 없다클린 코드도 은탄환 x항상 정답인 기술은 없음. 경험이 중요! 섹션 3. 단위 테스트수동 테스트, 자동화 테스트 Junit5, AssertJ단위 테스트: 작은 코드 단위(클래스 or 메서드)를 독립적으로 검증하는 테스트Junit5: 단위 테스트를 위한 프레임워크AssertJ: 테스트 코드 작성을 원활하게 돕는 테스트 라이브러리해피 케이스, 예외 케이스항상 예외 케이스가 있는지 고민하고 테스트를 작성해야 함경계값 테스트먼저 테스트 케이스를 세분화하고 경계값이 존재하는 경우는 경계값에서 항상 테스트를 할 수 있도록 고민을 하는게 중요테스트하기 쉬운/어려운 영역 (순수함수)관측할 때마다 다른 값에 의존하는 코드우리가 작성한 코드가 외부 세계에 영향을 주는 코드 섹션 4.TDD: Test Driven DevelopmentTDD테스트를 먼저 작성한 후에 그를 통해 기능을 만들고 그 다음에 기능을 수정할 때 테스트의 도움을 받을 수 있도록 하는 개발 방법론 중에 하나레드 - 그린 - 리팩토링레드: 실패하는 테스트를 먼저 작성하는 단계그린: 테스트가 통과할 수 있는 최소한의 코딩을 하는 단계리팩토링: 테스트를 통과하는 것을 유지하면서 구현 코드를 개선하는 단계섹션 5. 테스트는 []다@DisplayName - 도메인 정책, 용어를 사용한 명확한 문장DisplayName을 섬세하게!1. 명사의 나열보다 문장으로2. 테스트 행위에 대한 결과까지 기술하기3. 도메인 용어를 사용하여 한층 추상화된 내용을 담기4. 테스트의 현상을 중점으로 기술하지 말 것Given / When / Then - 주어진 환경, 행동, 상태 변화Given : 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등) When : 시나리오 행동 진행Then : 시나리오 진행에 대한 결과 명시, 검증TDD vs BDD 3. 학습 회고컴퓨터 공학과를 나왔고 실무에서 몇 년을 일했지만 객체 지향으로 코드를 작성하는 방법은 배운적이 없는 것 같습니다.캡, 추, 상, 다가 뭔지는 알고, SOLID가 뭔지는 알지만 코드에 잘 적용이 됐는지는 항상 의문이였죠.이 강의는 정말 기대했던 것 보다 더 많은 인사이트를 얻는 강의였습니다. 아마 당분간은 몇 번 더 돌려볼 것 같네요.그래도 지금까지 여러 블로그나 책들을 읽으면서 객체 지향적으로 코드를 작성하려고 노력했었는데요, 강의와 어느정도 결이 비슷하더라구요. 가는 길이 얼추 올바른 길이였다는 사실이 안심이 됩니다. 테스트 코드 강의는 정말 빠르게 봤던 것 같습니다. 강의가 출시되자마자 바로 봤었죠.그 때는 테스트 코드를 잘 작성하는 게 뭔지 모르기도 했고, 주변에서도 작성하는 사람이 단 한명도 없었기에 정말 도움이 되는 강의였습니다.이번에 속도 3배로 다시 쭉 훑었는데 처음 들었을 때 생각이 많이 났습니다.당시에는 몰랐는데 지금은 테스트 코드를 작성하는데 부담이 없는 것을 보니 성장하긴 했나봅니다. ㅋㅋ  4. 미션 회고미션 Day 7개인적으로 이번 미션에 대해 나에게 점수를 매긴다면 좋은 점수를 주지 못할 것 같네요.강의에서 배운 내용을 제대로 적용하지 못했다는 생각이 듭니다.(변명을 해보자면 시간이 너무 부족했다는... 💦💦) 저는 인터페이스로 빼서 구현하는 방법으로 코드를 꽤 많이 작성합니다. 그리고 객체를 조합으로 풀어서 코드를 작성하는 것을 선호합니다.그래서 리팩토링이 쉬울 줄 알았는데요, 강의 내용을 어떻게든 쑤셔넣으려고 하니까 잘 안되더라구요.. 😭아직은 이론과 실전에 갭이 있다는 뜻이겠죠?더 열심히 해야겠습니다!! 그리고 금요일에 진행된 코드 리뷰는 재미있었습니다. 😆😆😆딴 사람들이 작성한 코드를 편하게 리뷰하는 우빈님한테 감탄했던 것 같네요.역시 세상은 넓고 고수는 많다는?

BE워밍업클럽3기리팩토링테스트코드

워밍업 클럽 3기 BE 클린코드 (2주차 발자국) - 값진 경험을 하다

값진 경험을 하다대학교 때부터, 취준시절, 그리고 실무를 하고 있는 현재까지 단 한번도 객체지향적인 사고 방식으로 리팩토링을 하겠다고 마음먹었던 적은 없었다. 하지만 이번주에 처음으로 객체지향을 적용시키는 경험을 하였고, 이는 굉장히 값진 경험이었다. 배웠던 내용을 요약하자면.. 지난주 커리큘럼은 이론 위주였다면, 이번 주 커리큘럼에서 배울 수 있었던 것은 미션을 통한 리팩토링 실전 경험이었다. 리팩토링/클린코드에 필요한 자잘한 팁 (주석, 패키지 분리 등등..) 도 배울 수 있었지만 미션에서 얻었던 지식 적용 경험(?) 이 나에게는 가장 큰 수확이었다. 미션과정에서 느꼈던 감정은 아래에 정리해보도록 하겠다.미션 과정에서 느꼈던 점3일정도 미션 내용을 수행하기 위해 노력했기에 그만큼 얻었던 점들이 많았던 것 같다.특히 중복 로직 제거, 일급 컬렉션 적용, 추상화의 수준을 맞추는 메서드 추출 경험은 나 자신에게 칭찬할만한 경험이라고 생각했다. 실제로 지식공유자이신 박우빈님과 일치된 생각으로 비슷하게 리팩토링이 이루어졌다는 것을 확인했을 때, 참 뿌듯했다. 또한, 일급 컬렉션이라는 개념 자체를 이번 강의에서 처음 알게 되었는데 이를 실제 리팩토링 과정에 적용해보면서 필요 이유를 직접적으로 느끼게 되면서 실무에도 적용해보는 순간을 기대하는 중이다.하지만, 부족했던 점도 도드라졌다. 특히 SRP/OCP부분이다. 나름대로 객체의 역할을 분리하기 위해 노력했는데 생각보다 이를 적용한 포인트는 한,두군데 밖에 되지 않았다. 고작 csv 파일을 parsing하기 위한 CsvParser 객체를 둔 점..? 그정도인데, 내가 생각했던 것 보다 기존 코드에서 객체의 역할을 분리할 곳이 훨씬 많았다는 점에서 좀 아쉬웠다. 뭔가 기존 코드를 바라보는 습관때문이었던 것 같다. 한번도 객체지향적으로 리팩토링해보려고 하지 않았으니, 이러한 시야가 트이지 않았던 점이 문제가 아닐까 싶다. 그래서 지금부터라도 그런 시야를 키워보기로 했다! 그리고 지식고유자님이 StudyCafePass라는 인터페이스를 두고 SeatPass와 LockerPass를 구현한 점에서 신선한 충격을 느꼈다. 아, 이래서 인터페이스를 쓰는거구나 라는 확실한 감이 생겨서 비록 실질적으로 내가 구현한 내용은 아니지만, 왠지 모르게 나도 다음엔 이렇게 해볼 수 있을 것 같다. 라는 자신감을 가졌다! 좋은 경험을 할 수 있었던 한주였다.다음주부터 테스트코드를 작성해볼 텐데, 기존에 내가 테스트코드에 대해 가지고 있던 상식들이 또 어떻게 부숴질지 기대하는 중이다.  

백엔드리팩토링클린코드

세뇨르

워밍업 클럽 2기(클린코드, 테스트코드) 과제 (Day 4)

Readable Code: 읽기 좋은 코드를 작성하는 사고법(링크)코드 리팩토링기존 코드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 (order == null) { log.info("주문 정보가 없습니다."); return false; } if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } if (order.isInvalidTotalPrice()) { log.info("올바르지 않은 총 가격입니다."); return false; } if (order.hasNoCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } return true; }public class Order { private List<Item> items; private double totalPrice; private CustomrInfo customerInfo; public boolean hasNoItems() { return items == null || items.isEmpty(); } public boolean isInvalidTotalPrice() { return totalPrice <= 0; } public boolean hasNoCustomerInfo() { return customerInfo == null; } }SOLID 정리SOLIDSRPSingle Responsibility Principle (단일 책임 원칙)하나의 클래스는 단 한 가지의 변경 이유만을 가져야 한다.객체가 가진 공개 메서드, 필드, 상수 등은 해당 객체의 단일 책임에 의해서만 변경 되어야 함.관심사의 분리높은 응집도, 낮은 결합도OCPOpen-Closed Principle (개방-폐쇄 원칙)확장에는 열려 있고, 수정에는 닫혀 있어야 한다.기존 코드의 변경 없이, 시스템의 기능을 확장할 수 있어야 한다.추상화와 다형성을 활용해서 OCP를 지킬 수 있다.LSPLiskov Substitution Principle (리스코프 치환 원칙)상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.LSP를 위반하면, 상속 클래스를 사용할 때 오동작, 예상 밖의 예외가 발생하거나, 이를 방지하기 위한 불필요한 타입 체크가 동반될 수 있다.ISPInterface Segregation Principle (인터페이스 분리 원칙)클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.인터페이스를 잘게 쪼개야 함ISP를 위반하면, 불필요한 의존성으로 인해 결합도가 높아지고, 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.DIPDependency Inversion Principle (의존성 역전 원칙)상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조하는 것 의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화에 의존하는 것저수준 모듈이 변경되어도, 고수준 모듈에는 영향이 가지 않는다.

백엔드클린코드SOLID리팩토링

채널톡 아이콘