블로그

워밍업 클럽 그리고 그 이후 테스트코드와 클린코드 나의 생각

4주간의 스터디… 그리고 그 이후 나는 도대체 무엇을 하고 있을까? 지금 현재 내가 배운 내용 그리고 그 전과 그 이후과 얼마나 달라졌을까 비교하며, 회고를 하고 다시 한 번 마음을 다잡아보려한다.클린코드의 적용이 부분은 개인적으로 원래 잘하고 있던 부분이였다고 생각한다.추상화의 고민, 관심사를 어떻게하면 공통으로 분리할까 나를 위한 읽기 쉬운 코드를 어떻게 만들까?물론 이러한 영향을 준 것은 회사내 동료들의 아름다운 코드들과 아름답지 못한 코드들이고, 그 방법론은 클린코드, 리팩토링 등등 고전적인 책들과 다양한 읽기좋은 코드에 관련된 서적을 읽으면서 내 나름의 규칙을 만들기 시작했다.한 달이 지난 지금 내가 잘했던 그 부분은 유지하되, 새로운 법칙이 생겼다.나만의 용어 사전사실 제일 못하는게 이름 짓는 부분이다.아직도 못한다.하지만, 이러한 이름을 더 효과적으로 짓기 위해서, 강사님이 말씀해주신 풀에서 꺼내서 쓰는 용어사전을 차츰차츰 나름 정리하기 시작했다.이러한 용어들은 잘 정리된 오픈소스들에서 찾아보려고 노력을 많이 하였다.과감한 오버로딩오버로딩에 대해 사실 병적으로 싫어하던 시절이 있었다.파라미터에 따라 그 메서드 이름이 참 보기 어려워서 장황한 메서드를 만든적도 있다.public User createUserByNameAndYearAndAge(String name, int year, int age); public User createUserByName(String name); 하지만, 문득 자바 Lists 인터페이스를 보게되었고 중요한것은 오버로딩하나로 가독성만 확보되면 된다는 부분이었다.주석 사용논쟁이 가장 많은 부분 중 하나로서, 주석을 사용하는가? 사용하지않는가? 라는 부분이다.나는 후자에 가까운 개발자로서, 잘 설계된 코드는 주석보다 높은 가치를 한다고 믿었다…그런데, 사실 잘 설계된 코드란 것은 그 때 내가 그렇게 믿는 것 뿐이지, 후대에 내가 다시보면 이해 못하는 코드가 많다.결국 이러한 부분에 대한 이해를 돕는데는 주석을 적절히 활용하거나, 애초에 간결한 메서드 이름을 짓는 방법 밖에 없다.테스트 코드테스트 코드는 정말로 이 스터디의 전과 후과 눈에 띄게 달라졌다고 해도 과언이 아니다.이전의 테스트 코드는 사실 동료와의 테스트 코드에 대한 고민, 그리고 클린코드를 읽고 테스트코드의 필요성과 리팩토링등 다양한 실무에 부딪히면서 나의 필요에 따라서 테스트 코드를 작성했다면, 강의 이후는 테스트코드를 하나의 테스트 도구로서 활용을 제대로 하고 테스트코드의 장점과 강점에 대해서 나름대로 정확하게 이해를 하게 되었다.물론, 회사에서 내가 이렇게 테스트코드를 적극 도입하고 도입한 테스트코드에 대해 공유하며 나의 테스트코드에 대한 철학을 좀 더 고도화 시키는 부분들이 도움이 되었다.TDD대부분의 개발자들 (5년미만의 개발자들)은 TDD에 대해 환상을 가지고 있다.TDD에 대한 환상을 가지고 있다면, 일단 한 번 도전해 보는 것을 추천한다.물론 TDD를 위해 기존 잘 돌아가는 프로젝트를 아예 끄집어서 뒤엎는 것 보다.사내에 도움이 될만한 프로덕트를 만들면서 TDD를 적용하는 것을 추천한다.참고로 나는 TDD를 위해 백오피스를 했다 😃내가 생각하는 TDD (테스트 코드)TDD를 도입했는데, 내 생산속도가 느려졌어 라는 이야기를 종종 듣는다.TDD가 아닌 테스트코드도 마찬가지다.일단, 강사님이 말씀해주신 부분 중 하나 모든 영역을 테스트 커버하려고 하지않아도 된다. 라는 이야기를 세션 도중에 하셨다.또한, 테스트 코드를 사용하면서 하나의 철학이 생겼다.나의 테스트 코드는 ‘실용주의 테스트 코드’ 라는 철학으로 정의할 수 있다.물론, 저 책에서 영감을 받은 것은 아니고… 그냥 단순한 나의 원칙이다.실용주의 테스트 코드를 위한 나의 원칙테스트 커버리지에 매몰되지 말자주니어 개발자들은 이러한 커버리지에 매몰되는 경우가 상당히 많이 있다. ”테스트 코드 95% 커버리지” 물론 좋다고 생각한다. 근데, 95%의 커버리지를 채우는 시간보다. 차라리 로직에 대해서 완성도를 높이거나, 그 시간을 어느정도 분배하는게 맞다고 생각한다. 물론, 혼자서 하는 프로젝트에 대해서는 95%든 100%든 자기 만족이기에 상관없다고 생각들지만, 비즈니스 프로덕트에 커버리지에 매몰되는 것 보다. DDD에서 어떤 영역에 테스트 코드를 주로 작성했고, 왜 그 영역에 테스트 코드를 작성했는지에 대한 고민과 철학을 녹여내는게 중요하다.테스트 코드는 유지보수가 필요하다테스트 코드에 대해 관심을 가지게 해주는 동료와 고민했던 부분은 테스트 코드를 과연 유지보수를 적게 설계 하려면 어떻게 해야할까 라는 이야기를 많이 했었다. 사실 이러한 부분들 때문에, 테스트 코드를 작성할 때마다 ‘어짜피 이거 곧 깨질텐데…’ 라는 생각을 하게 되었다. 중요한 것은 깨지는게 맞는거 같다. 깨지면 고치면되고, 깨진다는 것은 그에 대한 사이드 이펙트들도 확인 할 수 있다는 점이다. 코드는 생물학과 마찬가지로 끝 없이 진화하고 테스트 코드는 그에 대한 가설이다. 가설은 어떠한 부분을 검증하기 위해 깨어질 수도 있고 새로 정의되어질 수도 있다. 컴퓨터는 공학적인 측면도 강하지만 과학적인 측면도 강하다.테스트코드는 가설이다테스트코드는 가설이기 때문에, 실제로 가설을 세우기 위해서는 이 가설이 맞는지에 대한 전반적인 이해가 필요하다. 예를 들어 내가 진화론이라는 가설을 새울 떼 유전진화학에 대한 학문에 대한 이해가 피상적으로 있는 상태에서는 가능할까? 사실, 불가능에 가깝다. 좋은 테스트 코드란 테스트 코드를 작성하기 위한 그 도메인에 대한 전반적인 이해도를 요구하고 수반한다. 실용주의적인 테스트 코드는 테스트 코드를 통해 도메인에 대한 지식도 높이는데 기여한다라는 생각에 기반한다.성장끝으로 이번 세션 이후로 나의 성장이 많이 되었다는 것도 느끼지만 회사에서 코드가 깔끔할 뿐만 아니라 테스트 코드까지 합리적인 이유와 검증가능한 코드다 라는 이야기를 들었다.그리고, 최근 1000줄 가까이 되는 레거시의 가독성을 확보하고, 그에 대한 테스트 코드로 검증하는 과정을 걸치면서 코드의 현대화도 많이 이루어냈다.레거시라는 것은 개인적으로 이렇게 정의하려고한다.‘가장 중요하고 필요한 기능이지만, 그 누구도 고치려고 하지 않은 미지의 영역’바꿔말하면 가장 중요하기에 나만 알아야하는게 아닌 모두가 알아야하고 모두가 아는 비용을 줄이기 위해서, 고민을 한다면 그리고 이게 합리적 이유라면 클린코드와 테스트코드 도입하지 않을 이유가 없지 않을까?

백엔드테스트코드클린코드인프런워밍업클럽

lkwo

워밍업 클럼 3기 BE 클린코드 1주차 발자국

1주차를 마무리하며...업무와 같이 할 수 있겠지 싶었는데, 강의의 내용이 알차서 생각보다 힘들었습니다.코드를 따라치며 수강했지만... 사실 제대로 이해 못하고 따라만 친 부분도 상당히 있었는데,다시 내가 직접 리펙터링 해가며 따라가야지 다짐을 하며 1주차 발자국을 남깁니다.강의 정리클린코드란 무엇인가라는 질문으로부터 강의가 시작됩니다.좋은 코드란 무엇일까요?저는 강의의 내용을 듣고 이렇게 정리했습니다.좋은 글과 마찬가지로 클린코드는 잘 읽히는 코드한 문장 한 문장에 주제가 또렷해 전달하고자 하는 내용을 읽는 이에게 정확하게 전달하는 코드강의에서 중간중간에 선조와 후손이라는 용어를 사용하십니다. 내가 쓰는 코드가 꼭 내것만은 아니라는 점을 계속 상기시켜주시면서, 공용의 코드를 어떻게 다뤄야할지는 말씀해주시는게 많이 도움이 될 것 같습니다.추상화추상이란 '사물을 정확하게 이해하기 위해서 사물이 지니고 있는 여러 가지 측면 가운데서 특정한 측면만 가려내어 포착하는 것'을 말합니다.'비가온다' 라는 표현을 '대기내에 수증기가 응결하여 물방울로 변하고, 이 물방울이 중력에 의해서 지표면으로 떨어진다'라고 친구한테 말하면 갸우뚱할 것입니다.이처럼 추상은 중요한 정보만 남겨 전달함으로써 상대방의 이해를 돕는 기능을 하는걸 실생활에서도 볼 수 있습니다.코드에서의 추상화그러면 코드에서는 어떻게 중요한 내용만 가려내고, 구체적인 내용은 숨겨 읽는 이의 이해를 도울 수 있을까요?강의에서 나온 내용들 중 기억에 남는 방법들을 나열해봅니다.코드 블럭내에서 추상화 레벨을 통일하기객체에 적절한 역할과 책임을 부여하고 메시지를 통한 협력을 하는 구조로 만들기SOLID 원칙을 생각해가며 코드를 작성하기객체지향의 다양한 기법을 활용하기상속과 조합Value Object컬렉션으로 포장한 일급컬렉션 사용하기Enum다형성  그리고 읽는 사람을 배려한 다양한 코드 작성법을 나열해봅니다.변수, 메서드 추상화를 통해 이름 짓기매직 넘버, 매직 스트링 상수화하기공백 라인 활용해 블럭내 코드 분간해주기조건문에 부정어 연산자 `!`줄이기

백엔드클린코드

10

우빈님의 세심한 코드 리뷰 - 인프런 워밍업 클럽 3기 백엔드 코드 ✨

인프런 워밍업 클럽 3기 백엔드 코드 발자국 1주차 🙊 시작이 반이다... 벌써 중간점검 ?인프런 워밍업 클럽이 시작한지 2주가 지났다.. 앞으로 남은 발자국이 2개 뿐이다.. 👣이번주에는 중간점검으로 온라인 라이브가 진행 되었다. 온라인 라이브에 대한 내용은 다음과 같다.미션 Day 4 에 대한 공통 피드백Q&A에 대한 답변미션 Day 7 코드리뷰 진행각 세션은 놀라울 정도의 세심한 우빈님의 피드백 덕분에 많은 인사이트를 얻게 되어 좋은 시간이었다. 나도 첫번째로 코드리뷰를 신청하고 라이브 마지막에 코드리뷰를 받았는데 너무 좋은 경험이었다. 💪(커피를 좋아하시기로 유명한 우빈님께 Q&A 세션 도중 저가 커피 브랜드 중 어디 브랜드가 제일 맛있냐는 질문이 나왔는데.. 드셔 보신 적이 없다고 답변해 주신 부분이 인상적이었다.. ㅋㅋ 😁)✨ 우빈님의 세심한 코드 리뷰위에서 이야기했듯이 코드리뷰를 첫번째로 신청해서 우빈님께 온라인 라이브 시간에 코드리뷰를 받았다.해당 미션은 "스터디 카페" 프로그램을 리팩토링하는 미션이였는데.. 작년 4분기에 강의를 수강했을 당시에 3번이나 진행하였다.첫 번째 리팩토링, 강의를 듣기 전에 리팩토링두 번째 리팩토링, 강의를 수강하며 리팩토링세 번째 리팩토링, 강의를 수강 후 정리하며 다시 리팩토링이번이 네 번째 리팩토링이였는데 할 때마다 왜 새로운 것인지.. 🥲그래도 손이 기억이라도 한 듯 나름 순조롭게(?) 미션을 진행하게 되었고, 추가적으로 리팩토링을 진행하였고 해당 부분을 리뷰를 받고 싶어 신청하게 되었다.(우빈님께서 4번이라는 부분에 놀라셨는지(?).. 디스코드 스레드에 댓글을 남겨주셨다! 🤣)다시 돌아와서.. 코드리뷰 받은 내용은 아래와 같다.1⃣ 중요 도메인 StudyCafePassType의 구조화 🔗 Github PR 링크StudyCafePassType 구조화 리팩토링♻ 리팩토링 코드public enum StudyCafePassType implements PassTypeSelectable, PassTypeFormatter { // 📝 인터페이스 구조화 HOURLY("시간 단위 이용권") { // 📝 사용자 입력에 대한 구조화 @Override public boolean selected(String userInput) { return "1".equals(userInput); } // 📝 사용자 출력 포맷에 대한 구조화 @Override public String format(StudyCafePass pass) { return String.format("%s시간권 - %d원", pass.getDuration(), pass.getPrice()); } } }✏ 우빈님 리뷰Q. 클래스 내부에서 사용자 입력값 및 출력값에 사용하는 인터페이스를 구현함으로써 오버 엔지니어링이 된 것 같은 느낌이 드네요.. 🤦‍♂️ A. 저도 그렇게 생각해요.. ㅋㅋㅋㅋ 오버 엔지니어링이기보다 PassType은 중요한 도메인 모델인데, Input에서만 의미를 가지는 사용자 선택지가 침투하고 있다. 사용자 선택 방법이 "a", "b", "c"로 바뀐다면? 단순히 입력 방식을 바꿨을 뿐인데 무료 도메인 모델이 수정되어야 하는 엄청난 사태가 발생한다. 항상 구조화를 하는 것이 정답은 아니다. Output format도 마찬가지이다. 책임이 우선이다. 적절한 책임의 분배가 객체의 결합도를 낮추고 응집도를 높이는 것이다. 🤔 돌아보기단순히, OCP를 적용하기 위해 접근해서 리팩토링 했었는데..적절한 객체 책임 분리를 하지 못했으며, 중요한 도메인 모델을 수정하는 엄청 큰 사이드 이펙트가 일어날 수 있다는 점을 간과 했다는 것이다.다음부터는 구조화를 남발하지 않고 책임에 집중해서 리팩토링 해야겠다..2⃣ 이용권을 읽는 부분과 읽은 부분의 개념을 추출하여 객체 분리 🔗 Github PR 링크ReadLockerPasses 객체 분리♻ 리팩토링 코드public class ReadLockerPasses { // 📝 LockerPasses를 해석하는 객체 분리 private final List<StudyCafeLockerPass> passes; private ReadLockerPasses(List<StudyCafeLockerPass> passes) { this.passes = passes; } // 📝 lines을 해석하여 List<StudyCafeLockerPass> 객체를 만들어준다. public static ReadLockerPasses ofLines(List<String> lines) { List<StudyCafeLockerPass> passes = lines.stream() .map(ReadLockerPasses::ofLine) .toList(); return new ReadLockerPasses(passes); } private static StudyCafeLockerPass ofLine(String line) { String[] values = line.split(CSV_SPLITTER); // ⭐️ CSV라는 방식에 종속적 StudyCafePassType studyCafePassType = StudyCafePassType.valueOf(values[0]); int duration = Integer.parseInt(values[1]); int price = Integer.parseInt(values[2]); return StudyCafeLockerPass.of(studyCafePassType, duration, price); } // 📝 StudyCafeLockerPasses를 생성해준다. public StudyCafeLockerPasses toPasses() { return StudyCafeLockerPasses.of(passes); } }✏ 우빈님 리뷰Q. 일급 컬렉션을 적용하기 위해 toPasses() 메서드를 생성했는데 Read~라는 네이밍을 가진 클래스에 많은 책임이 부여된 것 같아 네이밍이 모호한 것 같습니다. 좋은 방법이 있을까요..? 🧐 A. 많은 책임이라고 생각하신 이유가 있을까요? "ReadLockerPasses는 어디선가 읽은 lines를 가지고 StudyCafeLockerPasses를 만들어준다"의 책임으로 보여서, 어색하지 않으며 테스트 코드 작성도 가능하다. 그와 별개로 CSV라는 방식에 종속되어있다. CSV형식이 다른 방식으로 바뀌었을 때 같이 바뀌어야 하는 부분이 CSV_SPLITTER 부분이다. 의도한 것 이라면 상관없다. 🤔 돌아보기Read라는 클래스명을 가지고 있어 StudyCafeLockerPasses를 생성해주는 메서드가 존재해 많은 책임이 있다고 생각했는데..우빈님 리뷰 이후에 다시 보니.. 그렇게 어색한가 싶기도 하다.. ㅎㅎ해당 클래스의 작성 당시 CSV 방식을 의존하려는 의도는 없었다. 단순히 읽은 부분의 개념을 추출한 것인데.. 위의 클래스는 CSV_SPLITTER상수가 사용되어 의도하지 않게 CSV라는 방식에 종속적이게 된 것이다.CSV라는 방식이 변경되면 객체 로직이 바뀌어야 한다.객체 구현 시, 종속성에 대해서 방어적으로 접근할 필요가 있어보인다.3⃣ ProvideException 커스텀 예외🔗 Github PR 링크ProvideException 커스텀 예외♻ 리팩토링 코드// 📝 이용권을 가져오는 과정에서 생긴 에러의 커스텀 예외 클래스 생성 public class ProvideException extends RuntimeException { public ProvideException(String message) { super(message); } } public class StudyCafePassMachine { public void run() { try { outputHandler.showPassOrderSummary(order); } catch (AppException e) { outputHandler.showSimpleMessage(e.getMessage()); } catch (ProvideException e) { // 📝 커스텀 예외 catch outputHandler.showSimpleMessage("이용권을 제공받을 수 없습니다."); } catch (Exception e) { outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다."); } } }✏ 우빈님 리뷰Q. AppException 성격이랑 다른 것 같다고 생각되어 Provider 인터페이스에서 발생하는 예외 클래스 ProvideException를 별도로 생성하였습니다. A. 혹시 어떻게 다르다고 생각셨나요?? AppException의 의도는, 프로그램에서 발생할 수 있는 대부분의 애플리케이션 상황을 정의하는 최상위 예외 클래스이다. 만약 ProvideException을 별도로 표기하여 더 구체적인 상황을 나타내고 싶으면, AppException을 상속받아서 구성해야 한다. 그렇지 않으면 커스텀 예외 클래스가 늘어남에 따라 catch절도 같이 늘어날 것이다. 추가적으로, "이용권을 제공받을 수 없습니다."라는 메시지가 사용자 친화적이지 않다. 🤔 돌아보기리팩토링 당시, 초기 이용권을 가져와야만 프로그램이 실행된다는 관점에서 ProvideException의 커스텀 예외 클래스를 작성하였다.하지만 이용권을 가져오는 부분은 프로그램 내부에서 필요한 시점마다 호출하고 있어우빈님 리뷰대로 AppException 클래스를 상속받아서 작성하는 것이 더 나은 설계 같다.예외 메세지도 사용자 관점에서는 친화적이지 않은 것이 분명하다.내가 키오스크 시스템을 사용하다가 저런 메세지를 마주한다면... 화가 날 것 이다... 😡프로그램의 의도를 정확히 파악할 필요가 있어보인다. 또한 예외 메세지도 누가 보는지에 따라 고민해보는 습관을 길러야겠다.이렇게, 요청한 3개의 리뷰와 2개의 추가 리뷰를 받아 보았다..고작 3일 만에 7명이나 리뷰를 해주셨는데 세심하고 또 세심했다... 퀄리티가 상당했다.. ✨이번 온라인 라이브를 통해 우빈님에 대한 팬심과 존경심이 더욱 커졌다....! 📈리뷰해주신 내용으로 다시 리팩토링을 함으로써 한층 더 Readable Code에 대한 성장을 경험할 수 있었다. 🚀💡 자기만의 언어로 강의 키워드 정리하기 섹션 6. 코드 다듬기좋은 주석 - 주석의 양면성주석이 많다는 것 : 추상화가 덜 되고 가독성이 좋지 않은 코드 (코드 품질 저하 📉)주석이 필요한 경우 : 히스토리를 알 수 없을 경우, 주석으로 상세히 설명변수와 메서드 나열 순서변수 : 사용하는 순서대로 위치한다. (인지적 경제성 / 뇌 메모리 줄이기)객체의 공개/비공개 메서드 : 공개 메서드를 상단에 위치하고, 비공개 메서드 하단에 위치한다. 공개 메서드 중에서도 중요도의 순서에 따라 배치한다.공개 메서드 : 객체의 상태를 변경 하는 부분이 가장 상단에 위치하도록 - 상태 변경 >>> 판별 >= 조회비공개 메서드 : 출현한 순서대로패키지 나누기여러 파일들의 네임 스페이스를 관리하기 때문에 적당한 수준으로 잘 나누어야 한다.대규모 패키지 변경은 팀원과의 합의 필요 -> 추후 conflict가 생길 수 있다.기능 유지보수하기정렬 단축키, linting, style - sonarlint, editorconfig섹션 7. 리팩토링 연습메서드 추출로 추상화 레벨 맞추기Optionalreturn null / Optional 파라미터 사용은 안티패턴이다.객체에 메시지 보내기객체를 존중하고 메시지를 보내자.객체의 책임과 응집도⭐️ 추상화 관점의 차이 - FileHandler구현에 초점을 맞춘 추상화 VS 도메인 개념에 초점을 맞춘 추상화File을 read하는 부분의 로직들은 전부 FileHandler에 들어갈 것이다. 잘못된 객체 응집일 수도 있다..방법에 초점을 맞춘 설계 방식이 아닌 어떤 데이터를 가져오는 가에 대한 초점을 맞추는 것이 좋다.섹션 8. 기억하면 좋은 조언들능동적 읽기가지고 있는 리팩토링 기법들을 총동원해서 읽자. -> 리팩토링하면서 읽기눈으로만 보는 수동적 읽기는 권장하지 않는다.도메인 지식을 늘리기 위해서 능동적 읽기가 필요하다. (작성자의 의도 파악)오버 엔지니어링필요한 적정 수준보다 더 높은 수준의 엔지니어링예시 1. 구현체가 하나인 인터페이스구현체가 수정할 때마다 인터페이스도 수정해야 함코드 탐색의 어려움예시 2. 너무 이른 추상화정보가 숨겨지기 때문에 복잡도가 높아진다.후대 개발자들이 선대의 의도를 파악하기가 어렵다.은탄환은 없다클린 코드도 은탄환이 아니다.실무에서의 줄다리기지속 가능한 소프트웨어 품질 VS 기술 부채를 안고 가는 빠른 결과물 -> 클린 코드를 대비한 코드 센스가 필요하다.모든 기술과 방법론은 적정 기술의 범위 내에서 사용되어야 한다.항상 정답인 기술은 없다.한계까지 연습해보고, 적정 수준, 적정 시점을 깨닫는 것이 필요하다.섹션 3. 단위 테스트단위 테스트작은 코드(클래스 또는 메서드) 단위를 독립적으로 검증하는 테스트 -> 가장 기본이 되는 테스트검증 속도가 빠르고, 안정적수동 테스트, 자동화 테스트 -> 인지 필요사람이 검증하는 수동 테스트 -> sout으로 출력하고 눈으로 직접 확인기계가 검증하는 자동화 테스트Junit5, AssertJJunit5 : 단위 테스트를 위한 테스트 프레임워크AssertJ : 테스트 코드 작성을 원할하게 돕는 테스트 라이브러리 - 풍부한 API 메서드 체이닝 지원해피 케이스, 예외 케이스 -> 테스트 케이스 세분화예외 케이스 : 암묵적 혹은 드러나지 않은 요구사항에서 발견경계값 테스트범위, 구간, 날짜 경계값들로 테스트를 해야한다.테스트하기 쉬운/어려운 영역 (순수함수)테스트 하기 어려운 영역을 구분하고 분리하기외부로 분리할수록 테스트 가능한 코드는 많아진다.테스트하기 어려운 영역관측할 때마다 다른 값에 의존하는 코드 : 현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력외부 셰계에 영향을 주는 코드 : 표준 출력, 메시지 발송, 데이터베이스에 기록하기순수 함수 - 테스트하기 쉬운 영역같은 입력에는 항상 같은 결과외부 세상과 단절된 형태lombok@Data, @Setter, @AllArgsConstructor 지양양방향 연관관계 시 @ToString 순환 참조 문제섹션 4. TDD: Test Driven DevelopmentTDD 테스트 주도 개발 (Test Driven Development)로, 프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론선 기능 구현, 테스트 작성의 문제점 (일반적인 개발) - 구현순서 : 기능 -> 테스트테스트 자체의 누락 가능성해피 케이스만 검증할 가능성잘못된 구현을 다소 늦게 발견할 가능성선 테스트 작성, 기능 구현 (TDD) - 구현순서 : 테스트 -> 기능복잡도(유연하며 유지보수가 쉬운)가 낮은 테스트 가능한 코드로 구현할 수 있게 한다.테스트가 힘든 코드를 위한 코드 작성이 가능예를 들면 LocalDateTime.now()의 경우 외부세계로 분리해서 테스트를 하기 편한 코드를 작성할 수 있다.프로덕션 코드를 작성한 후 테스트 코드를 작성하기 귀찮을수도..쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백을 받을 수 있다.과감한 리팩토링이 가능해진다.테스트 코드가 보장TDD의 관점이전의 관점 : 테스트는 구현부 검증을 위한 보조 수단변화된 관점 : 테스트와 상호 작용하며 발전하는 구현부레드 - 그린 - 리팩토링Red : 실패하는 테스트 작성Green : 테스트 통과 하는 최소한의 코딩Refactor : 구현 코드 개선 테스트 통과 유지애자일 방법론 vs 폭포수 방법론애자일 방법론 https://agilemanifesto.org/iso/ko/manifesto.html반복적 개발(Iterative Development): 짧은 개발 주기(스프린트)를 반복하며 지속적으로 개선.유연성: 요구사항 변경을 수용할 수 있도록 유동적으로 진행.고객 참여: 개발 과정에서 지속적인 피드백을 반영하여 사용자 중심 개발 가능.자율적인 팀 구성: 개발팀이 자체적으로 의사 결정을 하며, 빠르게 문제를 해결함.폭포수 방법론단계적 개발: 요구 분석 → 설계 → 구현 → 테스트 → 배포 → 유지보수 순서로 진행됨.문서 중심: 각 단계마다 문서화가 철저하게 이루어짐.선형 구조: 이전 단계가 완료되어야 다음 단계로 넘어갈 수 있음.변경이 어려움: 초기에 요구사항을 확정하면 이후 변경이 어렵고 비용이 많이 듦.익스트림 프로그래밍XP(Extreme Programming, 익스트림 프로그래밍)는 애자일 방법론 중 하나로, 빠른 개발 주기와 지속적인 피드백을 중심으로 하는 소프트웨어 개발 방법론이다. 고객의 요구사항 변화에 빠르게 대응할 수 있도록 짧은 개발 반복 주기(Iteration)와 강한 협업 문화를 강조한다.스크럼, 칸반스크럼애자일 프레임워크로, 일정한 스프린트 동안 작업을 계획하고 진행하는 반복적이고 점진적인 개발 방식이다. 짧은 개발 스프린트를 통해 빠르게 결과물을 만들고 지속적으로 개선하는 것이 핵심이다.1⃣ 백로그 작성 – 제품 백로그에 모든 요구사항을 정리2⃣ 스프린트 계획 – 스프린트 기간 동안 수행할 작업 선정3⃣ 스프린트 진행 – 개발 진행 및 매일 스탠드업 미팅4⃣ 스프린트 리뷰 – 개발 완료된 기능을 검토5⃣ 회고(Retrospective) – 개선점을 찾고 다음 스프린트에 반영칸반Workflow와 가시성을 중심으로 한 애자일 프레임워크로, 지속적인 개선과 작업량 관리를 중점적으로 다룬다. 작업을 시각적으로 표현하여 현재 진행 상황을 쉽게 파악할 수 있도록 합니다.1⃣ Backlog: 해야 할 작업을 모아둠2⃣ To Do: 현재 진행할 작업3⃣ In Progress: 진행 중인 작업4⃣ Review/Test: 리뷰나 테스트가 필요한 작업5⃣ Done: 완료된 작업섹션 5. 테스트는 []다.테스트 코드는 문서다.프로덕션 기능을 설명해주는 것이 테스트 코드 문서다.다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완할 수 있다.고민했던 내용(테스트 코드)을 팀 자산(소스 코드)으로 공유할 수 있다.@DisplayName - 도메인 정책, 용어를 사용한 명확한 문장메서드명만으로 어떤 것을 검증하고자 하는 의도 파악이 어려움Junit5에 추가한 어노테이션이다.문장 형태로 섬세하게 테스트 검증에 대한 내용을 어노테이션안에 작성한다.섬세한 DisplayName특정 시간 이전에 주문을 생성하면 실패한다. ❌영업 시작 시간 이전에는 주문을 생성할 수 없다. ✅도메인 용어를 사용하여 추상화된 내용을 담기 -> 메서드 자체의 관점 보다 도메인 정책 관점 (특정 시간 -> 영업 시작 시간 ✅)테스트의 현상을 중점으로 기술하지 말 것 (~실패한다 ❌)Given / When / Then - 주어진 환경, 행동, 상태 변화Given : 시나리오 진행에 필요한 모든 준비 과정When : 시나리오 행동 진행Then : 시나리오 진행에 대한 결과 명시, 검증TDD vs BDDBDDTDD에서 파생된 개발 방법시나리오 기반한 테스트 케이스 자체에 집중하여 테스트한다.Junit vs SpockSpock은 Groovy언어로 BDD 패턴을 적용해서 테스트 코드를 작성할 수 있다.언어가 사고를 제한한다.명확하지 못한 테스트 코드는 사고를 제한할 수 있다.문서로서의 테스트를 신경 쓸 필요가 있다. 🏃 돌아보며..미션과 발자국을 정신없이 진행하다 보니 벌써 남은 인프런 워밍업 클럽도 2주밖에 남지 않았다.처음에 OT 라이브 당시 러너가 120명 정도였는데, 이번 중간 점검 라이브 때는 60명 정도로 줄어들었다.강의 내용 자체는 어렵지 않지만.. 2개의 강의(14시간 + 12시간)를 한 달만에 들으면서 미션과 발자국을 진행하는 건 쉽지 않아 보인다..나는 미리 강의를 수강해서 다행이다라는 생각이 든다.. 😅하지만, 쉽지 않은 만큼 성실히 참여한다면 단기간 내 성장하는 데 큰 도움이 될 것이다.그리고 이번 주에 코드리뷰를 신청하기 잘했다는 생각이 들었다. 🍀위에서도 여러 번 언급했지만 우빈님의 세심한 리뷰 탓(?)에내가 미션을 수행하는 데 있어 우빈님보다 세심하게 집착 했었나..? 반성하게 된다... 😭마지막 주차 온라인 라이브에서도 테스트 코드에 대한 코드리뷰가 진행된다고 한다.기회가 된다면 또 한 번 코드리뷰를 받아 단골 손님이 되고 싶다. 😂2주를 걸쳐, 읽기 좋은 코드의 스터디 과정은 이번주로 막을 내렸다.다음 주차부터는 테스트 코드에 대해 본격적으로 스터디하는 과정이 진행된다.남은 2주도 화이팅하며 좋은 성장을 이루길 기대해 본다. 🔥끝으로, 3월 중순이 되니 이제 슬슬 봄 내음이 나는 것 같다.. 🌸얼어붙은 개발 시장에도 봄이 찾아왔으면 좋겠다.. 🧊발자국 2주차 끄읕 ![출처]인프런 워밍업 클럽 : https://www.inflearn.com/course/offline/warmup-club-3-be-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

백엔드인프런워밍업클럽백엔드3기발자국박우빈클린코드읽기좋은코드

10 14일 전
lkwo

워밍업 클럽 3기 BE 클린코드 2주차 발자국

2주차를 마무리하며...2주 차에는 예제 코드의 리팩토링을 직접 해보는 과제와 중간 점검이 있었습니다.강의를 보며 따라 할 때는 괜찮았지만, 막상 코드에서 리팩토링을 하려니 무엇부터 시작해야 할지 막막하더군요.리팩토링은 강의만 본다고 해결되는 영역이 아니라는 것을 느꼈고, 체화되기까지 시간이 필요하겠다고 생각한 한 주였습니다.강의내용 정리좋은 주석이란?주석이 많다는 것은 코드에 적절한 추상화를 적용하지 못해, 주석으로 코드를 설명하려는 것은 아닌지 의심해봐야 합니다.좋은 주석이란 코드로 전달할 수 없는 정보를 담는 것입니다.예를 들어, 의사 결정의 히스토리 같은 내용을 기록하는 것이 좋은 주석입니다.또한, 주석도 코드와 마찬가지로 버전 관리가 필요합니다. 관련된 의사 결정이 변경되었다면, 주석도 잊지 말고 함께 수정해야 합니다. 변수와 메서드 나열변수는 사용하는 위치와 최대한 가까이 두어 뇌 메모리 부담을 줄이는 것이 좋습니다.메서드와 변수의 접근자 종류에 따라 위치를 정리할 수 있습니다.메서드는 접근자 종류뿐만 아니라, 중요도와 로직의 종류를 기준으로 배치하면 더 정리된 코드를 만들 수 있습니다. 패키지 나누기패키지는 단순한 디렉터리가 아니라, 문맥적인 정보를 제공하는 역할을 합니다.적절하게 패키지를 나누면 코드의 가독성과 유지보수성이 향상됩니다.  IDE 활용하기정렬 단축키를 활용하여 코드 스타일을 통일합니다.Linting & 스타일 도구를 사용하여 코드 품질을 높일 수 있습니다.SonarLintEditorConfig리팩토링 직접 해보기리팩토링리팩토링에는 합리적인 이유가 필요하다리팩토링을 할 때는 단순히 코드 줄 수를 줄이는 것이 목표가 아니라, 객체의 역할과 책임을 올바르게 분배하고, 적절한 추상화를 적용하는지가 중요합니다. 이를 위해 리팩토링 과정에서 내가 합리적으로 수정하고 있는지 계속 의심하며 진행했습니다. (제가 정말 합리적으로 했는지는 모르겠습니다만...) 리팩토링 순서 정하기처음 코드를 봤을 때, 어디서부터 손을 대야 할지 막막함을 느꼈습니다. 그래서 나름의 리팩토링 순서를 정하고 진행해보았습니다.내가 읽기 좋은 코드로 변경하기공백 추가하기 (가독성 개선)함수 분리하여 추상화하기테스트하기 쉬운 구조로 변경하기역할과 책임을 고려하여 인터페이스와 객체로 분리하기 가독성 개선하기패키지 분리하기   리팩토링에는 정답이 없다리팩토링은 정해진 답이 없다는 점이 가장 어렵게 느껴지는 부분인 것 같습니다.  중간점검Day 4 미션 피드백아래는 제가 미션으로 제출한 코드입니다.이번 점검에 피드백으로 얻은 새로운 인사이트를 나열해봅니다.반환 타입이 boolean인 경우, 예외를 발생시키기 전에 해당 메서드의 사용 현황을 먼저 파악한 후, 상황에 맞게 리팩토링해야 한다.예외를 던지는 것은 비용이 많이 들 수 있으므로, 필요할 때만 신중하게 사용해야 한다.  private static boolean VALID = true; public boolean validateOrder(Order order) { if (order.isItemEmpty()) { throw new AppException("주문 항목이 없습니다."); } if (order.isTotalPriceLessThenZero()){ throw new AppException("올바르지 않은 총 가격입니다."); } if (order.hasNotCustomerInfo()) { throw new AppException("사용자 정보가 없습니다."); } return VALID; }Day 7 미션 피드백물론 리뷰를 할 때, 말을 조심스럽게 해야할 테지만, 코드 리뷰의 목적은 다 같이 좋은 코드를 만들어 보자는 좋은 의미입니다.코드 리뷰를 할 때는 인격적인 모독을 하지 않는다.코드 리뷰를 받을 때는 코드와 나를 동일시 하지 않는다.배운 점 정리 정적 메서드 팩토리를 만들 때는 생성자를 private으로 감춘다.null 대신 Empty 객체를 만들어서 처리한다.단수/복수를 신경 써서 변수명과 메서드명을 짓는다.리팩토링에는 정답이 없다. 하지만, 효율적인 구조와 다양한 의견이 있다. 

웹 개발클린코드

정예은

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

강의 수강 노션 링크https://www.notion.so/DAY06-1b2010f075ca80d09e08d4dd35376dd5?pvs=4https://www.notion.so/DAY07-1b3010f075ca80b4945cd230929481ff?pvs=4https://www.notion.so/DAY09-1b4010f075ca805f8e1ec01a7c00b16c?pvs=4https://www.notion.so/DAY10-1b4010f075ca80a7a731da0aa16b8e43?pvs=4출처[워밍업클럽 리더블코드 ][워밍업클럽 테스트코드] 👣발자국2주차👣🏫배운 내용 🏫주석의 양면성자주 변하는 정보는 최대한 주석 사용 지양하기우리가 가진 모든 표현방법을 총 동원해서 → 코드에 녹여 → 주석 사용 지지뢰찾기 리팩토링게임의 상태를 주석으로 설정하는 대신 → ENUM으로 관리그래서 외부에서 호출해서 사용하기무한루프 반복 구조는 위험특정 상황에서만 반복문 돌도록 바꿔줘야한다.지뢰찾기 같은 케이스에서는 게임이 “진행중” 일때만 반복문 돌 수 있도록 처리하기변수와 메서드의 나열 순서💡상태변경 메서드 >> 판별 메서드 >> 조회 메서드 순으로 나열 하자이때, 메서드 우선순위는 공개 메서드에서 private메서드 순으로 내려와야한다.  자동테스트?그동안 내가 학원에서 배워온 건, 수동테스트 였나보다스프링부트에서 애너테이션을 활용하여 수동사냥만 해왔던 것..인가?그리고 단위로 /unit으로 쪼개서 (메소드,클래스별로) 테스트를 진행 ⇒ 단위테스트그러다보니 검증속도도 빠르고 안정적임  JUnit이란?단위 테스트를 위한 프레임워크 → 퀜트백 프레임워크풍부한API제공해주는 프레임워크로 테스트 코드 작성해보자   JUnit vs assertJ 두개의 차이점AssertJ 의 장점자연어 가까워 가독성이 좋다체이닝 방식이 가능함JUni5의 단점assertEquals는 단순히 "Expected: A, Actual: B" 결과물만 추출함기능이 단순하고 제한적  assertJ 다양한 메서드기본적인 검증 isEqualTo() : 두 값이 같은지 비교 isNull() : 값이 null인지 비교 isTrue() : 값이 true인지 비교컬렉션 검증 hasSize() : 컬렉션의 크기 비교 —> 리스트의 사이즈 찾기 contains() : 컬렉션에 특정 요소가 포함되어 있는지 확인 isEmpty() : 컬렉션이 비어 있는지 확인 🎶경계값 조건 * 정수가 3이상일때, A라는 조건 만족해야함.해피케이스경계값 활용하기 !즉, 3에대한 테스트를 짜보자.5에 대해는 만족하지만, 3이 만족이 안될 수 있잖아 !예외 케이스2로 조건값보다 더 아래쪽 범위로 테스트 하기❗인사이트❗칭찬깃 사용법에 대해 좀더 연구하고, 프로젝트에 적용하며 강의를 따라가려고 노력하였다.깃에 대해 전혀 몰랐던 사람으로,,, 개발 공부하기 위해선 깃 활용이 무척이나 중요하다는 걸 깨달았다.코드리팩토링시 강의를 보며 , 강사님이 로직 처리를 하는 한단계 한단계씩 끊어서 정리하였다.테스트코드 진행시, 테스트하기 어려운 부분을(요구사항에 맞게 테스트 로직을 짰는데, 그 요구사항이 개발하는 시점의 요구사항이랑 충돌이 될때 )잘 이해하고 숙지하며 이 로직에 대해선 따로 분리하여 테스트 코드 관리하는 시야가 필요하다. 아쉬움그러나, 동영상 일시정지를 하고 노션에 정리한다고 한들, 온전히 내것이 되는가? 아쉬움이 남아있다.이상태로 다시 한번 해보세요~ 주어지면 , 아무것도 못한다.내 스스로 코드를 짜보는 학습이 필요할 것 같다.이번 미션11 코드 제출도 어디서 어떻게 시작 해야 할지 막막하다 앞으로 어떻게?지금도 지뢰찾기 코드 마스터 하지도 않고, 미션 제출도 선생님 코드 따라치기만 했었다. 지뢰찾기 로직을 파악하기엔 내 머리가 아직 준비가 안되었고, 내 마음의 여유가 준비되지 않은 상태였다. 현재 국비학원 졸업작품으로 팀프로젝트를 지난주에 시작 하다보니, 우선순위는 팀포폴이다. 그래서 시간을 내어 지뢰찾기 코드를 마스터 하기에는 조금은 어려울 듯 하여 ,,,, 팀포폴이 어느정도 마무리가 되어가면 그때 지뢰찾기 자바 코드 눈에 익히고 리팩토링 수업을 다시 들으며 공부를 해야 할 것 같다는 생각이 든다.   🧑🏻‍💻두번째 중간점검 나의 코드를 다른분 코드와 비교해보자 !미션4 미션 공통 피드백static정적 메소드는 빼자 ( 인텔리제이 단축키 사용한 사람 적.발) 풀스택 취업 준비백엔드의 매력은 ?눈에 예쁘게 보이는거 좋아하는데 → 프론트 개발자도 고민 → 프론트 앤드를 어느정도 잘 할줄 아는 백앤드 개발자가 되기로 함성향상 잘 맞을 것 같았다. 복잡한 방식을 여러 방법으로 접근 할 수 있는게 성향이 잘 맞았다. 따라치기만 하는 지금 상황어려움 보다는 익숙함의 문제이다 . 어려움건 10%일뿐 .익숙하지 않아서 거부감이 드는 것 일뿐,진짜 어려운건 아님 . 석박사 해야지 알수있는 정도는 아님메타인지 및 의도적으로 수련하는 것이 가장 빠르고 명확하다 💡될때까지 반복해라.💡계층구조 패키지 나누기 기준이란?도메인중심 ( 유저, 히스토리, 오더 )도메인별로 관심사가 명확해짐프로젝트가 커서, 도메인별로 떨어져야한다 → 아주 좋아유저가 회원이라는 도메인이 정말 중요해서 떼어야함 → 아주 유리 하다컨트롤러,서비스 계층들이 각각 저 도메인별로 나누어져 있다보니, 패턴이 달라질 수 있음공통기능이 멀리 떨어져 있으니, 공통기능이 중복으로 생성 될 우려가 있음 레이어 중심 ( 컨트롤러,서비스,모델)한눈에 레이어러 보기 좋아.도메인간 결합도가 증가해서 MSA전환이 불리하다 💡작은 프로젝트이면 레이어중심이 좋다💡큰 프로젝트는 도메인 중심으로 잡자. 개발 면접a 먼저 개발 지식 질문 CS기초, 스프링등b 이력서 기반 질문인성 질문 (개발에 대한 태도 )질문의 빈도는 A>B>C그러나 C가 별로이면 무조건 탈락취업준비에 대해회사가 원하는 기술들 JD가 무엇인지 공통적으로 찾고 있는 기술스택이 무엇인지 찾아보기그리고, 그 회사만이 찾고있는 기술,팀 도메인이 무엇인지 챙겨보기예상질문리스트 검색해서 → Interview Question Driven 취준 하기

백엔드박우빈워밍업클럽클린코드백엔드발자국

정예은

[워밍업클럽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기박우빈백엔드백엔드스터디지뢰찾기클린코드리팩토링

양성빈

인프런 워밍업 스터디 클럽 2기 백엔드(클린코드, 테스트코드) 4주차 발자국

이 블로그 글은 박우빈님의 인프런 강의를 참조하여 작성한 글입니다.드디어 마지막 발자국 작성차례이다. 어느덧 벌써 수료날짜가 얼마 남지 않았다. 남은 시간 끝까지 달려보자. Presentation Layer 테스트외부세계의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증을 수행MockMVCMock 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크요구사항관리자 페이지에서 신규상품을 등록할 수 있다.상품명, 상품타입, 판매상태, 가격등을 입력받는다. 그래서 해당 요구사항으로 비즈니스 로직을 작성 후 해당 부분 테스트를 해보았다. 여기서 잠깐 주목할만한 부분이라면 바로 @Transactional(readOnly = true)와 @Transactional이다. readOnly옵션은 읽기 전용이라는 뜻이다. 해당옵션에서는 CRUD중에 R만 기능동작을 하게 한다. 또한 JPA에서 CUD스냅샷 저장과 변경감지를 하지 않아 성능향상에 이점을 줄 수 있다. 또한 CQRS에서 Command부분과 Read부분을 나누자는 것처럼 우리의 비즈니스 로직중에 해당 클래스를 readOnly옵션을 전체로 주고 CUD에 해당되는 부분만 @Transactional을 사용하자!또한 우리는 validation을 적용하고 예외를 처리하기 위해 spring-boot-starter-validation 의존성을 추가해주고 각 dto에 어노테이션들을 추가해주었다. 또한 각 예외상황에 맞게 처리할 RestControllerAdvice를 두었으며 공통응답객체를 만들어 진행을 해보았다. 여기서 유심히 볼 부분이 몇가지 존재한다.⚠ @NotBlank vs @NotNull vs @NotEmptyNotNull은 null값을 허용을 하지 않는 것이고 NotEmpty는 ""문자열만 허용을 하지 않으며 NotBlank는 이 둘을 다 포함하면서 " "문자열도 포함시키지 않는다.또한 해당 request DTO를 만들면서 하나의 DTO를 이용해서 presentation layer부터 Business Layer까지 쓰이곤 한다. 이런 점은 DTO를 두고 의존성을 둘 수 있다는 것이다. 이건 layered architecture에 어긋한다. 따라서 서비스용 DTO, 컨트롤용 DTO를 별도 개발해서 진행을 해보는 작업을 해보았다. 또한 컨트롤러에서는 최소한의 검증을 하고 그 외에는 서비스용으로 옮겨보는 법도 생각해보았다. 미션이번 미션은 레이어 아키텍쳐에 관하여 설명과 테스트 방법에 대해 나만의 언어로 풀어쓰는 미션이다. 처음에는 내 언어로 표현한다는게 막막했지만 차근차근 정리를 해가다보니 금방 쉽게 풀어써졌다.미션링크 Mockito로 Stubbing하기요구사항현재 관리자 페이지에서 당일 매출에 대한 내역을 메일전송을 받고 싶어한다.그래서 우리는 해당 요구사항을 바탕으로 비즈니스 로직을 작성하였다. 핵심은 테스트이다. 메일 전송은 외부 네트워크를 타고 전송이 되는 로직이라 매우 오랜 시간이 걸린다.(내 경험) 또한 메일을 계속 보내다보면 나중에 수 많은 테스트 메일이 쌓여 있는 것을 알 수 있다. 이럴때는 어떻게 할까? 메일 전송 행위자체를 mocking하면 되는데 이것을 stubbing이라고 한다. 우리는 메일 전송 로직을 mockBean을 이용해 mocking하고 행위 자체를 stubbing하여 테스트를 수행 할 수 있었다. Test Double출처배우 대역의 스턴트맨이 그 Test Double의 근원지이다.관련 용어Dummy: 아무것도 하지 않는 깡통객체Fake: 단순한 형태로 동일한 기능은 수행하나 프로덕션에서 쓰기에는 부족한 객체(ex. FakeRepository)Stub: 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체. 그 외에는 응답XSpy: Stub이면서 호출된 내용을 기록하며 보여줄 수 있는 객체. 일부는 실제 객체러첨 동작시키고 일부만 stubbingMock: 행위에 대한 기대를 명세하고 그에 따라 동작하도록 만들어진 객체 Stub과 Mock을 헷갈려한다. 동일한거 아닌가? 결론부터 말하자면 Mock은 Stub이 아니다. Mock은 행위에 대한 검증을 할때 사용하고 Stub은 상태에 대한 검증을 할때 사용된다. @Mock, @Spy, @InjectMocks순수 Mockito로 검증하는 시간을 가져보았다. 우리는 @SpringBootTest를 붙여서 통합 테스트를 가져보았는데 그렇게 하지 않고 순수 Mockito로 테스트할 수 있는 방법은 없을까?그래서 우리는 기존에 MailService를 테스트를 가져보았다. 의존성으로 주입된 것들을 mock()을 이용하여 만들어주고 MailService를 해당 mock()으로 만든 것을 생성자에 넣어주어서 실행했다.하지만 이렇게 하는것보다 더 좋은 방법은 @Mock과 @InjectMocks를 이용하는 방법이다. mock()으로 생성한 객체는 @Mock 어노테이션을 붙여서 사용이 가능하고 만들어진 mock 객체를 이용하여 생성한 객체 MockService는 @InjectMocks를 이용하여 더 간단히 사용이 가능하다. 추가적으로 해당 메서드 안에 특정메서드가 몇번 불렸는지 확인하는 것은 verify로 확인이 가능하다.@Mock을 사용할 때는 @ExtendWith(MockitoExtension.class)을 붙여줘야 한다.또한 @Spy를 이용할건데 사용법은 @Mock과 유사하다. 다른점은 @Mock을 사용할때 when().thenReturn()식을 이용했는데 @Spy는 doReturn().when().메서드()를 이용하면 된다. @Mock과 비교해보자. @Mock을 사용하면 해당 메서드만 mocking을 하여 사용하고 그 외의 메서드들은 작동을 안 한다. 하지만 @Spy를 사용하면 해당 객체에 호출한 메서드들이 전부 작동이 된다. 그래서 일반적으로 @Mock을 사용하지만 @Spy를 이용할 때도 있으니 알아두자. BDDMockitoBDD는 행위주도개발로 이전에 한번 살펴본 적이 있다. 이전에 실습한 코드를 보면 뭔가 이상하다고 느낄 수 있을 것이다.// given when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString())).thenReturn(true);분명 given절인데 when이 들어가져 있다. 그래서 만들어진 것이 BDDMockito다. BDDMockito의 given을 이용하면 아래와 같이 변경이 가능하다.given(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString())).willReturn(true);그럼 정확히 given에 given을 이용하니 명확해진다. Classicist VS. MockistClassicist입장은 다 mocking하면 실제 객체들의 동작 및 두 객체들이 합쳐서 동작할때 그에 따른 사이드 이펙트는 어떻게 검증할건데라는 입장이며 Mockist 입장은 모든 걸 다 mocking하면 각 객체들의 기능에 대한걸 보장하니 굳이 통합테스트를 할 필요가 없다라는 입장이다.우리는 Persistence Layer에서 단위 테스트를 진행하고 Business Layer에서는 통합 테스트를 Presentation Layer에서 두 레이어를 모킹하여 진행했다. 이것을 Mockist가 본다면 굳이 시간낭비하고 있다고 할 것이다.우빈님의 생각은 Classicist입장이시며 mocking을 할때는 외부시스템을 사용할 일이 있을때 사용한다고 하셨다.즉 결론은 mocking을 했다 하더라도 실제 프로덕션 코드에서 런타임 시점에 일어날 일을 정확하게 stubbing했다 단언할 수 없으니 통합테스트를 해보자는 것 같다.나의 입장은 이렇다. QuerDSL같은 외부자원을 사용할때 나는 해당 부분을 테스트할때 테스트를 진행하고 비즈니스 레이어에는 해당 부분을 mocking처리한다. 그리고 컨트롤러 부분에서 통합테스트를 사용하는데 이 부분은 한번 질문을 드려봐야겠다. 한 문단에 한 주제이전 강의인 Readable Code에서도 배웠고 학창시절 영어독해시간에도 배웠듯이 한 문단에는 하나의 주제로 이루어져야 한다. 테스트도 문서다. 즉, 각각 하나의 테스트는 하나의 문단이고 그 테스트 코드는 하나의 주제를 가져야 한다. 완벽하게 제어하기테스트 코드를 작성할 때는 모든 조건들을 완벽히 제어가 가능해야 한다. 우리는 이전에 LocalDateTime.now()을 사용하면서 현재시간에 따라 테스트가 성공할때도 있고 실패할때도 있는 경우를 보았다. 그래서 우리는 이를 해결하기 위해 이 현재시간을 상위에 넘기고 파라미터로 받는 방식을 택하였다.그러면 만약에 LocalDateTime.now()를 사용해도 테스트가 무조건 성공한다면 사용해도 되나? 우빈님께서는 지양한다고 하셨다. 그 이유는 그 테스트는 성공할진 몰라도 팀 단위에서 해당 코드를 사용하면 그게 프로젝트에 번져서 빈번히 사용할 확률이 높기때문이라고 하셨다. 나도 이점을 한번 주의해야겠다. 테스트 환경의 독립성을 보장하자테스트 환경은 대부분 given절에서 환경을 세팅한다. 그런데 이런 given절에서 다른 API를 사용하게 된다면 어떻게 될까? 해당 API와 테스트 코드가 결합도가 생기게 된다. 이런 부분을 방지하고 테스트코드 독립성을 보장시켜야 한다.@Test @DisplayName("재고가 부족한 상품으로 주문을 생성하려는 경우 예외가 발생한다.") 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", 2);     Stock stock2 = Stock.create("002", 2);     stock1.deductQuantity(1); // @todo     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("재고가 부족한 상품이 있습니다."); }위의 코드에서 todo부분을 살펴보자. 해당 코드는 주문생성관련 로직이다. 그런데 deductQuantity를 현재 재고보다 많이 차감시키면 해당 메서드에서 예외를 던질 것이다. 그러면 given절에서 테스트가 실패되고 내가 위에서 말했던 결합도가 생긴 케이스이다. 또한 이건 내 생각이지만 주문생성로직에 재고차감로직이 들어가 있으니 하나의 테스트코드에 주제가 2개가 되버리는 상황이 생긴다. 이런것을 방지하는 것이 좋다. 테스트 간 독립성을 보장하자테스트는 각각 수행되어야 하고 테스트 순서가 무작위여도 같은 결과가 나와야 한다. 하지만 만약 테스트에 공유자원을 사용하게 되면 예상치 못한 결과가 나올 우려가 있기에 테스트가 실패할 경우도 있을 것이다. 이런 점때문에 공유자원 사용은 지양하자. 한 눈에 들어오는 Test Fixture 구성하기Test FixtureFixture: 고정 틀, 고정되어 있는 물체 (given절에 생성한 객체들)테스트를 위해 원하는 상태로 고정시킨 일련의 객체 우리는 이에 관련해서 @BeforeEach BeforeAll에 대해 알아보았다. 그런데 @BeforeEach같은 경우 given절에서 만든 데이터를 넣는 행위는 지양하다고 하셨다. 결국 이것은 이전에 봤던 공유자원과 같은 역할을 한다. 또한 테스트는 문서인데 given절을 보려니 없어서 스크롤로 위로 왔다갔다 하는 상황이 발생하기 때문에 테스트 문서의 파편화가 일어난다.그러면 언제 @BeforeEach를 쓸까? 각 테스트 입장에서 봤을 때 아예 몰라도 테스트 내용을 이해하는데 문제가 없는가?라는 물음과 수정해도 모든 테스트에 영향을 주지 않는가?라는 물음에 괜찮다면 사용하자! 그렇지 않다면 지양하자.또한 data.sql을 이용해 미리 쿼리를 생성해 given절을 작성하는 행위는 지양하자. 왜냐하면 테스트 파편화가 일어나기도 하고 나중에 실무에 가면 수십개의 컬럼과 수십개의 테이블이 있고 이 테이블이나 컬럼이 바뀔때마다 수정을 해줘야 하기때문에 관리포인트가 늘어나기 때문이다.또한 given절에 객체를 생성 시, 필요한 파라미터만 주입받는것을 고려하면 작성하자. 해당 파라미터를 보고 이 파라미터는 테스트에 전혀 영향을 주지 않을 것 같은 것은 고정값으로 두고 파라미터를 빼는 것처럼 말이다.마지막으로 builder와 같이 given절에 들어가는 것을 하나의 테스트 config 클래스에 모아두는 행위는 지양하자. 왜냐하면 나중에 실무에서 작성하다보면 새로운 빌더가 생기고 메서드 오버로딩때문에 파라미터 순서만 바뀌는 빌더도 많이 생길 수 있으며 테스트 문서 파편화로 인해 더 불편해질 것 같다. 그래서 클래스마다 필요한 파라미터만 받아서 작성하는 것이 좋다. Test Fixture 클렌징deleteAll()과 deleteAllInBatch()에 차이에 대해 알아보자. 우빈님은 deleteAllInBatch()를 더 선호하신다고 하셨다. 그 이유에 대해 알아보자.deleteAllInBatch()는 delete from~ 절만 나가는 순수 테이블 전체 삭제에 용이하다. 하자민 단점이라하면 순서를 잘 생각해야 한다. 만약 A라는 테이블과 B라는 테이블이 M:N 연관관계를 맺어 중간테이블 AB라는 테이블이 있을때 AB부터 테이블을 삭제해주고 A, B순서대로 삭제를 해줘야 한다. 그렇지 않으면 예외가 발생한다.deleteAll()은 굳이 중간테이블을 삭제할 필요없이 중간테이블을 알고 있는 테이블만 삭제해도 알아서 삭제해준다. 하지만 단점은 해당 메서드를 실행하면 해당 엔티티를 먼저 전체 select를 하고 다음으로 delete from where 절이 나간다. 그리고 해당 절은 테이블에 있는 데이터 수 만큼 나가기 때문에 수많은 데이터가 존재하면 성능이 매우 떨어질 것이다.그래서 deleteAllInBatch()가 더 선호하는 이유를 알 것 같다. 하지만 이런것보다 side effect를 잘 고려해서 @Transactional을 잘 사용하는 것이 베스트일 것 같다. @ParameterizedTest테스트 코드에 if-else나 for문같이 조건문, 반복문등 읽는 사람의 생각이 들어가는 것을 지양해야한다고 말했다. 그래서 여러가지 테스트 로직이 하나의 테스트 코드에 있다면 분리하자고 하였다. 그런데 만약 단순히 값 여러개로 하나의 테스트를 하고 싶은경우 테스트를 나누는 것보다 @ParameterizedTest를 사용하는 것이 깔끔해진다.대표적인 예시로 내가 사이드프로젝트에서 작성했던 코드를 들 수 있을 것 같다.@ParameterizedTest @MethodSource("providedTestDataForSignup") @DisplayName("회원가입 통합 테스트 - 실패(유효하지 않은 회원가입 폼)") void member_signup_integration_test_fail_caused_by_invalid_signup_form(String name, String nickname, String email, String password, String confirmPassword, String phoneNumber, LocalDate birthday) throws Exception {     MemberSignupRequestDto requestDto = new MemberSignupRequestDto(name, nickname, email, password, confirmPassword, phoneNumber, birthday);     this.mockMvc.perform(post("/api/auth/signup")                   .contentType(MediaType.APPLICATION_JSON + ";charset=UTF-8")                     .accept(MediaType.APPLICATION_JSON + ";charset=UTF-8")                     .content(this.objectMapper.writeValueAsString(requestDto)))             .andDo(print())             .andExpect(status().isBadRequest())             .andExpect(jsonPath("message").exists())             .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_REQUEST_PARAMETER.getHttpStatus().name()))             .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_REQUEST_PARAMETER.getCode()))             .andExpect(jsonPath("timestamp").exists()); } private static Stream<Arguments> providedTestDataForSignup() {     return Stream.of(             Arguments.of("양성빈", "tester", "email@email.com", "1q2w3e4r5t!", "1q2w3e4r5t!", "010-1234-1234", LocalDate.of(1999, 1, 1)),             Arguments.of("양성빈", "robert", "test@email.com", "1q2w3e4r5t!", "1q2w3e4r5t!", "010-1234-1234", LocalDate.of(1999, 1, 1)),             Arguments.of("양성빈", "robert", "email@email.com", "1q2w3e4r5t!", "t5r4e3w2q1@", "010-1234-1234", LocalDate.of(1999, 1, 1)),             Arguments.of("양성빈", "robert", "email@email.com", "1q2w3e4r5t!", "1q2w3e4r5t!", "010-1111-1111", LocalDate.of(1999, 1, 1))     ); }이렇게 @MethodSource를 사용하는 경우 이외에오 @CsvSource를 이용하는 경우도 있고 다른 방법도 있으니 한번 찾아보면 좋을 것 같다. @DynamicTest공유변수를 가지고 여러개의 테스트가 사용하는 것은 지양하자고 하였다. 왜냐하면 테스트의 순서가 생겨버리고 서로 강결합이 되기 때문이다. 하지만 환경 설정 후 시나리오 테스트를 하는 경우가 있을 것이다. 그럴 경우 @DynamicTest를 이용하자. 사용법은 아래와 같다.@TestFactory @DisplayName("") Collection<DynamicTest> dynamicTests() {     // given     return List.of(             DynamicTest.dynamicTest("", () -> {                 // given                 // when                 // then             }),             DynamicTest.dynamicTest("", () -> {                 // given                 // when                 // then             })         ); } 테스트 수행도 비용이다. 환경 통합하기이제 전체 테스트를 돌려보자. 지금 테스트를 전체 돌려보면 2.x초라는 시간이 걸리고 spring boot 서버가 6번 뜨는 불필요한 행위가 발생한다.테스트 코드를 작성하는 이유가 인간의 수동화된 검증에 대한 불신도 있지만 가장 큰 이유는 시간을 아끼기 위해서이다. 하지만 테스트를 돌렸는데 2.x초라는 행위는 너무 아깝다. 그래서 우리는 하나의 통합추상 클래스를 만들어 service와 repository부분을 하나의 추상클래스를 상속받게 함으로 테스트 띄우는 횟수를 줄였다. 그리거 마지막 controller부분도 별도의 추상클래스를 만들어 해당 클래스를 상속받게 함으로 서버 띄우는 횟수를 줄여갔다. 결론적으로 총 서버는 2번 띄워졌고 2.x초에서 1.x초로 속도가 줄어갔다. Q. private 메서드의 테스트는 어떻게 하나요?정답은 할 필요도 없고 하려고 해서도 안된다. 클라이언트 입장에서는 제공되는 public 메서드만 알 수 있고 알아야하며 그 외의 내부 메서드는 알 필요가 없다. 또한 public 메서드를 테스트한다는 것은 내부 private 메서드도 자동으로 같이 테스트하는 것이므로 따로 테스트를 할 필요가 없다. 다만 만약 이렇게 해도 계속 위의 물음이 생각나면 객체를 분리할 시점인가를 검토해야한다. 그리고 객체를 분리시켜 해당 private 메서드를 해당 객체의 public 메서드로 두고 단위 테스트를 진행해야 할 것이다.나는 미션을 진행하면서 이런 물음이 생각났고 private 메서드를 리플렉션을 통해 테스트를 한 경우가 있는데 위의 해답을 듣고 나니 괜히 테스트를 한것이구나라는 생각이 들어 조금은 반성하게 된 계기였다. Q. 테스트에서만 필요한 메서드가 생겼는데 프로덕션 코드에서는 필요 없다면?답변은 만들어도 된다. 다만 보수적으로 접근해야 한다. 즉, 어떤 객체가 마땅히 가져도 되고 미래지향적이면 만들어도 상관없다. 학습 테스트잘 모르는 기능, 라이브러리, 프레임워크를 학습하기 위해 작성하는 테스트여러 테스트 케이스를 스스로 정의하고 검증하는 과정을 통해 보다 구체적인 동작과 기능을 학습관련 문서만 읽는 것보다 훨씬 재밌게 학습  Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구API 명세를 문서로 만들고 외부에 제공함으로써 현업을 원활하게 한다.기본적으로 AsciiDOC을 사용하여 문서를 작성한다. Spring REST Docs vs SwaggerSpring REST Docs장점테스트 코드를 통과해야 문서가 만들어진다. (신뢰도가 높다)프로적션 코드에 비침투적이다.단점코드 양이 많다.설정이 어렵다.Swagger장점적용이 쉽다문서에서 바로 API 호출을 수행해볼 수 있다.단점프로덕션 코드에 침투적이다.테스트와 무관하기 때문에 신뢰도가 떨어질 수 있다. 미션3 진행테스트코드의 마지막 미션을 진행하였다. 이번에 배운 어노테이션들, @BeforeEach가 적용하여 BDD 스타일 적용하는 실습등 미션을 진행했는데 해당 미션을 통해서 해당 어노테이션들에 대해 확실히 알 수 있었으며 BDD 스타일이 조금 적응된 것 같다.미션링크 깜짝 특강Day18 미션 공통 피드백핵심은 중복 제거가 아니고 도메인이다. 사용자, 게시물은 간접적이므로 setup()으로 댓글은 직접적이므로 given절에 배치해야 한다.Q&A잘문) REST API의 조건 중 하나인 hateoas에 대해 실무에서 많이 사용하는지와 제가 제대로 적용하고 있는지답변) 아직까지 사용한 곳을 본적이 없다고 하셨다. 그 이유를 생각해보시는데 단순히 hateoas를 적용하기에는 APP-FE-BE 간 긴밀한 스펙과 복잡한 동작들이 너무 많고 또 이미 만들어져 있는 구조를 hateoas 형태로 전환한다는것은 불가능에 가깝다고 느끼신다고 하셨다. 또한 실무에서는 대부분 그런 규칙을 지키는 것보다 다른 중요한 것들이 더 많다고 판단하기 때문이라고도 하셨다.코드리뷰우빈님께서 많은 칭찬을 해주셔서 몸둘 바를 모르겠지만 몇가지 피드백 사항이 있었다. 그 중에 제일 생각나는 피드백을 말하면 다음과 같다. given / when / then절에 설명하는 주석은 생략해도 좋다. 하지만 given절이 몇십줄이고 서로 다른 특징들이 나열되어 있다면 간단히 적는 걸 추천하신다고 하셨다.이것으로 모든 워밍업 클럽 진도가 완료가 되었다, 나 자신에게도 뜻 깊은 경험과 성장이 되었으며 이러한 경험의 반복을 이뤄나가고 싶다.

백엔드인프런워밍업스터디클럽2기백엔드클린코드테스트코드발자국

양성빈

인프런 워밍업 스터디 클럽 2기 백엔드(클린코드, 테스트코드) 3주차 발자국

이 블로그 글은 박우빈님의 강의를 참조하여 작성한 글입니다.어느덧 벌써 워밍업 클럽이 막바지로 가고 있는 것 같다. 워밍업 클럽을 참여 전의 나보다 많이 성장했는가를 항상 발자국 쓸때 돌이켜 물어보는 것 같다. 과연 성장을 했을끼? 나는 당당히 성장을 하였다고 생각을 한다. 해당 스터디를 통해 나의 생활도 지식도 성장이 되었다 생각하며 해당에 대한 물음은 워밍업 클럽 수료 후에 다시 되물음을 해보겠다.이번 주차에서는 이제 Readable Code 강좌가 완강이 되고 Practical Testing 강좌를 시작하는 주차다. 이번주도 열심히 달려본 내역들을 작성해보겠다. 강의소개이 강좌는 테스트가 처음이거나 테스트 코드는 들어봤거나 작성하려고 시도를 해본 경험이 있는등 테스트가 궁금한 모든 분들을 위해 나온 강의이다. 나도 해당 테스트를 어떻게 하면 잘 작성할지가 궁금하여 이 강좌를 듣게 되고 해당 워밍업 클럽을 참여하게 된 이유이기도 하다.테스트를 작성하는 역량은 채용시장에서 주니어 개발자에게 기대하는 요소 중 하나다. 채용시 구현과제 등에서 테스트 작성여부, 테스트 코드 구현방식을 확인한다. 또한 소프트웨어의 품질을 보장하는 방법으로 그 중요성을 알고 있는지도 확인을 하기도 한다고 한다.이번 강좌에서는 다음과 같은 목표를 두고 학습을 진행한다고 한다.📚 목표1. 테스트 코드가 필요한 이유2. 좋은 테스트 코드란 무엇일까?3. 실제 실무에서 진행하는 방식 그대로 테스트를 작성해가면서 API를 설계하고 개발하는 방법4. 정답은 없지만 오답은 존재한다. 구체적인 이유에 근거한 상세한 테스트 작성 팁벌써부터 많은 기대를 품으며 다음 강의로 바로 가봐야 겠다. 어떻게 학습하면 좋을까?효과적인 학습을 하기 위해 가장 먼저 선행되어야 하는 것은 바로 무엇을 모르는지 아는 것이다. 무엇을 모르는 지 아는것은 찾아볼 수 있게 된다는 것이다.우리는 학습을 하면서 이 부분은 완벽히 아는 부분, 이 부분은 반만 아는 부분, 이 부분은 처음 들어보는 부분으로 구분된다. 그래서 강좌에서 함계 학습한 키워드와 추가 학습을 위한 키워드를 분리하여 키워드 기반으로 정리를 해주신다고 하니 많은 기대를 가지며 다음 강의부터 본격적으로 달려 볼 예정이다. 테스트는 왜 필요할까?기술 학습에 있어서 '왜?'가 중요하다. 테스트하면 생각나는게 무엇일까? 나는 처음 테스트코드를 볼때 굳이 해야하나? 개발시간만 더 늘릴뿐일텐데라는 생각을 하였다.그런데 만약 테스트코드가 없이 실제 인간이 수동으로 테스트를 하면 매우 큰 문제들을 야기할 수 있다. 인간은 실수의 동물이기 때문이다. 또한 만약 기능을 개발할때 기존 기능을 건들게 된다면 기존 기능도 다시 테스트를 하는 시간낭비가 발생한다. ✅ 테스트 작성을 안하면?1. 커버할 수 없는 영역 발생2. 경험과 감에 의존3. 늦은 피드백4. 유지보수 어려움5. 소프트웨어에 대한 신뢰가 떨어딘다. 테스트 코드를 작성하지 않으면?변화가 생기는 매 순간마다 발생할 수 있는 모든 case를 고려해야한다.변화가 생기는 매 순간마다 모든 팀원이 동일한 고민을 해야한다.빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다.테스트코드가 병목이 된다면?프로덕션 코드의 안정성을 제공하기 힘들어진다.테스트 코드 자체가 유지보수하기 어려운 새로운 짐이 된다.잘못된 검증이 이루어질 가능성이 생긴다.올바른 테스트 코드자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고 수동 테스트에 드는 비용을 크게 절약할 수 있다.소프트웨어의 빠른 변화를 지원한다.팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.가까이 보면 느리지만 멀리보면 빠르다.샘플 프로젝트 소개 & 개발 환경 안내해당 테스트 섹션에서는 카페 키오스크 시스템을 만들면서 테스트 학습을 할 예정이다.🛠 개발환경- IntelliJ Ultimate- Vim(Plugin) 프로젝트 세팅인텔리제이를 활용하여 스프링부트 프로젝트를 생성하고 build.gradle의 의존성 정리를 하였다. 수동테스트 VS. 자동화된 테스트요구사항주문목록에 음료 추가/삭제 기능주문목록에 전체 지우기주문목록 총 금액 계산하기주문 생성하기 해당 부분을 토대로 콘솔기반 비즈니스 로직을 작성하였고 테스트 강의이니 해당 로직을 테스트 하기 위해 이 중 음료 추가에 대한 로직을 아래와 같이 작성했다.@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()); }위의 코드를 봤을 때 이렇게 테스트코드를 짜면 안된다고 직감을 했을 것이다. 왜냐하면 일단 최종단계에서 사람이 개입하고 어떤게 맞고 어떤게 틀리는지 모른다는 것이다. 또한 이 테스트는 100% 성공하는 케이스이기 때문에 뭔가 테스트라고 하기에 모호한것 같다. JUnit5로 테스트하기단위테스트작은 코드 단위를 독립적으로 검증하는 테스트검증속도가 빠르고 안정적이다.Junit5단위 테스트를 위한 테스트 프레임워크 AssertJ테스트 코드 작성을 원활하게 돕는 테스트 라이브러리풍부한 API, 메서드 체이닝 지원 해당 지식을 기반으로 우리가 이전 시간에 작성한 아메리카노부분과 카페머신의 대한 단위 테스트를 AssertJ를 이용하여 작성해보는 시간을 가졌다. 테스트 케이스 세분화하기스스로에게 질문해보자. 암묵적이거나 아직 드러나지 않은 요구사항이 있는지를 확인해보자. 그리고 해피케이스와 예외케이스를 둘다 생각하며 항상 경계값 테스트를 해보자.경계 값은 범위(이상, 이하, 초과, 미만), 구간, 날짜등을 일컫는다.그래서 우리는 음료에 여러잔을 담는 기능을 개발하고 해당 부분의 해피케이스에 관한 테스트를 작성했다. 또한 예외 케이스를 생각해 로직을 작성하고 해당 예외케이스에 대한 로직을 작성하게 되었다. 테스트하기 어려운 영역을 분리하기테스트하기 어려운 영역은 다음과 같다.관측할 때마다 다른 값에 의존하는 코드현재 날짜/시간, 랜덤 값, 전역변수/함수, 사용자 입력 등외부세계에 영향을 주는 코드표준출력, 메세지 발송, 데이터베이스 기록 그래서 우리는 실습으로 주문을 생성할때 가게 영업시간이 아닐시, 주문을 못하게 하는 상황의 로직을 작성했고 테스트코드 작성 시 문제가 생겼다. 내가 현재 새벽에 테스트코드를 돌렸고 영업시간 전이기에 테스트코드가 실패한것이다. 결국 이 부분은 날짜를 파라미터로 받게 변경하여 해결하였다.📚 순수함수- 같은 입력에는 같은 결과- 외부세상과 단절된 형태- 테스트하기 쉬운 코드 TDD: Test Driven Development프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현과정을 주도하도록 하는 방법론Red: 실패하는 테스트 작성Green: 테스트 통과하기 위한 최소한의 코딩Refactor: 구현코드 개서느 테스트 통과 유지피드백TDD는 빠르게 피드백을 자동으로 받을 수 있다.선 기능 후 테스트 작성테스트 자체의 누락 가능성특정 테스트 케이스(해피 케이스)만 검증할 가능성이 크다.잘못된 구현을 다소 늦게 발견할 가능성이 있다.선 테스트 후 기능 작성복잡도가 낮은 테스트 가능한 코드로 구현할 수 있게 된다.유연하며 유지보수가 쉬운쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백 가능과감한 리팩토링이 가능클라이언트 관점에서의 피드백을 주는 Test Driven 테스트는 []다.테스트는 무엇일까? 테스트는 문서라고 볼 수 있다.프로덕션 기능을 설명하는 테스트 코드 문서다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서 모두의 자산으로 공유할 수 있다. DisplayName을 섬세하게@DisplayName을 사용하여 테스트 명을 구체화할때 명사의 나열보단 문장으로 작성하는 것이 좋다. 또한 테스트 행위에 대한 결과를 기술하는데 도메인 용어를 사용하여 매서드 자체의 관점보다 도메인 정책 관점으로 한층 추상화된 내용을 담는것이 좋다. 마지막으로 테스트의 현상을 중점으로 기술하지 말자. 예를 들어 ~실패라기 보단 도메인의 내용을 담는것이 좋을 것 같다. BDD(Behavior Driven Development) 스타일로 작성하기TDD에서 파생된 개발방법함수단위의 테스트에 집중하기보다 시나리오에 기반한 테스트 케이스(TC) 자체에 집중하여 테스트개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준(레벨)을 권장 Given / When / ThenGiven: 시나리오 진행에 필요한 모든 준비 과정(객체, 값, 상태 등)When: 시나리오 행동 진행Then: 시나리오 진행에 대한 결과 명시 및 검증 어떤환경에서(Given) 어떤 행동을 진행했을 때(When) 어떤 상태 변화가 일어난다.(Then)라는것을 토대로 @DisplayName을 상세히 적을 수 있다. 미션이번 미션은 저번에 Readable Code에서 진행했던 마지막 과제 프로젝트인 '지뢰찾가', '스터디카페'중에 1개의 프로젝트를 가지고 테스트 코드를 작성해보는 시간을 가졌다. 조건은 BDD스타일로 3개이상의 클래스 총 7개 이상 테스트를 작성하는 것이었지만 나는 한층 공부한다는 마음으로 테스트 커버리지 툴인 jacoco를 가지고 스터디 카페부터 진행을 하였다. 그 결과 테스트 커버리지 98%라는 결과를 가지게 되었다. 그리고 조금 더 욕심이 나서 지뢰찾기도 일부 클래스를 진행하였다. 이번 미션을 해보면서 어려웠고 힘들었지만 테스트 작성에 많이 익숙해진 경험을 가지게 되었다. 깃허브 주소 레이어드 아키텍쳐(Layered Architecture)와 테스트레이어드 아키텍쳐에서는 아래와 같이 구성되어 있다.- Persentation Layer- Business Layer- Persistence Layer이렇게 레이어를 나눈 이유는 관심사의 분리때문일 것이다. 책임을 나눔으로서 유지보수성을 쉽게 가져가기 위함이다.🙋🏻 테스트 하기 어려워보여요!그렇게 보일 수는 있겠지만 앞선것과 기조는 비슷하다. 즉, 테스트하기 어려운 걸 분리하여 테스트하고자 하는 영역을 집중하며 명시적이고 이해할 수 있는 문서형태로 테스트 작성하는 것은 어떤 아키텍쳐든 동일하다.A와 B라는 모듈이 있다고 하자. 이 두 모듈을 더했을 때 뭐가 나올까? AB? BA? C? 누구도 예측하기 힘들다. 그래서 우리는 통합 테스트의 필요성이 느껴질 것이다.통합 테스트여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트일반적으로 작은 범위의 단위테스트만으로는 기능 전체의 신뢰성 보장X풍부한 단위 테스트 & 큰 기능 단위를 검증하는 통합 테스트의 조화가 필요. Spring / JPA 훑어보기 & 기본 엔티티 설계Spring스프링을 애기하면 먼저 라이브러리와 프레임워크의 차이를 묻는다. 라이브러리 같은 경우는 내 코드가 주최가 된다. 즉, 필요한 기능이 있다면 외부에 끌어와서 사용을 하는데 이게 라이브러리다. 반면 프레임워크는 이미 프레임(동작환경)이 있고 내 코드가 주최가 아니고 내 코드는 수동적으로 이 안에 들어가서 역할을 하는데 이게 프레임워크다.스프링을 애기하면 나오는 주요 3가지가 존재한다. IoC, DI, AOP다.- IoC(Inversion of Control): 객체의 생성과 의존성 관리를 프레임워크에 위임하는 개념.- DI(Dependency Injection): 의존성 주입을 통해 객체 간 결합도를 낮추고 확장성과 테스트 용이성을 향상시킴.- AOP(Aspect-Oriented Programming): 횡단 관심사(공통 기능)를 분리하여 코드 중복을 줄이고 모듈성을 개선.ORM객체지향과 RDB 페러다임이 다름.이전에는 개발자가 객체의 데이터를 한땀한땀 매핑하여 DB에 저장 및 조회ORM을 사용함으로써 개발자는 단순 작업을 줄이고 비즈니스 로직에 집중. JPAJava진영의 ORM 기술 표준인터페이스이고 여러 구현체가 있지만 보통 Hibernate를 많이 사용반복적인 CRUD SQL을 생성 및 실행해주고 여러 부가 기능들을 제공편리하지만 쿼리를 직접 작성하지 않기 때문에 어떤식으로 쿼리가 만들어지고 실행되는지 명확하게 이해하고 있어야 함Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA제공QueryDSL과 조합하여 많이 사용 Persistence Layer 테스트요구사항이 다음과 같다고 하자.키오스크 주문을 위한 상품 후보 리스트 조회하기상품의 판매 상태: 판매중, 판매보류, 판매금지판매중, 판매보류인 상태의 상품을 화면에 보여준다.id, 상품번호, 상품타입, 판매상태, 상품이름, 가격 이 요구사항을 바탕으로 우리는 엔티티설계부터해서 컨트롤러까지 즉, Presentation Layer, Business Layer, Persistence Layer까지 전반적으로 한 사이클을 돌면서 코드를 작성해보고 확인까지 진행해보았다. 그럼 이제 repository부분부터 테스트를 해보자.우리는 given-when-then 패턴으로 테스트 코드를 작성했다. 여기서 살펴볼 것은 @SpringBootTest와 @DataJpaTest이다. 이 둘의 비슷하지만 차이점을 살펴보면 @SpringBootTest는 모든 부분의 의존성들을 주입시켜주지만 @DataJpaTest는 JPA관련된 부분만 주입을 시켜준다. 따라서 @DataJpaTest가 더 가볍다. 하지만 우빈님께서는 @SpringBootTest를 선호하신다고 하신다. 그 이유에 대해서는 추후에 말씀주신다고 하셨다.그럼 Persistence Layer 역할에 대해 정리하면 아래와 같다.Data Access 역할비즈니스 가공로직이 포함되어서는 안된다. Data에 대한 CRUD에만 집중한 레이어여야 한다.ex) QueryDSL이나 별도 DAO를 사용하면서 비즈니스 로직이 침투할 가능성이 있을 수 있으니 이 점을 생각하면서 작성해야 할 것 같다. Business Layer 테스트비즈니스 레이어에 대한 역할을 살펴보면 아래와 같다.비즈니스 로직을 구현하는 역할persistence layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.트랜잭션을 보장해야한다. 그래서 우리는 새로운 요구사항을 통해 해당 비즈니스 레이어에 대한 테스트 코드를 작성해보는 시간을 가졌다. 새로운 요구사항은 아래와 같다.요구사항(1)상품번호 리스트를 받아 주문 생성하기주문은 주문상태 주문등록시간을 가진다.주문의 총 금액을 계산한다. 위의 요구사항으로 우리는 주문 엔티티를 설계하고 연관관계를 기존에 만든 상품 엔티티와 다대다 관계를 맺기 위해 중간 엔티티를 설계하고 각각으로 연관관계를 맺어두었다. 그리고 주문생성 로직과 테스트코드를 작성하는 시간을 가졌다.다음으로 우리가 작성한 테스트들을 동시에 돌려보았다. 하지만 실패되는 테스트를 보게되었다. 우리가 작성한 비즈니스 레이어 테스트가 전부 성공하는게 아니라 일부 실패가 되는 경우가 있다. 하지만 이 전에 Persistence Layer에 작성한 테스트를 동시 실행해보면 그것은 괜찮았다. 차이는 @SpringBootTest와 @DataJpaTest 두 어노테이션 차이였다. 두 어노테이션을 타고 들어가서 확인하면 @Transactional 어노테이션 유무 차이였다. 그래서 실패되는 비즈니스 레이어에 트랜잭션 어노테이션을 붙여주면 될 것 같아 보였지만 우빈님께서는 tearDown 메서드를 만드셔서 데이터를 클리닝하는 작업을 해주셨다. 그 이유는 추후에 말씀주신다고 하셨다. 요구사항(2)주문 생성 시 재고 확인 및 개수 차감 후 생성하기재고는 상품번호를 가진다.재고와 관련 있는 상품타입은 병음료, 베이커리다. 새로운 요구사항으로 재고 개념이 도입되었다. 그래서 해당 엔티티를 설계후 개수 차감 로직을 작성하였다. 여기서 위에서 언급한 수동으로 tearDown 메서드로 삭제를 하나하나 해주냐 아니면 @Transactional 어노테이션을 붙여주냐였다. 처음에 우리는 로직을 작성하고 해당 로직을 테스트할때 @Transactional어노테이션을 붙이지 않고 tearDown 메서드를 만들고 실행하였고 결과는 실패하였다. 정상적으로 재고가 감소가 안 된 것이다. 그래서 해당 로그와 쿼리를 보니 update 쿼리가 안 나간것이다. 원래 @Transactional 어노테이션을 붙이면 커밋종료시점에 더티체킹으로 update 쿼리가 발생한다. 하지만 지금은 우리가 수동으로 감소하는 전략을 하였기에 더티체킹 기능이 활성이 안 된 것이다. 🙋🏻 그러면 왜 insert쿼리는 잘 나간거에요?jpa repository를 타고 들어가보면 crud repository를 확인할 수 있다. 해당 구현체를 보면 save 메서드에 @Transactional 어노테이션이 잘 붙어져 있다. 이것은 delete도 마찬가지다.그래서 우리는 추후 살펴볼 것들이 있어 tearDown 메서드는 두고 @Transactional을 product 코드에 적용하기로 했다. 그리로 우빈님께서 이런 경우를 대비해 테스트에는 @Transactional을 붙이고 실질적으로 본 코드에는 안 붙이고 release하는 경우도 있으니 한번 생각하고 써야한다고 말씀을 주셨다.추가적으로 재고감소 로직은 동시성 이슈가 날 수 있는 대표적엔 케이스다. 지금은 키오스크가 1대밖에 없다 가정했지만 2대 이상이라면 동시성 이슈가 터질 것이다. 그래서 optimistic lock / pessimistic lock등을 고민해서 해결을 해야한다. 이 부분도 나중에 한번 더 스스로 공부해봐야겠다.

백엔드인프런워밍업스터디클럽2기백엔드클린코드테스트코드발자국

양성빈

인프런 워밍업 스터디 클럽 2기 백엔드(클린코드, 테스트코드) 2주차 발자국

이 블로그 글은 박우빈님의 인프런 강의를 참조하여 작성한 글입니다.어느덧 인프런 워밍업 스터디 클럽을 시작한지도 2주째가 시작된다. 그리고 이번주 1주에 대한 회고를 시작해보려고 한다.이번주도 여러가지를 배우고 많은 경험이 된 한 주였다. 그럼 회고를 시작하겠다. 완주 및 우수러너를 위해 오늘도 달려본다.주석의 양면성클린코드 관점에서 주석은 죄악이냐 아니냐 논쟁이 많다.주석이 많다는 것은 그만큼 비즈니스 요구사항을 코드에 잘못 녹였다는 이야기코드를 설명하는 주석을 쓰면 코드가 아니라 주석에 의존한다. 주석에 의존하여 코드를 작성하면 적절하지 않은 추상화 레벨을 갖게 되어 낮은 품질의 코드가 만들어 진다. 🙋🏻 아니 그러면 주석 언제 써요?- 우리가 리팩토링 할때 정말 큰 난관 중 하나가 히스토리를 전혀 알 수 없는 코드다.- 후대에 전해야 할 "의사결정의 히스토리"를 도저히 코드로 표현할 수 없을 때 주석으로 상세히 설명한다.- 주석을 작성할 때 자주 변하는 정보는 최대한 지양해서 작성한다.- 만약 관련 정책이 변하거나 코드가 변경되었다면 주석도 잊지 않고 함께 업데이트 해야한다. 주석이 없는 코드보다 부정확한 주석이 달린 코드가 더 치명적이다.우리가 가진 모든 표현방법을 총동원해 코드에 의도를 녹여내고 그럼에도 불구하고 전달해야할 정보가 남았을때 사용하는게 주석이다.이번 예제 실습은 주석이 달린 gameStatus를 enum으로 변경하고 관련 비즈니스 로직을 MineSweeper가 아니라 GameBoard에 있는게 어울려 그곳으로 변경하였다.변수와 메서드의 나열 순서변수는 사용하는 순서대로 나열한다.인지적 경제성메서드의 순서도 고려해보아야 하는데 객체의 입장에서 생각하자.객체는 협력을 위한 존재이다. 외부세계에 내가 어떤 기능을 제공할 수 있는지를 드러낸다. (정해진 답은 아니지만 우빈님 추천) 공개 메서드들을 상단에 배치하고 나머지를 private 메서드들로 나열한다.공개 메서드들끼리도 기준을 가지고 배치하는 것이 좋다.객체지향을 하다보면 중요한 객체의 경우 메서드가 수십개까지도 늘어날 수 있는데 중요도 순, 종류별로 그룹화하여 배치하면 실수로 비슷한 조직의 메서드를 중복으로 만드는 것을 일관성 있는 로직을 유지할 수 있다.상태변경을 최우선, 그 이후는 판별, 조회로직이 있는 메서드들 순으로 한다.비공개 메서드는 공개 메서드에서 언급한 순서대로 배치한다.공통으로 사용하는 메서드라면 (가장 하단과 같이) 적당한 곳에 배치한다. 이를 통해 우리의 예시 코드도 이와 같이 리팩토링 하는 작업을 해보았다.패키지 나누기패키지는 문맥으로써의 정보를 제공할 수 있다.패키지를 쪼개지 않으면 관리가 어려줘 진다.패키지를 너무 잘게 쪼개도 마찬가지로 관리가 어려워진다.대규모 패키지 변경은 팀원과의 합의를 이룬 시점에 하자.현재 기준으로 본인만 변경하고 있는 부분이라면 괜찮으나 여러 사람이 변경중인 부분이나 공통으로 사용하는 클래스들의 패키지를 한번에 변경하면 추후 브랜츠 충돌이 날 수 있다.따라서 처음 만들때부터 잘 고민해서 패키지를 나누는 것이 좋다.  기능 유지보수하기 (1) - 버그 잡기해당 시간에는 깃발이 전부 꼽았을때 승리조건으로 가는 오류를 고쳐보았다. 이 수정을 통해 우리가 객체지향적으로 작성하여 수정될 곳이 적었지만 만약 이전의 코드였다면 여러군데 고쳐야 할 우려가 있었을 것이다.기능 유지보수하기 (2) - 알고리즘 교체하기 DFS(깊이 우선 탐색) -> 재귀, StackBFS(너비 우선 탐색) -> Queue재귀를 이용한 DFS도 결국 stack이다.스레드마다 생기는 스택영역에는 함수를 호출할 때마다 frame이 쌓인다.frame은 지역변수, 연산을 위한 정보등을 담고 있다.stack영역은 결국 크기가 제한되어 있다.우리가 필요한건 각 Cell의 모든 메서드 정보가 아니다. 각 Cell의 CellPosition만 있다면 원하는 작업을 할 수 있다. 그래서 해당 부분을 통해 우리는 재귀 로직을 stack형태로 변경을 해보았다.IDE의 도움 받기읽기 좋은 코드란 결국 가독성이 좋아야 한다. 이것을 위해 IDE의 큰 도움을 받을 수 있다.코드 포맷 정렬: option + cmd + L | Ctrl + Alt + L코드 품질: SonarLintlint: 잠재적인 문제가 될 수 있는 오류, 버그, 스타일등을 미리 알려주는 코드품질 체크도구포맷규칙: .editorconfig여러사람과의 협업을 염두하면 IDE의 기본 포맷팅에 익숙해지는 것이 좋다.스타일은 혼자 결정하는 것이 아니라 팀 내 합의로 도출되어야 한다.한번 정해지면 절재적인것이 아니라 사용하면서 계속 의견을 듣고 개선/반영하는 것이 좋다. 이런 기능들을 이용하여 우리 예제 프로젝트들도 포맷팅 및 리팩토링을 해보았다. ex) Stack -> Deque연습프로젝트 소개이번에는 새로운 도메인인 '스터디카페 이용 시스템'을 리팩토링하기 전 코드 해석을 하였다. 우빈님께서는 아래와 같은 사항을 중점으로 리팩토링을 해보시라고 하셨다.1. 추상화 레벨 (메서드 추출등)2. 객체로 묶어볼 만한 것은 없는지..3. 객체지향 페러다임에 맞게 객체들이 상호협력하고 있는지4. SRP: 책임에 따라 응집도 있게 객체가 잘 나뉘어져 있는지5. DIP: 의존관계 역전을 적용할만한 곳은 없는지6. 일급 컬렉션그래서 미션을 진행해보고 한번 강의를 학습해야겠다.미션3해당 미션을 하면서 조금은 많은 부분을 느꼈다. 일단 이렇게 클린코드 관점으로 코드를 리팩토링하는 것이 처음이기에 매우 익숙치 않았고 상당히 오래 걸렸다. 일단 나 나름대로 처음에 소개해준데로 리팩토링을 해보았자만 현재 코드에 대해 나름대로 만족을 한다.미션3 깃허브 링크리팩토링 (1) - 추상화 레벨해당 부분에는 예제 프로젝트에서 중복제거 및 메서드 추출 및 객체에 메세지를 보내어 getter방지를 해보았다. 내가 했던 미션과 비교를 해보니 이 부분은 대강 얼추 방향성을 잘 따란것 같다. 나는 여기서 추가적으로 라커 정책을 구현하였는데 이 부분은 내가 잘한 부분인지는 아직도 헷갈린다.리팩토링 (2) - 객체의 책임과 응집도이번 강의에서는 배울 점이 많았다. 나는 해당 설정 관련 부분들을 config에 빼고 해당 config를 getter로 삼는 provider로 넘겨주는 식으로 하였다. 하지만 I/O통합 부분은 진짜 강좌를 보면서 "아! 이것도 있었지.."라는 생각이 들며 조금은 반성이 되었다. 나머지 일급 컬렉션, display()의 책임 분리, Order객체로 분리하여 비즈니스 로직 이관까지는 그래도 비슷하게 갔던것 같다. 나는 거기서 조금 if문 3개로 나눠진 display를 switch문으로 변경까지 조금 읽기 쉬운 코드로 변경해보았다.리팩토링 (3) - 관점의 차이로 달라지는 추상화해당 부분은 나는 DIP 생각 없이 지뢰때 했던것 처럼 초기화 로직과 실행로직을 분리하고 이렇게 생각하니 FileHandler부분도 두개의 메서드를 분리할 수 있지 않을까 싶었고 그렇게 분리를 하였는데 강의에서는 DIP원칙을 적용하여 했었던것이다. 왜 그런지 모르고 그냥 기계처럼 한 것이 조금 반성스럽고 고쳐야할 부분이라 생각이 든다.능동적 읽기복잡하거나 엉망인 코드를 읽고 이해하려 할 때 리팩토링 하면서 읽기공백으로 단락 구분하기메서드와 객체로 추상화 해보기주석으로 이해한 내용 표기하며 읽기우리에게는 언제든 돌아갈 수 있는 git reset --hard가 있다.핵심목표는 우리의 도메인 지식을 늘리는 것 그리고 이전 작성자의 의도를 파악하는 것 이전까지 나는 코드를 눈으로 해석하고 리팩토링 하려는 습관들이 있었다. 하지만 이번 강의를 통해 코드를 분리해보고 주석도 달아보면서 리팩토링하면서 읽어가야겠다는 습관으로 고쳐야겠다는 생각이 들었다.오버 엔지니어링필요한 적정수준보다 더 높은 수준의 엔지니어링ex) 구현체가 하나인 인터페이스인터페이스 형태가 아키텍쳐 이해에 도움을 주거나 근시일내에 구현체가 추가될 가능성이 높다면 OK.구현체를 수정할때마다 인터페이스도 수정해야함.코드 탐색에 영향을 줌 / 어플리케이션이 비대해짐ex) 너무 이른 추상화정보가 숨겨지기 때문에 복잡도가 높아진다.후대 개발자들이 선대의 의도를 파악하기가 어렵다. 지금 이 내용을 학습해보니 이전에 미션3에서 내가 구현한 코드들이 오버엔지니어링이였지 않을까라는 생각을 하면서 반성하게 되었다.은탄환은 없다클린코드도 은탄환은 아니다.실무: 2가지 사이의 줄다리기지속가능한 소프트웨어의 품질 vs 기술부채를 안고 가는 빠른 결과물대부분의 회사는 돈을 벌고 성장해야하고 시장에서 빠르게 살아남는 것이 목표이런 경우에도 클린코드를 추구하지 말라는 것이 아니라 미래시점에 잘 고치도록 할 수 있는 코드센스가 필요하다. 결국은 클린코드의 사고법을 기반으로 결정하는 것모든 기술의 방법론은 적정 기술의 범위 내에서 사용ex) 당장 급하게 배포나가야 하는데 동료에게 style관련된 리뷰를 주고 고치도록 강요하는 사람도구라는 것은 일단 그것을 한계까지 사용할 줄 아는 사람이 그것을 사용하지 말아야 할때도 아는 법이다.적정 수준을 알기 위해 때로는 극단적인 시도도 필요하다. 이것을 보고 미션때 오버 엔지니어링을 해보는것도 좋은 경험이 되었다고 다시 느끼게 되었다.📚 기술부채란?현 시점에서 더 오래 소요될 수 있는 더 나은 접근방식을 사용하는 대신 쉬운(제한된) 솔루션을 채택함으로써 발생되는 추가적인 재작업의 비용을 반영하는 소프트웨어 개발의 한 관점마무리하며드디어 해당 강의가 마무리 되었다. 여기서 가장 핵심은 추상이다. 우리는 또한 추상과 구체를 인식할 수 있다. 김창준님께서 집필하신 '함께자라기'라는 책을 보면 알듯이 추상과 구체를 넘나들어야 한다. 때로는 bottom-up 때로는 top-dowon을 사용하면서 추상적인 시각과 구체적인 시각을 자유롭게 사용해보고 조금 더 읽기 쉽고 좋은 코드를 작성하는 개발자가 되어야 겠다는 생각이 들었다.Day4 공통 피드백내 코드가 예시로 나왔다 우빈님께서 해주신 말씀은 아래와 같았다.1. boolean으로 return하고 있는 메서드에 예외를 발생시키는데 시도는 좋으나 항상 메서드의 사용현황을 파악 후 상황에 맞게 리팩토링을 하는 것이 좋다. 또한 예외 던지는 것은 비싸기에 항상이 아닌 신중하게 하시라고 조언을 주셨다.2. 추출한 메서드의 static 키워드가 존재한다면 인텔리제이 IDE에서 메서드 추출을 하면 자동으로 붙기에 알아서 제거해줘야 한다.3. 상황에 맞게 적절한 수준의 리팩토링이 좋다. 너무 자세히 가면 오버 엔지니어링이 된다.Day7 코드리뷰내가 만든 코드에서 StudyCafeConfigProvider라는 객체를 만들고 사용중이였는데 아래와 같이 이 안에는 전부 static 메서드만 있었다.public class StudyCafeConfigProvider {     private static final StudyCafeConfig CONFIG = new StudyCafeConfig();     public static InputHandler getInputHandler() {         return CONFIG.getInputHandler();     }     public static OutputHandler getOutputHandler() {         return CONFIG.getOutputHandler();     }     public static StudyCafeSeatReadProvider getStudyCafeSeatReadProvider() {         return CONFIG.getStudyCafeSeatReadProvider();     }     public static StudyCafeLockerReadProvider getStudyCafeLockerReadProvider() {         return CONFIG.getStudyCafeLockerReadProvider();     }     public static Map<StudyCafePassType, StudyCafePassType> getStrategyMap() {         return CONFIG.getStrategyMap();     }     public static Map<StudyCafePassType, LockerPolicyType> getLockerPolicyMap() {         return CONFIG.getLockerPolicyMap();     } }이런 경우에는 private constructor를 만드는것이 좋다고 SonarLint에서 알려준다. 하지만 내 인텔리제이에서는 SonarLint를 적용되어 있지만 경고가 따로 뜨지 않았는데 이 부분은 한번 자세히 살펴봐야겠다.자세한 리뷰 및 후기는 추가 포스팅을 하여 정리해봐야겠다.후기이번 주도 금방 지나갔다. 리뷰를 들으면서 많은 고민과 생각이 들었다. 또한 다른 러너분들의 코드를 보면서 신박한 생각과 좋은 점들이 눈에 보이기 시작했다. 우빈님이 주신 피드백과 다른분들의 코드중에 좋은 점들을 채택해서 더 좋은 코드들로 한번 리팩토링을 다시금 해봐야겠다. 다음주부터는 테스트 강의의 시작이다. 테스트도 조금은 걱정이 되지만 열심히 해서 조금 더 성장하는 주가 되었으면 하는걸로 마무리를 지어보겠다.

백엔드인프런워밍업스터디클럽2기백엔드클린코드테스트코드발자국

msj09252

[인프런 워밍업 클럽 스터디 2기] 백엔드 클린 코드, 테스트 코드 1주차 발자국

해당 글은 박우빈님의 강의를 수강하며 작성한 글입니다. 강의 수강1. 추상과 구체추상과 구체, 이 두 단어는 항상 같이 다니는 한 쌍이다.추상이란?구체적인 정보에서 어떤 이미지를 뽑아내는 것.중요한 정보는 가려내어 남기고 덜 중요한 정보는 생략하여 버린다.추상은 항상 구체적인 실재에서 시작해야 한다.위 내용을 통해 구체적인 내용에서 추상의 단계로 가는 방법이 있고, 그 반대의 방법도 존재한다.우리는 강의를 통해서 구체적인 지뢰찾기 코드를 추상화해가며 객체지향의 개념을 적용해보는 시간을 가졌다.  2. 논리, 사고의 흐름뇌 메모리 적게 쓰기최소한의 인지적 노력으로 최대의 정보를 제공해야 한다.뇌는 범주화라는 작업을 통해 무언가를 떠올린다.이 말은 즉, 누군가 작성한 코드(조상)가 남(후손)이 보아도 쉽게 읽고 이해할 수 있다면, 그것은 뇌 메모리를 적게 쓸 수 있게 한 효과적인 코드가 아닐까 싶다.Early returnEarly return 사용을 통해 else의 사용을 지양하자사고의 depth 줄이기공백 라인공백 라인도 의미를 가진다.읽는 사람으로 하여금 의미를 분절시켜 추가적인 정보전달이 가능하다.부정어 사용부정어구를 쓰지 않아도 되는 상황인지 체크부정의 의미를 담은 다른 단어가 존재하는지 고민하거나 부정어구 메서드 사용해피 케이스와 예외 처리 3. 객체 지향 패러다임객체 설계하기 : 미션으로 대체SOLIDSRP: Single Responsibility PrincipleOCP: Open-Closed PrincipleLSP: Liskov Substitution PrincipleISP: Interface Segregation PrincipleDIP: Dependency Inversion Principle 4. 객체 지향 적용하기상속과 조합상속보단 조합을 이용하자.조합과 인터페이스를 활용하는 것이 유연한 구조다.Value Object훌륭한 추상화 기법 중 하나기본 타입을 객체로 감싸서 의미 부여 및 추상화를 하는 것.동등성 vs 동일성일급 컬렉션Enum의 특성과 활용상수집합변경이 잦은 개념은 Enum보다 DB로 관리하자.다형성 활용하기숨겨져 있는 도메인 개념 도출하기도메인 지식은 만드는 것이 아니라 발견하는 것이다.미션Day 2추상과 구체의 예시 제시추상"노래를 듣는다" 라는 추상적인 문장을 구체화 해보았다구체이어폰에서 발생한 음파가 공기를 통해 외이로 도달한다.외이도를 통해 전달된 음파는 고막에 도달하고, 고막은 음파에 의해 진동한다.고막의 진동은 중이의 세 개의 작은 뼈로 전달된다.세 개의 뼈는 음파의 진동을 증폭시켜 내이의 달팽이관으로 전달한다.달팽이관에 있는 액체가 움직이면서 유모 세포를 흔들고, 이 과정에서 전기 신호로 생성한다.전기 신호가 청각 신경을 통해 뇌로 전달된다.뇌의 청각 피질이 신호를 분석하고 해석하여 소리를 인식한다.Day 41. 코드 리팩토링2. SOLID에 대한 자기만의 언어로 정리깃허브 링크 대체노션 링크 대체회고클린코드란 무엇인가 에 대해 다시 한번 고민하게 되는 주간이었다. 사이드 프로젝트를 진행하면서 주로 나의 코드를 내가 자주 보고 고치기 때문에 남이 어떻게 이해할지에 대해서 깊게 고민해보지 못했던 것 같다. 이번 기회를 통해 프로젝트 코드에서 고칠 부분이 상당히 많이 떠올랐고, 스터디 중간 중간 적용해보려고 노력하려 한다. 빡빡한 스케줄을 회사와 병행하다보니 강의 수강과 미션 수행을 모두 처리하는 것이 상당히 쉽지 않았다. 다행히 이번 주간에는 쉬는 날이 많아서 겨우 맞춰나간 것 같다. 많은 스터디원 분들이 성실히 수행하는 것을 보며 동기부여가 되고 있다.남은 3주도 잘 마무리하여 완주러너가 되고 싶다.

BE클린코드워밍업클럽

Rojojun

[워밍업 클럽 스터디 2기::백엔드] 1주차 발자국

1주일간의 학습 회고이번 한 주 동안 운이 좋게도 시간을 내어 강의를 모두 들을 수 있었다. 강의를 들으면서 강사님이 예제로 주신 프로젝트를 리팩토링하는 시간을 가졌고, 이를 어떻게 하면 더 효율적으로 리팩토링할 수 있을지 고민도 많이 했다. 나의 개발 철학: 누구나 이해할 수 있는 코드내가 코드를 작성할 때 가장 중요하게 생각하는 점은 바로 누구나 이해할 수 있는 코드다. 아무리 정교하고 복잡한 로직이라도 나 외에 다른 사람들이 이해하지 못한다면, 코드 자체는 잘 작성했을지 모르지만 그 코드는 좋은 코드라고 할 수 없다. 개발자는 혼자 일하는 것이 아니라 여러 명과 협업하는 직업이다. 또한 코드는 끊임없이 변화하고 발전하는 유기체라고 생각한다. 그렇기에 모든 사람이 이해할 수 있고, 지속적으로 유지보수할 수 있는 코드가 좋은 코드라고 생각한다. 이번 주 학습에서 잘한 점이번 주 학습에서 내가 스스로 잘했다고 생각한 부분은 기존에 알고 있던 개념들, 예를 들어 톰 롱의 좋은 코드 나쁜 코드, 켄트 백의 Tidy First, 로버트 C. 마틴의 클린 코드와 같은 책에서 배운 내용을 확장시켰다는 점이다. 또한, 내가 경험한 부분들을 도입해서 좀 더 깊이 있게 이해하려고 노력했다는 점이 좋았다. 아쉬웠던 점: 집중력 유지의 어려움하지만 아쉬웠던 점도 있었다. 집중력 유지가 생각보다 쉽지 않았던 것이다. 외부적 요인이든 내부적 요인이든 컨디션에 따라 학습의 진도가 들쑥날쑥했다. 컨디션이 좋은 날은 많은 진도를 나갔지만, 좋지 않은 날은 적은 진도를 공부하게 됐다. 그럼에도 꾸준히 공부하고 성취를 이뤄냈다는 점은 매우 중요한 성과다. 이번 주에 목표했던 학습을 어느 정도 완료했고, 다가오는 한 주는 더 구체적인 목표를 세워 동료와 함께 학습 내용을 토론하며 우리 회사에 적용해 볼 계획이다. 실습과 이론이 어우러진 미션이번 주차에는 두 가지 미션이 주어졌다. 하나는 구체라는 개념을 일상생활에 빗대어 설명하는 미션이었고, 다른 하나는 주어진 코드를 SOLID 원칙에 맞게 리팩토링하는 과제였다. 구체를 일상생활에 빗대어 설명하는 과제는 매우 흥미로웠다. 프로그래밍을 공부하다 보면 언어 자체가 번역투로 이루어져 있거나 어려운 용어가 많아 쉽게 이해하기 힘들 때가 많다. 하지만 일상생활에 비유해 설명하니 훨씬 쉽게 이해할 수 있었고, 동료들에게도 설명할 때 유용했다. SOLID 원칙 과제에서는 내가 생각하는 중복 문제를 예시로 들고 설명했다. 이 과정에서 초등학생에게 설명한다는 가정하에 설명을 시도해 보았다. 초등학생도 이해하지 못한다면, 나 역시 완전히 이해하지 못한 것이기 때문이다. 이는 전문가는 어려운 개념을 쉽게 설명할 수 있지만, 사기꾼은 쉬운 개념을 어렵게 설명한다는 유명한 말과 일맥상통한다. 회사 팀원들과의 협업이번 스터디는 회사 팀원들과 함께 진행하고 있는데, 팀원들과 함께 성장할 수 있는 좋은 기회가 되고 있다. 강의 내용도 매우 만족스러웠고, 팀원들의 반응도 긍정적이다. 누군가 이 강의를 추천하냐고 물어봤을 때, 나는 이렇게 대답했다. “클린 코드 책 읽어보셨나요? 읽으셨다면 좋은 강의고, 안 읽으셨다면 꼭 들어야 할 강의에요.” 클린 코드는 한 사람의 의견만이 정답은 아니라고 생각한다. 여러 사람의 이야기와 경험이 모여야 비로소 읽기 좋은 코드가 완성된다고 본다.

백엔드클린코드백엔드

양성빈

인프런 워밍업 스터디 클럽 2기 백엔드(클린코드, 테스트코드) 1주차 발자국

이 블로그 글은 박우빈님의 강의를 참조하여 작성한 글입니다.드디어 0기때 이후 첫 발자국을 작성해보는 시간이다. 처음 마음 먹었던 초심을 생각하며 0기때보다 더 나은 활동을 해보자고 다짐하며 이 글을 써내려간다.강의 소개처음 이 강의를 듣기 전, 나는 아래와 같이 생각했다.🤔 읽기 좋은 코드가 과연 무엇일까? 읽기 좋은 코드는 왜 필요할까?해당 물음을 가지고 강의를 듣기 시작하였다.우리는 코드를 작성(쓰기)보단 읽는데 시간을 더 많이 투자한다. 기존 코드에 내 코드를 추가하기 위해 내가 과거에 작성한 코드 혹은 다른 동료분들이 작성한 코드를 읽는다고 해보자. 하지만 이해가 안된다면 다시금 읽게 될 것이다. 그리고 많은 시간 끝에 이해라는 경지에 도달하거나 절망이라는 경지에 도달할 것이다. 결국 코드를 작성한다는 것은 모두가 이해하기 쉬운 읽기 기반의 코드를 작성한다는 의미이다. 그리고 코드를 잘 짠다라고 말하는 것은 결국 읽기가 좋다는 의미이고 읽기 좋은 코드는 결국 미래를 위해 유지보수를 위해 필요한 작업이라고 생각이 든다.또한 우린 가끔 이런 말을 하곤 한다. 나도 몇번 들었던 말이다.코드는 작성하고 난 순간부터 레거시다.여기서 레거시란, 오래되고 유지보수가 힘든 코드라고 생각하면 된다. 즉, 우리가 작성한 코드들은 먼 훗날의 레거시 코드가 되고 이 레거시 코드들을 유지보수 할 미래의 나 혹은 동료들을 위해 읽기 쉬운 코드를 작성함이 좋을듯 싶어진다. 나 또한 이번에 제대로 학습하여 실무에도 적용해볼 수 있는 기회가 되었으면 한다.✅ 읽기 쉬운 코드 작성하는 현재까지 생각- 미래를 위해 그래야함- 유지보수 하기 좋음- 미래의 나와 동료를 위해 읽기 쉬운 클린코드를 작성해야함. 강의 구성클린코드, 리팩토링 과정의 최고의 연습은 테스트코드를 작성하는 것이다.테스트 코드를 작성문화를 가진 기업들은 일반적으로 아래의 절차를 가진다.🛠 리팩토링 프로세스 (주관적인 생각)1. 리팩토링 대상/범위 확인2. 기능보장을 위한 테스트 코드 작성3. 리팩토링4. 테스트 코드 검증위의 프로세스로 리팩토링을 진행하지만 우빈님께서는 현재 테스트코드 없이 진행한다고 하셨다. 또한 롬복도 사용을 하지 않으신다고 하며, 순수코드에 집중한다고 하셨다.나 또한 이번에 우빈님 말씀대로 순수코드에 집중해보며, 시간과 여유가 있을 시, 내가 스스로 테스트 코드도 작성해봄이 좋을 것 같다! 강의에 사용할 용어들 📚 용어정리- 도메인(domain): 실무에 자주 사용되는 말들로, 해결하고자하는 문제영역들을 말한다.(비즈니스 요구사항 함축)- 도메인 지식: 도메인을 이해하고 해결하는데 필요한 지식- 레거시, 유산: 우리가 현자 가지고 있는 코드- 조상: 과거의 나 / 동료 / 이 코드를 작성했던 개발자분들- 후손: 미래의 나 / 동료 / 내가 만든 코드들을 보게 될 많은 개발자분들 우리가 클린코드를 추구하는 이유 우리는 왜 클린코드라는 것을 지켜야 하는걸까? 결론부터 말하면 가독성때문이다. 가독성이 좋다라는 것은 이해가 잘된다는 것을 말하며 그것은 곧 유지보수가 수월해지며 그 만큼 우리의 시간과 자원을 절약한다. 세상에는 클린코드라 지칭하는 수많은 원칙들과 조언들이 존재한다. 하지만 이런 원칙들을 관통하는 아주 중요한 주제가 있는데 그게 바로 추상이다. 이번부터 추상을 한번 알아보자. 프로그램의 정의 그러면 추상이라는 것을 구체적으로 들어가기 전에 프로그램이 무엇인지 알아보자. 내가 생각하는 프로그램은 아래와 같다. 프로그램 = 설치하는 것 쉽게 생각해서 프로그램은 설치하는것이라 알고 있었다. 거기서 프로그램의 구조는 데이터 + 코드로 이루어진다 생각한다. 그래서 앞으로 이 2가지를 가지고 데이터의 추상과 코드의 논리를 가지고 학습해볼 예정이다. 추상과 구체 이제 본격적으로 추상을 들어가보자. 추상하면 같이 따라 다니는 단어가 있는데 그것이 바로 구체이다. 이 두 단어는 매우 중요한 단어이다. 추상이라는 단어가 무엇일까? 우빈님께서 한자와 위키백과, 피카소의 명언을 참조하셔서 말씀을 해주셨듯이 추상은 아래와 같다. 📚 추상- 구체적인 정보에서 어떤 이미지를 뽑아내는 것이다- 중요한 정보는 가려내어 남기고 덜 중요한 정보는 생략하여 버린다.- 추상은 항상 구체적인 실제에서 시작해야한다.(feat. 피카소) 즉, 위에 이야기를 토대로 나온 것이 추상화 레벨이다. 즉, 추상화라 하는 것은 내가 생각했을 때 추상적인 것이 몇 %이고 구체적인것이 몇 %이냐라는 것이다. 쉽게 비유하면 이런것이다. 친구와 이야기를 나눌때 진지함 30%이고 농담 70% 주제야!라고 한다. (개인적인 이야기) 아무튼 추상화는 중요한것 같다. 하지만 막상 너무 어렵게 느껴진다. 하지만 오히려 인간은 추상화 능력이 매우 뛰어난 존재이다. 예시를 보자. 친구와 아래와 같이 이야기를 했다 해보자. 🗒 예시Q. 주말에 뭐했어?A. 나는 하나의 큰 공간에서 나의 신경을 이용해 나의 걷는 수단으로 하나의 큰 구체를 차는 행위를 했어! 이렇게 했을 때 친구의 반응은 아래와 같을 수 있다. 하지만 우리는 유추를 어느정도 해볼 수 있다. 🗒 예시Q. 주말에 뭐했어?A. 축구했어! 첫번째 예시는 구체이고 바로 위의 예시가 추상화 과정이다. 추상화: 정보 함축, 제거 구체화: 유추, 정보재현 이해 이것은 컴퓨터 과학에서도 사용된다. 🙋🏻 컴퓨터는 0과 1밖에 모르는데 어떻게 고수준의 작업을 할까? 먼저 정답을 이야기하면 바로 추상화과정때문이다. 그럼 자세히 들어가보자. 1bit가 무엇일까? 0과 1을 의미한다. 이것은 정보의 최소단위이고 이것은 결국 전구의 켜짐과 꺼짐을 나타내는데 이것을 존재성이라 한다. 1byte는 무엇일까? 용량의 최소단위이며 8bit를 묶어서 1byte라 한다. 또한 프로그래밍 언어에서 자료형을 이야기할때 몇byte를 묶어서 하나의 자료형을 나타낸다. 여기서 데이터(bit) 덩어리를 짤라서 묶고 어떻게 읽을것인가이다. 즉, 이것이 추상화이다. 또한 AND나 OR같은 논리 연산식도 데이터와 데이터가 만나 새로운 데이터를 만드는 방법을 의미한다. 즉, 종합해보면 이전에 프로그램을 데이터와 코드로 나타냈는데 위의 예시를 통해 각각 추상화가 가능하다는 것을 알 수 있다. 그러면 처음 질문으로 돌아가서 고수준 작업은 무엇일까? 고수준이라는 것은 추상화 레벨이 높다는 것이고 그와 반대로 저수준은 추상화 레벨이 낮다는 것이다. 대표적인 예시로 프로그래밍 언어와 기계어, 하드웨어와 운영체제와 어플리케이션, OSI7 Layer를 예로 보면 확 알 것이다. 그런데 우리는 읽기 좋은 코드를 이야기 하고 있는데 왜 추상화 과정이 필요할까? 적절한 추상화는 복잡한 데이터와 복잡한 로직을 단순화하여 이해하기 쉽도록 돕는다. 즉, 읽기 쉽다! 예시를 통해 보자. 성빈나라에 축구라는 것을 뻥뻥이라고 해보자. 그리고 친구한테 뻥뻥이했다라고 말하면 친구는 못알아 들을 것이다. 이렇게 못알아 듣는 이유 즉, 추상으로부터 구체를 유추하지 못한 이유는 추상화 과정에서 중요정보를 부각시키지 못했고 상대적으로 덜 중요한 정보를 제거했기 때문이다. 또한 해석자가 동일하게 공유하는 문맥이 아니기 때문이다. 이 말은 중요한 정보는 기준이 각각 다르고 즉, 도메인 별로 추상화 기준이 다르다는것을 알 수 있다. 그래서 다른 실무진들이 도메인 지식이 중요하다는 것도 이와 같기 때문이라 생각이 든다. 잘못된 추상화는 side-effect를 유발하며 이는 매우 critical하다. 적절한 추상화는 해당 도메인의 문맥 안에서 정말 중요한 핵심개념만 남겨서 표현한다. 적절한 추상화의 대표적인 행위가 바로 이름짓기인데 한번 이름짓기에 대해 살펴보자. 이름 짓기 개발자들이 업무를 하면서 무엇이 가장 힘드냐고 물어보면 10의 9은 "이름짓기"가 힘들다고 말한다. 나 또한 실무에서 변수명같은것을 짓는데만 30분을 소비한 경험이 있곤 하다. 그런데 진짜로 이름 잘 짓는게 중요할까? 나는 중요하다고 본다. 중요하지 않은 일을 이렇게 시간투자하면서 많은 개발자들이 힘들게 시간을 투자하고 있지 않을 것이다. 이름 짓는다는 행위는 추상적인 사고를 기반으로 한다.추상적인 사고는 첫째, 표현하고자 하는 구체에서 정말 중요한 핵심개념만을 추출하여 잘 드러내는 표현이며, 우리 도메인의 문맥안에서 이해되는 용어이다. 그럼 이름 짓는 주의 점들을 살펴보겠다. 단수, 복수 구분 끝에 -(e)를 붙여서 어떤 데이터(변수, 클래스)가 단수인지 복수인지 나타내는 것만으로도 읽는이에게 중요한 정보를 전달 할 수 있다. 이름 줄이지 않기 줄임말이라는 것은 가독성을 제물로 바쳐 효율성을 얻는 것으로 대부분 잃는 것에 비해 얻는 것이 적다. 즉, 자재하는 것이 좋으나 관용어처럼 많이 사람들이 사용하는 줄임말이 존재한다. column → col, latitude → lat, longitude → lon, count -> cnt 위의 예시에서 count를 줄이는 것은 우빈님은 비추하신다고 하셨다. 왜냐하면 겨우 5글자에서 3자로 줄이고 cnt만 봤을때 count라고 연상이 안된다고 하셨는데 그 이야기를 들어보니 나도 뭔가 와 닿지 않았다. 이전까지 실무에서도 귀찮을때 이렇게 줄이곤 했는데 조금 반성하게 되는 계기가 된 것 같았다. 또한 자주 사용하는 줄임말이 이해할 수 있는 것은 사실 문맥을 보고 알 수 있다. 예를 들어 익명 클래스에 저런 관용어가 있다고 해보자. 처음 보는 신입 개발자는 알 수 없을 것이다. 은어, 방언 사용 X 농담에서 파생된 용어, 일부 팀원, 현재 우리팀만 아는 은어 금지해야한다. 만약 다른 개발자가 해당 코드를 보면 이해하기 힘들기 때문이다. 또한 되도록 도메인 용어를 사용하자! 우리도 실무에서 도메인 용어 사전을 엑셀로 만들어서 사용하곤 한다. 이러닝 도메인을 가진 우리의 예로 보면 스코라는 단위를 표현할때 sco라는 표현 혹은 sc를 많이 사용한다. 물론 이렇게 정의되었더라도 하나로 정하면 그 프로젝트에서는 그것으로 밀고 사용해야한다. 좋은 코드 보고 습득하기 비슷한 상황에서 자주 사용하는 용어, 개념을 습득하자. github에 open되어 있는 라이브러리나 프레임워크의 코드를 보면서 용어들을 정리해 볼 필요가 있다고 느껴졌다. ex. pool, candidate, threshold등 (일상 용어 != 코드용어) 그럼 이제 코드를 통해 우빈님이 제공해준 코드를 고쳐보자. 자세한 것은 강의를 통해 확인해보고 간략히만 설명해보겠다. 아래와 같이 의미가 없는 변수들을 변경해보았다. 대표적으로 for문의 i와 j의 변수를 아래와 같이 변경해보았다. for (int row = 0; row < 8; row++) {   for (int col = 0; col < 10; col++) {     board[row][col] = "□";   } } 이런식으로 나머지도 수정을 해보았다. 추가적으로 나는 주석도 달아보았다. 주석이 없으니 우빈님과 코드를 읽으면서 뭔가 난해한 부분도 많기 때문에 주요 로직에 주석을 다는것도 Readable code이지 않을까? 메서드와 추상화 잘 쓰여진 코드의 메서드는 반드시 1개의 주제를 가져야 한다. 메서드의 선언부로 우리는 구체적인 내용을 추상화 할 수 있다. 만약 그렇게 하기 힘들다면 그 메서드는 2가지 이상의 일을 하고 있다고 볼 수 있다. 역할분리가 힘든 메서드라고 나는 생각한다. 그래서 메서드를 작성할때 생략할 정보와 의미를 부여하고 드러낼 정보를 구분하는것이 중요하다. 즉, 추상화가 중요하다. 만약 아까처럼 메서드 내용을 보고 이름을 유추하기 힘들다면 그 메서드 안에 의미를 담을 수 있는 더 작은 단위로 쪼개고 그 쪼갠것을 보고 하나의 유추할 수 있는 포괄적 의미의 메서드 이름을 적어보는 것이 좋을 것 같다. 메서드 선언부 메서드 선언부는 아래와 같이 구성되어 있다. 반환타입 메서드명(파라미터) {} ✅ 용어정리메서드 명과 파라미터를 통틀어서 메서드 시그니처라는 용어를 사용한다.메서드 시그니처를 통해서 자바에서 오버로딩이 가능하다. 메서드는 추상화된 구체를 유추할 수 있는 적절한 의미가 담긴 이름이어야 한다. 또한 메서드의 파라미터라는 정보를 통하여 더 풍부한 의미를 전달할 수 있다. 보통 메서드 명을 동사로 시작하는 경우가 있지만 반드시 일 필요는 없다. 단순 데이터 반환 같은 경우는 명사로 하여도 무방하다. 파라미터와 같이 사용할때 메서드 명은 보통 전치사로 끝나는게 좋다고 하셨다. int selectedColIndex = convertColFrom(cellInputCol); int selectedRowIndex = convertRowFrom(cellInputRow); 위와 같이 표기하면 확실히 읽을때 명확해짐을 볼 수 있었다. 파라미터의 타입, 개수, 순서를 통하여 의미전달이 또한 가능하다. 아래의 코드를 살펴보자. public void createLecture(String title, String localDateToString) {} public void createLecture(String lectureTitle, LocalDate openDate) {} 위의 두 메서드중에 2번째것이 명확하다는 것을 알 수 있을것이다. 첫번째 메서드는 메서드를 사용할때 두번째 파라미터에 무엇을 넣어야 하는지 불분명하기 때문이다. 여기서 파라미터는 외부와 소통하는 창이라고 볼 수 있다. 사용하는 입장에서 어떤 정보가 필요한지 알려주는 기능을 한다. 또한 메서드 시그니처에 납득이 가는 적절한 타입의 반환값을 돌려줘야 한다. 만약 납득이 안 간다면 문제가 있는 메서드일 확률이 높다. 또한 void 대신 충분히 반환할만한 가치가 있는 값이 있는지 고민을 해보는게 좋을 것이다. 그리고 추가적으로 메서드를 리팩토링 과정을 거쳐 추상화함에 있어서 너무 긴 코드들을 메서드로 묶는다는 생각보단, 추상화가 필요한 부분을 리팩토링한다고 생각하는것이 좋다. 단 1줄이더라도 추상화할만한 가치가 있다면 하는것이 좋다. 그래서 강의를 통해 예제 프로젝트를 기능단위로 묶어서 메서드로 분리함으로 조금 더 깔끔한 코드로 리팩토링 실습을 해보았다. 추상화 레벨 우리는 이전까지 엄청 긴 코드들(구체)에서 추상화 과정을 거쳐서 메서드를 추출해보았다. 이렇게 메서드를 추출하는 그 순간 읽는 자 기준으로 외부세계와 내부세계의 경계가 생긴다. 당연하게 생각해보면 쭉 코드를 읽다가 갑자기 메서드를 보면 살짝 멈칫할 것이다. 여기서 외부세계란 추상화 레벨이 높은 세계이고 내부세계란 추상화 레벨이 낮은 구체라고 생각하면 좋을 것이다. 그래서 내부세계에서는 구체적인 내용이 들어가 있고 외부세계에서는 메서드의 필요한 파라미터를 내부세계로 넘겨서 메서드명과 반환타입을 전달하게 될 것이다. 또한 하나의 세계 안 에서 추상화 레벨은 동등해야 한다. 만약 그렇지 못한다면 레벨을 맞추게 추상화 과정을 거쳐야 한다. 그래서 실습을 통하여 같은 추상화 레벨이 되도록 메서드들을 분리해보았는데 나는 여기서 몇몇 이해가 안되는 부분이 있었다. 이 부분은 질문을 해봐야 겠다. 매직 넘버, 매직 스트링 매직 넘버, 매직 스트링을 알기 전에 상수부터 알아보자. 상수로 추출한다는 것은 하나의 이름을 부여한다는 것이고 즉, 추상화 한다는 것이다. 매직 넘버와 매직 스트링은 의미를 갖고 있으나 상수로 추출되지 않은 숫자나 문자열을 의미하며 이름을 부여함으로 읽기 좋은 코드에 다가간다. 실습을 통해 매직 넘버와 매직 스트링을 상수로 추출해보았다. 미션1 해당 미션을 진행하면서 구체와 추상에 대한 개념을 확실히 이해할 수 있었다. 조금은 어색했지만 예시들을 몇개 해보니 당연한 것들이었다.제가 작성한 미션 링크는 아래와 같습니다. 미션1 링크  뇌 메모리 적게 쓰기 정리하는 시스템에서 중요한 과제는 최소한의 인지적 노력으로 최대의 정보를 제공해야 한다고 뇌 과학에서 이야기를 한다. 지금 내가 생각해보면 그렇다. 인간의 뇌가 컴퓨터 메모리라고 하였을때 우리의 뇌는 싱글 스레드 기반으로 처리할 것이다. 그리고 다른 사물을 보았을때 다른것을 생각할때 컨텍스트 스위칭이라는 작업을 거쳐야 한다. 우리의 뇌는 다른 것을 생각할때 범주화라는 작업을 거쳐야 한다. 범주화란 특정 대상의 특징들을 분류하는 작업을 말한다. 즉, 범주화를 통해 우리의 뇌는 최소한의 기억 데이터로 최대 효과를 볼 수 있다. 우리의 뇌는 싱글 스레드 기반으로 돌아간다고 하였고 만약 멀티태스킹을 진행할 경우 수행능력이 떨어지며 속도가 저하된다. 그래서 우리의 뇌는 최소의 인지로 최대의 효율을 볼 수 있다. 이것은 코드도 같다. 결국 이것을 기반으로 우리의 뇌에 적은 메모리가 올라가야 읽기 쉬운 코드가 된다. 후손들이 코드를 읽을때 독자의 메모리를 어떻게 효과적으로 쓸지 고민을 해야 좋은 코드가 된다 생각한다. Early return if else if else구조는 우리의 뇌 메모리를 생각해야 한다. 왜냐하면 else if문을 생각하면 앞의 조건식을 한번 더 생각해야하기 때문이고 else문도 마찬가지다. 그럼 해결책은 무엇이 있을까? 이 조건식들을 하나의 메서드로 추출하고 else if문과 else문을 지우고 if문들을 만들어 거기서 바로 return을 하는 것이다. 이것은 if문뿐만 아니라 switch문에도 같이 적용된다. 즉 결론은 아래와 같다. Early return으로 else의 사용을 지양 그래서 해당부분을 예제 코드에서 리팩토링 하는 과정을 거쳐봤다. 사고의 depth 줄이기 사고의 depth 줄이기가 나오면 아래의 2가지를 들 수 있다. 1. 중첩 분기문, 중첩 반복문2. 사용할 변수는 가깝게 선언하기 그럼 하나하나 살펴보자. 중첩 분기문, 중첩 반복문 왜 사고의 depth를 줄여야 하나? 이유는 이전에 설명했듯이 간단하다. for (int i = 1; i <= 9; i++) {   for (int j = 1; j <= 9; j++) {     if (i >= 2 && j < 8) {       doSomething();     }   } } 위의 코드를 실행한다고 생각해보자. 그러면 첫번째 반복문이 뇌에 입력이 될 것이다. 그리고 두번째 반복문을 만나면 그것도 뇌에 입력이 될 것이다. 그 다음 조건식을 보면 이것또한 뇌에 입력이 되어 같이 생각을 하면 메모리를 많이 쓰게 된다. 그러면 어떻게 추출할까? 아래와 같이 추출할 수 있을듯 하다. for (int i = 1; i <= 9; i++) {   doSomethingI(); } private void doSomethingI() {   for (int j = 1; j <= 9; j++) {     doSimethingIJ();   } } private void doSimethingIJ() {   if (i >= 2 && j < 8) {       doSomething();     } } 위와 같이 메서드로 추출하면 외부의 벽이 생긴다. 그렇게 뇌의 메모리를 분리시켜서 생각하게 할 수 있다. 각각의 메서드는 다른 메서드들이나 조건들을 생각을 안해도 된다. 그렇다고 무조건 1 depth로 만들자고 하는 것은 아니다. 보이는 depth를 줄이는데 급급한것이 아니라 추상화를 통한 사고과정의 depth를 줄이는 것이 중요하다. 2중 중첩구조로 표현하는 것이 사고하는데 도움이 된다면 메서드 분리보다는 그대로 두는 것이 좋다. 때로는 메서드 분리하는 것이 더 혼선을 주기 때문이다. 우리의 실습에서는 메서드 분리보단 Array-Stream을 사용하여 해결을 하였다. 그 이유는 row와 col을 사용하는 이중 반복문이 있는데 이를 각각 메서드로 분리하는게 이상하기 때문이다. 그 이유는 row-col은 하나의 세트처럼 작동하기에 분리가 조금 애매했다. ⚠ 주의그렇다고 무조건 Stream을 쓰라는 말도 아니다. 사용할 변수는 가깝게 선언하기 당연한 애기다. 애를 들어 변수를 선언하고 100줄정도 있다가 그 변수를 사용한다면 선언한 이 변수가 뭐였는지 다시 찾아보는 비효율적인 행동을 해야한다. 우리는 예제코드의 Scanner를 가깝게 위치시켰고 그러다보니 무한반복문에 위치하게 되어 이것을 상수로 바꿔보고 리팩토링 하는 과정을 보았다. 공백 라인을 대하는 자세 공백라인도 의미를 가진다. 복잡한 로직의 의미단위를 나누어 보여줌으로 읽는 사람에게 추가적인 정보전달이 가능하다. 해당 부분을 실습을 통해 의미단위로 끊어보았다. 부정어를 대하는 자세 부정연산자는 생각을 2번하게 만든다. 해당 조건을 먼저 이해해야하고 해당 조건이 아닌 상황을 한번 더 생각해야하기 때문이다. 즉, 가독성이 떨어진다. 그래서 해결방안은 아래와 같다. 1. 부정어구를 쓰지 않아도 되는 상황인지 체크2. 부정의 의미를 담은 다른 단어가 존재하는 지 고민 or 부정어구 메서드 구성 해당 부분을 토대로 예시코드를 리팩토링 해보았다. 해피 케이스와 예외 처리 일반적인 사람들은 해피 케이스에 대해 몰두하는 경향이 존재한다. 하지만 예외를 항상 생각하고 사용자 입력을 불신한 상태에서 예외처리를 꼼꼼이 하는 것이 견고한 소프트웨어를 개발하는 자세일 것이다. 그럼 어떻게 예외처리를 할 수 있을까? 먼저 예외처리 전에 예외 발생 가능성을 낮추는 방법이 있다. 어떤 값의 검증이 필요한 부분은 주로 외부세계와 접점일테니 반드시 검증을 꼼꼼이 구체적으로 해야한다.(ex. 사용자 입력, 객체 생성자, 외부서버의 요청) 또한 의도한 예외와 의도치 못한 예외를 구분해야한다. 의도한 예외는 커스텀 Exception 클래스로 정의하여 두고 그외의 의도치 못한 예외는 Exception클래스로 두는등의 행위를 하고 spring boot같은 웹 어플리케이션에서 ExceptionHandler를 만들어서 처리를 하는등의 행위를 거쳐야 한다. 또한 null을 주의해야한다. 코틀린같은 언어는 언어단에서 처리를해주지만 자바같은 경우는 NullPointerException을 주의해야한다. 그래서 항상 NullPointerException을 방지하는 방향으로 경각심을 가져야 한다. 또한 메서드 설계시 return null을 자제해야한다. 만약 불가피 하다면 Optional 클래스를 이용해보자. Optional Optional은 비싼 객체이다. 꼭 필요한 상황에서 반환타입으로만 사용하자. ⚠ 주의만약 파라미터같은 경우에 사용하는 경우는 피해야한다. 아마 이 부분은 IDE에서 경고표시를 해줄 것이다. 왜 피해야하냐면 분기 케이스가 3개 생기기 때문이다. (Optional이 가진 데이터가 null인지 Optional 그 자체가 null인지 생각때문) 마지막으로 Optional을 반환받았다면 최대한 빠르게 해소해야 한다. Optional 해소 분기문을 만드는 isPresent()-get()대신 풍부한 API를 사용하자. (ex. orElseGet(), orElseThrow 등등) ⚠ 주의orElse와 orElseGet에 사용에 주의하자! orElse같은 경우 항상 실행을 한다. 이게 무슨말이냐면 호출할 필요가 없는 경우에도 항상 실행된다. 그래서 파라미터로 확정된 값이 있을 경우 사용하자. 반면 orElseGet은 null일때만 실행을 한다. 그레서 우리 예제 코드에서 Scanner의 입력을 올바르지 않게 했을때의 부분들을 리팩토링하며, 해당 예외를 커스텀 예외와 의도치 않은 예외를 구분하여 처리하였다. 여기서 나는 조금 더 세밀한 예외정의를 하는게 좋지 않을까라는 생각을 해본다. 추상의 관점으로 바라보는 객체 지향 프로그래밍 패러다임에는 아래와 같이 크게 3가지로 나뉜다. 1. 절차지향: 지금까지 예제코드 작성했듯이 컴퓨터 처리구조처럼 차례대로 실행하는 흐름을 가지는것2. 객체지향: 객체간 협력을 통한 프로그래밍3. 함수형: 순수함수 조합으로 프로그래밍 🙋🏻 순수함수란?외부의 요인 없이 같은 input을 넣었을때 같은 output이 나오는것을 의미한다. 즉, 가변데이터는 멀리하고 side effect없이 프로그래밍하는 것을 의미한다. 그럼 우리는 자바로 프로그래밍을 하므로 객체지향에 대해 알아보자. 여기서 객체란 어떤 목적으로 설계된 추상화된 무엇을 의미한다. 즉, 쉽게 말해 데이터와 코드의 조합으로 보면 된다. 객체지향을 하면서 각각의 많은 객체들을 생성하다보니 객체간 협력과 객체가 담당하는 책임이 높아졌다. '캡추상다'라는 용어를 들어봤나? 나는 처음이다. 하지만 뭔가 대강은 알것 같았다. 캡추상다는 아래와 같다. 1. 캡슐화: 객체 데이터를 숨기고 가공로직을 숨기고 일부만 들어내는것. 즉, 추상화개념이다.2. 추상화3. 상속: 상속은 강력한 기능이지만 너무 남발하면 안된다.4. 다형성: 일종의 추상화이다. 상위 추상화 레벨에서 요구되는 그런 특징들만 뽑아서 인터페이스화해서 사용하는 것을 말한다. 이렇게 하면 구현체 여러개에 바껴서 사용이 가능하다. 객체지향에서만 쓰이는 애기는 아니지만 객체지향을 공부하다보면 관심사 분리 개념이 나온다. 관심사 분리란 특정 관심사에 따라 객체 생성 및 기능과 책임을 나눈다. 이렇게 하는 이유는 나중에 유지보수성이 좋아지기 때문이다. 또한 관심사 분리는 일련의 개념을 모아 이름을 짓고 기능을 부여하는데 일종의 추상화라 확인이 가능할 것이다.(높은 응집도와 낮은 결합도) 객체 설계하기 우리는 메서드 추출과정에서 외부세계와 내부세계가 나눠짐으로 추상화시킨다고 이야기 한 적이 있다. 객체도 마찬가지다. 적절한 관심사들을 분리하여 객체로 만들고 공개 메서드를 통해 외부세계와 소통함으로 객체의 책임을 들어낼 수 있다. 이런 객체들이 모여 객체간 협력을 할 수 있다. 객체 추상화 비공개 필드(데이터), 비공개 로직(코드)공개 메서드 선언부를 통해 외부세계와 소통각 메서드의 기능은 객체의 책임을 드러내는 창구객체의 책임이 나뉨에 따라 객체간 협력이 발생 객체가 제공하는 것절차지향에서 잘 보이지 않았던 개념을 가시화관심사가 한군데로 꼽히기 때문에 유지보수성 증가여러 객체를 사용하는 입장에서는 구체적인 구현에 신경쓰지 않고 보다 높은 추상화 레벨에서 도메인 로직 다룸 아래는 회원의 나이의 유효성 검사를 나타내는 예이다. 이런 식으로 관심사를 집중 시킬 수 있지 않을까? ⚠ 새로운 객체를 만들 때 주의점 1개의 관심사로 명확하게 책임이 정의되었는지 확인한다.메서드를 추상화할때와 유사객체를 만듬으로써 외부세계와 어떤 소통을 하려고 하는지 생각변경된 요구사항 및 리팩토링 과정에서 객체의 책임과 역할이 변경될 수 있다.생성자, 정적 팩토리 메서드에서 유효성 검증이 가능하다.도메인에 특화된 검증로직이 들어갈 수 있다. class Member {   private int age;   public Member(final int age) {     if (this.age < 0) {       throw new IllegalArgumentException("회원의 나이는 0살 미만일 수 없습니다.");     }     this.age = age;   } } setter 사용자제 데이터는 불변이 가장 좋다. 변하는 데이터라도 객체가 핸들링 할 수 있다.객체 내부에서 외부세계의 개입없이 자체적인 변경이 가능한지 확인해보고 가공으로 처리할 수 있을지를 확인해보자.만약 외부에서 가지고 있는 데이터로 데이터 변경 요청을 해야하는 경우 set~이라는 단순한 이름보다 update~같이 의도를 들어내자. getter 사용자제 getter라도 처음에는 사용을 자제하자. 반드시 필요한 경우에만 추가하자.외부에서 객체 내 데이터가 필요하다고 getter를 남발하는것은 객체에 대한 실례되는 행동이다.객체에 메세지를 보내는 방법을 선택해보자. Member member = new Member(); member.getPassword() // 비유하자면 어떤 사람이 강제로 비밀번호를 뺏는 형식이라 볼 수 있다. 필드의 수는 적을수록 좋다. 불필요한 데이터가 많을수록 복잡도가 높아지고 대응할 변화가 많아진다.필드A를 가지고 계산할 수 있는 A필드가 있다면 메서드 기능으로 제공단, 미리 가공하는 것이 성능상 이점이 있다면 필드로 가지고 있는 것이 좋을 수 있다. 그러면 이제 실습의 대략적인 요약을 해보자. 우리는 예제 코드로 Cell이라는 객체로 관심사를 분리 시켰다. 그에따라 변경되는 static 변수들을 변경하고 로직도 변경을 했다. 하지만 여기서 우리는 새로운 도메인 지식을 획득했다. 이제까지 셀이 열렸다/닫혔다라는 개념을 통해 로직을 작성해왔다면 지금은 Cell이라는 객체로 관심사를 분리함으로 사용자가 체크했다라는 개념이 나온것이다. 기존 String기반의 Sign기반의 BOARD가 있고 이를 상황에 따라 표시 할 sign을 갈아끼우는 형태에서 이제 Cell을 정보를 담을 공간의 객체가 생성되었다. BOARD는 Cell을 갈아끼우는 것이 아니라 사용자의 입력에 따라 Cell의 상태를 변화시키는 방향으로 진행하고 리팩토링을 거쳤다. SOLID SRP: Single Responsibility PrincipleOCP: Open-Closed PrincipleLSP: Liskov Substitution PrincipleISP: Interface Segregation PrincipleDIP: Dependency Inversion Principle SRP: Single Responsibility Principle 하나의 클래스는 단 1가지의 변경 이유(책임)만을 가져야만 한다.객체가 가진 공개 메서드, 필드, 상수등은 해당 객체의 단일 책임에 의해서만 변경되어야 한다.관심사 분리높은 응집도, 낮은 결합도(서로다른 두 객체간 의존성 최소화로 결합도를 낮춰야 한다.) 🔥 중요'책임'을 볼 줄 아는 눈이 필요하다. 그래서 우리는 이를 토대로 예제 프로젝트를 리팩토링해보았다. 처음에는 우리의 main메서드에 실행하는 부분을 별도의 GameApplication으로 분리하여 실행로직과 게임 로직을 분리하였다. 다음으로 입출력 부분을 각각 별도의 Handler클래스로 분리하여 리팩토링을 진행해보았다. 마지막으로 상수로 되어있는 BOARD를 객체로 분리하여 마지막 리팩토링을 분리해왔다. 이렇게 실습하면서 이제까지 나는 객체지향적으로 작성하지 못했다는 것을 깨달았다. 즉, 단일책임의 원칙을 지키지 못한것이다. 단일책임원칙은 약간 비유적으로 표현하면 이런것 같다. 축구로 비유하면, 축구에서 11명의 선수들은 각자 포지션이라는 책임을 가지고 있다. 각 선수는 자기 포지션에 맞는 역할만 해야 한다는 것과 같아. 예를 들어, 수비수는 수비에만 집중하고, 공격수는 공격에만 집중해야 팀이 효율적으로 움직일 수 있을것이다. 만약 수비수가 공격도 하고 골키퍼 역할까지 하려고 한다면 팀이 혼란에 빠질것이다. 즉, 한 선수에게 여러 역할을 맡기면 혼란이 생기고 효율성이 떨어지기 때문에, 선수마다 책임을 명확하게 분리하는 것이 중요하다. OCP: Open-Closed Principle확장에는 열려있고 수정에는 닫혀있어야 한다.기존 코드의 변경없이 시스템의 기능을 확장할 수 있어야한다.추상화와 다형성을 활용해서 OCP를 지킬 수 있다. 우리는 위를 토대로 예제코드를 리팩토링하였다. 예제코드에 게임 난이도라는 추가 요구사항이 나왔다. 하지만 이것을 유동적으로 변경 전에 알파벳 제한과 숫자가 2자리가 되면 오류가 발생함을 알 수 있었다. 그래서 이것을 해결하고 인덱스 치환 로직을 SRP원칙에 따라 별도 객체로 분리하고 리팩토링 작업을 하였다. 이제 추가요구사항인 난이도를 넣을때 우리는 인터페이스를 이용해 메서드 선언부를 선언해주고 구현체들을 각각 만들어 해결을 하였다. 축구로 비유하면, 축구 팀의 감독이 선수들에게 전술을 지시할 때, 기존 선수들의 역할을 크게 바꾸지 않고도 새로운 전략을 추가하는 상황과 비슷하다. 예를 들어, 새로운 선수가 팀에 합류하거나 새로운 전술을 추가하려고 할 때 기존에 있던 선수들의 역할을 건드리지 않고, 그 새로운 선수나 전술만 추가하는 식이다. 만약 기존의 모든 선수 배치를 다 바꿔야 한다면 팀이 혼란에 빠지지 않을까? 대신 기존의 포지션을 그대로 두고, 새로운 포지션을 하나 더 추가해 전술을 보완하는 것이 OCP에 비유로 들 수 있을 것 같다. 다른 비유로는 레고 블록으로 비유할 수 있다. 레고 집을 만들 때, 기존에 완성된 부분을 해체하지 않고도 새로운 부품을 추가해 더 멋진 집을 만들 수 있는 것이 OCP에 해당한다. LSP: Liskov Substitution Principle상속구조에서 부모클래스의 인스턴스를 자식클래스의 인스턴스로 치환이 가능해야 한다.자식클래스는 부모클래스의 책임을 준수하며 부모클래스의 행동을 변경하지 않아야 한다.LSP를 위반하면 상속클래스를 사용할때 오동작이 발생한다. 예상 밖의 예시가 발생하거나 이를 방지하기 위한 불필요한 타입체크가 동반된다. 보통 부모클래스보다 자식클래스가 하는 기능들이 더 많은게 일반적이다. 그래서 조심해야한다. 부모가 일하는 곳에 자식이 갔다가 올바른 방향으로 동작하지 않거나 예상치 못한 예외를 발생시키거나 이를 방지하기 위해 자식의 타입을 체크하는 분기가 발생한다. 이런 분기는 뇌 메모리에 더 올라가기에 불필요한 분기가 생기니 코드가 읽기 불편해진다. 그래서 우리는 예제코드에서 Cell이라는 것을 부모클래스(추상클래스)로 두고 각각 빈 셀, 숫자 셀, 지뢰 셀 클래스를 만들어 리팩토링 하는 과정을 해보았다. 축구로 비유하면, 팀의 주전 선수가 교체 선수로 대체될 때에도 팀의 전술이 동일하게 잘 유지되어야 한다는 것과 같다. 예를 들어, 만약 주전 공격수가 부상당해서 교체 선수가 들어온다고 해도, 그 교체 선수가 주전 선수의 역할을 잘 수행할 수 있어야 팀의 전술에 문제가 생기지 않을것이다. 교체 선수가 갑자기 공격을 포기하고 수비만 하려 한다면, 팀의 전술이 흐트러질 것이다. 즉, 교체 선수는 주전 선수와 같은 역할을 할 수 있어야 한다는 것이 LSP의 핵심이다. 즉, 축구처럼 자식 클래스가 부모 클래스를 언제든지 대체할 수 있도록 만드는 것이 LSP다. ISP: Interface Segregation Principle클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야한다.예를 들어 하나의 인터페이스에 선언부가 여러개 있고 여러 구현체들이 이 인터페이스를 구현할때 일부 구현체가 모든 선언부가 필요없는 경우 어떻게 할까?인터페이스를 쪼개면 된다.ISP를 위반하면 불필요한 의존성으로 인해 결합도가 높아지고 특정기능의 변경이 여러 클래스에 영향을 미칠 수 있다. 그래서 우리는 실습예제에서 동작부분과 초기화 부분을 인터페이스로 구현했는데 만약 다른 게임이 초기화 부분이 필요없다 가정하고 실습을 했다. 그럴 경우 간단히 이 인터페이스를 분리시켰다.축구로 비유하자면, 모든 선수가 똑같은 훈련을 받기보다는, 각 포지션에 맞는 훈련만 받는 것이 더 효율적이다는 것과 비슷하다. 예를 들어, 공격수는 골을 넣는 훈련을 집중적으로 해야 하고, 골키퍼는 공을 막는 훈련에 집중해야하지 않을까? 공격수가 골키퍼 훈련까지 받게 된다면 불필요한 훈련으로 시간만 낭비하게 된다. 각 선수는 자신의 역할에 맞는 훈련만 받아야 하는 것이다. DIP: Dependency Inversion Principle상위 수준의 모듈(추상)은 하위 수준의 모둘(구체)에 의존해서는 안된다. 모두 추상화에 의존해야 한다.의존성 순방향: 고수준 모듈이 저수준 모듈을 참조하는 것의존성 역방향: 고수준, 저수준 모듈이 모두 추상화에 의존 저수준 모듈이 변경되어도 고수준 모듈에는 영향을 끼치면 안된다. 의존성: 하나의 모듈이 다른 하나의 모듈을 알고 있거나 직접적으로 생성하는 것을 말한다. 우리의 예제코드에서 이제 위의 개념으로 실습을 하면 아래와 같다. 우리는 ConsoleInputHandler과 ConsoleInputHandler을 직접 객체를 생성해서 하고 있었다. 하지만 콘솔이라는 것은 너무 구체화되어 있다. 만약 갑자기 웹기반으로 변경되면 많은 부분을 변경해야할 것이다. 그것을 방지하기 위해 두 클래스를 인터페이스로 변경하고 구현을 하는 방식으로 하면 해결이 된다. 축구로 비유하자면, 감독이 특정 선수 개인의 능력에 의존하지 않고, 그 선수의 포지션에 맞는 역할에 의존해야 한다는 것과 비슷하다. 예를 들어, 감독이 특정 공격수에만 의존해서 전략을 짠다면, 그 선수가 부상당하거나 경기에 나가지 못할 경우 큰 문제가 생길것이다. 대신, 공격수라는 포지션에 맞는 역할을 정의하고, 누구든 그 역할을 수행할 수 있도록 전략을 짠다면, 주전 선수가 빠지더라도 팀의 전략은 계속 유지될 수 있을것이다. 만약 감독이 호날두나 메시에 의존해서 전술을 짜다가 그 선수가 없을 시, 팀은 사면초가에 빠질 것이다. 🧐 꿀팁DIP를 애기하면 Spring의 DI와 IoC를 헷갈려 하는 것 같다. 나도 처음에는 그랬다. 둘을 비교해보자.1. DI: 의존성 주입이며 일반적으로 생성자를 통해 주입한다. 여기서 생각나는 숫자는 3인데 두 객체가 서로 의존성을 맺을려면 제 3자가 역할을 해주는데 바로 IoC Container인 Spring Context이다.2. IoC: 제어의 역전이라 하며 이것은 Spring에서만 통하는 개념은 아니다. 제어의 역전이란 프로그램의 흐름을 순방향은 개발자가 가져가야하지만 이것을 역전시켜서 프레임워크가 담당하고 나의 코드를 프레임워크 일부로 동작시킨다. 이러면 우리는 직접 생성 및 생명주기 관리를 프레임워크에 위임시킬 수 있으며 우리는 그냥 사용하기만 하면 된다. 미션2 미션2를 하면서 조금은 논리적 사고 및 객체지향적으로 어떻게 해야하고 읽기 좋은 코드를 만들기 위해 어떻게 할지 또한 SOLID에 대한 정리를 해보았다. 상세 내용은 아래 내가 작성한 포스트를 확인하자! 미션2 블로그 SOLID관련해서만 잠깐 정리를 해보았다. 정리 정리를 해보면 아래와 같다. 1. SRP (Single Responsibility Principle) - 단일 책임 원칙각 클래스는 하나의 책임만 가져야 한다는 원칙이야. 클래스가 여러 책임을 가지면, 하나의 기능을 변경할 때 다른 기능에도 영향을 줄 수 있다. 축구로 비유하자면, 공격수는 공격만, 골키퍼는 수비만 담당하는 것처럼, 각 클래스는 명확한 역할을 가져야 한다.2. OCP (Open-Closed Principle) - 개방-폐쇄 원칙클래스는 확장에 열려 있고, 수정에는 닫혀 있어야 한다는 원칙이다. 즉, 새로운 기능을 추가할 때 기존 코드를 변경하지 않고 확장할 수 있어야 한다는 뜻이다. 축구로 비유하면, 새로운 전술을 추가할 때 기존 선수 배치에 큰 변화를 주지 않고도 쉽게 새로운 선수를 추가하는 것과 같다.3. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙하위 클래스는 상위 클래스의 자리를 대체할 수 있어야 한다는 원칙이다. 축구로 설명하자면, 교체 선수가 주전 선수의 역할을 대신할 수 있어야 팀의 전술이 무너지지 않듯이, 자식 클래스는 부모 클래스의 기능을 해치지 않고 그대로 사용할 수 있어야 한다.4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. 축구에서는 공격수와 수비수가 똑같은 훈련을 받지 않고, 각자의 포지션에 맞는 훈련을 받는 것처럼, 인터페이스도 필요한 기능만 나눠서 설계해야 한다.5. DIP (Dependency Inversion Principle) - 의존 역전 원칙상위 모듈은 하위 모듈에 의존하지 않고, 둘 다 추상화된 인터페이스에 의존해야 한다는 원칙이다. 축구로 비유하면, 특정 선수에게 의존하지 않고 포지션 자체에 의존하는 것처럼, 상위와 하위 모듈은 서로 구체적인 것이 아닌 추상적인 규약에 의존해야 한다. 이 다섯 가지 원칙을 잘 따르면, 시스템의 유지보수성, 확장성, 유연성이 훨씬 좋아진다. 마치 축구 팀이 각 선수의 역할을 잘 분배하고 새로운 전술을 도입할 때 기존 시스템에 혼란이 없도록 만드는 것처럼 말이다. 상속과 조합 상속과 조합은 무엇일까? 상속은 지난시간에 배워서 알겠지만 부모 클래스를 자식클래스가 상속받아 부모의 기능을 그대로 이용하는 것이다. 조합은 상속과 항상 같이 따라 다니는데 객체간 협력을 이용할때 조합을 이용한다. 이렇게 볼때 알 수 있듯이 상속보단 조합이 객체지향적으로 더 좋다는 것을 알 수 있아보인다.상속보단 조합을 이용하자.상속은 시멘트처럼 굳어진 구조다. 그래서 수정이 어렵다.부모와 자식의 결합도가 높다.조합과 인터페이스를 활용하는 것이 유연한 구조다.상속을 통한 코드의 중복제거가 주는 이점보다 중복이 생기더라도 유연한 구조 설계가 주는 이점이 더 크다. 옛날에는 하드웨어 성능이 안 좋아서 코드 중복 제거가 매우 큰 효과를 주었다. 하지만 오늘날은 하드웨어 성능이 굉장히 뛰어나다. 그래서 상속보단 조합을 이용해 유연한 설계를 하는 것이 좋다. 하지만 이렇다 해서 상속을 아예 사용하지 말자라는 말은 아니다. 확실한 상속구조거나 상속이 주는 효과가 매우 뛰어날것 같다고 느껴지면 상속을 이용하자. 그 외에는 조합으로 다 해결된다. 상속은 또한 부모 자식간의 결합도가 높아 캡슐화가 안 되어 있다고 한다. 이 점도 생각해보자.그래서 우리는 기존 추상클래스로 되어있는 Cell을 인터페이스로 만들고 Cell의 상태 변경을 하는 필드들을 CellState에 두어 분리를 해보았다. 처음에 이 실습을 하면서 CellState는 그냥 enum으로 하면 안될까 고민을 하였고 추후에도 이렇게 변경이 없으면 한번 질문해봐야 겠다. Value Object Value Object는 훌륭한 추상화 기법 중 하나다. 기본 타입을 객체로 감싸서 의미 부여 및 추상화를 하는 것이다.도메인의 어떤 개념을 추상화하여 표현한 값 객체값으로 취급하기 위해서 불변성, 동등성, 유효성 검증등을 보장불변성: final 필드, setter금지동등성: 서로 다른 인스턴스여도(동일성이 달라도) 내부의 값이 같으면 같은 값 객체로 취급, ```equals()``` & ```hashCode()``` 재정의가 필요하다.유효성 검증: 객체가 생성되는 시점에 값에 대한 유효성 보장 그래서 우빈님께서는 만원 지폐의 일련번호로 이에 대한 예시를 들어주셨다. 일련번호가 달라도 하나의 만원으로 보는것과 같다고 하셨다. 🙋🏻 동등성 vs 동일성동등성은 객체간 메모리 주소가 달라도 내부의 값이 같다면 같은 객체로 취급을 하자는 것이며 동일성은 메모리 주소 값이 같냐는 의미이다. 동일성 비교는 ```==``` 연산자로 비교하며 동등성 비교는 재정의한 ```equals()```로 해결한다. VO vs EntityEntity는 식별자가 존재한다. 식별자가 아닌 필드의 값이 달라도 식별자가 같으면 종등한 객체로 취급equals() & hashCode()로 식별자 필드만 가지고 재정의 할 수 있다.식별자가 같은데 식별자가 아닌 필드의 값이 서로 다른 두 인스턴스가 있다면 같은 Entity가 시간이 지남에 따라 변화한것으로 이해할 수 있다.VO는 식별자 없이 내부의 모든 값이 다 같아야 동등한 객체로 취급한다. 개념적으로 전체 필드가 다 같이 식별자 역할을 한다고 생각하자. 그래서 우리의 예제 프로젝트를 리팩토링해보았다. rowIndex와 colIndex는 하나의 세트로 움직이니 이것을 CellPosition이라는 VO를 만들어 리팩토링을 진행하였고 이에 대해 세부적인 로직들을 개선해보았다. 예를 들어 재귀로직을 RelativePostion으로 두어서 stream API를 이용하여 해결을 하였다.이처럼 코드가 점점 개선되니 뭔가 조금은 많은 부분이 느껴졌다. 실무에서도 한번 신규 프로젝트를 진행할때 도입해볼만한 생각이 듣게 되었다. 하지만 아직까지 능수능란하게 해결을 할지는 고민이 되는 것 같다. 일급 컬렉션 일급 컬렉션이란 무엇일까? 일단 컬렉션은 List, Map, Set같은 것들이다. 그럼 일급은 무엇일까? 해당 용어에 파생된것이 일급시민이다. 일급 시민다른 요소에게 사용가능한 모든 연산을 지원하는 요소변수로 할당될 수 있다.파라미터로 전달 될 수 있다.함수의 결과로 반환될 수 있다. 일급함수함수형 프로그래밍 언어에서 함수는 일급시민이다.함수형 프로그래밍 언어에서 함수는 일급시민이다.함수는 변수에 할당될 수 있고 인자로 전달될 수 있고 함수결과로 함수가 반환될 수 있다. 일급 컬렉션컬렉션을 포장하면서 컬렉션만을 유일하게 필드로 가지는 객체컬렉션을 다른 객체와 동등한 레벨로 다루기 위함단 하나의 컬렉션 필드만 가진다.컬렉션을 추상화하며 의미를 담을 수 있고 가공로직의 보금자리가 생긴다.가공로직에 대한 테스트도 작성할 수 있다.만약 getter로 컬렉션을 반환할 일이 생긴다면 외부조작을 피하기 위해 꼭 새로운 컬렉션으로 만들어서 반환. 이것을 이용해 우리는 체크했는지 유무 메서드의 ```Cell[][]``` stream API를 이용한 부분을 Cells라는 일급 컬렉션을 만들어 변경을 해보았으며 또한 우리는 빈 셀, 숫자 셀, 지뢰 셀을 보드에 넣어주는 것을 CellPositions라는 일급컬렉션을 통해 리팩토링 및 버그 수정도 해보았다. 아직 일급컬렉션을 처음 공부해봐서 많아 와 닿지는 않지만 이 부분을 한번 다른 곳에도 적용해봄으로 익숙해져야겠다. Enum의 특성과 활용Enum은 상수집합이며 상수와 관련된 로직을 담을 수 있는 공간이다.상태와 행위를 한 곳에서 관리할 수 있는 추상화된 객체이다.특정 도메인 개념에 대해 그 종류와 기능을 명시적으로 표현해줄 수 있다.만약 변경이 정말 잦은 개념은 Enum보다 DB로 관리하는 것이 좋다. 그래서 우리는 기존에 SIGN들을 OutputConsoleHandler쪽에 이관하면서 enum을 활용해 리팩토링 과정을 거쳐보았다. 다형성 활용하기이번에는 예제에 있는 반복된 if문을 다형성을 통해 해결해보았다. 해당 if문들에서 변화하는것(조건, 행위 -> 구체)과 변화하지 않은 것(틀 -> 추상)을 나누기로 하였고 그를 위해 인터페이스를 만들어 처리를 하였고 해당 인터페이스를 만들어 스펙들을 정리하였다. 그리고 스펙들을 하나의 클래스에 두어 해당 객체의 메서드만 호출해서 해결하게 하였다. 하지만 해당 문제는 클래스가 많아지면 즉, 스펙이 더 많아 질경우 소스코드를 빈번히 해결해야하는 문제가 있었고 그래서 enum을 통해 해결했다. enum에 인터페이스를 구현시키고 각기 다른 행위 자체를 enum안에 상수에 별도로 구현하는 법을 알았다. 조금 신박했고 유용한 방법인것 같았다. 뭔가 실무에도 한번 써볼법한 방법이였다. 이를 통해 변하는 것과 변화하지 않은 것을 분리하여 추상화하고 OCP를 지키는 구조로 만들었다. 숨겨져 있는 도메인 개념 도출하기도메인 지식은 만드는 것이 아니라 발견하는 것이다.객체지향은 현실을 100% 반영하는 도구가 아니라 흉내내는 것이다.현실세계에서 쉽게 인지하지 못하는 개념도 도출해서 사용할 때가 있다.설계할때는 근시적, 거시적 관점에서 최대한 미래를 예측하고 시간이 지나 만약 틀렸다는 것을 인지하면 언제든 돌아올 수 있도록 코드를 만들어야 한다.완벽한 설계는 없다. 그 당시의 그들의 최선이 있을뿐 해당부분을 가지고 우리의 예시코드를 리팩토링해봤다. 만약 다양한 설정기능들이 추가가 될 경우 변경할 곳이 많아졌다. 그래서 우리는 config관련 클래스를 만들고 설정부분들을 변경해주는 것을 해보았다.해당 강의를 듣고 나는 조금 반성했다. 이전에 조상님들이 작성한 레거시 코드를 보고 많은 비판을 했기 때문이다. 그 당시에는 그게 최선일텐데 또한 설계관련해서 한번 생각 나는 영상이 인프콘에 재민님께서 발표한 영상인데 지금 다시 한번 봐보고 공부해봐야겠다. 📚 추가 참조https://johnlock.tistory.com/405https://maily.so/trendaword/posts/52a9a219 

백엔드인프런워밍업스터디클럽백엔드클린코드테스트코드발자국

양성빈

[인프런 워밍업 스터디 클럽] 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 - 발자국 3주 차

1. 들어가며테스트 코드 강의는 미리 봤었고, 그 때 굉장히 오래 걸렸던 것 같네요. 아마 커리큘럼대로 따라가려면 굉장히 힘들었을 것 같아요...이번 주차 내용은 저한테는 굉장히 도움이 많이 됐었던 내용이였습니다.실제로 참고해서 회사 프로젝트에 테스트 코드를 적용하기도 했었구요.테스트 코드에 대한 막연한 두려움을 없애준 강의라서 정말 듣기 잘했다고 생각한 강의였습니다.  2. 학습했던 내용 나만의 키워드로 작성하기섹션 6. Spring & JPA 기반 테스트Layered Architecture관심사의 분리 때문에 레이어를 분리단위 테스트 VS. 통합 테스트여러 객체가 협력해서 어떤 하나의 기능을 동작한다면 통합테스트가 필요함단위 테스트만으로 커버하기 어려운 영역들이 생기기 때문IoC, DI, AOPORM, 패러다임의 불일치, HibernateSpring Data JPA@SpringBootTest VS. @DataJpaTest VS. @WebMvcTest@SpringBootTest스프링에서 통합 테스트를 위해 제공하는 에노테이션@SpringBootTest 에는 트랜잭션이 달려있지 않음 -> 더 선호@DataJpaTestjpa 관련된 빈들만 주입해서 서버를 띄워주기 때문@WebMvcTest컨트롤러 레이어만 딱 떼서 테스트를 하기 위해 컨트롤러 관련 빈들만 올릴 수 있는 가벼운 테스트 어노테이션@Controller와 @ControllerAdvice 등과 같은 빈만 주입 됨 @Transactional (readOnly = true)읽기 전용으로 하면 CRUD 작업 중에 CUD 작업이 동작하지 않음CQRSCommand 와 Query 를 분리해서 서로 연관이 없게끔 하려는 것@RestControllerAdvice, @ExceptionHandler커스텀 예외를 사용해서 처리하는 것도 자주 쓰이는 방법Spring bean validation@NotNull, @NotEmpty, @NotBlank, ...컨트롤러에서는 최소한의 검증만 하고 도메인 레이어나 서비스 레이어에서 검증할 것들은 따로 처리해서 책임을 잘 분리하기MockMvc MockMvc 란 Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크ObjectMapper@MockBean스프링 컨테이너에 mockito 로 만든 Mock 객체를 넣어주는 역할예시로, ProductService 빈에다가 적용을 하면 ProductService Mock 객체를 대신 스프링 컨테이너에 넣어줌  3. 학습 회고너무나 바쁜 한 주를 보냈습니다. 정신이 없었네요.옛날에 정리한 내용과 인강을 2배속으로 다시 봤습니다. 분명 정리를 했는데 처음 듣는 것 같은 내용들이 있어서 당황하긴 했습니다 ㅎㅎ.. 역시 완전히 자신의 것으로 만드려면 수많은 반복이 필요한 것 같네요.이번 기회에 반복할 수 있어서 좋은 기회였다고 생각합니다.특히 반복하면서 옛날에 들을 때는 인지하지 못했던 실무적인 관점이 다시 보이더라구요? 굉장히 의미 있었던 것 같습니다. 4. 미션 회고미션 Day 11사실 이번 미션은 회고하기가 애매합니다.냉정하게 저 스스로에 대한 평가를 하자면 열심히 했다고 보기가 어렵기 때문이죠.스프링 기반이 아니라서 단순히 미션의 기준에 맞춰서 필요하다고 생각한 것만 처리했습니다. (사실 코틀린 스터디를 따로 시작했는데 여기에 에너지를 너무 쏟아서,,,)다음주 라이브 시간에 딴 분들의 코드 리뷰를 보면 깨달음과 반성을 동시에 할 것만 같네요.. 😅😅😅

백엔드워밍업클럽3기백엔드클린코드자바

천준민

워밍업 클럽 3기 BE 클린코드&테스트 - 3주차 발자국

📖 강의 요약통합 테스트여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트일반적으로 작은 범위의 단위 테스트만으로는 기능 전체의 신뢰성을 보장 할 수 없다,풍부한 단위테스트 & 큰 기능 단위를 검증하는 통합 테스트 ORM객체 지향 패러다임과 관계형 DB 패러다임의 불일치ORM을 사용함으로써 개발자는 단순 작업을 줄이고, 비즈니스 로직에 집중할 수 있다.  Persistence LayerData Access의 역할비즈니스 가공 로직이 포함 되어서는 안된다. Data에 대한 CRUD에만 집중한 레이어 Business Layer비즈니스 로직을 구현하는 역할Persistence Layer와의 상호 작용 (Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개 시킨다.트랜잭션을 보장해야 한다. Presentation Layer외부 세게의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증 !! CQRS (Command and Query Responsibility Segregation)데이터 저장소로 부터 읽기와 업데이트 작업을 분리하는 패턴= Command와 Query 를 분리 하기💡 미션 [Readable Code] 강의의 두 프로젝트(지뢰찾기, 스터디카페) 중 하나를 골라, 단위 테스트를 작성해 봅시다.미션 제출 url : https://github.com/2unmini/readable-code/tree/mission/day11 최대한 테스트 코드를 작성할때 현재 코드를 건드리지 않고 짤려고 했다 하지만 final Scanner 부분에서 문제가 발생했다 . 현재 코드는 각각의 테스트는돌아가지만 전체 테스트를 할때는 통과하지 못하지만 Scanner를 상수로 두지 않고 기존 코드를 변경한다면 테스트 코드도 성공적으로 잘 돌아갈 것이다. 💬 회고 👍 테스트에 관한 옛날에 대한 나의 생각나에게 테스트는 란 1+1 =2 처럼 이미 결과가 도출 되어있는 상황인데 테스트 코드를 작성 할 필요가 있을 까? 라는 의문 투성이 밖에 없었다. 정답은 아닐지는 모르지만 강의와 스터디를 통해 왜 테스트 코드를 작성해야 하는가를 어느 정도 이해를 하게 되는 것 같았다. 나만 1+1 =2 라고는 생각해도 이걸 뒤 받침해 줄 근거 및 신뢰성을 높이려면 테스트 코드가 필요해야 겠다.📎출처Practical Testing: 실용적인 테스트 가이https://www.inflearn.com/course/practical-testing-%EC%8B%A4%EC%9A%A9%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C/dashboard

백엔드워밍업클럽3기클린코드테스트코드

인프런 워밍업 스터디 클럽 3기 백엔드-code 3주 차 발자국

이 글은 박우빈님의 Practical-Testing 강의 를 참조하여 작성한 글입니다.어느덧 수료까지 일주일만을 앞두고 있다. 벌써 75%나 진행되었다니,,,이때까지 강의 안 밀리고 시간 안에 미션 제출한 내 자신 칭찬한다👍🏻 (아직 강의 섹션3개, 미션 2개 남음)남은 한 주도 열심히 해서 수료해야지!! 이번 주는 테스트 코드가 필요한 이유 및 레이어드 아키텍쳐 내에서 각 레이어드별 테스트 코드 작성하는 법에 대해 배웠다.강의를 듣고 테스트 코드를 작성해야 하는 이유에 대해서 완전 설득이 되었다.기존에는 테스트 코드 작성하는 것이 너무 귀찮고 시간이 오래 걸린다는 이유에서 꺼려했지만, 점점 리팩토링 또는 기능을 추가할 때마다 수동으로 테스트 하는게 더욱 귀찮았다. 또한 수동으로 테스트를 했어도 항상 찝찝함이 존재했다.그래서 우빈님 강의를 들으면서 엄청난 공감을 느꼈고, 강의를 열심히 학습해서,,,많이 배워야겠다는 다짐을 다시 하게된 것 같다.다음으로 readable-code 강의에서 만든 스터디 카페 이용권 프로그램에 대해 스스로 테스트 코드를 작성해 보는 시간을 가졌다.이번에는 열심히 작성한 후 리뷰 신청도 하였다.테스트 코드 관련해서는 리뷰를 한번도 받아본 적이 없기도 하고, 코드 작성 중 궁금한 점이 생겨 리뷰 신청을 하는 용기를 내보았다..ㅋㅋㅋ다음 주 중간점검 때 리뷰를 해주실 예정인데 기대된다!!!학습 내용 요약 테스트는 왜 필요할까?빠른 피드백리팩토링, 신규 기능 추가 등 변화가 생기는 매순간마다 테스트 코드를 통해, 기존 코드가 정상 동작하는 지 빠르게 피드백을 받을 수 있다.만약 테스트 코드를 작성하지 않는다면?변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 한다.변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 한다.자동화자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있다.(인간이 수동으로 테스트를 하게 된다면 실수할 확률이 매우 높음)안정성빠르게 변화하는 소프트웨어에서 테스트 코드를 통해 100%는 아니지만 안정성을 보장할 수 있다. 단위테스트단위테스트란?작은 코드 단위를 독립적으로 검증하는 테스트통합테스트에 비해 준비해야 할 코드가 적으며, 검증 속도가 빠르고 안정적이다.단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없다는 단점이 존재한다.JUnit5: 단위 테스트를 위한 테스트 프레임 워크AssertJ: 테스트 코드 작성을 원활하게 돕는 테스트 라이브러리 (풍부한 API, 메서드 체이닝 지원) TDD: Test Driven Development선 기능 구현, 후 테스트 작성테스트 누락 가능성 존재해피 케이스만 검증할 가능성 존재잘못된 구현을 늦게 발견할 가능성 존재선 테스트 작성, 후 기능 구현 (TDD)복잡도가 낮은, 테스트 가능한 코드로 구현할 수 있게 한다(유연하며 유지보수 쉬운 코드)쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해줌구현에 대한 빠른 피드백을 받을 수 있음 -> 과감한 리팩토링 가능해짐 Spring / JPA 훑어보기 & 기본 엔티티 설계라이브러리내 코드가 주체가 돼서 필요한 기능이 있다면 외부에서 끌어와서 사용하게 되는데, 이 때 외부에 있는 것들을 라이브러리라 한다.따라서 라이브러리는 내 코드가 주체가 되는 환경이고 능동적인 특징을 지닌다.프레임워크이미 갖춰진 동작할 수 있는 그런 환경들이 구성이 돼 있고, 내 코드가 이 프레임 안에 들어가는 수동적인 역할을 한다.따라서 프레임워크에서는 내 코드가 수동적인 존재가 된다.ORM 객체 지향 프로그래밍(OOP)과 관계형 데이터베이스(RDB) 간의 구조적 차이를 해소하기 위해 사용되는 기술이다.데이터베이스에 CRUD하는 작업을 객체 기반으로 처리함으로써 개발자들이 기본적인 쿼리 작성하는 단순 작업을 줄이고 비지니스 로직에 집중할 수 있게 해준다.JPAJava진영의 ORM 기술 표준인터페이스이며, 여러 구현체가 있지만 주로 Hibernate를 많이 사용한다. Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA제공한다. Persistence Layer 테스트데이터에 접근하는 역할로 데이터 CRUD와 연관된 메서드들이 위치해 있다.비지니스 로직이 포함돼서는 안됨!@DataJpaTestJPA와 관련된 의존성들만 주입해준다 -> @SpringBootTest보다 가볍다.어노테이션 내부에 @Transactional이 포함되어 있어 테스트 후 데이터가 롤백된다. Business Layer 테스트비지니스 로직에 관련된 메서드들이 위치해 있다.@Transactional vs sql을 이용한 데이터 삭제실제 코드에서는 @Transactional을 사용하지 않는데, 단순히 롤백만을 위해 테스트코드에서 @Transactional을 사용하면, 실제 작동 방식과 다르게 작동할 수 있다.따라서 테스트 코드 작성시 Transactional의 부작용에 대해 인지하고 사용할 것!!리포지토리 테스트와 로직이 많이 없는 얇은 서비스 테스트는 결이 비슷하다왜냐하면 리포지토리에 대한 결이 그대로 오기 때문이다.하지만서비스가 더 기능이 추가될수록 발전을 하기 때문에, 동일한 테스트라고 생각이 되더라도 작성을 하는 게 좋다! (검증이 추가될 수도 있기 때문)클래스 상단에 @Transactional(readOnly = true) 로 표현하고 실제 트랜잭션이 사용되는 메서드에 @Transactional로 표현해주기! 미션Day 11 - 스터디 카페 이용권 선택 시스템 테스트 코드 작성하기나의 코드: https://github.com/Jiihyun/readable-code/pull/3🤔 고민사항 1. StudyCafeSeatPassTest 클래스테스트 케이스 반복을 줄이기 위해 @CsvSource를 사용해 보았다.이로 인해 데이터만 추가하면되니 테스트 케이스를 쉽게 확장할 수 있다는 장점을 느꼈다.하지만 StudyCafePassType을 직접 문자열로 작성했다보니, 후에 passType이 수정될 경우 테스트 코드 내 passType을 직접 수정해야 하기 때문에 유지보수 비용이 증가할 것 같다는 생각이 들기도 했다..그래서 현업에서는 어떤 방식으로 테스트를 하지? 하는 궁금증이 생겼던 것 같다.두 방법에 대해 트레이드오프가 존재하는데 어떤 걸 더 중요시하게 여겨 테스트하는지 궁금하다!2. FileReaderTest 클래스파일을 잘 읽어와 데이터를 의도한 대로 파싱하였는지 테스트하고 싶었다.하지만 반환 타입이 일급컬렉션으로 되어 있고,컬렉션 내의 데이터를 확인하는 메서드는 프로그램에서 사용되지 않기 때문에 존재하지 않았다.테스트를 공부하면서 배운 것 중 하나는 테스트만을 위한 메서드는 최대한 자제해야 한다고 했다.그래서 오로지 테스트를 위해 컬렉션의 크기 등을 확인할 수 있는 메서드를 추가하고 싶지 않았고, 그래서 일급 컬렉션의 메서드를 직접 호출하여 테스트를 진행했다.그치만 이렇게 짜여진 테스트도 본 적이 없는데....이런 방식으로 테스트를 해도 괜찮은지 궁금하다 ㅋㅎㅎㅋ마무리앞으로 이제 일주일 남았다..!! 벌써...?다음 주 학습 내용에는 평소에 궁금했던 내용들에 대해 다루기 때문에 매우 기대가 된다.배워본 적 없는 내용이라 시간을 많이 투자해야 할 것 하지만,,,열심히 학습해서 내 것으로 만들어야지!

백엔드클린코드테스트코드발자국워밍업스터디클럽

Versa

[3기] 워밍업 클럽-백엔드-code 2주차 발자국

미션 회고DAY7 미션은 스터디카페 코드를 추상화를 진행하는 것이었는데 거의 10줄도 바꾸지 못했다.이것저것 해야될 것 같고 하고 싶은건 많은데 코드로 막상 옮기려니 루미큐브를 하듯이다 벌려놓다가 마지막 하나가 부족해서 다시 처음으로 rollback 을 해야하는 것의 반복이었다.아직 바쁘다는 핑계로 한두번 손을 대고는 더 못댔다.그래도 아예 시작을 못할뻔 했는데 조금이라도 적용해보려고 노력한 내 자신에게 심심한 박수를 주었다.다음에 다시 할때는 한 클래스라도 더 추상화할 수 있을 것이라 생각한다.회고사실 강의도 섹션 5부터는 앞서가는 사람을 쫓아가려는 느낌으로 강의를 들었다. 그래서 그런지 DAY7 과제를 할때 내 생각만큼 추상화를 진행할 수 없었다. 영어를 배울대 읽기와 듣기가 되어도 쓰기와 말하기와 같이 출력하는 것은 다르듯이 실제로 코드를 적용하는 것은 쉽지 않은 일이었다.아직은 읽기와 듣기를 위해 열심히 지식을 입력하는 단계라고 생각한다. 이번 워밍업에서 한번에 다 이해할 수 있을 것이라 생각하지말고 한번 듣고 다음에 또 복습하고 적용해보는 시간을 꼭 가져야겠다.이번 발자국은 남기기 위해 작성했다면 다음 발자국은 남기고 싶어서 남기는 발자국이었으면 좋겠다.

백엔드백엔드워밍업클린코드

[2주차 발자국] Readable Code 적용기

인프런 ‘Readable Code: 읽기 좋은 코드를 작성하는 사고법’을 수강한 후, 작성한 내용입니다.📌 2주차 강의 (Readable Code)코드 다듬기주석의 양면성주석이 많다 ⇒ 비즈니스 요구사항을 코드에 잘 녹이지 못했나 의심해보자.추상화로 설명이 덜 된 것은 아닐까?주석에 의존하면 적절하지 않은 추상화 레벨을 가지게 된다.좋은 주석우리가 가진 모든 표현 방법을 총동원해 코드에 의도를 녹여내고, 그럼에도 불구하고 전달해야 할 정보가 남았을 때 사용하는 주석의사 결정의 히스토리를 도저히 코드로 표현할 수 없을 때, 주석으로 상세하게 설명!번수와 메서드의 나열 순서변수는 사용하는 순서대로 나열하자.인지적 경제성메서드의 순서는 객체의 입장에서 생각해보자.객체는 외부 세계와 어떻게 소통할 것인지가 중요공개 메서드의 스펙을 통해 외부 세계와 소통객체는 협력을 위한 존재외부 세계에 내가 어떤 기능을 제공할 수 있는지를 드러낸다.공개 메서드끼리도 기준을 가져보자.상태 변경 > 판별 > 조회 메서드중요한 것은, 나열 순서로도 의도와 정보를 전달할 수 있다는 것!패키지 나누기패키지는 문맥으로서의 정보를 제공할 수 있다.패키지를 쪼개지 않으면 관리가 어렵다.너무 잘게 쪼개도 안됨대규모 패키지 변경은 팀원과의 합의를 이룬 시점에!기능 유지보수하기객체 지향적으로 책임이 잘 분리되어 있다면,문제가 되는 위치를 발견하여 수정하기 쉽다!알고리즘 교체와 같은 작업이 수월하다!IDE 도움 받기코드 포맷팅sonarlinteditconfig리팩토링 연습스스로 리팩토링을 진행해보고 비교해보는 시간이유를 가지고 리팩토링을 진행하자.변경 포인트는 그 이유를 명확하게 설명할 수 있어야 한다!!감으로 하는 리팩토링은 설득할 수 없는 코드가 될 확률이 높다.객체에 메시지를 보내자.객체의 책임과 응집도를 고려하자.기억하면 좋은 조언들능동적 읽기눈으로 복잡하게 보고 이해하려 애쓰는 것보다 직접 경험해보며 읽자.복잡하거나 엉망인 코드를 읽고 이해하려 할 때, 리팩토링하면서 읽기공백으로 구분메서드와 객체 추상화주석으로 이해한 내용 표기하며 읽기핵심 목표는 도메인 지식을 늘리는 것.그리고 이전 작성자의 의도를 파악하는 것.오버 엔지니어링필요한 적정 수준보다 더 높은 수준의 엔지니어링구현체가 하나인 인터페이스아키텍처 이해에 도움을 주거나, 근시일 내에 구현체가 추가될 가능성이 높다면 괜찮음구현체 수정 시, 인터페이스도 수정코드 탐색에 영향을 준다.너무 이른 추상화정보가 숨겨지기 때문에 복잡도가 높아짐은탄환은 없다만능 해결사 같은 기술은 없다.객체 지향적인 체스 프로그램but, 체스는 500년 동안 변하지 않았다.실무 : 2가지 사이의 줄다리기지속 가능한 소프트웨어의 품질 VS 기술 부채를 안고 가는 빠른 결과물모든 기술과 방법론은 적정 기술의 범위 내에서 사용되어야 한다.도구라는 것은, 한계까지 사용할 줄 아는 사람이 그것을 사용하지 말아야 할 때도 아는 법이다.📌 2주차 강의 (Practical Testing)Intro강의 소개무엇을 학습하는가?테스트 코드가 필요한 이유좋은 테스트 코드란 무엇일까?실무에서 진행하는 방식 그대로 테스트를 작성해가면서 API를 설계하고 개발하는 방법구체적인 이유에 근거한 상세한 테스트 작성 팁어떻게 학습하면 좋을까?무엇을 모르는지 아는 것 = 찾아볼 수 있게 된다는 것선택과 집중을 통해 아는 영역을 넓혀가고, 익숙하지 않은 것들을 익숙하게 하고, 들어보지 못했던 것을 들으면서 나에게 노출시키자.인덱스를 통해 영역을 넓혀가자.테스트는 왜 필요할까?테스트 코드를 작성하지 않는다면,변화가 생기는 순간마다 발생할 수 있는 모든 Case 고려해야 한다.변화가 생기는 순간마다 모든 팀원이 동일한 고민을 해야 한다.빠르게 변화하는 소프트웨어의 안정성 보장 X테스트 코드가 병목이 된다면,프로덕션 코드의 안정성 보장 X테스트 코드 자체가 유지보수하기 어려운 짐잘못된 검증이 이루어질 가능성 O테스트를 통해 얻고자 하는 것 ⇒ 빠른 피드백, 자동화, 안정감따라서, 올바른 테스트 코드는자동화 테스트로 빠른 시간에 버그를 발견하고 비용을 절약한다.소프트웨어의 빠른 변화를 지원한다.팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.단위 테스트수동 테스트 vs 자동화된 테스트콘솔에 찍힌 내용을 보고 판단하는 테스트최종 단계에서 사람이 개입해야 함다른 사람이 봤을 때, 무엇을 검증하고 어떤 것이 맞는 상황인지 알 수 없다.과연 이것이 자동화된 테스트일까?JUnit5로 테스트하기단위 테스트작은 코드 단위를 독립적으로 검증하는 테스트작은 코드 = 클래스, 메서드독립적 = 외부 상황에 의존적이지 않아야 한다.검증 속도가 빠르고, 안정적이다.JUnit 5단위 테스트를 위한 테스트 프레임워크AssertJ테스트 코드 작성을 원활하게 돕는 테스트 라이브러리풍부한 API, 메서드 체이닝 지원리스트 사이즈 검증할 때 다음을 이용하자..hasSize().isEmpty()테스트 케이스 세분화하기해피 케이스예외 케이스assertThatThrownBy()경계값 테스트범위, 구간, 날짜 등테스트하기 어려운 영역 분리하기생성 시간 검증에 대한 테스트에서 생성 시간을 메서드 내부에서 측정한다면?테스트하는 시간에 따라 테스트 결과가 달라진다.외부로 분리하자!외부로 분리할수록 테스트 가능한 코드는 많아진다.테스트하기 어려운 영역관측할 때마다 다른 값에 의존하는 코드현재 시간에 의존하는 코드외부 세계에 영향을 주는 코드테스트 하기 좋은 영역 (순수함수)같은 입력에는 항상 같은 결과외부 세상과 단절된 형태TDD : Test Driven Development프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론RED → GREEN → REFACTORRED : 프로덕션 코드 없이 테스트를 먼저 작성GREEN : 테스트가 통과할 수 있는 최소한의 코딩REFACTOR : 테스트 통과를 유지하면서 구현 코드 개선기능 구현 후, 테스트를 작성하면,테스트 자체의 누락 가능성특정 테스트 케이스만 검증할 가능성잘못된 구현을 다소 늦게 발견할 가능성테스트를 먼저 작성하면,복잡도가 낮은, 테스트 가능한 코드로 구현쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백과감한 리팩토링테스트를 구현부 검증을 위한 보조 수단이 아니라, 테스트와 상호 작용하며 발전하는 프로덕션 코드을 만든다고 바라본다.즉, 객체 사용자 관점에서의 피드백을 준다!추가 인덱스애자일 방법론일정한 주기를 가지고 빠르게 제품을 출시하여 고객의 요구사항, 변화된 환경에 맞게 요구를 더하고 수정해나가는 방법익스트림 프로그래밍빠른 개발 속도를 유지하며 고객이 원하는 요구들을 지속적으로 피드백하는 방법의사소통, 단순성, 용기, 피드백, 존중하나의 방법론 : TDD스크럼, 칸반스크럼 : 스프린트라는 일정기간 안에 완료할 수 있는 작업으로 업무를 분할한다.칸반 : 동시에 개발이 진행될 수 있는 아이템의 수를 제한하여, 업무의 병목 현상과 리소스 낭비를 처리할 수 있도록 한다.테스트는 [ ]다.테스트는 [문서]다.프로덕션 기능을 설명하는 테스트 코드 문서다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점 보완과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜, 모두의 자산으로 공유DisplayName을 섬세하게비교음료 1개 추가 테스트음료 1개 추가하면 주문 목록에 담긴다.명사의 나열보다 문장으로테스트 행위에 대한 결과까지 기술하기비교특정 시간 이전에 주문을 생성하면 실패한다.영업 시작 시간 이전에는 주문을 생성할 수 없다.도메인 용어를 사용하여 한층 추상화된 내용을 담기메서드 자체의 관점보다 도메인 정책 관점으로!테스트의 현상을 중점으로 기술하지 말 것BDD 스타일로 작성하기함수 단위의 테스트에 집중하기보다, 시나리오에 기반한 테스트케이스(TC) 자체에 집중하여 테스트개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 레벨 권장Given / When / ThenGiven : 시나리오 진행에 필요한 모든 준비 과정When : 시나리오 행동 진행Then : 시나리오 진행에 대한 결과 명시, 검증명확하게 표현하지 못한 테스트는 나중에 우리의 사고를 제한하는 허들이 될 수 있다!추가 인덱스JUnit전통적인 Java 기반의 테스트 프레임워크어노테이션 사용SpockGroovy 기반의 BDD(Behavior-Driven Development) 프레임워크BDD 스타일 테스트로 가독성이 더 좋다.Groovy 언어로 작성해야하므로 작성이 어려울 것 같다.📌 Day 7 미션 & 중간 점검 라이브Day 7 미션 : 변경 포인트는 그 이유를 명확하게 설명할 수 있어야 한다!Day 7 미션은 ‘스스로 스터디 카페 이용권 선택 시스템 리팩토링해보기’였다.미션 수행 전 강의를 들으며 내용들에 대해 거부감 없이 받아들여졌고, 빨리 코드를 짜보고 싶다는 생각이 들도록 하였다. 하지만 실제로 코드를 작성하면서 고민되는 지점들도 꽤 많았고, 근거를 가지고 리팩토링을 하는 과정이 쉽지는 않았다. ‘이게 더 나은거 같은데…?’하며 감으로 진행한 부분도 꽤 있었던 것 같다. 미션을 진행하고 강의를 수강하면서 ‘변경 포인트는 그 이유를 명확하게 설명할 수 있어야 한다.’라는 말이 내가 유의해야 할 말로 느껴졌다.중간 점검 라이브중간 점검 라이브는 Day 4 미션 공통 피드백, Q&A, 코드 리뷰 순서로 진행되었다.Day 4 미션 공통 피드백Day 4 미션 공통 피드백 다음과 같다.추출한 메서드에 static이 있는 경우boolean을 return 하는 메서드에 예외 throw오버 엔지니어링나도 메서드를 추상화하기 위해 추출하다보면, IDE에서 static 키워드를 붙여서 따로 지워준 적이 있다. 추출하여 메서드명만 적지 말고, 전체적으로 확인해보자.상태를 체크하고 boolean을 반환하는 메서드가 있을 때 예외를 던지는 것 보다 상황에 맞게 리팩토링 하는게 좋은 것 같다. validateOrder에서 false를 반환해서 유효하지 않은 주문을 나타내면 되지 않을까?미션의 안내 사항을 잘못 이해한 것 같다. Order의 내부 구현 없이 단순한 리팩토링을 제안하는 것이 요구사항이다. 그런데 나는 ‘Order의 메서드를 추가하더라도 구현 내용은 안적어도 된다.’라고 이해했다. 미션에서 요구했던 내용은 Order에 추가적으로 구현되는 사항 없이 리팩토링을 진행하라고 했던 것 같다.Q&AQ&A의 많은 내용들이 나에게 도움이 되었다. 다른 분들께서 질문해주신 내용들이 생각 외로 많은 인사이트를 얻을 수 있었다.질문하고자 했던 내용들이 다른 분 질문에 있기도 했고 개발 외 질문을 하고 싶어서 우빈님께 개인 취향의 저가 커피 브랜드 1순위와 선호하는 취향에 대해 여쭤보았다.우빈님께서는 저가 커피는 거의 안 드신다고 한다… 그래도 재밌는 영상을 추천해주셨다.항상 카페가면 필터 커피가 있으면 필터 커피를 먹어보거나 그 카페의 스페셜한 원두를 맛을 보려 노력하는데, 항상 먹고 나면 기억이 나지 않는다… ‘이게 어떤 맛이었더라..?’ 하게 된다. 이런 소소한 것에서 취미를 가지는 것도 재밌는 것 같다.코드 리뷰기간 내에 신청해서 우빈님께 코드 리뷰를 받을 수 있었다! 라이브 동안 다른 분 코드 리뷰 해주시는 것도 같이 경청하였는데, 생각보다 내가 놓친 부분들을 여기서 얻을 수도 있었다. 물론 내가 작성한 코드를 리뷰 해주신 것이 직접적으로 바로 와닿았지만, 다른 분 코드 리뷰에서도 배울 점이 있었다.다른 분 코드 리뷰를 해주시면서 퀴즈로 내신 부분이 있다. 퀴즈 내용은 간략하게 설명하면 ‘try-catch에서 예외를 throw했는데 왜 잡히지 않을까?’이다. 코드를 보여주셨을 때 문제가 없어보였다. 그래서 나는 ‘import가 잘못된거 아니야?’라고 생각을 해서 정답을 맞췄다!사실 미션 진행하면서 정말 유사한 문제를 겪은 적이 있었다. 분명 메서드 파라미터가 똑같은데 파라미터가 다르다고 컴파일 에러가 났었다. 미션 특성 상, 맨 처음 리팩토링 이전 패키지와 이후 패키지의 클래스는 똑같다. 그래서 리팩토링 패키지에서 import를 잘못하면, 다른 패키지의 클래스를 가져온다. 그래서 문제가 발생했었는데 이와 똑같은 문제라 운이 좋게도 맞출 수 있었다.영광의 흔적!!코드 리뷰 해주시는 것을 보며 내가 배운 점들은 다음과 같다.Enum의 valueOf 메서드를 통한 생성valueOf 대신 String을 받아서 객체를 생성하는 정적 메서드를 구성사물함 이용 가능 여부를 StudyCafePassType에 저장하는 부분도 좋음get/set은 관용적인 어구로 필드에 대한 getter/setter가 아니더라도, 메서드에 get~~, set~~ 으로 네이밍하는 건 지양하자.early return 보다 else-if가 더 배타적인 내용을 나타내지 않을까?if 절 내에 return을 봐야한다.구조화보다 적절한 책임의 분배가 더 중요하다.위 내용은 다른 분들의 코드 리뷰에서 내가 배운 부분들이다. 다음은 내가 질문하거나 리뷰해주신 내용이다.있을 수도, 없을 수도 있는 필드에 대한 관리를 어떻게 해야할까요?사물함 이용권을 별도 필드로 관리하고 EMPTY 객체를 넣는다? (내용이 정확히 기억이 나지 않는다….)PassReader - FilePassReader로 PassReader의 책임을 부여하고, File에 저장된 Pass를 읽는 구현체로 분리해도 괜찮을까요?괜찮은 접근인 것 같다.Order order = Order.create() 후, order.add(pass) 와 같은 형식으로 주문이후에 주문의 내용이 바뀔 위험성이 존재하므로, 만들어놓고 추가하는 것보다 마지막에 한 번에 Order 객체를 만드는 것이 더 안정적이다. 위험성을 제거하자. 가변보다 불변이 더 좋다!우빈님꼐서 약 1시간 45분동안 라이브를 진행해주셨다. 긴 시간동안 라이브로 진행한다는 것이 쉽지 않으실텐데, 덕분에 나는 유익한 시간을 보낼 수 있었던 것 같다! 직접적으로 우빈님과 소통할 수도 있고 내가 얻을 수 있는 인사이트도 챙겨 소중한 시간이었다. 처음에 말씀하셨던 ‘함께 자라기’를 위해 더 노력해야겠다.

백엔드워밍업스터디발자국클린코드테스트

허진혁

워밍업 클럽 3기 BE 클린코드&테스트 - 2주차 발자국

코드 리팩토링을 통한 성장: 주간 회고1. 리팩토링을 시작하게 된 계기이번 주는 코드 리팩토링을 집중적으로 진행했다. 기존 코드에서 개선할 점을 찾아 수정하는 과정에서 단순한 문법 개선을 넘어, 코드의 유지보수성과 가독성을 향상시키는 것이 얼마나 중요한지 다시금 깨닫게 되었다.리팩토링을 진행한 코드(PR): GitHub 링크이 과정을 통해 단순한 기능 구현이 아니라, 보다 효율적인 코드 작성법과 객체 지향적인 사고 방식을 익히게 되었다.2. 리팩토링 과정과 개선 포인트(1) 중복 코드 제거 및 메서드 추출기존 코드에서는 비슷한 로직이 여러 메서드에서 반복적으로 사용되고 있었다. 이를 해결하기 위해 메서드 추출 기법을 적용했다.기존 코드에서는 여러 곳에서 동일한 로직을 복사-붙여넣기 했지만, 공통 로직을 별도의 메서드로 분리하여 재사용성을 높였다.이 과정에서 단일 책임 원칙(SRP, Single Responsibility Principle) 을 더욱 깊이 이해할 수 있었다.(2) 가독성을 높이는 네이밍 개선리팩토링 전에는 변수명과 메서드명이 애매하여 코드의 의도를 명확히 파악하기 어려웠다. 개선 과정에서 다음과 같은 기준을 적용했다.메서드명은 동작을 명확하게 설명할 수 있도록 동사 + 목적어 형식으로 변경변수명은 의미를 명확하게 전달할 수 있도록 명명 (예: temp → formattedDate)코드 리뷰 과정에서 네이밍의 중요성을 다시금 깨달았다. 가독성이 높아지면 코드의 이해도가 높아지고, 유지보수도 쉬워진다.(3) 불필요한 의존성 제거 및 클래스 분리기존 코드에서는 한 클래스가 너무 많은 역할을 담당하고 있었다. 이를 해결하기 위해 책임을 분리하고, 역할에 맞는 클래스를 생성했다.기존의 거대한 클래스에서 역할별로 클래스를 분리하여 객체 지향적인 구조로 개선불필요한 의존성을 제거하고, 의존성 역전 원칙(DIP, Dependency Inversion Principle) 을 적용하여 유연성을 높였다.SRP를 적용한 후, 코드의 변경이 필요할 때 한 곳만 수정하면 되어 유지보수성이 크게 향상되었다.3. 다른 개발자들의 경험에서 배운 점이번 리팩토링을 진행하면서 다른 개발자들이 작성한 후기도 참고했다. (Inflearn 블로그 링크 모음)여러 후기에서 공통적으로 강조하는 몇 가지 핵심 사항을 발견했다.리팩토링의 본질은 단순한 코드 변경이 아니라 유지보수성과 확장성을 높이는 것코드가 동작한다고 끝이 아니라, 더 나은 코드로 개선하는 과정이 필요함.리팩토링은 협업과 코드 리뷰를 통해 더욱 효과적으로 이루어진다혼자 작업할 때는 발견하지 못했던 문제점들이, 코드 리뷰를 통해 드러남.다른 개발자들의 시각에서 개선점을 찾는 것이 중요함.객체 지향 원칙을 적용하는 것이 리팩토링의 핵심이다SOLID 원칙을 고려하며 리팩토링할 때 코드가 더욱 구조적으로 개선됨.특히 단일 책임 원칙(SRP), 의존성 역전 원칙(DIP) 을 적용하면 코드의 확장성이 크게 증가함.이번 리팩토링을 통해 나 또한 이 점을 깊이 체감했다.4. 리팩토링을 통해 얻은 교훈이번 경험을 통해 얻은 가장 큰 교훈은 "리팩토링은 단순한 코드 수정이 아니라, 코드의 가치를 높이는 과정이다." 라는 것이다.코드는 팀원과 미래의 나를 위한 문서와 같다. 가독성이 좋고, 유지보수가 쉬운 코드가 진짜 좋은 코드다.코드 리뷰를 적극적으로 활용하자. 다른 개발자들의 피드백을 통해 더 나은 개발자가 될 수 있다.객체 지향 원칙을 익히고 실천하자. SOLID 원칙을 고려하며 개발하는 것이 장기적으로 가장 효율적인 방법이다.이러한 리팩토링 경험을 반복하면서, 더욱 성장하는 개발자가 되어야겠다는 다짐을 하게 되었다. 앞으로도 주간 단위로 배운 내용을 정리하며 지속적인 성장을 기록할 예정이다.✍ 앞으로의 다짐매주 코드 리팩토링을 진행하고, 개선된 내용을 블로그에 정리하기코드 리뷰 문화를 적극적으로 활용하고, 동료 개발자들과 협업하며 성장하기SOLID 원칙과 디자인 패턴을 공부하고, 실무에서 적용할 수 있도록 연습하기이번 리팩토링 경험을 통해 얻은 교훈을 앞으로도 개발 과정에 적용하며, 더 나은 개발자로 성장해 나가겠다!

백엔드워밍업클럽3기클린코드

클린코드 발자국 2주차

## 5. 코드 다듬기### ✨ 요약- 주석이 많다는 것은 코드가 비즈니스 로직을 명확히 반영하지 못했다는 신호일 수 있다. - 코드만으로도 의미를 전달할 수 있도록 가독성을 높이는 것이 중요하다.- 메서드와 변수 정렬 방식에도 신경 쓰자. - public → private 순서, 상태 변경 > 판별 > 조회 순서를 유지하면 가독성이 올라간다.- 패키지 구조를 의미 있게 나누는 것이 중요하다. - 너무 세밀하게 나누면 관리가 어렵고, 너무 큰 단위로 묶으면 유지보수가 어렵다. - 프로젝트의 규모와 요구사항에 맞는 적절한 구조를 고민해야 한다.---## 6. 리팩토링 연습### ✨ 요약- 리팩토링은 단순한 코드 변경이 아니라, 코드의 응집도를 높이고 결합도를 낮추는 과정이다. - 리팩토링을 할 때는 단순한 코드 변경이 아니라, 객체 간의 관계와 책임을 다시 한번 점검하는 것이 중요하다.- DIP(의존 관계 역전 원칙)를 고려하면서 설계해야 한다. - 의존성을 인터페이스에 두고, 구체적인 구현체를 변경할 수 있도록 설계하면 유연성이 증가한다.- 일급 컬렉션의 개념을 적극적으로 활용해야겠다. - 컬렉션을 직접 노출하는 것은 유지보수 측면에서 위험하므로, 이를 감싸는 클래스를 활용하면 더 안전하다.---## 7. 기억하면 좋은 조언들### ✨ 요약- 리팩토링과 코드 리뷰는 꾸준히 해야 한다. - 직접 코드를 개선해보면서 배우는 것이 가장 좋은 학습법이다.- 오버 엔지니어링을 피하기 위해 현실적인 선택을 하자. - 너무 많은 추상화나 인터페이스는 오히려 유지보수를 어렵게 만들 수 있다.- "한계까지 사용해본 개발자가 적정 수준을 더 잘 안다." - 새로운 기술을 도입할 때는 충분히 사용해보고, 어느 수준에서 활용해야 할지 감을 잡아야 한다.---## 8. Outro### ✨ 요약- 처음부터 완벽한 추상화를 만들려고 하기보다는, 개선해 나가는 과정이 중요하다. - 개발을 하면서 추상화의 필요성이 점점 더 명확해지는 순간이 있다. - 이를 경험하면서 더 좋은 설계를 할 수 있게 된다.- 변경이 용이한 코드를 작성하는 것이 가장 중요하다. - 코드의 가독성을 유지하면서도, 변경이 필요할 때 쉽게 수정할 수 있도록 구조를 만들어야 한다.---## 최종 결론- 객체 지향 설계의 핵심은 변경에 유연한 구조를 만드는 것 - 좋은 코드는 단순히 동작하는 코드가 아니라, 이해하기 쉽고 유지보수하기 쉬운 코드 - 완벽한 설계는 존재하지 않으며, 지속적인 개선을 통해 더 나은 구조를 찾아가는 과정이 중요하다.  

백엔드클린코드

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

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

백엔드리팩토링클린코드

인프런 워밍업 스터디 클럽 3기 백엔드-code 2주 차 발자국

이 글은 박우빈님의 readable-code강의 를 참조하여 작성한 글입니다.정신없이 진도표를 따라 강의를 수강하고 미션을 진행하였더니 일주일이 순식간에 지나가버렸다.처음에는 할 만하다고 느꼈었는데,,,강의 내용을 습득하여 스스로의 힘으로 코드에 적용까지 하려니 우빈님이 당부하신 대로 쉽지 않았던 나날들이었던 것 같다,,🥹특히 이번 스터디카페 리팩토링 미션에 뻥 안치고 10시간 이상은 투자한 것 같은데,,,ㅋㅋㅋㅋ(미션 당일 + 자고 읽어나서 맑은 정신으로 한 번 더 도전)이후에 리팩토링 강의를 수강하며 내가 전혀 고려하지 못했던 부분을 리팩토링하신 것을 보고 놀랐다. 뿐만 아니라, 리팩토링을 뚝딱 해내시는 모습에서도 감탄했다.원래 누군가가 뭔가를 쉽게 해내는 것처럼 보이면 진짜 고수라는 말이 딱 맞는 것 같다...!강의 속에서 리팩토링 하시는 모습은 굉장히 쉬워보였는데...혼자서 해내려니 정말 막막했다 ㅋㅋㅋㅋ그래도 나도 경험을 더 쌓고 나면, 지금보다 더 짧은 시간 안에 더 객체지향적으로 리팩토링을 해낼 수 있겠지!!이를 위해 다른 분들이 리팩토링 하신 코드도 많이 읽으면서 여러 번 리팩토링 해봐야겠다. 학습 내용 요약 주석의 양면성주석의사 결정의 히스토리 를 도저히 코드로 표현할 수 없을 때, 주석으로 상세하게 설명하자주석이 많다 == 비지니스 요구사항을 코드에 잘못 녹였다 는 의미가 성립됨주석을 작성할 때, 자주 변하는 정보는 최대한 지양해서 작성하자 (그렇지 않으면, 주석도 신경써서 계속 업데이트 해줘야 하는 단점 존재)변수와 메서드의 나열 순서상태 변경 > 불리언 등 판별 >= 조회 메서드 순으로 메서드 순서 나열 패키지 나누기패키지: 문맥으로써의 정보를 제공할 수 있음대규모 패키지 변경은 팀원과의 합의를 이룬 후 하자본인만 사용하는 부분이면 괜찮지만, 여러 사람과 공통으로 사용하는 클래스들의 패키지를 한번에 변경하면 충돌이 발생할 수 있음! 은탄환은 없다클린코드가 은탄환은 아니다 (클린코드가 무조건적인 정답은 아니다)요구사항이 변경될 일이 없는 코드는 절차지향적인 코드가 오히려 정답일 수 있음 (e.g., 체스는 500년동안 규칙이 변하지 않았음)실무는 지속가능한 소프트웨어의 품질 vs 기술부채를 안고 가는 빠른 결과물 사이의 줄다리기무조건적으로 클린 코드를 추구하기보다는 주어진 기간을 최대한 맞추게끔 결과물을 내놓고, 이후 미래에 잘 고치도록 할 수 있는 코드 센스가 필요 (e.g., 주석으로 리팩토링 할 부분 남겨 놓기 등) 미션 Day 4 미션 피드백기존 코드 (return 타입 - boolean)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; } 내가 리팩토링 한 코드 중 일부 (1)public class Order { private long id; private List<Item> items; private Customer customer; public Order(final List<Item> items, final Customer customer) { validateOrder(items, customer); this.items = items; this.customer = customer; } private void validateOrder(final List<Item> items, final Customer customer) { if (doesNotHaveItems(items)) { throw new RuntimeException("주문 항목이 없습니다."); } if (doesNotHaveCustomerInfo(customer)) { throw new RuntimeException("사용자 정보가 없습니다."); } } public boolean doesNotHaveItems(final List<Item> items) { return items.isEmpty(); } public boolean doesNotHaveCustomerInfo(final Customer customer) { return customer == null; } } => boolean을 return하고 있는 기존 메서드에 대한 리팩토링으로 예외를 던지는 것으로 변경하는 것은 좋을 수도, 나쁠 수도 있다.(자칫하면 오버엔지니어링이 될 수 있음)📌 예외를 던지는 것은 비싸기 때문에, 신중하게 메서드의 사용현황을 파악 후 상황에 맞게 리팩토링 할 것!! 내가 리팩토링 한 코드 중 일부 (2)public class Item { private static final int MINIMUM_VALUE = 1; private long id; private String name; private int price; public Item(final long id, final String name, final int price) { validatePositivePrice(price); this.id = id; this.name = name; this.price = price; } private void validatePositivePrice(final int price) { if (price < MINIMUM_VALUE) { throw new RuntimeException("올바르지 않은 가격입니다."); } } }고민이었던 부분: Order 클래스 내에 Item 클래스를 새로 만들어, 이를 리스트 형태로 Order에 의존성 주입하도록 코드 리팩토링을 진행하였다. 이 때 Item 객체를 생성할 때 마다 금액이 양수인지 검증하고 있는데, Order 클래스에서 전체 총 금액이 양수인지 검증을 다시 한번 해주는 게 좋을지 고민이 됐었다.=>  우빈님 답변: 이미Item 객체를 생성할 때 금액의 유효성을 보장하고 있으니, Order 클래스는 굳이 하지 않아도 될 것 같다.물론 비지니스 로직이 복잡해지면, 오히려 필요하다고 느끼는 순간이 올 수 있으니, 그 때 추가해도 늦지 않다! Day 7 미션미션 한 줄 소개: 스터디 카페 이용권 선택 시스템 리팩토링 하기 리팩토링 한 부분 코드 중복 제거 및 메서드 추출StudyCafePassMachine의 의존성 config에서 주입: StudyCafePassMachine은 필요한 의존성을 외부에서 주입받기만 하고, 내부에서 어떻게 사용하는지는 외부에 노출하지 않을 수 있다!일급 컬렉션 활용: 일급 컬렉션으로 분리함으로써, 원래는 private 메서드라 테스트하지 못했던 로직도 테스트 가능해짐!무분별한 getter 사용이 아닌, 객체에 메세지 보내기public boolean isSamePassTypeWith(final StudyCafePassType studyCafePassType) { return passType == studyCafePassType; }passType를 비교해야 하는 곳에서 passType를 게터를 통해 비교해주는 게 아닌, isSamePassTypeWith 메서드와 같이 객체에 메세지를 전달하자!스터디 카페 이용권 인터페이스 적용: 이부분은 리팩토링을 잘 하였는지 감이 안온다..시도에 의의를 두자 ㅎ헤ㅔㅎpublic interface StudyCafePassHandler { boolean isAppliable(final StudyCafePassType studyCafePassType); StudyCafePasses findCandidateStudyCafePasses(final StudyCafePasses studyCafePasses); } ========= # StudyCafePassMachine private void processUserSelection(final StudyCafePassType studyCafePassType) { final StudyCafePasses availablePasses = getAvailablePasses(studyCafePassType); outputHandler.showPassListForSelection(availablePasses); final StudyCafePass selectedPass = inputHandler.getSelectPass(availablePasses); if (studyCafePassType == StudyCafePassType.FIXED) { checkLockerPass(selectedPass); return; } outputHandler.showPassOrderSummary(selectedPass, null); }Hourly, Weekly, Fixed라는 3종류의 카페 이용권이 존재하기 때문에, 인터페이스를 정의하여 if문 사용을 자제하고, 상황에 맞는 이용권을 가져왔다. 리팩토링 놓친 부분FileHandler: 데이터를 어디로부터 어떻게 가져올 것인가에만 초점이 맞춰져 있음-> File관련 로직이 들어나면 FileHandler 가 방대해질 것개선 방향: provider를 통해 어떤 데이터를 필요로 하는가에 초점을 맞출 것SeatPassProviderLockerPassProvider domain영역에 view관련 로직 침투public enum StudyCafePassType { HOURLY("1", "시간 단위 이용권"), WEEKLY("2", "주 단위 이용권"), FIXED("3", "1인 고정석"); private final String command; private final String description; StudyCafePassType(final String command, final String description) { this.command = command; this.description = description; } public static StudyCafePassType from(final String userInput) { return Arrays.stream(StudyCafePassType.values()) .filter(studyCafePassType -> studyCafePassType.command.equals(userInput)) .findAny() .orElseThrow(() -> new AppException("잘못된 입력입니다.")); } } PassType은 중요한 도메인 모델인데, Input과 관련된 의미를 지닌 command가 침투되었다. passType을 선택하는 command가 변경된다면, 단순히 입력 방식을 바꿨을 뿐인데 도메인 모델을 수정해야 하는 좋지 않은 상황이 발생한다.StudyCafePassOrder 도메인 추출스터디 카페 좌석 이용권 + 사물함 이용권을 합친 Order 도메인을 새로 추출할 수 있다.이로 인해 FIxedPassType에만 적용되는 사물함 로직 분기문을 간단하게 처리할 수 있었다!StudyCafePass 내 LOCKER_TYPES 상수 선언public enum StudyCafePassType { HOURLY("시간 단위 이용권"), WEEKLY("주 단위 이용권"), FIXED("1인 고정석"); private static final Set<StudyCafePassType> LOCKER_TYPES = Set.of(FIXED); private final String description; StudyCafePassType(String description) { this.description = description; } public boolean isLockerType() { return LOCKER_TYPES.contains(this); } public boolean isNotLockerType() { return !isLockerType(); } }전혀 생각지도 못했지만, 알아두면 참 좋은 객체에 메세지를 보내는 방법에 대해 배웠다.LOCKER_TYPES를 StudyCafePassType enum내에 적용하여 처리할 수 있다니도메인 지식이 부족해서 그랬나, 나는 생각지도 못했던 방법이다.나는 상위 도메인에서 if문을 통해 매번 확인해주었는데, 우빈님이 하신 방법이 더 책임 분리가 잘되어있고 객체에 메세지를 보내는 좋은 방법인 것 같다!!또한 Locker type이 늘어나도, set에 내용만 추가해주면 된다는 점에서 유지보수도 훨씬 쉬울 것 같다! 마무리강의를 굉장히 빠른 시간 안에 완강했다!!!그렇지만 강의 내용을 완전히 내 것으로 만들었다기엔 부족하다,,,,강의 볼 때는,,'이럴 때 조합, 일급 컬렉션, VO 등을 적용 하는구나~!' 를 배우면서, 앞으로 스스로 잘 판단해 낼 줄 알았는데,,,혼자서 해 보려니까 너무나도 막막했다.그래도 강의 내에서 주신 미션을 통해 내가 어느 부분이 부족한지 파악할 수 있었던 것 같다. 무엇보다도 너무 재밌었다,,,시간 가는 줄 모르고 했던 것 같다.이제 강의는 끝이 났지만 지뢰찾기랑 스터디카페 코드에 대해 복습할 것이다. 리팩토링 적용하기 어려웠던 부분을 반복 작성해 보면서 체득시킬 생각이다. 이렇게 반복하다보면 우빈님의 사고법을 체득할 수 있겠지요...?다음 주부터는 테스트 코드에 대해 배우는데,,평소 테스트 코드에 대해 공부를 많이 하지 않았어서 새로 배우는 양이 어마어마 할 것 같다.강의 내용을 잘 습득할 수 있도록 메타인지 열심히 해야겠다!나 자신 아자아자 화이팅이다💪🏻 [출처]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

백엔드클린코드테스트코드발자국워밍업스터디클럽

겸손한 북극곰

[워밍업-백엔드3기] ReadableCode 2주차 회고

ReadableCode 를 완강했다.실습 과제로 마무리하며, 클린 코드는 많이 보고 연습하며 습득해야할 필요성을 느꼈다. 🔢 클린코드의 두번째 학습Readable Code: 읽기 좋은 코드를 작성하는 사고법 - 박우빈2주차 새롭게 배운 내용일급 컬렉션 단 하나의 컬렉션 필드를 가지며, 책임을 새로 가질 수 있는 객체이다.Enum상태와 행위를 함께 관리할 수 있는 추상화된 객체이다.Early Return 과 부정문 관리부정문은 두번의 사고를 거쳐야 한다.else if 는 이전 과정을 모두 기억해야 하는 장기 사고적인 부분이다.예외 관리예외는 비싸다. 예외는 개발자를 위한 정보이다.문맥을 활용하여 의도를 전달하자메서드의 순서, 패키지 또한 의도이다.Optional 을 조심해서 사용하자. 클린 코드 사고를 기반으로 결정하는 코드 센스 능력을 기르자   🤔 2주차 회고리팩토링 실습을 진행하며, 머리로 이해한 개념을 직접 적용하는 것의 어려움을 느꼈다.마치 악기를 배우듯이 꾸준히 내용을 확인하고 적용하며 습득해야하는 습관 과제 같았다. 미션 제출과 중간 라이브를 통해 다른 사람들의 리팩토링을 보는 시간을 가질 수 있었다.동일한 바탕으로 다양한 접근 방식을 확인할 수 있었다.코드에 정답은 없다. 클린 코드를 만들 수 있는 센스를 몸에 익히고 큰 그림을 보며 코드를 짜는 능력을 길러야겠다.'완벽한 코드는 없다. 그 당시의 최선일 뿐!'클린 코드 학습을 고민하는 분이 있다면 박우빈님 강의를 추천한다!   

백엔드클린코드ReadableCode박우빈님강의워밍업클럽

채널톡 아이콘