🎁 모든 강의 30% + 무료 강의 선물🎁

블로그

10

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

👣 발자국 책갈피인프런 워밍업 클럽 3기 백엔드 코드 발자국 1주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 2주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 3주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 4주차✅미션PR 책갈피[미션 Day 2] 추상과 구체 예시 작성[미션 Day 4] 리팩토링 & SOLID 원칙[미션 Day 7] 리팩토링 연습[미션 Day 11] 단위테스트 작성[미션 Day 16] 레이어드 아키텍처 특징 및 테스트 작성법[미션 Day 18] Mock 어노테이션 종류 및 차이점 & BDD 패턴 매치🙊 시작이 반이다... 벌써 중간점검 ?인프런 워밍업 클럽이 시작한지 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 25일 전
leeebug

워밍업 클럽 스터디 3기 FS - 2주차 발자국

인프런 워밍업 클럽 스터디에 참여하고 벌써 2주 차도 마무리에 접어들고 있다. 4주간의 스터디이기 때문에 생각보다 일정이 타이트하여 시간관리가 무엇보다도 중요한 시기라고 생각한다.이번 주에는 파일 업로드 기능을 구현해야하기에 1주 차 과제를 급하게 마무리하고 지난주 토요일부터 서둘어서 미리 학습을 시작했다.깃 레포지토리의 경우에는 지난주에 사용했던 템플릿을 거의 수정없이 그대로 사용해서 개발환경 구축은 크게 어렵지 않았다.이번 주 강의에서는 Supabase Storage를 사용했는데 API가 잘 준비되어있어서 사용 방법을 익히는데도 크게 어렵지는 않았다. 다만 아래에서도 언급하겠지만 Supabase Storage는 AWS S3 기반으로 구현되어 강력한 네이밍 규칙이 적용되어 개인적으로는 Supabase Storage와 Supabase DB를 함께 사용했다. 📝 2주차 학습Supabase Storage클라우드 기반 객체 저장소로, AWS S3와 유사한 방식으로 파일을 저장하고 관리하는 서비스파일 및 이미지 업로드 및 관리 기능 제공PostgreSQL과 연동 가능권한 관리(RLS) 및 퍼블릭/프라이빗 파일 설정 가능Supabase SDK 또는 Restful API로 사용 가능  ✔ 파일명 규칙 (Supabase & AWS 공통) ASCII 문자, 숫자, 일부 특수 문자 허용 (- _ . /) 파일명을 /로 구분하여 폴더처럼 사용 가능 (folder/image.png) 공백 포함 가능하지만, URL Encoding이 필요할 수 있음 파일명에 한글, 이모지, 특수문자가 포함될 경우 정상적으로 업로드되지 않을 가능성이 있음 → URL-safe 변환 권장 React DropzoneReact에서 간편하게 파일 Drag & Drop 기능을 구현할 수 있는 라이브러리HTML5 File API를 활용하여 파일 업로드를 쉽게 구현할 수 있는 기능을 제공파일 타입, 크기, 개수 등 다양한 제약 조건 설정 가능비동기로 파일을 처리할 수 있는 onDrop 이벤트 제공 📋 2주차 미션💬 GitHub 저장소🚀 데모 영상 보러가기미션 해결 과정 요약2주 차 미션은 Next.js, React Query, TailwindCSS를 사용하여 이미지 업로드 앱을 구현하기였다. 필수 구현 기능으로는 이미지 업로드 기능(클릭 업로드 방식과 Drag & Drop 방식, 다중 업로드)과 이미지 삭제와 이미지 검색 기능 구현하기였다. 추가 기능은 파일의 마지막 수정 시간을 화면에 출력하는 UI 구현하기였다. 여기에 과제의 완성도를 높이기 위해서 개인적인 챌린지로 파일명에 한글 또는 특수문자 포함된 파일 업로드 기능, 1MB 미만으로 이미지를 압축하는 기능, 다운로드 기능을 추가로 구현하였다. 과제 추가 구현 기능✅ 마지막 수정 시간 표시const { error: insertError } = await supabase.from(DB_TABLE_NAME).insert({ name: file.name, originalName: originalFileName, imageId: uploadedFile.id, imageUrl: publicUrl, createdAt: new Date(file.lastModified).toISOString(), })생성: DB에 파일 데이터 업로드 시 createdAt에 file.lastModified를 ISOString 형식으로 저장 if (dbData) { const { error: updateError } = await supabase .from(DB_TABLE_NAME) .update({ name: file.name, originalName: originalFileName, imageUrl: publicUrl, updatedAt: new Date().toISOString(), }) .eq('imageId', uploadedFile.id)수정: DB에 해당 ID가 존재할 경우 updatedAt(string | null)에 현재 시간을 ISOString 형식으로 저장// DropImageManager 컴포넌트에서 생성 시간, 수정 시간을 포멧팅하여 DropImage 컴포넌트에 프롭스로 전달 const localCreatedAt = getLocalTime(image.createdAt) const localUpdatedAt = image.updatedAt ? getLocalTime(image.updatedAt) : null <!-- JSX 정렬이 잘 안되서 렌더링 형태만 봐주세요! --> <div className="w-5/6 truncate"> <span className={`text-[0.7rem] font-semibold ${localUpdatedAt ? 'text-mint-800' : 'text-gray-500'}`} > {localUpdatedAt ? localUpdatedAt : localCreatedAt} </span> {localUpdatedAt && ( <span className="text-[0.7rem] font-semibold text-mint-800"> (수정)</span> )} </div>출력: updatedAt이 존재할 경우 updatedAt과 (수정) 을 함께 출력, updatedAt: null이라면 createdAt를 출력 개인 챌린지 기능✅ 파일명 자동 변환 후 이미지 업로드하는 기능을 구현 (UX 개선)파일명 검증: 정규식을 활용하여 한글 및 특수 문자 포함 여부를 확인자동 변환: 검증 후 8자리 랜덤 문자열로 안전한 파일명 생성업로드 처리: 변환된 파일명으로 File 객체 생성 후 formData.append로 원본 파일명 함께 전송서버 액션: Supabase Storage에 저장 후, 완료 시 DB에 메타데이터 저장하여 연동결론: 파일명 변환을 자동화하여 업로드 오류를 방지하고, 원본 파일명도 유지하여 검색 및 관리 UX 개선✅ 파일 용량이 1MB 초과 시 자동 압축 후 업로드하는 기능을 구현 (UX 개선)browser-image-compression 라이브러리를 사용하여 파일의 용량 검증 후 1MB 초과 시 이미지 압축 후 업로드결론: 이미지 최적화로 업로드 속도 향상, 스토리지 비용 절감 효과✅ Blob URL을 활용한 다운로드 기능 추가 (UX 개선)Blob URL 생성: 업로드된 이미지를 fetch()로 가져와 Blob으로 변환다운로드 기능 구현: window.URL.createObjectURL(blob)으로 브라우저에서 직접 다운로드 가능하도록 처리결론: Blob URL 다운로드 방식을 적용하여 최적화된 이미지를 빠르게 다운로드 받을 수 있도록 개선🚧 기능 구현 시 어려웠던 부분Supabase Storage에 전달하는 File 객체 커스텀 불가원본 파일명을 추가하려 했으나, File 객체 자체를 수정하는 것이 제한적이다.파일 객체를 복사하여 원본 파일명을 추가하는 방법 시도전개 연산자를 사용하여 객체 복사 후 원본 파일명을 추가하려 시도하였으나 file 객체는 일반적인 방법으로는 복사할 수 없는 특별한 객체이다.ExtendedFile 확장 클래스로 인스턴스를 생성했으나 서버에 전달되지 않는 문제 발생확장된 ExtendedFile 객체를 formData에 담아 서버로 전송했지만, 서버에 정상적으로 전달되지 않았다.최종 해결 방법formData.append("file", file) formData.append("originalFileName, file.name)file 객체와 원본 파일명을 함께 서버로 전송 후 가공하여 Supabase Storage의 파일명에는 안전한 파일명만 저장하고 DB에 스토리지Id, 원본 파일명, 안전한 파일명, 이미지URL 등 정보를 저장했다. 🧾 ERD 다이어그램👀 2주차 회고아직 갈 길이 멀지만, 리팩토링을 통해 Next.js의 장점을 살릴 수 있는 구조로 점점 개선되어가는 과정을 경험하면서 이번 주 역시 알차게 보냈다고 생각한다.이번 주는 특히 MVP 패턴과 비슷한 형태로 컴포넌트 구조를 잡는 것에 익숙해지는 것을 개인적인 목표로 삼았다. 처음부터 MVP 패턴을 염두해 두고 설계한 것은 아니었지만, 진행하다보니 자연스럽게 MVP와 유사한 패턴으로 정리되어 가는 것을 느꼈다.화면 렌더링 시 상호작용이 필요하지 않은 정적인 요소들까지 클라이언트 컴포넌트로 관리하면 불필요한 하이드레이션 부담이 증가할 수 있다는 점을 다시 한번 체감했다.클라이언트 컴포넌트 내에서도 역할을 나눠 서비스 레이어나 상태 관리만 담당하는 매니져 컴포넌트와 프롭스로 상태를 전달받아 단순히 화면을 렌더링을 담당하는 UI 컴포넌트로 분리하는 연습을 진행했다.이러한 구조로 개선하면서 클라이언트 컴포넌트의 부담을 줄이고, 유지보수성을 높이는 방향으로 점차 최적화되고 있다는 점이 느껴졌다. 아직 개선해야 할 부분이 많지만 점진적으로 개선하여 더 나은 아키텍쳐를 만들어가는 과정이 의미 있었다고 생각한다.

풀스택워밍업클럽3기발자국회고과제미션

10

단위 테스트 작성법 그리고 Mock - 인프런 워밍업 클럽 3기 백엔드 코드✨

👣 발자국 책갈피인프런 워밍업 클럽 3기 백엔드 코드 발자국 1주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 2주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 3주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 4주차✅미션PR 책갈피[미션 Day 2] 추상과 구체 예시 작성[미션 Day 4] 리팩토링 & SOLID 원칙[미션 Day 7] 리팩토링 연습[미션 Day 11] 단위테스트 작성[미션 Day 16] 레이어드 아키텍처 특징 및 테스트 작성법[미션 Day 18] Mock 어노테이션 종류 및 차이점 & BDD 패턴 매치⏰ 벌써 1분기 끝...약 한 달간의 인프런 워밍업 클럽 백엔드 코드 3기 여정이 끝났다.올해는 유난히 바빠서 그런지, 시간이 유독 더 빨리 가는 것 같다. 벌써 1분기가 끝났다는 게… 믿기지 않는다. 😂우빈님께서는 온라인 세션 때 시간이 빨리간다는 농담을 해주시곤 한다.우빈님의 지인이 ‘시간이 너무 빨리 가서 곧 크리스마스 트리를 설치해야겠다’고 하셨던 말씀이 기억에 남는다. ㅋㅋ 🤣 (맞나? 이게? 자세히 기억은 아나지만..)아무튼! 마지막 주차 최종 점검 온라인 라이브 세션을 마지막으로 스터디를 완주하였다. 👏기대하고 기다리던 코드 리뷰를 다시 받게 되었다!✨ 두번째 우빈님의 세심한 코드 리뷰이번 코드 리뷰는 작성한 단위 테스트 코드에 대한 리뷰를 받았다.중간점검 때 받았던 리팩토링 코드 리뷰보다는 과제가 다소 정형화(?) 되어 있어서 공통 피드백이 많긴 했다.다시 한번 우빈님의 세심한 리뷰에 놀랐다. 😮🔗 Github PR 링크단위테스트 작성 PR1⃣ 사용자 입력에 대한 테스트 방법이건 내가 PR에 궁금했던 질문 중 하나였다. 🧐프로덕션 코드를 수정하면 안된다는 제약을 걸고, 테스트 코드를 작성하려고 했기 때문에..!사용자 입력을 받는 Scanner에 대한 테스트는 어떻게 하는지 궁금했다.🧪️ 테스트 하려고 했던 코드public class InputHandler { private static final Scanner SCANNER = new Scanner(System.in); ...(중략)... }✏️ 우빈님 리뷰Q. 프로덕션 코드 수정 없이 사용자 입력 테스트가 가능할까요..? 🤔 (Scanner 클래스를 외부세계로 분리하면 가능할 것 같긴합니다..) 입력에 대한 테스트도 가능하면 전체 통합테스트도 가능할 것 같습니다!!! A. Scanner 때문에 어렵긴 하죠. InputStream을 생성자로 받는 형태로 변경하고, Scanner를 생성해주는 방식이라면 가능할 겁니다. 🤔 돌아보기역시, 프로덕션 코드를 수정하지 않으면 테스트가 어렵다는 말씀을 주셨다.우빈님 리뷰를 반영하여 프로덕션 코드 부분을 InputStream을 생성자로 받는 형태로 리팩토링해봐야겠다.2⃣ 테스트 커버리지에 대한 우빈님의 관점리뷰 신청 시에, "테스트 커버리지의 집착"에 대해 언급을 했었는데..세심한 우빈님께서 포인트를 짚어주셨다...!! 🥹✏ 우빈님 리뷰A. 연습 시에 커버리지를 극한까지 올리는 데에 집중해보는 것 -> 👍 그러나 실무에서는 '주어진 시간 안에' 중요도가 높은 순으로 테스트를 할지 말지를 결정해야 합니다. 물론 전부 다 할 수 있으면 best 겠죠 :) 🤔 돌아보기실무에서의 테스트 커버리지에 대한 관점을 말씀주셨다...테스트 커버리지를 높이는 것도 중요하지만, 비즈니스 우선이라는 점을 반드시 인지하자.테스트 코드 작성 시, 중요도를 따져보는 연습을 해봐야겠다.그리고 실무에서의 커버리지에 대한 집착은 지양하도록 하자.대신, 사이드 프로젝트에서는 커버리지를 극한까지 끌어올리는 연습에 집중해보는 것도 좋겠다.3⃣ 한 눈에 들어오게끔 'given' 절 작성하기다음 코드는 given 절에 선언한 컬렉션 변수가 너무 길어서 private 메서드로 분리한 형태이다.@DisplayName("좌석 패스로 기간과 타입이 동일한 사물함 패스를 찾는다.") @Test void findLockerPassBy() { // given List<StudyCafeLockerPass> list = lockerPassList(); StudyCafeLockerPasses lockerPasses = StudyCafeLockerPasses.of(list); ...(중략)... } private List<StudyCafeLockerPass> lockerPassList() { return List.of( StudyCafeLockerPass.of(StudyCafePassType.FIXED, 4, 11000), StudyCafeLockerPass.of(StudyCafePassType.WEEKLY, 4, 17000), StudyCafeLockerPass.of(StudyCafePassType.FIXED, 12, 11000), StudyCafeLockerPass.of(StudyCafePassType.FIXED, 4, 18000), StudyCafeLockerPass.of(StudyCafePassType.HOURLY, 8, 11000), StudyCafeLockerPass.of(StudyCafePassType.FIXED, 10, 11000) ); }✏️ 우빈님 리뷰A. lockerPassList()가 private 메서드라 list가 무엇인지 한 눈에 잘 들어오지 않는다. 어차피 정해진 리스트라면, 상단에 상수로 관리하면 어떨까? 네이밍도 list -> allLockerPasses로 "모든" 사물함 패스 라는 의미를 주면 모든 사물함 패스가 존재할 때, 내 좌석권에 맞는 사물함 패스를 찾는다는 내용으로 변수명을 변경하면 좀 더 이해하기 쉬울 것 같다. 🤔 돌아보기당시에 완전 뜨끔했던 리뷰였다.. 💯내가 작성한 코드를 보니 메서드도 메서드인데 왜 변수명을 저렇게 작성했을까?라는 의문이 든다. 🤦‍♂테스트 코드의 given 절은 중복 제거보다도 '한눈에 들어오는 것'이 더 중요하다고 하셨다.읽는 사람의 '뇌 메모리'를 덜 쓰게끔 given 절을 설계하는 연습이 필요해 보인다.리뷰를 바로 반영하여 아래의 코드로 리팩토링 했다. ♻private static final List<StudyCafeLockerPass> ALL_LOCKER_PASSES = List.of( // 👍 상수로 추출 및 네이밍 변경 StudyCafeLockerPass.of(StudyCafePassType.FIXED, 4, 11000), StudyCafeLockerPass.of(StudyCafePassType.WEEKLY, 4, 17000), StudyCafeLockerPass.of(StudyCafePassType.FIXED, 12, 11000), StudyCafeLockerPass.of(StudyCafePassType.FIXED, 4, 18000), StudyCafeLockerPass.of(StudyCafePassType.HOURLY, 8, 11000), StudyCafeLockerPass.of(StudyCafePassType.FIXED, 10, 11000) ); @DisplayName("좌석 패스로 기간과 타입이 동일한 사물함 패스를 찾는다.") @Test void findLockerPassBy() { // given StudyCafeLockerPasses lockerPasses = StudyCafeLockerPasses.of(ALL_LOCKER_PASSES);4️⃣ 전역적인 기능을 Stub시, 주의 하기다음은, 엑셀에 있는 패스권 목록을 가져오는 부분을 'mocking'한 부분이다.@DisplayName("파일을 읽어서 좌석 패스를 가져온다.") @Test void getSeatPasses() { try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) { // given mockedFiles.when(() -> Files.readAllLines(any())) .thenReturn(List.of( "WEEKLY,2,4000,0.0", "WEEKLY,12,120000,0.3", "HOURLY,4,6500,0.1" )); } }✏️ 우빈님 리뷰A. mockStatic으로 Files mocking 👍 다만, Files.readAllLines()를 stubbing하는 등의 전역적인 기능을 조작하는 것은 멀티스레드로 병렬 테스트를 수행할 때 문제가 될 수 있으므로 주의 필요 🤔 돌아보기mockStatic을 이용해서 작성한 코드가 병렬 테스트 수행 시, 테스트 코드가 깨질 수 있다는 사실을 처음 알게 되었다.간단한 테스트라면 괜찮을 수 있지만, 실무에서는 반드시 지양해야겠다.이번 코드 리뷰는 실무에서의 주의할 점에 대해 많이 언급해주셨다. ⭐️테스트 커버리지의 양면성 및 실무에서의 지양mockStatic의 병렬 테스트 시, 사이드 이펙트 발생단순히, 테스트 코드를 많이 작성하는 것보다 중요도 높은 혹은 의미있는 테스트 코드를 작성하려고 노력해야 겠다. ✨"이 글이 우빈님께 닿지는 않겠지만..😅우빈님! 감사드립니다.🙇‍♂"💡 자기만의 언어로 키워드 정리하기섹션 7. Mock을 마주하는 자세Test Double, Stubbing1⃣ Dummy아무것도 하지 않는 깡통 객체단순히 인자를 채우기 위해 사용되며, 호출되지 않음class DummyUser implements User { @Override public String getName() { return null; // 의미 없는 값 } }2️⃣ Fake단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체 (ex. FakeRepository)class FakeUserRepository implements UserRepository { private Map<Long, User> users = new HashMap<>(); @Override public User findById(Long id) { return users.get(id); } public void save(User user) { users.put(user.getId(), user); } }3️⃣ Stub테스트에서 요청한 것에 대해 미리 준비한 결과르 제공하는 객체, 그 외에는 응답하지 않는다.특정한 고정된 값을 반환하는 객체테스트에서 정해진 응답이 필요할 때 사용class StubUserRepository implements UserRepository { @Override public User findById(Long id) { return new User(id, "stub_user"); } }4️⃣ SpyStub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체, 일부는 실제 객체처럼 동작시키고 일부만 Stubbing 할 수 있다.메서드 호출 여부, 호출 횟수 등을 검증하는 데 사용class SpyEmailSender implements EmailSender { private int sendCount = 0; @Override public void sendEmail(String message) { sendCount++; } public int getSendCount() { return sendCount; } }5️⃣ Mock행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체@Test void testMockExample() { EmailSender emailSender = mock(EmailSender.class); emailSender.sendEmail("test@example.com"); verify(emailSender).sendEmail("test@example.com"); // 호출 검증 }Stub과 Mock차이Stub는 상태 검증, Mock은 행위 검증Stub는 메서드가 특정 값을 반환하도록 설정하기 때문에, 반환한 값에 대한 검증을 한다.Mock은 특정 메서드가 정확히 호출되었는지 검증하는 역할을 한다.Stubbing이란?Mock의 행위를 지정하는 것, 즉 Mock 객체의 행동을 조작하는 것Mockito의 when, thenReturn 메서드를 활용하여 Stubbing 할 수 있다.@Mock, @MockBean, @Spy, @SpyBean, @InjectMocksSpy는 이해할 때 기능 중에 스파이가 있다(?)라고 기억하면 편하다 ㅎㅎ 😂BDDMockitoBDD 스타일의 Mockito 버전으로 given(), willReturn(), then() 등을 사용하여 직관적인 테스트를 작성할 수 있다.Classicist vs. MockistMockist : 모든 테스트를 mocking 위주로 하자라는 입장Classicist : 진짜 객체간의 협업을 통한 보장 (mocking을 무조건 하지말라는 건 아님)각각 객체에 대한 테스트가 잘 되도 협업 시에는 모르는 문제가 나올 수 있다. (A + B = AB? BA? C?)외부 시스템 로직이 있을 때는 mocking 처리하는 것이 좋다.섹션 8. 더 나은 테스트를 작성하기 위한 구체적조언테스트 하나 당 목적은 하나!테스트 코드 내부의 분기문이나 반복문처럼 고민을 요구하는 코드는 로직이 여러가지이기 때문에, 테스트 케이스가 여러개이다.테스트 케이스 별로 각각의 테스트 코드를 작성하자.완벽한 제어given 데이터를 만들 때 LocalDate.now(), LocalDateTime.now() 사용하지 않는 게 좋다.테스트 코드 실행 시마다 의도하는 바가 달라지기 때문에 영향을 끼친다.제어할 수 없는 값을 제어 가능하게 변경하자.테스트 환경의 독립성, 테스트 간 독립성공유변수, 연관관계가 있는 테스트코드는 지양하자.Test Fixturegiven절에 최대한 명시한다.메서드를 추출할 때 필요한 파라미터만 명시한다.별도의 data.sql 데이터를 추출하지 않는다.단위테스트 내에서 모두 표현한다.deleteAll(), deleteAllInBatch()@Transactional은 사이드 이펙트를 고려해서 클렌징해야한다.결국은 테스트도 비용이다... 아무리 h2 인메모리 DB를 사용한다지만 deleteAll()처럼 다수의 쿼리가 발생하면 테스트 비용이 증가한다.deleteAllInBatch() 벌크성으로 데이터를 클렌징하다.@Transactional와 deleteAllInBatch() 혼용해서 사용하는 것이 좋다.@ParameterizedTest, @DynamicTest@ParameterizedTest동일한 테스트를 다른 입력값으로 테스트 할 때 사용@ValueSource, @CsvSource, @MethodSource 등 다양한 소스로부터 테스트가 가능하다.@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.") @ParameterizedTest @CsvSource({"HANDMADE, false", "BOTTLE, true", "BAKERY, true"}) void containsStockType3(ProductType productType, boolean expected) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isEqualTo(expected); } private static Stream<Arguments> provideProductTypesForCheckingStockType() { return Stream.of( Arguments.of(HANDMADE, false), Arguments.of(BOTTLE, true), Arguments.of(BAKERY, true) ); } @DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.") @ParameterizedTest @MethodSource("provideProductTypesForCheckingStockType") void containsStockType4(ProductType productType, boolean expected) { // when boolean result = ProductType.containsStockType(productType); // then assertThat(result).isEqualTo(expected); }@DynamicTest테스트를 실행할 때 동적으로 생성하는 방식@TestFactory를 사용한다.@DisplayName("재고 차감 시나리오") @TestFactory Collection<DynamicTest> stockDeductionDynamicTest() { // given Stock stock = Stock.create("001", 1); return List.of( DynamicTest.dynamicTest("재고를 주어진 개수만큼 차감할 수 있다.", () -> { // given int quantity = 1; // when stock.deductQuantity(quantity); // then assertThat(stock.getQuantity()).isZero(); }), DynamicTest.dynamicTest("재고보다 많은 수의 수량으로 차감 시도하는 경우 예외가 발생한다.", () -> { // given int quantity = 1; // when & then assertThatThrownBy(() -> stock.deductQuantity(quantity)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("차감할 재고 수량이 없습니다."); }) ); }수행 환경 통합하기더 자주, 더 빠르게 수행하는 환경을 구축하자.공통 환경을 추출해서 통합 클래스를 만들어 서버 뜨는 횟수를 줄인다.private method test수행할 필요가 없다.욕망이 강하다면 객체 분리의 신호이다.테스트에서만 필요한 코드프로덕션 코드에 만들어도 되지만 최대한 보수적으로 생성섹션 9. Appendix지만 중요한 것들학습 테스트잘 모르는 기능, 라이브러리, 프레임워크를 학습하기 위한 테스트 코드여러 테스트 케이스를 스스로 정의하고 검증하는 과정을 통해 구체적인 동작과 기능을 학습Spring Rest Docs테스트 코드를 통한 API 문서 자동화 도구API 명세를 문서로 만들고 제공함으로써 협업을 원활하게 한다.👨🏻‍💻 미션 회고[미션 Day 16][미션 PR][미션 Day 16] 레이어드 아키텍처 특징 및 테스트 작성법1⃣ 레이어드 아키텍처 특징 및 테스트 작성법레이어드 아키텍처의 특징을 개념 위주로 정리하고, 레이어별 테스트 작성법은 예제 코드를 활용해 정리하였다.특히, 레이어별 테스트를 작성하는 과정에서 Test Fixture와 데이터 클렌징 개념을 함께 학습하며, 이를 예제 코드에 적용하였다.차후에 실무 및 사이드 프로젝트에서 레이어드 아키텍처를 직접 적용해 보며 응용해볼 계획이다.[미션 Day 18][미션 PR][미션 Day 18] Mock 어노테이션 종류 및 차이점 & BDD 패턴 매치2⃣ Mock 어노테이션 종류 및 차이점 & BDD 패턴 적용📌 Mock 어노테이션 종류 및 차이점Mockito의 주요 어노테이션(@Mock, @Spy, @InjectMocks, @MockBean, @SpyBean)의 차이를 자기만의 언어로 정리하였다.순수한 Mock 기반 단위 테스트와 Spring Context 기반 통합테스트에서 각각 어떤 어노테이션을 사용해야 하는지 이해하였다.📌 BDD 패턴 적용댓글의 주요 로직을 테스트하는 클래스 CommentTest을 작성하였다.각 테스트 케이스에서 댓글 도메인을 테스트 하기 위한 사용자와 게시글을 생성하는 코드를 @BeforeEach 절에 배치하였다.🏃 돌아보며..짧다면 짧고, 길다면 긴 인프런 스터디 여정을 드디어 완주했다. 👏👏👏(잠시나마, 한숨을 돌릴 수 있게 되었다. 😮‍💨)한 줄 평을 해보자면, "정말 너무 좋기만 했다."생각보단 업무와 병행하며 쉽지 않은 일정이긴 하지만!?내가 듣고 싶었던 강의의 강의료만 내고..강사님과의 네트워킹을 하며.. 스터디에 참여하고..단기간에 성장까지 경험할 수 있다면, "참여하지 않을 이유가 있을까?"🤔다음 기수 때도 기회가 된다면 지원을 해 볼 생각이다!!그리고! 주변에서 참여를 고민한다면!? 바로 적극 지지 해줄 것 같다.인프런 워밍업 클럽 스터디 만만세!! 🙌강사진과 운영진분들, 진심으로 고생 많으셨습니다. 감사합니다! 🙇‍♂[출처]인프런 워밍업 클럽 : 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 25일 전
10

[인프런 워밍업 클럽 3기 - 백엔드 코드] 발자국 1주차

👣 발자국 책갈피인프런 워밍업 클럽 3기 백엔드 코드 발자국 1주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 2주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 3주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 4주차✅미션PR 책갈피[미션 Day 2] 추상과 구체 예시 작성[미션 Day 4] 리팩토링 & SOLID 원칙[미션 Day 7] 리팩토링 연습[미션 Day 11] 단위테스트 작성[미션 Day 16] 레이어드 아키텍처 특징 및 테스트 작성법[미션 Day 18] Mock 어노테이션 종류 및 차이점 & BDD 패턴 매치🤔 인프런 워밍업 클럽 스터디 신청 계기나는 인프런 워밍업 클럽 스터디 신청하기 한 달 전에 스터디 과정에 쓰이는 로드맵 강의를 이미 완강하였다.그럼에도 불구하고 강의를 수강하기 전부터 우빈님의 팬(?)이였기 때문에 이메일을 받자마자 신청을 할 수 밖에 없었다.항해 플러스 8기 백엔드 과정을 앞두고 약 2주간 겹치는 구간이 있어 잠시나마 고민을 하긴 했지만?..복습 차원에서 미션과 발자국을 통해 학습내용을 더 딥하게 파고들어 성장하고자 신청하게 되었다.👣 발자국이란?인프런 워밍업 클럽에서 이야기하는 발자국이란 다음과 같다.주 1회, 일주일 동안 배운 내용을 바탕으로 남기는 학습 일기이자 회고이다.매주 강의를 요약하고 기록하면서 강의 내용을 제대로 이해했는지 확인하고, 보완할 부분을 찾는다.기록을 통해 배운 내용에 대해 메타인지 하는 시간을 가진다.나는, 이번 과정에서는 강의를 재수강하지는 않고섹션 별로 우빈님께서 제공해주신 키워드 정리를 통해 학습을 진행할 예정이다.💡 자기만의 언어로 키워드 정리하기섹션 2. 추상추상과 구체 : 이름 짓기 + 메서드 선언부 = 추상화 행위실제 코드를 작성하다보면 이름 짓기에 시간을 가장 많이 투자한다.이름 짓기의 중요성이 크기 때문에 반복되는 훈련을 통해 이름 짓기의 스킬 향상이 필요해 보인다. 추상화 레벨메서드를 추출하는 과정에서 외부셰계와 내부세계가 분리가 되고, 이 과정에서 추상화 레벨을 잘 고려해야한다.추상화 레벨이 동등해야 자연스럽게 코드가 읽힌다.[다른 추상화 레벨]public class Order { public void process() { paymentService.pay(); // 고수준 추상화 📈 System.out.println("결제 완료"); // 저수준 추상화 📉 } } [동등한 추상화 레벨]public class Order { public void process() { paymentService.pay(); // 고수준 추상화 📈 printCompletedPay(); // 메서드를 분리하여 고수준의 추상화를 동등하게 유지 ✅ } private void printCompletedPay() { System.out.println("결제 완료"); } } 매직 넘버 + 매직 스트링 : 상수 혹은 Enum을 활용하여 이름 짓기를 통해 가독성이 좋아지게 한다.섹션 3. 논리, 사고의 흐름뇌 메모리 적게 쓰기 (인지적 경제성)읽기 좋은 코드는 읽는 사람으로 부터 뇌를 편안하게 한다.반대로 읽기 힘든 코드는 읽는 사람의 뇌를 피곤하게 한다. Early return : Early return은 아래쪽 내용을 읽을 필요 없어 뇌 메모리 적게 쓰기에 효과적이다.사고의 depth 줄이기중첩 분기문가 중첩 반복문을 무조건 depth를 줄이기보다, 추상화를 통한 사고의 depth를 줄이는 것이 중요하다.사용할 변수는 가깝게 선언한다.공백 라인 : 공백 라인도 리팩토링 시 중요한 요소이다. 공백 라인이 존재하지 않은 코드는 읽기 어렵다.부정어부정연산자는 가독성을 해친다. 여러 번의 사고를 거치면서 코드를 읽어야 한다.한 번에 사고할 수 있도록 부정연산자를 제거하고 메서드로 추출했다면 이름 짓기를 적절하게 수정해야한다.해피 케이스, 예외 처리해피 케이스보다 예외처리에 더 신경써서 코드를 작성해야 한다.예외는 의도하지 않은 예외랑 의도한 예외가 있다.섹션 4. 객체 지향 패러다임객체, 협력과 책임, 관심사의 분리, 높은 응집도와 낮은 결합도협력과 책임 : 객체간의 협력, 객체가 담당하는 책임관심사의 분리 : 관심사에 따라 객체를 만들어 낼 수 있다.높은 응집도 : 특정한 관심사로만 이루어진 설계낮은 결합도 : 각 관심사끼리는 독립적이여야 한다.[낮은 응집도]public class OrderService { public Order createOrder(Product product) { // 주문 생성 로직 } public void sendEmail(Order order) { // 이메일 전송 로직은 OrderService의 관심사가 아니다. ❌ } } [높은 응집도로 해결]객체를 분리함으로써 응집도가 올라간다.public class OrderService { public Order createOrder(Product product) { // 주문 생성 로직 } } public class OrderEmailSender { // 객체를 분리하여 높은 응집도로 설계 ✅ public void sendEmail(Order order) { // 이메일 전송 로직 } } [높은 결합도]public class OrderService { private EmailService emailService = new EmailService(); // 높은 결합도 ❌ } [낮은 결합도로 해결]인터페이스와 DI를 활용하여 결합도를 낮춘다.public interface EmailSendable { } public class OrderEmailSender implements EmailSendable { } public class OrderService { private final EmailSendable emailSendable; public OrderService(EmailSendable emailSendable) { this.emailSendable = emailSendable; // 높은 결합도를 해결한다. ✅ } }객체끼리의 협력과 책임을 통해 프로그램을 만들 수 있다.가장 중요한 것은 관심사의 분리이고 높은 응집도와 낮은 결합도를 가진 설계를 가지는 것이 중요하다. getter/setter 자제하기, 객체에 메시지 보내기setter는 최대한 지양하는 것이 좋다.getter를 남발하는 건 객체를 존중하지 않는 것이며 무례하면서 폭력적인 행위이다.SOLID: SRP, OCP, LSP, ISP, DIP아래의 미션 Day 4 과정에 대한 내용을 통해 자세히 다룬다.DI/IoC섹션 5. 객체 지향 적용하기상속과 조합 : 상속은 결합도가 높기 때문에 조합을 활용하는 것이 더 좋은 설계이다.Value Object, EntityVO는 불변성, 동등성, 유효성을 보장해야하며 도메인의 개념을 추상화한 객체이다. (ex. Money 객체)Entity는 식별자가 존재한다. 식별자가 같으면 동등한 객체로 취급한다.일급 컬렉션 : 컬렉션을 Wrapping한 객체를 뜻한다. 컬렉션을 추상화하여 의미를 담을 수 있고, 가공 로직의 보금자리가 생긴다. (VO와 비슷하다.)Enum : 상수의 집합, 상수들에 대한 로직을 담을 수 있다.추상화와 다형성 활용하여 반복되는 if문 제거 -> OCP 지키기변하는 것 : 조건 & 행위 (구체)변하지 않는 것 : 조건을 만족하는가? / 행위를 수행한다. (추상)변하는 것과 변하지 않은 것을 구분해서 보는 훈련이 필요하다.숨겨져 있는 도메인 개념 도출하기 : 변경이 많이 일어날 것 같은 미래를 예견하고 이런 것을 도입해보면 어떨까? 하고 숨겨진 도메인 개념을 도출도메인 지식은 만드는 것이 아니라 발견하는 것이다.객체지향은 흉내내는 것이다.미래를 예견하고 도메인 개념을 도출해보자.[이메일 전송 개념에서 미래를 예견하고 숨겨진 도메인 개념을 도출]현재의 최선에서는 이메일만 발송하지만 미래에 카카오톡 알림톡이나 혹은 다른 알림에 대한 도메인 개념을 도출해보자public interface Sendable { // 미래를 예견하고 도매인 개념을 도출 ✅ void send(String message); } public interface EmailSendable extends Sendable { @Override void send(String message); }👨🏻‍💻 미션 회고 [미션 Day 2]미션 PR : https://inf.run/dmW3B1⃣ 추상과 구체 예시강의 내용에서도 계속 등장하듯이 추상과 구체는 이번 강의에서 가장 핵심이 되는 단어들이다.이 미션을 접했을 때, 단순히 추상과 구체에 대한 예시를 들기보다 개발적인 관점에서 추상과 구체에 대해 접근하려고 노력했다. (적절한 예시인지는 잘 모르겠으나..😂)해당 미션을 통해, 추상화가 단순히 쉽지 않다는 걸 느꼈으며 개발적인 관점에서도 더 읽기좋은 코드를 작성하기 위해선 추상화를 잘 해야겠다라는 생각이 들었다. [미션 Day 4]미션 PR : https://inf.run/1P5bV1⃣validateOrder 메서드를 읽기 좋은 코드로 리팩토링 하기해당 메서드를 봤을 때는 엄청 어지러웠다.. 🤣 본능적으로 빨리 리팩토링 하고 싶다는 생각이..추상화 기법들을 통해 도메인 객체를 분리하고 도메인 내부세계로 검증 로직을 구현하였다.객체 분리 : 일급 컬렉션으로 의미있는 Items의 객체로 분리하였고, Member라는 객체를 생성하여 customerInfo에 대한 검증 로직을 구현하였다.사고의 depth 줄이기, 부정 연산자 지양 : 중복 분기문과 부정 연산자를 포함한 조건식을 리팩토링하여 읽는 사람으로 하여금 뇌 메모리 적게 쓰게 끔 리팩토링했다.미션 PR에도 코멘트를 달았지만 도메인 레이어의 로깅이 하는 구현 부가 포함된 게 신경이 쓰인다.도메인 레이어에서 의존성을 주입받지는 않아 객체 분리가 맞는지 모호하다.[기존 코드]public boolean validateOrder(Order order) { if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } else { if (order.getTotalPrice() > 0) { if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } else if (!(order.getTotalPrice() > 0)) { log.info("올바르지 않은 총 가격입니다."); return false; } } return true; }[리팩토링 이후 코드]public class Order { private static final Logger log = Logger.getLogger(Order.class.getName()); private final Items items; private final Member member; private final int totalPrice; private Order(Items items, Member member, int totalPrice) { this.items = items; this.member = member; this.totalPrice = totalPrice; } public boolean validate() { if (items.isEmpty()) { log.info("주문 항목이 없습니다."); return false; } if (totalPrice <= 0) { log.info("올바르지 않은 총 가격입니다."); return false; } if (member.hasNotInfo()) { log.info("사용자 정보가 없습니다."); return false; } return true; } } 2⃣ SOLID에 대하여 자기만의 언어로 정리사실, 객체 지향에 대해 학습한 사람들 중 SOLID를 모르는 사람은 없을 것 이다.그치만 여기서의 포인트는 자기만의 언어이다.위에서도 강의에 대한 정리내용을 작성할 때 "💡 자기만의 언어로 키워드 정리하기" 라는 제목으로 작성하였다.학습한 내용을 온전히 내 것으로 만들기 위해선 반드시 나의 언어로 작성해야 내 것이 되고, 차후에 정리한 내용을 다시 봤을 때도 금방 기억을 더듬을 수 있을 것 이다.그래서 나는 텍스트 보다 코드로 봤을 때의 이해도가 더 빠르기 때문에 SOLID 원칙의 내용을 정리할 때 불필요한 텍스트를 줄이고 코드로 내용을 정리하였다.🏃 돌아보며..위에서도 언급했지만, 항해 플러스 백엔드 스터디 2개도 병행하고 있어 참여를 망설였지만.. 신청하기 잘한 것 같다.(2기가 마지막이 될까봐 조마조마 했던 1인.. 우빈님 감사합니다.. 🙇‍♂)온라인 밋업을 통한 우빈님과 소통하는 것도 그렇고..여러가지 미션의 내용도 단조롭지 않고 수준이 높은 것 같아 다시 한번 메타인지를 경험하게 해준다.강의의 경우, 시간상 처음부터 끝까지 다시 보지는 못하겠지만.. 불과 몇 개월 전 수강한 강의임에도 단어들이 어색하다... 😂어색했던 부분은 다시 강의를 보며 이해하고 보완하였다.읽기 좋은 코드를 작성하기 위해 학습한 내용을 기반으로 적절한 추상화를 적용하는 훈련을 하며 내 코드가 읽기 좋은 코드가 되도록 노력해야겠다. [출처]인프런 워밍업 클럽 : https://inf.run/Y4cf2강의 : https://inf.run/yTUP4 

인프런워밍업클럽백엔드3기발자국

10 25일 전
10

살짝 서두른 Spring 기반 테스트 코드 - 인프런 워밍업 클럽 3기 백엔드 코드✨

👣 발자국 책갈피인프런 워밍업 클럽 3기 백엔드 코드 발자국 1주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 2주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 3주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 4주차✅미션PR 책갈피[미션 Day 2] 추상과 구체 예시 작성[미션 Day 4] 리팩토링 & SOLID 원칙[미션 Day 7] 리팩토링 연습[미션 Day 11] 단위테스트 작성[미션 Day 16] 레이어드 아키텍처 특징 및 테스트 작성법[미션 Day 18] Mock 어노테이션 종류 및 차이점 & BDD 패턴 매치🎼 알레그레토 : 조금 빠르게 3주 차부터는 미션과 발자국을 조금 서둘러 진행할 예정이다.앞서 언급했듯이, 이번 주부터 항해 플러스가 시작되어 최대한 진도를 빠르게 빼보려고 한다. 현재 날짜(3월 17일) 기준으로 Day 16 미션과 3주 차 발자국을 작성 중이며,이번 주 안에 Day 18 미션과 4주 차 발자국도 작성을 완료하는 것이 목표이다.(가능할지는 모르겠지만 ㅎㅎ) 조금 급하게 진행하는 감이 있어 개인적으로 많이 아쉽다.워밍업 클럽에만 온전히 집중할 수 있었다면 더 많은 성장을 할 수 있었을 텐데 말이다... 하지만, 후회는 하지 않는다. 다음 기수의 존재는 우빈님만 아시겠지만, 다음에 또 없을 수도 있는 기회를 놓치고 싶지 않았기 때문이다. 👍나는 위의 이미지처럼 Trello를 활용해 인프런 워밍업 클럽에 참여하고 있다.미션 제출 날짜가 일정하지 않다 보니, 제출 하루 전에 노티를 받도록 설정해 두고 유용하게 사용 중이다. 🙃💡 자기만의 언어로 키워드 정리하기 섹션 6. Spring & JPA 기반 테스트Layered Architecture레이어드 아키텍처의 단점 : 기술에 대한 강결합이 심하다는 단점이 존재Hexagonal Architecture도메인 모델은 외부의 것들을 아예 모른다.도메인 모델 중심 (멀티 모듈 및 시스템이 커진다면..)단위테스트 vs. 통합테스트단위테스트 만으로는 커버하기 어려운 영역이 존재 (여러 모듈 및 여러 객체가 협력하기 때문에)통합테스트란?여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없다.IoC, DI, AOPORM, 패러다임의 불일치, HibernateSpring Data JPAQueryDSL@SpringBootTest vs @DataJpaTest@DataJpaTest는 @SpringBootTest보다 가볍다.@DataJpaTest보다는 @SpringBootTest를 더 선호@DataJpaTest는 @Transactional이 있어 롤백이 된다.@SpringBootTest는 클렌징을 해주어야 한다.@SpringBootTest vs @WebMvcTest@SpringBootTest는 E2E 테스트, 즉 통합테스트 시 사용하는 어노테이션이다.@WebMvcTest는 Presentation Layer에 대한 단독 테스트시 사용하는 어노테이션이다.다른 레이어들은 mocking을 통해 동작을 제어한다.@Transactional(readOnly = true)테스트에서 사용 시, 롤백 되는 것에 유의 해야 한다.트랜잭션 경계 설정을 해야한다.엔드포인트를 잘 설계해야 한다.Optimistic Lock, Pessimistic Lock낙관적 락 : 데이터 충돌이 자주 발생하지 않을 것이라 낙관적으로 가정하고, 트랜잭션을 진행하는 방식데이터를 읽을 때는 락을 걸지 않고, 데이터를 업데이트 시 버전 비교하여 충돌 여부 판단성능 저하를 최소화, 동시성을 높이는 데 유리비관적 락 : 데이터 충돌이 자주 발생할 것이라 비관적으로 가정하고, 트랜잭션이 데이터를 사용할 때 미리 잠금을 거는 방식데이터 일관성을 유지하는 데 초점트랜잭션이 완료될 때까지 다른 트랜잭션이 데이터를 수정할 수 없음데드락 발생 가능CQRS명령 조회 책임 분리 : Command Query Responsibility Segregation읽기(조회)와 쓰기(명령)의 책임을 분리하는 소프트웨어 아키텍처 패턴장점성능 최적화확장성 증가데이터 모델 최적화비지니스 로직의 명확한 분리단점복잡성 증가데이터 동기화 문제트랜잭션 관리 어려움@RestControllerAdvice, @ExceptionHandler@RestControllerAdvice : ControllerAdvice의 기능을 하는데 JSON으로 응답을 해주는 Advice커스텀 예외를 던지고 @RestControllerAdvice의 @ExceptionHandler에서 예외를 처리할 수 있다.Spring bean validation@NotNull, @NotEmpty, @NotBlank도메인 요구사항에서 나오는 validation과 책임 분리해야한다.Controller 단에서는 최소한의 validation을 통한 검증이 이루어져야 한다.@WebMvcTestObjectMapperJackson 라이브러리에서 제공하는 클래스로, Java 객체와 JSON 간의 변환을 담당하는 역할직렬화, 역직렬화를 수행Mock, Mockito, @MockBeanMock : 실제 객체 없이 동작을 모방하여 단위 테스트를 수행하는 가짜 객체Mockito : Java에서 Mock 객체를 쉽게 생성하고 관리할 수 있는 라이브러리@MockBean : Spring 컨텍스트에 Mock 객체를 등록하여 실제 빈을 대체@Mock : 순수한 자바에서 Spring 컨텍스트가 필요하지 않을 때 사용@MockBean : Spring 컨텍스트에서 특정 빈을 Mocking 하고 싶을 때 사용👨🏻‍💻 미션 회고[미션 Day 11][미션 PR]#6스터디 카페 이용권 선택 시스템 단위 테스트 작성 미션 조건은 다음과 같다. ✔각 프로젝트 모두 강의 중에 작성한 tobe 패키지 코드를 기준으로 함 (lesson 6-4 가 가장 마지막 버전)✔3개 이상의 서로 다른 클래스 & 총 7개 이상의 테스트 작성 ➡ 단, 같은 인터페이스를 구현하고 있는 구현체들은 1개 클래스로 간주한다.✔무엇을 테스트하고자 했는지를 잘 나타낸 @DisplayName 작성하기✔BDD(given/when/then) 스타일 따르기 (주석으로 표기) 가장 작은 단위의 메서드 부터 단위 테스트를 작성하면서,테스트 커버리지를 높이기 위해, 모든 주요 로직에 대한 단위테스트를 작성하고자 했다. 단위테스트를 작성하면서 강의에서도 강조한 내용 중에 하나인 @DisplayName을 잘 작성하기 위해 많이 고민했다. '권'이라는 Pass의 의미를 '패스'로 통일하여 일관성있게 작성하였다.읽는 사람으로 하여금 뇌 메모리를 적게 쓰게 하기 위해 @DisplayName도 최대한 추상화해서 작성하려고 노력했다.테스트의 현상을 중점적으로 작성하지 않으려고 하였다. 하지만, 사용자 입력을 받는 클래스인 InputHandler의 단위테스트를 작성하는 과정에서 어려움이 있었다.InputHandler 내부에서 static으로 선언되어 있어 mocking도 하기 어려웠다.public class InputHandler { private static final Scanner SCANNER = new Scanner(System.in); }프로덕션 코드를 수정하지 않는다는 요구사항을 지키면서는 테스트가 어려웠다. 만약 프로덕션 코드를 수정한다면 테스트가 가능해질 것이다. 👆 Scanner를 외부세계로 분리하면 테스트가 가능할 것 같다.✌ InputHandler 상위의 인터페이스를 생성 후, 테스트 전용 support 성격의 구현체를 만들어서 테스트가 가능할 것 같다.  미션 제출 후, 차후에 프로덕션 코드를 수정해서 테스트를 작성 해볼 예정이다.🏃 돌아보며..Day 11 미션은 제출이 끝이 아니라, 중간점검 라이브에 이어 마지막 라이브에서 코드 리뷰 단계가 남아있다.중간 점검 때 세심한 코드 리뷰를 받고 수정하면서 많은 성장을 했기에 이번에도 신청하지 않을 이유가 없었다. 다른 러너 분들도 꼭 받아봤으면 한다. 😊아직까지 혼자 신청해서 뻘줌해서가 아니라... 진짜 좋은 기회이자 경험이다... 🤣코드 리뷰 대상자로 뽑히길 기대하며.. 마지막 4주차도 화이팅 💪[출처]인프런 워밍업 클럽 : https://inf.run/Y4cf2강의 : 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 25일 전
바다다다

[인프런 워밍업 스터디 클럽 3기 FE] 3주차 발자국

수강강의1: 따라하며 배우는 자바스크립트 A-Z수강강의2: 따라하며 배우는 리액트 A-Z[19버전 반영]Tip: 워밍업 스터디 클럽을 참여하게 되면 [할인쿠폰]으로 강의를 할인 된 가격에 수강할 수 있다! 3주차 강의 수강3주차를 되돌아 보며..이번주 강의는 전반적으로 React와 친해지는(?) 시간을 가지게 된거 같다.React로 실제 화면을 구현해보고 각종 함수와 툴들을 사용해서 개발의 편의성을 높이는 것을 체감할 수 있었다.특히 주어진 미션과제를 수행하면서 React의 동작성, 상태관리에 대해서 더 깊게 알 수 있을 시간이 되었다.사실 이번주는 강의를 많이 듣지 못했다ㅠ 현업과 병행하다 보니 퇴근하고 강의 듣는다는 게 쉬운 일은 아니다. 특히 이번주는 시험검증 기간이라 이슈가 조금 많이 발생해서 야근도 잦고 그랬다... (모든 직장인, 학생 분들 화이팅입니다ㅠ 열심히 살아가봐요!) 그리고... 미션과제도 밀려서 주말에 몰아서 구현을 했다.미션과제를 수행하면서 중간중간 필요한 지식들은 강의를 들으면서 기록하며 학습을 진행했다.Javascript 강의 때와는 다르게 React 는 실습위주 이다보니 강의 내용을 정리한게 많지는 않다. (강의보면서 코딩따라 해보며 기능을 확인하고 미션에 적용하려고 했다)강의 내용을 다시 정리해보며..(주로 개인적으로 새롭게 알게된 것, 다시 기억해야하는 것들 위주로 정리했다)리액트는 프레임워크가 아니라 라이브러리React는 라이브러리, Vue, Angular는 프레임워크왜 라이브러리? 리액트는 전적으로 UI를 렌더링하는 것에 관여하기 때문상태관리, 라우팅, 테스트 등등을 위해 다른 라이브러리가 추가적으로 필요함 (vue, angular는 이런 것들이 이미 포함되어 있음)프레임워크: 어떠한 앱을 만들기위해 필요한 대부분의 것을 가지고 있는 것라이브러리: 어떠한 특정 기능을 모듈화 해놓은 것프레임워크는 라이브러리의 집합리액트 컴포넌트 - Component리액트 앱을 이루는 최소한의 단위, 여러 컴포넌트를 조합하여 하나의 페이지가 완성되는 것클래스형 컴포너트 - class component함수형 컴포넌트 - funcional component브라우저가 그려지는 원리와 가상돔리액트의 주요 특징 중 하나가 가상돔을 사용하는 것이다웹 페이지 빌드 과정 (CRP) - Critical Render Pathbrowser가 서버에서 페이지에 대한 HTML문서를 응답으로 받고, 해당 문서를 읽는다. 그 후 스타일을 입히고 뷰포트에 표시하게 된다DOM tree생성 → Render Tree생성(화면에 표시되는 모든 노드의 콘텐츠 및 스타일 정보를 포함) → Layout(reflow) → Paint(화면에 그리기)화면에서 DOM에 변화가 생기면 Render Tree부터 다시 랜더링 해야한다는 문제점!! → 비효율적→ 가상돔: 실제 DOM을 메모리에 복사해준 것데이터가 바뀌면 가상돔에 랜더링되고 이전에 생긴 가상돔과 비교해서 바뀐 부분만 실제 돔에 적용 시킴. 바뀐 부분을 찾는 과정을 diffing이라고 부르고, 바뀐 부분만 실제 돔에 적용시켜주는 것을 재조정(reconciliation)이라 한다.리액트 구조 살펴보기이름이 수정되면 안되는 파일public/index.html → page템플릿src/index.js → 자바스크립트의 시작점SPA는 어떻게 가능하게 되나?HTML5에서 History API를 사용해서 가능하게 함. (실제 react-dom에서 사용하는 방식)JSXJavascript syntax extension자바스크립트의 확장 문법이다React에서 의무적으로 사용하는 것 XJSX는 babel을 통해 변환된다컴포넌트에 여러 엘리먼트 요소가 있다면 반드시 부모요소 하나로 감싸줘야한다.JSX Keykey는 리액트가 변경, 추가 또는 제거된 항목을 식별하는데 도움이 된다. 요소에 안정적인 ID를 부여하려면 배열 내부의 요소에 키를 제공해야 한다 (key값을 기준으로 바뀐 가상돔을 감지한다)key에는 unique한 값을 넣어주자. index는 비추!React State컴포넌트의 렌더링 결과물에 영향을 주는 데이터를 갖고있는 객체이다. state가 변경되면 컴포넌트는 리랜더링(re-rendering)된다. 또한 state는 컴포넌트 안에서 관리된다.React HooksReactConf 2018에서 발표된 class없이 state를 사용할 수 있는 새로운 기능리액트의 생명주기react hooks를 통해 함수형 컴포넌트에서도 생명주기를 사용할 수 있게 데이터를 가져와서 컴포넌트 시작하자 마자 API호출하고 많은 부분을 사용할 수 있게되었다→ 코드가 간결해짐 (useEffect를 통해 componentDidMount, componentDidUpdate, componentWillUnmount를 다 수행해줌)HOC(higher order component)를 Custom react hooks로 대체해서 너무 많은 wrapper컴포넌트를 줄이게 된다.→ HOC: 화면에서 재사용 가능한 로직만을 분리해서 component를 만들고, 재사용 불가능한 UI와 같은 다른 부분들은 parameter로 받아서 처리하는 방식 (wrapper가 많아지면 데이터의 흐름을 파악하기 어려워짐)State, Propsstate해당 컴포넌트 내부에서 데이터를 전달할 때state는 mutable 변경가능하다state 이 변하면 re-render된다props상속하는 부모컴포넌트에서 자식 컴포넌트로 전달한다읽기전용으로 작년 컴포넌트 입장에서는 변하지 않는다. (변하게 하고자 하면 부모 컴포넌트에서 state를 변겨해줘야한다)구조 분해 할당(Destructuring)배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JS표현식클린코드를 위해 객체, 배열에 대해 구조 분해 할당Redux상태 관리 라이브러리redux데이터는 하나의 방향으로만 흐른다flow: react component에서 어떠한 이벤트가 발생하면 → Action 객체(어떤 작업을 수행할 것인지 description을 담고 있음)를 통해 reducer함수에게 액션을 발생하라고 한다.(dispatch) → reducer함수에서 로직 처리 → store내부 상태 업데이트: redux store state update → 새롭게 업데이트 된 상태를 이용해서 다시 렌더링 → component re-renderingRedux Tookitredux로직을 작성하기 위한 공식 권장 접근 방식 강의 내용을 들으면서 아쉬웠던 점..앞서 언급한 대로 이번주에는 강의보다는 실습 과제에 조금 더 초점을 두었다. 미션을 수행하면서 모르는 부분을 강의에 가서 찾아 듣고 다시 구현하고 하는 식으로 학습을 진행했다.React는 이론 보다는 실제 사용하고 구현해보는게 더 빠르게 성장하는거 같다. 특히 상태관리, 페이지 라우터 등 강의에서도 알려주시지만, 미션과제를 수행하면서 직접 기능을 동작시킬 수 있게 구현하는게 더 깊이 이해할 수 있었던 거 같다.아쉬운점은ㅠ 강의를 많이 못 들었다는거? 수료식까지 아직 시간이 조금 남았으니까 남은 강의도 열심히 들어보겠다.3주차 미션 완료React미션 두 개(포켓몬, 쇼핑몰)를 토요일 벼락치기로 수행했다.간단하다고 생각했는데 React가 아직 익숙하지 않아 중간중간 오류가 많이 발생했었고, 특히 프론트엔드는 화면이 바로 눈에 보이다 보니까 CSS 스타일 적으로 내맘대로 적용되지 않아서 화가났다(?). 그래도 tailwindcss에서 만들어진 component, icon들을 활용해서 미션을 수행완료할 수 있었다.React - Mission3: 포켓몬 도감 앱 만들기Demo: https://pokemon-mission-3.netlify.app/Source Code: https://github.com/rim0703/React-study/tree/react/mission3/React/3-pokemonpokeAPI 라는 오픈소스 API를 제공하는 곳이 있어서 외부API를 호출해서 구현했다API문서가 얼마나 중요한 역할을 하는지 이번 미션에서 깊이 깨달았다.. ㄷㄷ(백엔드 화이팅)(지연로딩 적용) 사용자의 화면 로딩 속도를 고려하면 첫 화면에 20개씩 불러오도록 했고, 스크롤이 하단에 도달하면 추가로 호출하도록 구현했다.(Trick) 시연화면에서 로딩이 출력되는데 사실 화면이 너무 빨리 렌더링 완료돼서 로딩이 보이지 않아 로딩 동글뱅이를 보기 위해 setTimeout으로 1초 지연을 넣었다 ^^;;(검색창) 조금 버그가 있다. 페이지를 영문이 아닌 한글로 만들다보니 검색도 한글로 하도록 했는데 상태를 상위-하위 컴포넌트 간에서 전달하다 보니 조금 어려웠다. 또한 API문서 기본은 영문으로 제공해서 영문과 한글을 매칭시키는 별도의 작업도 필요했다. redux를 사용해서 검색창을 상단 navigation바에 위치해보려고 한다.  React - Mission6: 리덕스를 이용한 쇼핑몰(버거몰) 앱 만들기Demo: https://burger-mall-mission-6.netlify.app/Source Code: https://github.com/rim0703/React-study/tree/react/mission6/React/6-shopping-mall1주차 미션 내용에서 언급했듯이.. JS강의에서 만들었던 메뉴 화면을 그대로 따와서(CSS공부가 아니니까^^) 쇼핑몰과 비슷한 기능들이 있는 버거몰을 만들어보았다.(컴포넌트화) HTML,JS,CSS로 구현된 내용을 React로 옮겼고, 원래 파일 1개씩 정의된 내용을 React Component로 분류하였다.(장바구니-localstorage) DB를 별도로 연동된게 아니여서 localstorage를 활용해서 장바구니를 구현하였다.(Redux) 리덕스 강의 내용에 기반한 프로젝트이지만 Redux강의를 듣고 이해가 잘되지 않아서 우선 기본 방식으로 앱을 구현하였다. Redux공부를 조금 더 하고 리팩토링을 진행할 예정이다. 마지막으로 3주차 회고이제 수료까지 얼마남지 않았는뎅,, 남은 시간동안 강의를 열심히 들어야겠다.너무 빡빡한 일정이었지만 3주차까지 버텨오고 React에 대해 친해질 수 있는 시간이되어서 뿌듯하다.현업에서 Vue를 사용하다 기술 전환을 위해 React를 도입한다고 해서 급하게 스터디에 신청해서 힘들지만 조금만 더 버티면되지 않을까? 직장과 병행하니.. 이게 쉬운일은 아닌거 같다. 모든 직장인들 화이팅입니다!흑흑.. 미션 기능 중에 동작하지 않는게 있어서 수정하고 다시 배포하고 이제 자러간당ㅠㅠ다음주는 더 성장한 내가 되어 있기를!! 모두 완주까지 화이팅입니다~(2025.03.23 새벽 03:26)<끝>

프론트엔드워밍업스터디3기프론트엔드미션JSJavascriptReactRedux회고

치현

[인프런 워밍업 스터디 클럽 3기 풀스택] 2주차 발자국

학습 내용인프런 워밍업 클럽 스터디 2주차로, 이번 주는 드롭 박스 프로젝트와 함께 Supabase의 Storage를 다뤄볼 수 있는 시간이었다. Supbase Storage1. 기본 구성 요소Files: 모든 종류의 미디어 파일 저장 가능 (이미지, GIF, 비디오 등)Folders: 파일을 체계적으로 구성하기 위한 디렉토리 구조Buckets: 파일과 폴더를 담는 최상위 컨테이너 (접근 규칙별로 구분)2. 접근 제어 모델Private Buckets (기본값) RLS(Row Level Security) 정책을 통한 접근 제어JWT 인증 필요Signed URL을 통한 임시 접근 가능Public Buckets파일 조회 시 접근 제어 없음URL만 있으면 누구나 접근 가능업로드/삭제 등 다른 작업은 여전히 접근 제어 적용3. 보안 기능RLS 정책 설정 가능SELECT (다운로드)INSERT (업로드)UPDATE (수정)DELETE (삭제)소유권 관리owner_id 필드로 리소스 소유자 추적JWT의 sub claim 기반 소유권 할당4. 이미지 변환 기능 (Pro Plan 이상)실시간 이미지 최적화크기 조정품질 조정 (20-100)WebP 자동 최적화변환 옵션resize 모드: cover, contain, fillwidth/height 지정 (1-2500px)최대 파일 크기: 25MB최대 해상도: 50MP5. 인증 방식S3 액세스 키서버 사이드 전용모든 버킷에 대한 완전한 접근 권한세션 토큰클라이언트 사이드 사용 가능RLS 정책 기반 제한된 접근6. 통합 기능Next.js 이미지 로더 지원AWS S3 호환성PostgreSQL DB와 연동7. 제한사항파일명은 AWS S3 명명 규칙 준수 필요HTML 파일은 보안상 plain text로 반환이미지 변환 기능은 Pro Plan 이상에서만 사용 가능미션 2 구현 내용과제 구현 저장소Dropbox 중파일의 마지막 수정(업로드) 시간을 표시하는 기능 추가 하기 export interface FileObject { name: string bucket_id: string owner: string id: string updated_at: string created_at: string last_accessed_at: string metadata: Record<string, any> buckets: Bucket }=> DropboxImage 컴포넌트가 prop으로 받는 image의 타입은 FileObject로 그 중 업로드시간은 created_at을 의미하기에 이를 이미지에 추가하였다.(사진 참고) 포인트 1: 한글 파일명 es-hangul 사용// 안전한 파일명 생성을 위한 유틸리티 export class FileNameConverter { // 안전한 문자 패턴 정의 private static readonly SAFE_CHARACTERS = /^[a-zA-Z0-9!\-_.*'()]+$/; private static generateRandomString(length: number = 8): string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; return Array.from({ length }, () => chars.charAt(Math.floor(Math.random() * chars.length)) ).join(""); } // 파일명이 안전한 문자들로만 구성되었는지 확인 private static isSafeFileName(name: string): boolean { return this.SAFE_CHARACTERS.test(name); } // 안전하지 않은 문자를 포함한 파일명을 안전한 형식으로 변환 private static convertToSafeFileName(name: string): string { try { // 파일명 정규화 const normalized = name.trim().normalize(); // 한글이나 특수문자가 있는지 확인 const hasKorean = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(normalized); const hasSpecialChars = /[^A-Za-z0-9]/.test(normalized); if (!hasKorean && !hasSpecialChars) { return normalized; } // 한글이 있는 경우 로마자로 변환 시도 if (hasKorean) { const romanized = romanize(normalized); if (romanized && romanized !== normalized) { // 로마자 변환 결과에서 안전하지 않은 문자 제거 return romanized.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); } } // 변환 실패 시 랜덤 문자열 생성 return this.generateRandomString(); } catch (error) { console.error("Conversion error:", error); return this.generateRandomString(); } } // 원본 파일명을 안전한 형식으로 변환 static encode(fileName: string): string { console.log("Original filename:", fileName); const extension = fileName.split(".").pop() || ""; const nameWithoutExt = fileName.slice(0, fileName.lastIndexOf(".")); const safeName = this.isSafeFileName(nameWithoutExt) ? nameWithoutExt : this.convertToSafeFileName(nameWithoutExt); console.log("Safe filename:", safeName); return `${safeName}_${Date.now()}.${extension}`; } // 파일명에서 타임스탬프 제거하여 원본 이름 추출 static decode(fileName: string): string { const [name] = fileName.split("_"); return name || fileName; } }포인트 2 : 업로드 날짜 표시export function formatDate(timestamp: string): string { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); // 1일 이내 if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); if (hours < 1) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes}분 전`; } return `${hours}시간 전`; } // 30일 이내 if (diff < 30 * 24 * 60 * 60 * 1000) { const days = Math.floor(diff / (24 * 60 * 60 * 1000)); return `${days}일 전`; } // 그 외 return date.toLocaleDateString("ko-KR", { year: "numeric", month: "long", day: "numeric", }); } 회고파일명 변환하는데 생각보다 시간이 많이 소요됐다.여찌저찌 구현은 헀지만, 이미지가 어떻게 encoding되고 decoding되는지 일련의 과정에 대한 공부가 필요함을 느끼는 이번주 였다.  

풀스택풀스택인프런워밍업스터디클럽Next3기SupabaseReact프론트엔드2주차발자국

천준민

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

📖 강의 요약 2⃣ 추상우리가 클린 코드를 추구하는 이유"가독성"혼자만 사용한다면 아마 필요 없는 이야기 일수도 있을 것이다 하지만 개발자는 협업이 중요하다고 들었다.나만 코드를 읽는 것이 아니라 협업을 하는 사람들끼리 컨벤션도 중요하지만 그 전에 가독성 있는 코드를 짜면 원활한 작업을 할수 있을 것이라고 생각한다. 추후 발생하는 비용들도 줄어들 것이다.이번 섹션에서 강조하는 것은 제목처럼 추상인 것 같다.추상을 알아보자"추상"중요한 정보는 가려내어 남기고 덜 중요한 정보는 생략하여 버린다.미션에서 제출한 내가 이해한 추상과 구체를 위와 같이 예시를 들수 있을 것 같다.[추상] 인프런 워밍업 클럽 3기 BE 클린코드 & 테스트를 수료 한다. [구체] 2 개 강의(Readable Code: 읽기 좋은 코드를 작성하는 사고법&Practical Testing: 실용적인 테스트 가이드 )를 100 % 수강한다. 주 1회(총 4회) 발자국 작성을 한다. OT를 포함한 총 3회 온라인 라이브를 출석한다 미션 6개 중 5개 이상 기한 내 제출 완료한다.추상화의 가장 대표적인 행위는 이름 짓기,메서드 선언, 동등한 추상화 레벨, 매직 넘버,매직 스트링 제거 등 이 있다.위 주제에서 공통적으로 이름을 잘 짓는 것이 기반이 되는 것 같다. ex) 변수이름, 메서드 이름, 상수 이름 3⃣ 논리, 사고의 흐름이번 섹션에서 한마디로 정리하자면 빼고 생각하자 였다. (로직이 복잡하면 그 내가 먼저 생각 할 수 있는 상황부터 먼저 거르고 생각하기)추가적으로 부정어, 예외처리, null,optional에 대해 한번 더 생각해보고 작성해보기 4⃣ 객체 지향 패러다임이번 섹션은 좀 반성을 좀 하였다. 특히 강의 중 getter 사용 에 대한 이야기 였는데 무조건 getter를 사용하면 객체안에 정보를 가져올수 있잖아라고 생각하는 나에게 생각을 많이하게 되었다.핵심 : 관심사를 분리, 높은 응집도, 낮은 결합도, getter를 사용하기 전에 객체의 메시지를 보내서 그 값을 사용할수 있는 지 부터 생각하는 능력 기르기 SOLID 원칙✅SRP : 하나의 클래스는 단 한 가지 변경 이유(= 책임) 만을 가져야 한다.✅OCP: 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.✅LSP: 자식 클래스는 부모클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.✅ISP : 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다.✅DIP : 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 5⃣ 객체 지향 적용하기이번 섹션은 내가 잘 이해를 잘못한 것 같아 다시 보아야 할 것 같다. 지금은 일기 정도로 작성 하지만 이해하여 꼭 적어보자강의를 듣고 옛날에 내가 작성한 코드(전체 코드 x)가 생각 났다.public enum ReservationStatus { PENDING("PENDING") { @Override public boolean canChangeTo(ReservationStatus status) { switch (status) { case APPROVED, CANCELED, EXPIRED -> { return true; } } return false; } }, APPROVED("APPROVED") { @Override public boolean canChangeTo(ReservationStatus status) { return status.equals(APPROVED); } }, CANCELED("CANCELED") { @Override public boolean canChangeTo(ReservationStatus status) { return status.equals(CANCELED); } }, EXPIRED("EXPIRED") { @Override public boolean canChangeTo(ReservationStatus status) { return status.equals(EXPIRED); } };지금 보니 메서드 명도 바꿀 필요가 있고 switch 사용으로 유지보수 어려움, if-else 사용 등 고쳐야할 부분이 많은 것 같다.목표 : 강의를 어느 정도 이해하여 코드를 바꿔보기 💡 미션 코드와 설명을 보고, [섹션 3. 논리 의 사고 흐름]에서 이야기 하는 내용을 중심으로 읽기좋은 코드로 리팩토링 해보기 블로그: https://zunmin4030.tistory.com/56 먼저 사이즈가 0이면 어떤 행위를 하고 0보다 크면 어떤 행위 0보다 작으면 어떤 행위를 한다라는 접근으로 코드를 이해 하는 것 부터 시작하였다. 거기에 대한 궁금증을 가지고 섹션별 내가 적용할 수 있는 부분을 생각해보았다. 이름짓기 , 부정 연산자 제거, Early return을 생각하여 과제를 해결하다보니 처음 코드를 말로 정리했을 때 생긴 궁금증을 해소 할수 있었다. 💬 회고 👍 잘한 점배운 것을 하나씩 적용해 보며 과제를 수행 해 나갔다.😮‍💨 아쉬웠거나 보완하고 싶은 점섹션 5에 대한 이해를 하지 못해 한번 더 공부 해야겠다. 📎출처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

백엔드인프런워밍업클럽backend3기

codestudy

[인프런 워밍업 스터디 클럽 3기 PM/PO] 4주차 발자국

[인프런 워밍업 스터디 클럽 3기 PM/PO] 4주차 발자국  인프런 워밍업 스터디 클럽 3기 PM/PO 4주차 발자국인프런 워밍업 스터디 클럽 3기 PM/PO 4주차 발자국📚 주간 학습 내용 요약1. 제품 발견(Product Discovery)의 개념과 중요성제품 발견이란 "무엇을 만들지 결정하는 과정" (deciding what to build)많은 제품들이 stakeholder-driven, sales-driven 방식 때문에 실패함제품팀(PM, 디자이너, 엔지니어)이 단순히 요구사항 수행보다 권한과 자율성을 가져야 함구시대적 프로세스(아이디어→로드맵→요구사항→디자인→개발)를 탈피해 이터레이션이 필요2. 성공적인 제품의 4가지 필수 요소Valuable(가치): 고객이 구매하고 유저가 사용하고자 하는 제품Usable(사용성): 사람들이 사용법을 익히고 사용할 수 있는 제품Feasible(실현 가능성): 우리 기술력으로 구현할 수 있는 제품Viable(지속 가능성): 사업적으로 타당하고 규제 및 이해관계자 요구를 충족하는 제품3. 제품 발견의 두 단계 프로세스Problem/Opportunity Discovery: 어떤 문제/기회에 집중할지 찾는 과정Solution Discovery: 문제를 어떻게 해결할지, 기회를 어떻게 활용할지 찾는 과정PM은 문제 정의에, 디자이너와 엔지니어는 문제 해결에 오너십을 갖되 긴밀한 협업 필요4. 가설(Assumptions)과 검증모든 제품 개발은 여러 가정에 기반함가치(Value), 사용성(Usability), 실현 가능성(Feasibility), 사업적 타당성(Viability) 관련 가정을 검증리스크가 크고 근거가 부족한 가정부터 우선적으로 검증해야 함검증 방법: 심층 인터뷰, 사용성 테스트, 데이터 분석, 프로토타이핑, A/B 테스트 등5. 제품 발견을 위한 전략적 프레임워크기회 솔루션 트리(Opportunity Solution Tree):고객 니즈/페인 포인트/욕구를 발굴하고 그룹핑기회를 상위 기회 밑에 구조화하여 집중할 영역 선정솔루션 아이디에이션, 가설 검증 및 실행으로 진행북극성 프레임워크(North Star Framework):지표에 초점을 맞춘 제품 발견 방법론북극성 지표: 제품팀이 영향을 끼칠 수 있고 장기적 사업 성과에 도움되는 지표인풋-아웃풋 관계 파악을 통한 가설 검증 중요6. 프로덕트 그로스(Product Growth)의 이해PMF(Product Market Fit) 달성 후에야 진정한 그로스 의미 가짐그로스 워크의 4가지 영역: Feature Work, Growth Work, PMF Expansion, Scaling Work그로스 레버(Growth Lever): Acquisition(획득), Retention(유지), Monetization(수익화)7. Acquisition(고객 획득) 전략마케팅/세일즈 외에도 제품을 통한 고객 획득 방법 중요신규 고객을 충분히 많이, 비용 효율적으로 획득하는 것이 목표비용 효율성 측정 지표: CAC, LTV, Payback Period네트워크 효과, UGC(User Generated Content), 검색엔진 최적화 활용B2B 제품에서도 Product-Led Growth(PLG) 방식 확산 중제품을 통한 획득의 장점: 자연스러운 성장, 광고효과, 마케팅 비용 절감8. Retention(유지)과 Activation(활성화)"Retention is King" - 리텐션이 그로스의 핵심 요소리텐션 측정 방식: 코호트 리텐션, Day N 리텐션, Bracket/Unbounded 리텐션리텐션이 좋은 제품은 장기적으로 큰 성장 차이를 만듦Activation은 리텐션을 위한 첫 번째 레버로, 사용자가 핵심 가치를 경험하는 과정Setup → Aha → Habit Moment의 3단계 구조:Setup Moment: 사용자가 제품 가치 경험을 위한 준비 완료Aha Moment: 사용자가 처음으로 제품의 핵심 가치 경험Habit Moment: 제품의 핵심 가치 경험이 습관화유저 온보딩은 단순한 기능 소개가 아닌, 핵심 가치 경험을 돕는 전체 과정사용자 행동 모델 (Nir Eyal의 'Hooked'): Trigger → Action → Reward → Investment9. Engagement(참여도) 측정과 개선리텐션을 위한 두 번째 레버로, 사용자의 제품 사용 활발도인게이지먼트 지표:Breadth(넓이): DAU, WAU, MAU (사용자 수)Depth(깊이): 기능 사용 깊이, 사용 시간Frequency(빈도): DAU/MAU, 재방문율Efficiency(효율성): 과업 성공률BJ Fogg의 행동 모델: Behavior = Motivation × Ability × PromptMotivation(동기): 즐거움, 기대, 소속감Ability(능력): 시간, 비용, 노력 등 행동 용이성Prompt(트리거): 행동 유도 알림인게이지먼트 핵심 시스템: 알림(Trigger) → 액션(Action) → 보상(Reward) → 투자(Investment)10. Monetization(수익화) 전략어디서 수익을 얻을 것인가?: 사용자, 광고주, 제3자무엇에 대한 대가로 돈을 받을 것인가?: Value Unit 정의가격 책정 고려 요소: 비용(Cost), 경쟁(Competition), 가치(Value)Usage-Based Pricing vs Seat-Based PricingIncentive Alignment: 고객과 회사의 이익 방향 일치시키기수익 극대화 전략:무료 사용자 그룹 늘리기 (Freemium, Free Trial)사용자가 지불하고 싶게 만들기 (Perceived Value 강화)인지편향 활용하기 (앵커링, 손실 회피 등)적절한 시점에 Prompt 제공하기11. 그로스 모델(Growth Model)제품 성장 메커니즘을 도식화한 개념적 지도제품의 성장에 영향을 끼치는 요소들을 시각화그로스 모델 예시: AARRR, Growth Loop, North Star Framework정성적 모델링부터 시작하여 각 요소의 영향력 이해12. PM/PO의 팀 협업 방식Hand-Off 방식보다 Collaboration 중심으로 접근엔지니어와 협업: 스테레오타입 버리기, 기술 이해하기디자인 피드백 주기: 프로젝트 이해 → 사용자 관점 경험 → 이슈 파악 → 우선순위화시도, 회고, 개선의 사이클 구축"PM이 항상 정답을 가질 필요는 없다" - 함께 좋은 방안 찾기🌟 핵심 학습 인사이트배운 점제품 성공의 핵심은 무엇을 만들지 결정하는 Product Discovery 과정에 있음성공적인 제품은 가치, 사용성, 실현 가능성, 지속 가능성 모두를 충족해야 함리텐션은 "왕(King)"으로서 장기적 성장의 핵심이며, Activation과 Engagement를 통해 개선그로스 모델링을 통해 제품 성장 메커니즘을 이해하고 전략적으로 접근 가능PM, 디자이너, 엔지니어는 각자의 영역만 담당하는 것이 아닌 협업이 핵심적용할 점문제/기회 발견과 솔루션 발견의 두 단계로 제품 발견 과정 체계화Setup → Aha → Habit의 명확한 경로 설계로 사용자 활성화 강화리텐션 개선을 위한 인게이지먼트 지표(Breadth, Depth, Frequency, Efficiency) 모니터링제품을 통한 Acquisition, Retention, Monetization 전략 구체화디자이너, 엔지니어와의 효과적인 협업 방식 도입BJ Fogg 행동 모델을 활용한 사용자 행동 유도 설계💭미션에 대한 회고이번 미션에서는 "코촉촉" 반려동물 돌봄 서비스의 Opportunity Solution Tree를 만들어보았습니다. 처음으로 OST를 작성해보는 경험이었는데, 생각보다 복잡했습니다.제품의 근본적인 문제와 기회를 식별하는 것부터 시작해서, 가장 중요한 기회를 선택하고 솔루션을 도출하는 과정을 경험했습니다. 특히 "바쁜 현대인의 반려동물 돌봄 시간 부족"과 "낯선 사람에게 반려동물 맡기는 불안감"이라는 두 가지 핵심 기회에 집중했습니다. 어려웠던 점은 너무 많은 기회와 솔루션이 떠올라서 정말 중요한 것을 선택하는 것이었습니다. 하지만 "전략의 핵심은 무엇을 하지 않을 것인가를 정하는 것"이라는 마이클 포터의 말을 떠올리며 집중할 부분을 좁혀갔습니다.앞으로 실제 프로젝트에서 OST를 활용한다면, 더 많은 사용자 리서치와 데이터를 기반으로 기회를 발굴하고, 팀원들과 함께 더 다양한 솔루션을 도출해보고 싶습니다. -------------------------수고 많으셨습니다!

기획 · PM· PO워밍업스터디클럽3기PM

codestudy

[인프런 워밍업 스터디 클럽 3기 풀스택] 4주차 발자국

4주차 학습 내용강의 및 학습 내용React/Next.js 기본 레이아웃 구성Flex 속성 활용: flex, justify-center, items-center 등의 특성 학습컴포넌트 구조화 방법과 레이아웃 관리테일윈드 CSS를 활용한 그라데이션 배경 적용bg-gradient-to-r from-cyan500 to-blue500 등의 문법 활용로그인/회원가입 화면 구현SetView 상태관리를 통한 화면 전환 구현사용자가 로그인과 회원가입 사이를 전환할 수 있도록 기능 개발이메일과 패스워드 입력폼 설계 및 유효성 검사 구현Supabase 활용Supabase Auth를 통한 인증 시스템 구현메시지 테이블 설계와 컬럼(id, message, sender, receiver, is_deleted, created_at) 구성Supabase Realtime을 활용한 실시간 채팅 기능 구현RLS(Row Level Security) 정책 설정으로 데이터 보안 강화채팅 기능 구현메시지 전송 및 수신 기능 개발메시지 상태(읽음/안읽음) 관리를 위한 is_read 필드 활용메시지 삭제 기능 구현(is_deleted 필드 활용)UUID를 사용한 사용자 식별 및 메시지 보내기/받기 구현디자인 및 UI 개선Material Tailwind 활용className 속성을 통한 스타일링 관리반응형 디자인 구현(모바일 최적화)유저 아바타 구현을 위한 랜덤 이미지 API 활용학습 회고Instagram 클론코딩 4주차에서는 채팅 기능 고도화를 진행했습니다.Supabase의 실시간 데이터 처리 기능을 활용해 메시지 전송, 수신, 상태 관리를 구현했습니다.로그인/회원가입 페이지의 UI를 개선하고 테일윈드 CSS로 그라디언트 배경 효과를 적용했습니다.React와 Next.js, Supabase를 조합해 백엔드 인프라 없이도 강력한 기능을 빠르게 구현하는 현대적인 웹 개발 방식을 경험했습니다.실시간으로 채팅이 된다는 점이 너무 재밌었습니다.🛠 미션 해결 과정채팅 기능 고도화 미션 구현구현 과정데이터베이스 테이블 설계:message 테이블 확장: is_read, is_deleted 필드 추가blocked_users 테이블 생성: blocker_id, blocked_id 필드 포함reports 테이블 생성: reporter_id, reported_id, message_id, reason 필드 포함각 테이블에 적절한 외래 키(Foreign Key) 설정Row Level Security 정책 구현:메시지 읽기, 쓰기, 업데이트 권한 설정"사용자가 수신한 메시지 읽음 표시 가능" 정책 추가차단 및 신고 기능에 대한 사용자별 접근 권한 설정메시지 삭제 기능 구현:Message 컴포넌트에 삭제 버튼 추가is_deleted 필드를 업데이트하는 서버 액션 구현삭제된 메시지는 UI에서 필터링하여 표시하지 않도록 처리메시지 읽음/안읽음 표시 기능:is_read 필드를 이용해 메시지 상태 관리메시지를 읽을 때 자동으로 상태 업데이트UI에 읽음 상태 표시 (읽음/안읽음)사용자 차단 기능 구현:사용자 차단 버튼 및 확인 모달 추가blocked_users 테이블에 차단 정보 저장차단된 사용자와의 메시지 교환 제한메시지 신고 기능 구현:부적절한 메시지 신고 버튼 및 신고 사유 입력 모달 추가reports 테이블에 신고 정보 저장신고 후 사용자 피드백 제공실시간 업데이트 기능 개선:Supabase Realtime을 통한 INSERT와 UPDATE 이벤트 동시 구독메시지 상태 변경 시 실시간으로 UI 업데이트새 메시지 알림 구현로그인/회원가입 페이지 버그 수정:화면 전환 로직 오류 수정회원가입/로그인 버튼 기능 정상화 미션 해결 회고구현 내용데이터베이스 테이블 설계: message, blocked_users, reports 테이블 구성Row Level Security 정책 구현: 메시지 읽기/쓰기/업데이트 권한 설정메시지 기능 구현:메시지 삭제 기능읽음/안읽음 표시 기능사용자 차단 기능메시지 신고 기능Supabase Realtime을 통한 실시간 업데이트 기능 개선로그인/회원가입 페이지 버그 수정배운 점Supabase RLS(Row Level Security) 정책 설정을 통한 데이터베이스 보안 구현 방법WebSocket 기반 Supabase Realtime을 활용한 실시간 UI 업데이트 구현TypeScript를 활용한 타입 안전성 확보Supabase를 활용한 서버리스 백엔드 구축의 효율성실시간 기능이 사용자 경험에 미치는 중요성 

풀스택supabase워밍업3기

천준민

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

📖 강의 요약Dummy : 아무것도 하지 않는 깡통 객체Fake : 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체Stub : 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체. 그 외에는 응답하지 않는다.Spy : Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체, 일부는 실제 객체 처럼 동작 시키고 일부만 Stubbing할수 있다. (상태 검증)Mock : 행위에 대한 기대를 명세하고, 그 에 따라 동작하도록 만들어진 객체 (행위 검증)Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원활하게 한다.기본적으로 AscliDoc을 사용하여 문서를 작성한다.장점테스트를 통과해야 문서가 만들어 진다.프로덕션 코드에 비침투적이다단점코드양이 많다설정이 어렵다💡 미션 1. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.2. 아래 3개의 테스트가 있습니다. 내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요?(@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)미션 제출 url : https://inf.run/7N27s  💬 회고 인프런 워밍업 클럽 스터디가 종료 하였다 1달동안 클린코드와 테스트 코드 강의를 들으며 코드에 대해서 생각해보는 좋은 기회 였다.📎출처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기

lch9502

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

1. 들어가며시간이 굉장히 빠르네요. 얼마전에 OT를 듣고 진도표가 굉장히 빡세다고 느껴졌었는데 워밍업 클럽이 마무리가 되었습니다. 👏👏저는 개인적으로 어느정도 강제성이 생겨서 더 좋았던 것 같습니다. 하지만 직장인이 하기에는 시간적으로 부담이 되는게 사실입니다... 어찌어찌 완강을 했고, 실제로 실무에서 조금씩 개념들을 적용해보는 시간들이 가지려고 합니다.그리고 아직 강의가 100% 저만의 키워드로 정리가 되지는 않았기 때문에 몇 번 회독을 해야할 것 같긴 합니다.  2. 학습했던 내용 나만의 키워드로 작성하기섹션 7. Mock을 마주하는 자세Test Double, Stubbingdummy실제 객체를 모방하기만 한 아무 동작도 안하고 아무 행위도 안하는 깡통 객체입니다.그래서 활용할 일은 거의 없습니다.fake실제 객체의 행위와 동일한 기능은 수행하나, 좀 더 단순한 형태로 수행을 합니다.그래서 프로덕션에서 쓰기에는 부족한 객체입니다.stub테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체입니다.spyStub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체입니다.mock행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체입니다.@MockMockito에서 제공하는 어노테이션으로, 테스트에서 사용할 가짜 객체(mock)를 생성합니다.주로 단위 테스트에서 의존성을 대체하기 위해 사용됩니다.@MockBeanSpring Boot의 테스트에서 사용되는 어노테이션으로, Spring ApplicationContext에 mock 객체를 등록합니다.주로 통합 테스트에서 사용되며, Spring의 의존성 주입을 통해 mock 객체를 주입받을 수 있습니다.@SpringBootTest와 함께 사용됩니다.@Spy한 객체에서 일부는 실제 객체의 기능을 쓰고 싶고 일부만 stubbing 을 하고 싶을 때 @Spy 를 사용합니다.@SpyBean@MockBean과 유사하지만, 실제 객체의 메서드를 호출할 수 있는 spy 객체를 생성합니다.@InjectMocks테스트 대상 클래스의 생성자, 필드, 또는 setter 메서드를 통해 mock 객체를 자동으로 주입합니다.주로 단위 테스트에서 사용됩니다.BDDMockitogiven-when-then 스타일로 작성을 도와주는 Mockito 를 감싼 것Classicist VS. MockistClassicist: 진짜 객체로 최대한 테스트를 해야한다Mockist: 각각 테스트를 잘 했으니 통합 테스트를 할 때는 다 Mocking 처리해서 기능 보장된 것들은 Mocking으로 빠르게 쳐내고 테스트를 빠른 시간안에 한다.개인적으로 둘 중 뭐가 중요하다보다는 테스트 코드를 잘 작성하는 게 중요한 것 같다. 섹션 8. 더 나은 테스트를 작성하기 위한 구체적 조언테스트 하나 당 목적은 하나!한 가지 테스트에서는 한 가지 목적의 검증만 수행하는게 좋음.완벽한 제어현재 시간, 랜덤 값, 외부 시스템과 연동하는 경우 등에 대해서 제어할 수 있게 설정하는게 중요테스트 환경의 독립성, 테스트 간 독립성팩토리 메서드 보다는 순수한 빌더나 순수한 생성자로만 테스트 given 절을 구성하는게 더 좋음공유 자원에 대한 것들은 사용하지 않아야 함.Test Fixture@BeforeAll 과 @BeforeEach 에 Test Fixture 구성하지 않기data.sql 에 Test Fixture 구성하지 않기Test Fixture 의 메서드는 필요한 파라미터로만 구성하기픽스처를 만들기 위한 빌더들을 한 곳에 모으는 것을 지양하기deleteAll(), deleteAllInBatch()테스트도 결국 비용관계 정도만 생각을 하면 충분히 벌크 연산인 deleteAllInBatch() 로 한번에 깔끔하게 날릴 수 있기 때문에 deleteAllInBatch() 사용이 낫다.@ParameterizedTest, @DynamicTest@ParameterizedTest: 딱 하나의 테스트 케이스인데 값을 여러 개로 바꿔보면서 테스트를 하고 싶은 경우@DynamicTest: 하나의 환경을 설정해놓고 사용자 시나리오 테스트수행 환경 통합하기빠른 시간 안에 효과적으로 테스트를 수행할 수 있을지 고민해야 함private method test할 필요가 없다.테스트에서만 필요한 코드최대한 지양하는것이 좋다.  3. 학습 회고저의 목표는 단순히 개념을 익히는 것이 아니라, "실무에서 직접 사용해보는 것"입니다.물론 키워드만 알고 있어도 도움이 되겠지만, 직접 적용해 본 경험이 있는 것과 없는 것의 차이는 매우 크다고 생각합니다.이번 과정에서 정말 많은 개념과 기술을 배웠고, 이제는 이를 실제 코드로 구현하고, 직접 사용해보면서 제 것으로 만들어야겠죠.배움에서 그치지 않고, 실무에서도 활용할 수 있는 개발자로 성장해 나가겠습니다! 🚀🔥 정말 도움이 많이 되는 강의 2개였습니다.워밍업 클럽 같은 더 좋은 기회가 있다면 또 참여하고 싶네요.  4. 미션 회고미션 Day 16, 18크게 어려운 미션은 없었습니다. 간단한 키워드 정리만 있었죠.미션이 쉬웠던 이유는 아마 테스트 코드 강의에서 과부하가 걸릴거라고 생각했던 것 같습니다. ㅋㅋㅋ처음 강의를 들을 때 기존에 작성했던 테스트 코드는 사실 테스트 코드라고 부르기 어려웠던 것 같습니다.우빈님이 소개해준 테스트 방식이 정답은 아니지만 굉장히 많은 도움이 됐던 것은 사실이고 현재도 소개받은 테스트 방식으로 작성하고 있습니다.조금씩 적절하게 응용할 수 있는 좋은 자산이 되었습니다! 

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

leeebug

워밍업 클럽 스터디 3기 FS - 4주차 발자국

3주차 과제를 일찍 마무리하고 4주차는 조금 일찍 학습을 시작했기 때문인지 유난히도 길었던 마지막 주차도 끝나간다.이번주는 특히나 굉장히 많은 이슈를 경험했다. 결론부터 말하자면 채팅방 생각보다 구현하기 쉽지 않았다. 간단하게 하나만 언급하자면 URL로 장난질 치는 것에 대한 방어로직 구현이 특히 어려웠다. 사실 예전에 firebase 기반으로 미니 SNS를 구현했던 경험이 있는데 이번에 채팅을 구현했으니 이 프로젝트를 그대로 고도화해서 SNS를 완성해볼 계획이다. (실제로 완성할 수 있을지는...)📝 4주차 학습Supabase Auth이메일/비밀번호, OAuth, Magic Link, SMS 인증 등 다양한 인증 방식을 지원하는 인증 서비스supabase.auth.signUp, signInWithOAuth, getUser() 등으로 유저 관리와 세션 제어가 가능JWT 기반으로 RLS와 연동되며, 로그인 상태 자동 유지 및 세션 갱신 기능도 제공Supabase RealtimePostgreSQL의 Listen, Notify 기능을 기반으로 실시간 데이터 동기화 제공테이블 변경(Insert, Update, Delete)을 클라이언트에서 실시간으로 브로드캐스트supabase.chaeenel()로 원하는 이벤트를 구독Supabase RLS(Row Level Security)데이터베이스의 행(Row) 단위로 접근 제어를 설정하는 보안 기능Create Policy를 적용하여 유저별로 조회/ 수정 권한을 세밀하게 조정활성화 시 명시적인 권한 정책 필수아래는 개인적으로 나머지 공부로 학습하고 적용해본 라이브러리입니다.ZodTS 환경에서 런타임 스키마 검증과 타입 추론을 제공하는 유효성 라이브러리z.object() 등 메서드로 구조화된 데이터의 유효성 검사 수행 및 타입 자동 생성서버 및 클라이언트 모두에서 안전한 폼 및 api 검증에 활용React Hook Form과 함께 사용하기 유용한 라이브러리React Tostify토스트 메시지를 손쉽게 띄울 수 있는 라이브러리간단한 API로 다양한 유형의 토스트 알림 제공강력한 커스터마이징 제공Kyfetch API 기반의 모던한 HTTP 클라이언트간결한 문법 제공자동 재시도, 에러 핸들링, 인터셉터 등 확장성과 다수의 편의 기능 포함📋 4주차 미션💬GitHub 저장소👉체험하러 가기 미션 해결 과정 요약이번주 미션의 필수 구현 과제는 Supabas Auth를 사용한 회원가입, 로그인 기능 구현 및 Supabase Realtime을 활용한 1:1 채팅 기능 구현하기였다. 추가 구현 과제는 메시지 삭제, 메시지 알림, 메시지 읽음 여부 표시, 채팅 신고, 유저 차단 기능 등 자유롭게 구현하기였는데 시간 관계 상 전부 구현하긴 어려워서 비교적 쉬운 메시지 삭제와 메시지 알림을 제한적으로 구현했다. 원래는 DB 스키마를 꼼꼼하게 고민하고 시작했어야하는데 급하게 하다보니 처음 계획했던 내용과 많이 달라졌다.myon_users.id -> auth.users.idSupabase Auth로 회원가입된 유저만 등록 가능myon_rooms.userA_id -> myon.users.idmyon_rooms.userB_id -> myon.users.id회원가입된 유저만 채팅에 참여 가능myon_messages.sender_id -> myon_users.id회원가입된 유저만 채팅 전송 가능myon_rooms.last_message_id -> myon_messages.id가장 최근 메시지 미리보기 시 테이블 join에 활용myon_users.username회원가입 시 입력한 닉네임을 기반으로 한글과 특수 문자 등을 제거한 후 중복 발생 시 유틸함수를 통해서 suffix를 불여서 고유한 username을 자동 생성(회원가입 시 입력 폼의 간소화를 위한 선택)✅ 이메일 로그인, 회원가입GET app/auth/signup/callbackPOST api/user/email/register✅ OAuth 로그인, 회원가입GET app/auth/oauth/kakao/callbackPOST api/user/oauth/register우선 회원 기능부터 만들기 시작했는데 강의 패턴을 참고하여 자동 생성되는 auth.users 테이블만 사용하여 회원 로직을 만들었는데 메타 데이터의 형태가 provider 별로 일정하지 않고.. 무엇보다도 auth.users 테이블은 커스텀이 제한적이기 때문에 public.users 테이블을 별도로 관리하였다. 카카오 계정 로그인의 경우 user_metadata를 커스텀 인터페이스로 관리하여 타입 오류를 방지하였다.또한 auth.users 테이블만 단독 사용시의 문제는 회원가입 단계에서 사전에 이메일 중복 검증이 어렵다는 점도 단점이었다.찾아보니 보안상의 이유로 Supabase 내부적으로 auth.users 테이블을 직접 조회하는 기능은 별도로 제공하지 않아서 가입 요청 후에 에러를 캐치할 수 있는 구조이기 때문에 이부분도 public.users를 조회하여 이메일 중복 검증을 통과한 경우에만 회원가입 요청을 할 수 있도록 처리했다.회원가입이나 로그인 인풋 유효성 검증은 Zod + react-hook-form 라이브러리도 대체했다.이메일 회원가입이메일 로그인✅1:1 채팅POST, GET api/rooms/[roomId]POST api/messagesGET api/messages/[roomId]문제는 채팅 기능 구현이었는데 채팅 기능 자체는 Realtime 구독으로 어렵지않게 완성했으나 문제는 방어로직 구현이었다. 몇 가지 예시를 들자면 /direct-message 로 접근 시에 해당 페이지에서 나 자신을 제외한 모든 유저 리스트를 불러온 뒤, /direct-message/:roomId 로 동적 라우트를 구현할 때, 처음에는 고유성을 보장하기 위해서userA_username-userB_username-suffix 형태로 roomId를 생성하는 유틸 함수를 사용했는데 렌더링 시 마다 suffix가 변동되기 때문에 서버단에서 해당 URL이 유효한 URL인지 검증하기가 쉽지 않아서 userA_username과 userB_username을 정렬하여 항상 동일한 roomId를 생성하는 순수 유틸 함수로 변경하여 해당 문제를 해결하였다.export function generateRoomId({ usernameA, usernameB }: { usernameA: string; usernameB: string }) { const sortedUsernames = [usernameA, usernameB].sort() return `${sortedUsernames[0]}-${sortedUsernames[1]}` }과제 추가 구현 기능✅ 메시지 삭제(Soft Delete)PATCH api/message/[messageId]메시지 삭제는 두가지 방식이 있는데 DELETE 메서드를 사용하여 DB Row에서 아예 삭제하는 하드 삭제와 실제 DB Row에서 삭제하지 않지만 is_delete 같은 플래그를 true 하여 클라이언트단에서 감추는 방식인 소프트 삭제 방식이 있다. 하드 삭제의 경우 DB 공간 절약이 필요하거나 탈퇴 회원 정보 등 영구 삭제가 필요한 경우에 적합하고 소프트 삭제의 경우는 복구가 필요하거나 삭제 이력을 추적해야하는 경우에 적합한데 채팅은 로그를 남기는게 중요해서 개인적으로는 소프트 삭제로 구현했다.메시지 호버 시 삭제 아이콘 표시삭제된 메시지✅ 메시지 알림(토스트 메시지 활용)별도 API route 없이 구독으로 구현그냥 마무리하기 아쉬워서 추추가 기능으로 구현했다. 예전부터 토스트 메시지에 관심이 많았는데 직접 구현해보니 생각보다 비효율적이라서 react-tostify 라는 라이브러리를 적용했다.토스트 메시지는 스크롤이 최하단이 아닌 지난 메시지를 읽고 있을때만 우측 상단에 스택 형태로 알림을 보내도록 구현했다. 👀 4주차 회고이번 주는 지난 스터디 기간 동안 진행했던 프로젝트를 배포하는 과정이 포함되어 있었기 때문에, 추가적인 기능보다는 안정적인 배포에 중점을 둘 계획이었으나.. 다행히도 지난주에 미리 매를 맞아두었기 때문에 이번 주 과제 배포는 크게 문제 없이 마무리할 수 있었다.다만 실제 배포 경험이 많지 않다 보니 환경 변수 관련 이슈를 자주 겪게 되었고, OAuth Redirect URL 설정 누락, 빌드 시 타입 오류 등의 경험으로 배포 시 고려해야 할 요소들을 더 잘 이해하게 되었다. 앞으로는 다른 프로젝트 배포 시 참고할 수 있도록 트러블슈팅 내역을 꼼꼼하게 정리하는 습관을 들일 계획이다.이번주에 과제를 진행하면서 딱 한가지 아쉬웠던 점이라면 2주차부터 꾸준히 적용해오던 Container-Presentational Component 패턴을 이번에는 적용시키지 못했다는 점이다. 이번주 과제가 전반적으로 복잡도가 높다보니 관심사 분리를 코드에 녹여내지 못했으나 점진적으로 리팩토링을 통해서 개선해나가기 위해서 백로그에 기록해두었다. 스터디 이후..이번에 구현한 채팅 기능은 실제 배포를 해보니 전송 시 약간의 딜레이가 발생하는 것을 발견했다. 지금 타이밍에서 메시지 전송에 대한 낙관적 업데이트를 적용해서 최적화하는것이 가장 시급한 과제라고 생각한다.끝으로 이번 프로젝트는 이후에 포트폴리오로 활용할 수 있도록 고도화 작업을 이어갈 생각이며, 동시에 타입스크립트에 대한 이해를 더 심화시키고, 쉽진 않겠지만 최근 관심이 생긴 테스트 코드 작성 관련 학습도 병행해나가 보려 한다.정말 마지막으로.. 풀스택 과정을 포함한 모든 3기 스터디 러너분들, 멘토님들과 서포터분들, 워밍업 클럽 관계자 여러분들 모두 고생하셨습니다👏 여러분들 덕분에 좋은 인사이트 얻어갑니다 :)  

풀스택워밍업클럽3기풀스택Next.js4주차회고미션

suover

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

Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드강의와 함께한 인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드 (Java, Spring Boot)4주차 발자국 입니다.학습 내용 요약이번 주에는 Practical Testing: 실용적인 테스트 가이드 강의에서 계층형 아키텍처(Layered Architecture) 의 개념을 다시 한번 정리하고, 스프링 기반 테스트에서 자주 사용하는 Mock/Spy 기법과 관련 애너테이션들을 배웠습니다.Layered Architecture (계층형 아키텍처)Presentation Layer: 사용자 입력과 응답을 담당. API 요청/응답 혹은 프론트엔드 인터페이스를 제공하며, 기본적인 파라미터 검증과 변환만 수행.Business Logic Layer: 애플리케이션의 핵심 비즈니스 로직과 규칙을 구현. 서비스(Service)나 도메인(Domain) 로직이 위치하며, 트랜잭션 관리의 주된 대상이 됨.Persistence Layer: 데이터베이스/파일시스템 등 영속성 저장소와 직접적으로 상호작용. Repository(DAO) 형태로 CRUD를 담당.Mockito & Spring Test 관련 애너테이션@Mock / @Spy / @InjectMocks: 순수 Mockito 환경(스프링 컨텍스트 없이)에서 가짜 객체(또는 부분 가짜 객체)를 만들어 단위 테스트를 쉽게 작성할 수 있게 함.@MockBean / @SpyBean: 스프링 애플리케이션 컨텍스트를 기동하는 통합 테스트 환경에서 특정 Bean을 Mock 또는 Spy로 교체하여, 외부 의존성을 간단히 제어하고자 할 때 활용.테스트 설계 방향각 레이어별로 테스트 범위를 명확히 하여, “Persistence → Business → Presentation” 순으로 책임을 분리해 점진적으로 테스트 커버리지를 넓힘.@MockBean / @SpyBean을 과도하게 사용하지 않도록 주의: 꼭 필요한 부분만 모의(Mock/Spy) 처리하고, 나머지는 실제 로직을 이용해 통합 테스트를 안정적으로 수행.테스트 시나리오별로 @BeforeEach를 활용하는 방법과, 각 테스트 메서드에서 독립적으로 given-when-then을 구성하는 방법을 비교.학습 회고얻은 인사이트레이어별 책임과 역할이 분명해야 복잡한 서비스에서도 테스트 설계가 한결 명확해짐을 느꼈습니다.Mock/Spy를 올바른 상황에서만 사용하면 외부 의존성을 최소화하여 빠른 단위 테스트를 작성할 수 있으나, 반대로 불필요하게 남발하면 테스트 유지보수가 어려워짐을 체감했습니다.어려웠던 점@MockBean과 @SpyBean을 사용해 스프링 컨텍스트를 띄울 때 테스트 속도가 느려지거나, 동일 타입 Bean이 여러 개일 때 어느 Bean을 대체하는지 혼동될 수 있었습니다.레이어별 테스트를 작성하면서, 중복으로 보이는 코드(예: 테스트 준비 로직)를 어느 정도까지 @BeforeEach로 추출해야 할지 고민이 되었습니다.미션🎯 Day16 미션: Layered Architecture 구조의 레이어별 특징과 테스트 방법요구사항Layered Architecture에서 각 레이어가 하는 역할, 특징, 그리고 테스트 방식을 자기만의 언어로 정리하기나만의 정리Presentation Layer역할: 사용자나 외부 시스템의 요청을 받고 응답을 전달. 파라미터 검증, DTO 변환, HTTP 상태 코드 결정 등을 담당.특징: 비즈니스 로직을 직접 수행하지 않고, 유효성 체크 후 Service 호출 → 결과 반환에 집중.테스트 방법: 컨트롤러 통합 테스트(@SpringBootTest, @WebMvcTest + MockMvc)를 통해 실제 요청/응답 형식을 모의하거나, 프론트엔드와의 e2e 테스트를 진행해볼 수도 있음.Business Logic Layer역할: 애플리케이션의 핵심 규칙, 알고리즘, 트랜잭션 등의 로직 담당.특징: 여러 Repository를 조합하여 도메인 로직을 수행하고, 예외 처리, Validation 등의 업무 로직을 포괄적으로 관리.테스트 방법: 단위 테스트를 통해 특정 비즈니스 로직의 정확성을 검증. 필요 시 Repository를 Mock 처리(예: @Mock, @MockBean)하여 DB 의존성을 제거하고 로직만 집중 테스트.Persistence Layer역할: 데이터의 저장/조회/수정/삭제 등 영속성 처리에 집중.특징: 쿼리 작성, DB 연결, CRUD 로직을 추상화한 Repository/DAO 형태로 제공.테스트 방법: 실제 DB 혹은 In-Memory DB(H2 등)를 활용한 통합 테스트(@DataJpaTest). 쿼리 정확도, 트랜잭션 처리, 성능 등을 검증하기에 좋음.🎯 Day18 미션 1: @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks의 차이점 정리요구사항각 애너테이션이 어떤 환경(단위 테스트/통합 테스트)에서 사용되고, 어떤 특징이 있는지 정리하기1) @Mock주로 사용 환경: Mockito (단위 테스트)스프링 빈 등록 여부: X (별도 Bean 아님)특징:순수 Mock 객체를 생성하여, 내부 로직 없이 호출 기록/결과(Stubbing)만 지정할 수 있음단위 테스트에서 외부 의존성을 배제하고 특정 로직만 검증할 때 유용2) @MockBean주로 사용 환경: Spring Boot Test(통합 테스트)스프링 빈 등록 여부: O (빈으로 등록됨)특징:스프링 애플리케이션 컨텍스트에 등록된 실제 Bean을 Mock으로 교체다른 Bean이 해당 Bean을 의존하고 있으면, 그 의존성도 가짜(Mock)로 처리됨@SpringBootTest나 @WebMvcTest 등 스프링 컨텍스트가 뜨는 환경에서 사용3) @Spy주로 사용 환경: Mockito (단위 테스트)스프링 빈 등록 여부: X (별도 Bean 아님)특징:부분 가짜(Spy) 객체를 생성기본적으로 실제 메서드 동작을 유지하면서, 필요한 부분만 가짜(Stubbing)로 설정 가능복잡한 객체를 테스트할 때, 일부는 실제 로직을 실행하고 일부만 Mock 처리할 수 있어 유연함4) @SpyBean주로 사용 환경: Spring Boot Test(통합 테스트)스프링 빈 등록 여부: O (빈으로 등록됨)특징:스프링 컨텍스트 내 실제 Bean을 Spy로 교체기본적으로는 실제 로직을 수행하되, 특정 메서드만 가짜로 동작시키거나 호출을 기록할 수 있음통합 테스트 환경에서 부분 Mocking이 필요할 때 적합5) @InjectMocks주로 사용 환경: Mockito (단위 테스트)스프링 빈 등록 여부: 해당 없음특징:@Mock 또는 @Spy로 만들어진 객체들을 자동으로 주입(생성자, 세터, 필드 순)해줌예: @InjectMocks UserService라면, UserService가 의존하는 Repository나 다른 객체들을 @Mock으로 생성해 주입받을 수 있음단위 테스트에서 여러 의존 객체를 편리하게 Mock/Spy로 대체할 수 있도록 지원 🎯 Day18 미션 2: 테스트 시나리오(@BeforeEach, given, when, then) 구성요구사항아래 3개의 테스트 메서드가 있을 때, “어떤 준비 로직을 @BeforeEach로 뽑고, 어떤 부분을 각 테스트의 given절에 둘지”, “when절을 어떻게 구성할지”를 구상해보기예시 테스트 3종사용자가 댓글을 작성할 수 있다.사용자가 댓글을 수정할 수 있다.자신이 작성한 댓글이 아니면 수정할 수 없다.나의 구성@BeforeEach void init() { // 공통 데이터를 미리 생성하지 않습니다. /* * 각 테스트가 각각의 상황(사용자, 게시물, 댓글)을 자유롭게 구성할 수 있도록, * 여기서는 사전에 아무것도 세팅해두지 않습니다. * 테스트마다 필요한 데이터 타입이나 조건이 다를 수 있으므로, * 각 테스트 메서드 내부에서 직접 객체를 생성하고 준비해 주는 방식을 채택했습니다. */ } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { // given // 1-1. 사용자 생성에 필요한 내용 준비 // 1-2. 사용자 생성 // 1-3. 게시물 생성에 필요한 내용 준비 // 1-4. 게시물 생성 // 1-5. 댓글 생성에 필요한 내용 준비 // when // 1-6. 댓글 생성 // then // 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { // given // 2-1. 사용자 생성에 필요한 내용 준비 // 2-2. 사용자 생성 // 2-3. 게시물 생성에 필요한 내용 준비 // 2-4. 게시물 생성 // 2-5. 댓글 생성에 필요한 내용 준비 // 2-6. 댓글 생성 // when // 2-7. 댓글 수정 // then // 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { // given // 3-1. 사용자1 생성에 필요한 내용 준비 // 3-2. 사용자1 생성 // 3-3. 사용자2 생성에 필요한 내용 준비 // 3-4. 사용자2 생성 // 3-5. 사용자1의 게시물 생성에 필요한 내용 준비 // 3-6. 사용자1의 게시물 생성 // 3-7. 사용자1의 댓글 생성에 필요한 내용 준비 // 3-8. 사용자1의 댓글 생성 // when // 3-9. 사용자2가 사용자1의 댓글 수정 시도 // then // 검증 }@BeforeEach정말 모든 테스트에 공통으로 필요한 세팅(예: DB clean-up, 동일한 테스트 데이터 세팅 등)이면 이곳에 배치.하지만 테스트마다 시나리오가 크게 다른 경우, 오히려 각 테스트 내부(given 절)에서 데이터를 생성하는 편이 가독성과 유연성이 좋아질 수 있음.given 절테스트에 필요한 사전 조건(사용자, 게시물, 댓글 등) 및 Mock/Stubbing이 필요하다면, 해당 로직을 배치.시나리오별로 조금씩 다른 상황을 구성할 때, @BeforeEach보다 각 테스트 내부의 given 부분에 명시적으로 작성하는 방식이 선호됨.when 절실제 테스트 대상 메서드를 호출하는 구간. 한 개의 테스트에서 “정확히 하나의 when 절”을 유지해, 명확히 어떤 동작을 검증하는지 드러내도록 함.then 절결과 검증(Assertions) 수행. 저장된 데이터 확인, 예외 발생 여부 확인, 반환값 확인 등.미션 회고레이어별 특징을 자기 언어로 설명단순히 문서상 개념을 반복하기보다, 실제 현업/프로젝트에서 마주치는 구조를 떠올리며 다시 풀어내보니, 각 계층이 왜 필요한지, 테스트를 어떻게 접근해야 하는지 더 명확해졌습니다.Mock/Spy 애너테이션 정리정리를 해보니 실제로 코드에 적용할 때 어떤 상황에서 무엇을 써야 하는지가 더 분명해졌습니다.@MockBean/@SpyBean이 스프링 컨텍스트를 사용하는 통합 테스트에서 유용하지만, 속도와 Bean 교체 이슈가 있어 주의가 필요하다는 점이 인상적이었습니다.3가지 테스트 시나리오에서 @BeforeEach & given/when/then 배치시나리오에 따라 @BeforeEach를 최소화하거나, 여러 테스트에 공통적으로 필요한 부분만 추출하는 방식이 각각 장단점이 있음을 확인했습니다.테스트 구조를 “given-when-then”으로 고정하면 가독성이 올라가지만, 너무 세세한 단계 분리는 오히려 장황해질 수 있으므로 균형을 잡아야겠습니다.회고칭찬하고 싶은 점레이어별 테스트 기법과 Mock/Spy 전략이 한층 체계적으로 잡혔습니다.실제 코드를 작성하며, “가짜 객체를 어디까지 써야 하나? 외부 연동은 어떻게 테스트하나?” 같은 고민을 많이 해 봐서, 앞으로는 목적에 맞는 테스트를 설계하는 능력이 향상될 것 같습니다.아쉬웠던 점 & 보완 계획Mock 남발 시 발생할 수 있는 문제(설정 복잡도 상승, 실제 로직 변경 시 Stubbing 깨짐)를 더 구체적인 예제로 다뤄보면 좋을 것 같습니다.@BeforeEach와 개별 테스트 내부 given 절의 적절한 균형점을 찾으려면, 실제 현업 수준의 다양한 테스트 케이스를 더 경험해 봐야겠습니다.다음 목표예외 상황 테스트 강화단순 성공 케이스뿐 아니라, 비정상 입력이나 예외 케이스를 좀 더 체계적으로 정리하고 테스트에 반영. 도메인 별로 테스트 슬라이싱@DataJpaTest, @WebMvcTest 등 스프링이 제공하는 슬라이스 테스트 방식을 적극 활용해 보기.테스트 실행 속도 최적화통합 테스트와 단위 테스트를 적절히 조합하여, 빠르면서도 신뢰성 있는 테스트 환경을 구축해 보기.이번 4주차에는 계층형 아키텍처를 다시 한번 복습하면서 Mock/Spy, @MockBean/@SpyBean 등 스프링 테스트 환경에서 자주 쓰이는 기법들을 정리하고 적용해 보았습니다. 학습 내용과 미션을 통해 각 레이어가 가진 의미와 책임이 더욱 또렷해졌고, 가짜 객체를 어떻게 잘 활용해야 하는지 감이 잡힌 것 같습니다.앞으로도 학습 내용을 기록하고 회고하면서, 점점 더 탄탄한 테스트 코드를 작성해 가도록 하겠습니다.감사합니다!

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

보키

[인프런 워밍업 클럽 3기 - BE/Project] 2주차 회고 발자국 🐾

1주차와 마찬가지로 KPT 회고 프레임워크를 선택해서 작성해보려고 한다! Keep(만족, 지속하고 싶은 부분)이번 2주차는 개인프로젝트에서는 RDB 모델링을 마친 뒤 엔티티를 정의했으며 현재는 조회 API를 만들고 있고, 강의에서는 Presentation Layer에 대한 부분을 수강했다. 강의가 개인프로젝트보다 좀 빠르지만, 그만큼 개인 프로젝트에 적용할 수 있는 부분에 대한 인사이트를 미리 얻어갈 수 있는 것 같아서 좋다.Problem(부족, 아쉬웠던 부분)또 내 과욕이 부른 스불재(스스로 불러온 재앙) 느낌의 아쉬운 부분을 써보자면.. RDB를 너무 현업과 비슷하게 만드는 것을 목표로 잡지 않았나 싶다..ㅎㅎ 뭐 어쩌겠어~ 벌린 일이니 해야지..!!강의에 대한 내용은 아쉬운게 전혀 없다!! JSP에서 Tag(<% %>)가 빠진, Spring 공식 팀에서도 밀고 있는 템플릿 엔진인 Thymeleaf를 잘 알고 계시고, 다시 배워보니 좋았다!여담으로, Template for Spring으로는 JSP / Thymeleaf / FreeMarker / Groovy Markup / Jade4j / JTE(Java Template Engine) / Velocity / JMustache / Pebble가 있다.어느것을 택하든 자유이나 Spring에서 공식적으로 Support하고 Advertise하는 제품(라이브러리)를 사용하는 것을 추천한다.Try(도전, P에 대한 해결책 등으로 다음번에 보완하거나 시도할 부분)엔티티를 너무 많이 만들고 관계도 1:N, N:1, N:M(을 풀기위한 중간 엔티티) 여러가지를 사용한만큼 JPA를 잘 알고 사용해서 열심히 풀어나가면 되는 문제이고, SSR대신 CSR로 해서 내가 익숙한 Vue보다는 React with TS로 화면을 구성해보려고 한다.그리고 테스트코드도 좀 더 다양한 방법으로 도전해보면 어떨까싶다(TODO).예를 들면 TestContainer라던지.. 아니면 MockMVC대신에 RestAssured(또는 RestAssuredMockMvc), WebTestClient등을 사용해볼 수도 있겠고 RestDocs 또는 RestDocs와 Swagger를 함께 사용하는 restdocs-api-spec를 써볼수도 있을 것 같다.일단 큰 목표는 마음과 머릿속 한켠에 자리를 잡아두고 MVP(Minimum Viable Product)라고 불리는 최소 기능 제품을 만들어서 정상작동하는데 문제가 없는 제품을 만들고 고도화를 통해서 기능개선 또는 기능추가, 사용성개선 등을 생각해보는게 맞지 않나 싶다!

백엔드인프런워밍업클럽3기프로젝트KotlinSpringboot스프링

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기백엔드클린코드자바

suover

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

Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드강의와 함께한 인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드 (Java, Spring Boot)3주차 발자국 입니다.학습 내용 요약이번 주에는 Spring & JPA 환경에 계층형 아키텍처(Layered Architecture) 및 테스트 기법(통합 테스트, MockMvc, JPA 테스트 등)을 학습했습니다.1⃣ Spring & JPASpring이 제공하는 IoC/DI 개념과 ORM(JPA, Hibernate) 기반 데이터 접근 방식을 이해했습니다.엔티티(Entity) 설계를 통해 도메인 모델링을 하고, 리포지토리(Repository) 계층으로 데이터 CRUD를 담당하도록 분리했습니다. 2⃣ Layered ArchitecturePresentation Layer(Controller)Business Layer(Service)Persistence Layer(Repository)컨트롤러는 요청/응답을 처리하고, 비즈니스 로직은 서비스가 담당하며, 데이터 접근은 리포지토리를 통해 수행합니다.3⃣ 테스트 코드 작성단위 테스트: 특정 클래스나 메서드 단위 검증. 예: JUnit + AssertJ 사용통합 테스트: 스프링 컨텍스트를 로드해 여러 레이어(Controller, Service, Repository)의 협력을 검증.MockMvc를 사용해 테스트하는 방식을 배웠습니다.4⃣ 유효성 검증(Bean Validation)DTO에서 @NotEmpty, @NotNull, @Positive 등 애노테이션을 사용하여 입력값을 검증하고,RestControllerAdvice로 BindException 등을 처리하여 예외 상황 시 API 응답을 표준화했습니다.✨ 학습 회고Spring & JPA 학습을 통해, 계층형 아키텍처 전체 흐름을 이해해야 함을 느꼈습니다.테스트 코드에서, 단위 테스트와 통합 테스트의 경계와 필요성을 체감했습니다.JUnit5 + AssertJ로 간단한 메서드 단위 검증을 할 때는 편리했지만, MockMvc로 검증하거나, @DataJpaTest로 테스트할 때는 더 많은 설정이 필요했습니다. 그만큼 테스트 커버리지가 넓어지며 신뢰도가 높아진다는 장점을 느꼈습니다.Validation과 ExceptionHandler를 통해 잘못된 입력을 막고, 표준화된 에러 응답을 제공하는 프로세스가 얼마나 중요한지 깨달았습니다.미션 해결 과정🎯 스터디카페 이용권 선택 시스템 테스트 미션미션스터디카페 이용권 선택 시스템의 테스트 코드를 작성하는 미션.미션 목표메시지 출력 테스트할인 로직 테스트사물함 선택 테스트 해결 과정OutputHandlerTest환영 메시지 + 공지사항 “프리미엄 스터디카페” 등의 문구가 제대로 찍히는지 확인.주문 내역 요약에 “이용 내역”, “이용권”, “사물함”, “총 결제 금액” 같은 키워드가 포함되는지 검증.StudyCafePassOrderTest할인율 및 가격 계산이 의도대로 동작하는지 (예: (250,000 + 10,000) - 할인액 25,000 = 235,000) 확인.사물함이 있는지 없는지에 따라 Optional<LockerPass>가 올바르게 반환되는지 테스트.StudyCafeSeatPassTest할인율이 0일 때 할인 금액이 0원인지, 0.1이면 정상 할인액이 계산되는지.시간권(HOURLY)은 cannotUseLocker() == true, 고정석(FIXED)은 false인지 등을 테스트로 보장. 회고도메인 로직이 잘 분리되어 있어 테스트 작성이 수월했습니다. 만약 복잡한 코드가 여기저기 있었다면, 테스트도 훨씬 어렵고 중복될 뻔했습니다. 또한 예외 처리 부분에 대한 테스트 코드의 필요성도 느껴졌습니다. 예외 상황에 대한 흐름을 명확하게 검증하고, 안정적인 동작을 보장하기 위해서는 예외 처리에 대한 테스트도 함께 구성되어야 한다고 생각했습니다.🔗 테스트 미션 깃허브 링크회고스스로 칭찬하고 싶은 점도메인 흐름 파악: Controller → Service → Repository 순으로 데이터가 흐르는 구조를 더욱 제대로 이해하게 되었습니다. 다양한 스프링 테스트 어노테이션(WebMvcTest, SpringBootTest, DataJpaTest 등)을 직접 적용하며, 각 용도에 맞게 테스트를 구분해본 경험이 유익했습니다.아쉬웠던 점 & 보완할 점테스트 격리: 통합 테스트 시 상태를 초기화하는 과정이 번거로웠고, 테스트 순서에 따라 의존성이 생기지 않도록 더 철저히 관리해야 함을 느꼈습니다.테스트 시나리오 다양성 부족: 대부분 해피 케이스 위주로 검증한 감이 있어, 더욱 폭넓은 예외 상황에 대한 시나리오를 추가 작성하면 안정성이 높아질 것 같습니다.다음 주 학습 목표Mock과 Test Double: Mock과Test Double 개념을 학습하고, 적절히 사용해 볼 예정입니다.테스트 환경 독립성:테스트 시 독립적인 환경을 유지하는 방법을 살펴볼 예정입니다.테스트 개선 기법:조금 더 간결하고 가독성 높은 테스트 작성을 시도해 볼 계획입니다.이번 주에는 Spring & JPA 환경에서의 테스트 코드 작성을 중점적으로 경험 했습니다. 이를 통해 서비스가 단계적으로 확장되더라도, 테스트 코드가 안정적인 작동을 보장할 수 있다는 점을 다시금 깨달았습니다. 앞으로도 테스트 우선 방식으로 새로운 기능이나 리팩토링을 진행해, 안전하고 효율적인 협업 환경을 유지해 나가겠습니다.감사합니다!

백엔드인프런워밍업클럽스터디백엔드클린테스트코드발자국회고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기 풀스택] 3주차 발자국

학습 내용인프런 워밍업 클럽 스터디 3주차로,이번 주는 넷플릭스 프로젝트를 다루는 시간이었다.useInfiniteQuery와 Jotai(recoil 대체 전역상태 라이브러리)를 사용해볼 수 있었다.미션 3 구현 내용과제 구현 저장소Netflix 중 찜하기 관련 기능 포인트 1: favorites 테이블 추가actions/favoriteActions.ts"use server"; import { createServerSupabaseClient, handleError, PostgrestError, } from "@next-inflearn/supabase"; // Movie 타입 정의 export type Movie = { id: number; image_url: string; overview: string; popularity: number; release_date: string; title: string; vote_average: number; // Movie 타입에 favorites 필드 추가 favorites?: { // optional field로 추가 id: number; } | null; }; // SearchMoviesResponse 타입 정의 export type SearchMoviesResponse = { data: Movie[]; page: number; pageSize: number; hasNextPage: boolean; }; // 에러 케이스를 위한 타입 정의 type SearchMoviesError = { data: never[]; count: number; page: null; pageSize: null; error: PostgrestError; }; // 성공 케이스를 위한 타입 정의 type SearchMoviesSuccess = { data: Movie[]; page: number; pageSize: number; hasNextPage: boolean; }; export async function searchMovies({ search, page, pageSize, }: { search: string; page: number; pageSize: number; }): Promise<SearchMoviesSuccess> { const supabase = await createServerSupabaseClient(); // 현재 사용자 정보 가져오기 const { data: { user }, } = await supabase.auth.getUser(); // 쿼리 설정 const query = supabase .from("movie") .select( user ? ` *, favorites!left ( id ) ` : "*", // 로그인하지 않은 경우 favorites 정보를 가져오지 않음 { count: "exact" } ) .ilike("title", `%${search}%`) .order("popularity", { ascending: false }); // 로그인한 경우 현재 사용자의 즐겨찾기만 조회하도록 필터링 if (user) { query.eq("favorites.user_id", user.id); } const { data, count, error } = await query.range( (page - 1) * pageSize, page * pageSize - 1 ); const hasNextPage = count ? count > page * pageSize : false; if (error) { return { data: [], page, pageSize, hasNextPage: false, }; } // 반환된 데이터를 Movie 타입에 맞게 변환 const moviesWithFavorites = (data || []).map((movie: any) => ({ ...movie, favorites: movie.favorites?.[0] || null, // 즐겨찾기 정보 포함 })); return { data: moviesWithFavorites, page, pageSize, hasNextPage, }; } export async function getMovie(id: string) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("movie") .select("*") .eq("id", id) .maybeSingle(); handleError(error); return data; } 포인트 2: profiles 테이블 추가utils/AuthProvider.tsx"use client"; import { useEffect } from "react"; import { createBrowserSupabaseClient } from "@next-inflearn/supabase"; import { useSetAtom } from "jotai"; import { userAtom } from "@/utils/jotai/atoms"; export function AuthProvider({ children }: { children: React.ReactNode }) { const setUser = useSetAtom(userAtom); const supabase = createBrowserSupabaseClient(); useEffect(() => { // 현재 세션 확인 supabase.auth.getSession().then(({ data: { session } }) => { setUser(session?.user ?? null); }); // Auth 상태 변경 구독 const { data: { subscription }, } = supabase.auth.onAuthStateChange((_event, session) => { setUser(session?.user ?? null); }); return () => subscription.unsubscribe(); }, [supabase, setUser]); return children; } 포인트 3: movies / favorites / profiles 테이블 연결favorites 테이블 내 profiles 테이블 내 회고 시간이 부족하다는 핑계로,더 디벨롭할 수 있는 부분이 많음에도 불구하고 생각했던 것들을 다 구현하진 못했던 것 같다.다만, 이렇게 틀을 갖춰놧으니 추후에 추가적인 기능을 정의해서 구현해보기 너무 좋을 것 같다.  

풀스택풀스택인프런워밍업스터디클럽Next3기SupabaseReact프론트엔드3주차발자국

leeebug

워밍업 클럽 스터디 3기 FS - 3주차 발자국

워밍업 클럽도 벌써 3주차!처음 스터디를 시작할 때와 비교하면 가장 큰 소득은 React Query에 익숙해졌다는 것 그리고 무언가에 몰입하면서 성취감을 느꼈다는 것이다.단순히 기능을 구현하는 걸 넘어서 최적화나 에러 핸들링까지 고민하는 과정이 꽤 재밌었다.이번주에는 인피니트 쿼리와 추가 기능으로 좋아요 기능을 구현해야해서 지난주와 마찬가지로 조금 일찍 학습을 시작했다.깃 레포는 역시 첫번째 과제에 사용했던 템플릿을 거의 수정없이 그대로 사용해서 역시나 개발환경 구축은 무리없이 진행했다.다만, 한 가지 아쉬운 점이라면 터보레포 같은 모노레포 도구를 도입했어야 하지 않았나 하는 생각이 들었다는 점이다.현재 방식은 각 주차별 과제를 독립적인 레포로 관리하고 있는데, 공통 유틸이나 자주 사용하는 설정 파일을 계속 복붙하는 과정에서 의외로 피로감을 느꼈다.우선 마지막 과제까진 지금의 방식을 유지하고 스터디 마무리 이후에 터보 레포에 대해서는 개별적으로 학습을 해볼 예정이다.📝 3주차 학습useInfiniteQuery무한 스크롤 및 페이지네이션을 위한 React Query 훅fetchNextPage를 사용해 추가 데이터 요청getNextPageParams로 다음 페이지 여부 관리 useInViewreact-intersection-observer 라이브러리의 훅특정 요소가 화면에 보이는지 감지(뷰포트 진입 여부로 확인)무한 스크롤 구현 시 useInfiniteQuery와 함께 사용threshold, rootMargin으로 감지 범위 조절 가능📋 3주차 미션💬 GitHub 저장소🚀 데모 영상 보러가기미션 해결 과정 요약이번주 미션의 필수 구현 과제는 무한 스크롤과 SEO 추가, 영화 검색 기능 구현하기였다. 추가 기능으로 영화 좋아요 기능을 구현했는데, 예전에 SNS를 만들때 경험해봤던 기능이라 쉽게 구현할 수 있을 것이라 기대했다. 하지만 예상과는 달리, 유저 식별 기능이 없다는 점이 문제였다. SNS 좋아요 기능 구현 당시에는 사용자 ID 기반으로 좋아요를 관리했지만 이번 프로젝트는 익명 유저 환경이라 데이터를 어떻게 저장하고 관리할지 고민이 필요했다.처음에는 movies, users, liked_movies 3개의 테이블을 생성하여 user_id, movies_id를 복합키로 설정해 브라우저별 익명 유저를 관리하는 방식을 시도했으나 구현 복잡도가 너무 높아지는 문제로 단순화하는 방식으로 변경했다.movies 단일 테이블에 like_count 필드를 추가하고 브라우저별로 좋아요 상태를 관리하는 방식으로 해당 기능을 구현했다. 이 방식의 단점은 브라우저 변경 시 개인별 좋아요 리스트를 추적할 수 없다는 점이지만 애초에 유저 식별 기능을 배제한 상황에서 선택할 수 있는 최적의 방식이라고 판단하여 적용했다.그리고 강의에서는 movies.id를 auto increment id로 구현했지만 더 나은 확장성을 위해서 uuid를 고려했다. 다만 uuid는 URL에서 사용하기 불편하여 가독성이 좋은 slug 칼럼을 별도로 추가하였다. API 요청 파라미터를 id에서 slug로 대체하면서 가독성과 SEO 최적화까지 함께 챙겨갈 수 있었다.slug Column 추가ALTER TABLE myreel_movies ADD COLUMN slug TEXT UNIQUE;중복되는 Row 제거 (제공되는 DB에 중복되는 데이터가 9건 발견되었다.)DELETE FROM myreel_movies WHERE id NOT IN ( SELECT id FROM ( SELECT id, title, order_index, ROW_NUMBER() OVER (PARTITION BY title ORDER BY order_index ASC) AS row_num FROM myreel_movies ) ranked WHERE row_num = 1 );영화 title 기준으로 slug 생성예시 - 'Dune: Part Two' -> 'dune-part-two'UPDATE movies SET slug = LOWER(REGEXP_REPLACE(title, '[^a-zA-Z0-9]+', '-', 'g')) WHERE slug IS NULL;과제 추가 구현 기능✅ 영화 좋아요 추가api/movies/:slug/likeconst likeMovie = async () => { try { const res = await fetch(`${baseUrl}${API_ENDPOINTS.LIKE(slug)}`, { method: 'POST', }) if (!res.ok) { throw new Error(CLIENT_ERROR.MOVIE_LIKE_FAILED.message) } const data: LikeMovieResponseDTO = await res.json() setLikeCount(data.like_count) // 서버에서 받아온 새로운 좋아요 수로 업데이트 setIsLiked(true) // 로컬 스토리지에 영화 추가 또는 업데이트 const likedMovies: LikedMovie[] = JSON.parse(localStorage.getItem('likedMovies') || '[]') // 이미 좋아요를 누른 영화가 있다면, likeCount를 업데이트 const existingMovieIndex = likedMovies.findIndex((movie) => movie.slug === slug) if (existingMovieIndex >= 0) { likedMovies[existingMovieIndex].likeCount = data.like_count // 좋아요 수 업데이트 } else { // 좋아요를 누른 적이 없다면 새로 추가 const newLikedMovie = { slug, likeCount: data.like_count } likedMovies.push(newLikedMovie) } localStorage.setItem('likedMovies', JSON.stringify(likedMovies)) } catch (error) { console.error(error) } }✅ 영화 좋아요 삭제api/movies/:slug/unlikeconst unlikeMovie = async () => { try { const res = await fetch(`${baseUrl}${API_ENDPOINTS.UNLIKE(slug)}`, { method: 'POST', }) if (!res.ok) { throw new Error(CLIENT_ERROR.MOVIE_UNLIKE_FAILED.message) } const data: LikeMovieResponseDTO = await res.json() setLikeCount(data.like_count) setIsLiked(false) // 로컬 스토리지에서 해당 영화 정보 삭제 const likedMovies: LikedMovie[] = JSON.parse(localStorage.getItem('likedMovies') || '[]') const updatedLikedMovies = likedMovies.filter((movie) => movie.slug !== slug) // 로컬 스토리지 갱신 localStorage.setItem('likedMovies', JSON.stringify(updatedLikedMovies)) } catch (error) { console.error(error) } }개인 챌린지 기능✅ 메인 페이지 최상단으로 가는 버튼 추가메인 페이지에서 500px 이상 스크롤 내릴 경우 최상단으로 이동하는 버튼 생성behavior: 'smooth' 로 부드럽게 이동 ✅ 검색 결과 없을 경우, 좋아요 많은 순 추천 영화 6개 노출되는 기능 구현좋아요가 많은 영화 외에도 최근 개봉한 영화 같은 다양한 리스트 제공 예정api/movies/most-liked👀 3주차 회고지난주에 적용했던 매니져 컴포넌트 / UI 컴포넌트로 분리하는 방식이 Container-Presentational Component 패턴 과 유사한 방식이라는 것을 다른 러너분의 발자국을 통해 알게되었다. 궁금해서 조금 더 찾아보니, 이 패턴은 과거 클래스형 컴포넌트 시절에는 HOC(High Order Component)와 함께 많이 사용되었지만, 함수형 컴포넌트에서도 여전히 유효한 방식이라는 것을 알게되었다. 이번주에는 기존 패턴을 유지하면서도, 비즈니스 로직을 최대한 커스텀 훅으로 분리하는 연습을 진행했다. 이를 통해 컴포넌트의 역할을 더욱 명확하게 나누고, 재사용성과 유지보수성을 높이는 방향으로 조금씩 개선되고 있다는 것을 체감했다.👻 배포 관련 이슈 (3월 22일 추가)4주차에 스터디 기간 개발한 4개의 프로젝트를 모두 배포하는것이 기존 스터디 일정이지만.. 시간적 여유가 생겨서 1~3주차 프로젝트를 미리 배포해봤다. vercel은 기존에 사용하던 툴이었는데 한번에 3개의 프로젝트를 배포하려고 시도하는 과정에서 수 많은 에러를 경험했다. 4주차 프로젝트 배포시, 추후 다른 프로젝트 배포시에 참고할 수 있도록 간단하게 정리해본다. ✅ @/components/... 앨리어스 관련 캐싱 이슈문제 개발 환경에서는 정상 작동하던 import가 Vercel 배포 시에만 Module not found 에러 발생원인Vercel의 캐싱 문제 또는 파일명 인식 관련 문제(대소문자, 내부 경로 변경 후 캐시 꼬임)해결@ 앨리어스 문제를 의심하여 상대 경로로 변경 후 재배포 시도 -> 해결 안됨컴포넌트 경로의 대소문자 확인후 재배포 시도 -> 해결 안됨 Title.tsx 파일명을 AppTitle.tsx로 변경하여 강제로 캐시 무력화 후 재배포 시도 -> 해결 ✅ params 비동기 처리 관련 타입 에러 (Next.js 15)문제page.tsx에서 params를 비동기적으로 처리하려 하자, params 타입이 Promise로 인식되어 타입 오류 발생원인 (깃헙 이슈 참고)Next.js 15 내부적으로 PageProps가 비동기적 처리를 기대하거나 타입 추론이 변경됨params 타입이 Promise<any>로 추론되어 관련 에러 발생PageParams 제네릭 타입 해석 충돌next dev에서는 정상 작동하지만 next build 시 오류 발생해결params의 인터페이스를 명시적 타이핑 -> 해결 안됨params의 타입 any로 명시하고 타입 단언으로 처리 -> 해결 안됨배포 시 안정성 확보를 위해서 Next.js 14 + React 18 버전으로 롤백 -> 해결✅ Tailwind CSS 적용 안됨문제배포된 페이지에서 Tailwindcss 클래스가 적용되지 않음원인Next.js 15 -> Next.js 14, React 19 -> React 18로 롤백하는 과정에서 관련된 의존성 충돌이 일어난것으로 예상됨해결Tailwindcss, postcss, autoprefixer 의존성 삭제 후 캐시 초기화 후 재설치 -> 해결 ✅ 환경 변수(NEXT_PUBLIC_BASE_URL) 미설정으로 fetch 실패문제빌드 시 fetch 요청이 localhost:3000으로 날아가면서 ECONNREFUSED 에러 발생원인Vercel 환경 변수 설정 시 NEXT_PUBLIC_BASE_URL 값을 localhost:3000으로 설정하여 에러 발생해결해당 환경 변수를 실제 배포 URL로 변경 후 재배포 시도 -> 해결

풀스택워밍업클럽3기회고발자국3주차

바다다다

[인프런 워밍업 스터디 클럽 3기 FE] 2주차 발자국

수강강의1: 따라하며 배우는 자바스크립트 A-Z수강강의2: 따라하며 배우는 리액트 A-Z[19버전 반영]Tip: 워밍업 스터디 클럽을 참여하게 되면 [할인쿠폰]으로 강의를 할인 된 가격에 수강할 수 있다!2주차 강의 수강2주차를 되돌아 보며..이번주에는 지난주 Javascript강의를 마무리하고 드디어 React에 대한 학습에 진입하게 되었다.Javascript후반부 강의에서는 주로 디자인 패턴, Iterator, Generator에 대해 알아가고 마지막으로 Todo app, spread sheet 등을 따라 만들어 보면서 Javascript문법에 대해 조금 더 익숙해지고 복습하는 시간을 가지었다.React는 초반에 설치와 프로젝트 환경설정을 시작으로 처음부터 앱을 따라 만들어 보면서 React의 구조에 대해 알아보았다. TODO앱과 Netflix따라 만들기 강의를 따라서 코딩을 진행을 하였지만 환경설정이나 알수없는 오류등이 발생해서 바로 미션 과제를 중점으로 학습을 진행하였다.강의에서 언급한 이론을 실제 미션 과제에서 실습으로 결과물을 얻을 수 있어서 뿌듯했다. 지난주 과제에 비해 난이도가 많이 높아져서 진도를 따라잡기에 많이 버거웠던것도 사실이다. (특히 직장인이라면...More and more...hard..)강의 내용을 다시 정리해보며..(주로 개인적으로 새롭게 알게된 것, 다시 기억해야하는 것들 위주로 정리했다)Generator Generator사용자의 요구에 따라 다른 시간 간격으로 여러 값을 반환할 수 있음일반 함수 → 한번 실행으로 끝까지 실행됨generator 함수 → 일시적으로 정지될 수도 있고, 다시 시작할 수도 있음function* sayNumbers() {...} 별표로 generator함수 생성yield 함수를 정지시킴 → 일반함수에서 return과 동일function* sayNumbers(){ yield 1; yield 2; yield 3; } const number = sayNumbers() console.log(number.next()) // {value:1, done:false} console.log(number.next()) // {value:2, done:false} console.log(number.next()) // {value:3, done:false} console.log(number.next()) // {value:undefined, done:true} lazy evaluation에 활용가능계산의 결과값이 필요할 때까지 계산을 늦춰서 필요한 데이터를 필요한 순간에 생성Singleton Pattern클래스의 인스턴스화를 하나의 객체로 제한하는 디자인 패턴. 시스템 전체에서 작업을 조정하는 데 정확히 하나의 객체가 필요한 경우에 유용하다클래스가 존재하지 않는 경우 클래스의 새 인스턴스를 생성하는 메서드로 클래스를 생성하여 구현할 수 있음. 인스턴스가 이미 존재하는 경우 해당 개체에 대한 참조를 반환⇒ 객체는 1개만 만들고 그것을 계속 재사용하는 것 (값이 한곳에서 바뀌면 전역으로 바뀜)Factory Pattern특수 함수인 팩토리 함수를 사용하여 비슷한 객체를 많이 만들 수 있음⇒ 비슷한 객체를 반복적으로 생성해야하는 경우 사용Mediator Pattern (중재자 패턴)객체 그룹에 대한 중앙 권한을 제공한다. (예: 채팅방)Observer Patternevent-driven시스템을 이용하는 것 (예: facebook알림)topic에 대해 register하고 publisher-subscriber구조를 가지게 됨Module Pattern코드를 더 작고 재사용 가능한 조각으로 분할하는 것을 도와준다js코드를 작성하고 export지시자를 변수나 함수 앞에 붙이면 외부 모듈에서 해당 변수나 함수에 접근가능하게 된다import로 외부 모듈을 가져올 수 있음모듈은 특수한 키워드나 기능과 함께 사용되므로 HTML에서 script태그를 넣을 때 type='module' 속성을 추가해줘야 한다항상 strict모드로 실행된다지연 실행 된다인라인 모듈 스크립트도 비동기 처리할 수 있다외부 orgin에서 스크립트를 불러오려면 CORS헤더가 있어야 한다중복된 스크립트는 무시한다 (최초 호출될 때 한번만 실행)구형브라우저에서는 <script nomodule> ... </scirpt> 로 module을 지원하지 않을 경우 예외처리를 할 수 있음리액트는 프레임워크가 아니라 라이브러리React는 라이브러리, Vue, Angular는 프레임워크왜 라이브러리? 리액트는 전적으로 UI를 렌더링하는 것에 관여하기 때문상태관리, 라우팅, 테스트 등등을 위해 다른 라이브러리가 추가적으로 필요함 (vue, angular는 이런 것들이 이미 포함되어 있음)프레임워크: 어떠한 앱을 만들기위해 필요한 대부분의 것을 가지고 있는 것라이브러리: 어떠한 특정 기능을 모듈화 해놓은 것프레임워크는 라이브러리의 집합리액트 컴포넌트 - Component리액트 앱을 이루는 최소한의 단위, 여러 컴포넌트를 조합하여 하나의 페이지가 완성되는 것클래스형 컴포너트 - class component함수형 컴포넌트 - funcional component브라우저가 그려지는 원리와 가상돔리액트의 주요 특징 중 하나가 가상돔을 사용하는 것이다웹 페이지 빌드 과정 (CRP) - Critical Render Pathbrowser가 서버에서 페이지에 대한 HTML문서를 응답으로 받고, 해당 문서를 읽는다. 그 후 스타일을 입히고 뷰포트에 표시하게 된다DOM tree생성 → Render Tree생성(화면에 표시되는 모든 노드의 콘텐츠 및 스타일 정보를 포함) → Layout(reflow) → Paint(화면에 그리기)화면에서 DOM에 변화가 생기면 Render Tree부터 다시 랜더링 해야한다는 문제점!! → 비효율적→ 가상돔: 실제 DOM을 메모리에 복사해준 것데이터가 바뀌면 가상돔에 랜더링되고 이전에 생긴 가상돔과 비교해서 바뀐 부분만 실제 돔에 적용 시킴. 바뀐 부분을 찾는 과정을 diffing이라고 부르고, 바뀐 부분만 실제 돔에 적용시켜주는 것을 재조정(reconciliation)이라 한다.  강의 내용을 들으면서 아쉬웠던 점.. 강의 내용에 대해서 불만족 스러운 부분은 없다! 너무 알차게 구현되어진 강의이고 다만 진도에 따라서 많은 양의 강의를 한꺼번에 들어야 하다보니 모두 소화하기에는 어려웠다.강의 외에 미션과제도 수행해야 하다 보니 시간적으로 지난주에 비해 많이 부족했다. 아래 미션과제에서도 볼 수 있겠지만, 지난주와 달리 기본적인 모든 기능을 구현하지는 못했다.(특히! 디즈니플러스 클론코딩ㅜ)2주차 미션 완료Javascript미션은 지난주에 이어서 그나마 완성하기 했으나,React미션은 초보자의 입장에서 강의내용 수준으로 따라가기에는 어려웠다.처음이다보니 모든게 낯설게 느껴졌고, 그래도 다른 강의 및 자료를 통해 많은 학습을 할 수 있었다.특히 컴포넌트를 나누는 기준과 React에서 컴포넌트를 조합해서 웹 페이지를 구성한다는 것을 미션 실습과제를 통해 좀 더 쉽게 깨달을 수 있었던 것 같다. Javascript - Mission6: 비밀번호 생성기Demo: https://rim0703.github.io/React-study/Javascript/6-pw-generator/Source Code: https://github.com/rim0703/React-study/tree/main/Javascript/6-pw-generator Javascript - Mission7: 타이핑 테스트Demo: https://rim0703.github.io/React-study/Javascript/7-typing-test/Source Code: https://github.com/rim0703/React-study/tree/main/Javascript/7-typing-test React - Mission1: 예산 계산기Demo: https://gentle-starburst-b5ca45.netlify.app/Source Code: https://github.com/rim0703/React-study/tree/react/mission1 React - Mission2: 디즈니 플러스 앱Demo: https://disney-plus-clone-mission2.netlify.app/Source Code: https://github.com/rim0703/React-study/tree/react/mission2참고자료: https://www.youtube.com/watch?v=3NHYl0Lo74A이건 할말이 많은데.. 사실 아래 데모처럼 메인화면만 완성했다. 맨땅에서 시작하기에는 너무 막막해보여서 유튜브에 클론코딩 영상이 있어서 따라 만들어보았다.추가로 메인 로그인 화면, 영화 상세 내용 팝업화면이 추가로 구현일 필요하다.(추후 시간이 있으면 보완할 예정!)마지막으로 2주차 회고이번 한주간은 너무 정신없이 지난거 같다. 강의, 미션과제 외에 처음으로 접하는 React 내용에 설정이나 오류 등에 대해 검색하면 이슈 attack을 하기 위해 많이 시간이 필요했다. 이런 부분은 점차 많이 연습해보면 적응하지 않을까 싶다.진도표에 따르면 이제 3일만 더 수강하면 강의내용과 미션은 끝나는 일정이다. 최대한 남은 시간에 React강의를 끝내고 미션 과제 구현에 집중할 계획이다. 단기간에 많은 시간과 노력을 쏟아올린 만큼 프론트엔드에서 React에 대한 부담감(?) 두려운 마음을 조금이나마 떨쳐보고자 한다.강의수강이 끝나면 지금까지 구현한 미션 과제에 대해 보완할 부분이 있는지, 그리고 지금까지 강의를 들으면서 노션에 정리해두었던 내용을 다시 한번 복습할 계획이다.마지막 남은 기간도 화이팅 해보자!!!<끝>

프론트엔드워밍업스터디3기프론트엔드2주차발자국JavascriptHTMLCSSReact

suover

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

Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드강의와 함께한 인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드 (Java, Spring Boot)2주차 발자국 입니다.학습 내용 요약이번 주는 클린 코드 작성법에 이어 코드 구조와 리팩토링, 그리고 테스트 코드 작성법을 본격적으로 학습했습니다.1⃣ 주석의 양면성주석은 코드 자체로 표현하기 어려운 의사결정의 히스토리를 기록할 때 사용해야 합니다.자주 변하는 정보는 주석으로 표현하면 안 되고, 주석은 코드와 함께 꾸준히 관리해야 합니다.부정확한 주석은 주석이 없는 코드보다 더 해롭습니다.2⃣ 변수와 메서드의 나열 순서변수는 사용하는 순서대로 나열하고, 공개 메서드를 먼저 배치하여 외부 세계에 제공하는 기능을 명확히 드러냅니다.비공개 메서드는 공개 메서드에서 호출되는 순서로 나열하여 코드 읽기 흐름을 자연스럽게 만듭니다.나열 순서도 의도를 표현하는 중요한 수단이 될 수 있음을 배웠습니다.3⃣ 패키지 나누기패키지 구조는 도메인 문맥을 반영하여 설계하며, 너무 잘게 나누거나 너무 크게 뭉치는 것을 지양해야 합니다.팀 내 합의가 중요하며 초기 설계 단계에서 패키지 구조를 충분히 고민해야 합니다.4⃣ 적정 기술의 사용모든 기술은 적정한 수준과 시점에서 활용해야 하며, 무리한 추상화나 오버 엔지니어링은 오히려 개발 효율성을 떨어뜨립니다.클린 코드는 만능 해결책이 아니며, 상황과 목적에 맞는 균형 잡힌 접근이 중요합니다.5⃣ 테스트 코드와 TDD자동화된 단위 테스트(JUnit5, AssertJ)를 통해 신뢰성과 유지보수성을 높이고, 빠른 피드백을 받을 수 있습니다.TDD(Test Driven Development)를 통한 선(先) 테스트 작성, 후(後) 기능 구현 방식을 이해했습니다.Given-When-Then 구조를 이용한 BDD(Behavior Driven Development) 스타일로 작성하면 도메인 로직을 명확히 표현할 수 있습니다.미션 해결 과정🎯 스터디카페 이용권 선택 시스템 리팩토링 미션미션'스터디 카페 이용권 선택 시스템'의 기존 시스템에 중복된 로직과 혼재된 책임을 명확히 분리하고, 객체지향 원칙을 준수하여 코드 구조를 개선하는 리팩토링 미션입니다.접근 관점추상화 레벨 맞추기: 중복 로직을 별도 메서드로 추출했습니다.객체의 책임 분리: StudyCafeOrder 객체를 추가해 할인 계산 로직을 전담했습니다.DIP(의존성 역전 원칙): 파일 접근 로직을 인터페이스(StudyCafeRepository)로 추상화하여 확장성을 확보했습니다.해결 과정1. 기존의 복잡한 분기 조건(if-else)을 메서드로 깔끔하게 정리했습니다.Before: if (HOURLY) { ... } else if (WEEKLY) { ... } else if (FIXED) { ... } 로직이 길고, 사물함 로직도 그 안에서 처리.After:public void run() { // 1) passType 선택 (시간권/주단위/고정석) StudyCafePassType passType = inputHandler.getPassTypeSelectingUserAction(); // 2) pass 선택 StudyCafePass selectedPass = selectPass(passType); // 3) 사물함 선택 (고정석 only) StudyCafeLockerPass lockerPass = maybeSelectLocker(selectedPass); // 4) 주문 객체 생성 & 결과 출력 StudyCafeOrder order = new StudyCafeOrder(selectedPass, lockerPass); outputHandler.showOrderResult(order); }이점: 각 단계가 메서드로 분리되어 이해하기 쉬움. passType별로 중복된 분기 로직이 크게 줄었음.      2. 할인 및 총금액 계산 로직을 전담하는 객체를 만들어 로직을 명확히 했습니다.Before: 할인금액, 총액 계산 로직이 OutputHandler.showPassOrderSummary 안에 위치.After: StudyCafeOrder라는 도메인 객체가 이 로직을 담당.public int getDiscountAmount() { return (int) (selectedPass.getPrice() * selectedPass.getDiscountRate()); } public int getTotalPrice() { int discountPrice = getDiscountAmount(); int lockerPrice = (lockerPass != null) ? lockerPass.getPrice() : 0; return selectedPass.getPrice() - discountPrice + lockerPrice; }이점: 필요 시 “할인 정책”이 바뀌어도 StudyCafeOrder만 수정하면 되며, 출력부는 깔끔하게 유지.   3. 파일 접근 방식을 인터페이스와 구현체로 나누어 유지보수와 확장성을 높였습니다.Before: StudyCafeFileHandler라는 클래스에서 직접 파일 접근 + 변환.After: StudyCafeRepository(추상) + StudyCafeFileRepository(구현).StudyCafePassMachine은 StudyCafeRepository repository에만 의존 → DIP 준수.향후 DBRepo나 InMemoryRepo 추가 시 StudyCafeRepository만 구현하면 됨. 회고리팩토링을 통해 코드 가독성과 유지보수성이 눈에 띄게 향상되었습니다. 특히 객체지향 원칙(SRP, DIP)을 철저히 적용한 결과, 각 객체가 단 하나의 명확한 책임을 가지면서, 향후 변경이 발생해도 최소한의 수정만으로 대응 가능해졌습니다. 규칙의 절대성이 아닌 "적정선"을 찾는 것이 매우 중요하다는 점을 깊이 깨달았습니다.🔗 리팩토링 미션 깃허브 링크회고스스로 칭찬하고 싶은 점학습한 내용을 즉시 코드에 적용하며 원칙을 체화하고자 노력한 점단순한 기능적 개선을 넘어, 구조적이고 근본적인 리팩토링을 고민한 점아쉬웠던 점 & 보완할 점코드 리뷰에서 코드 확장성과 유지보수성을 높이기 위한 설계가 부족했음을 느꼈습니다. 다음에는 코드 확장성과 유지보수성을 고려한 구조를 더욱 더 고민해 보겠습니다.지나치게 추상화에 치우치는 경향을 느꼈고, 다음에는 더 실용적인 균형을 유지하려 합니다.다음 주 학습 목표다음 주부터는 TDD 기반으로 더욱 실질적이고 구체적인 테스트 코드 작성을 연습할 계획입니다.리팩토링과 테스트가 결합된 실습을 통해, 코드를 더욱 안정적으로 관리하는 방법을 익힐 예정입니다.이번 주 스터디를 통해 "코드는 언제나 적정한 수준의 추상과 구체 사이에서 균형을 잡아야 한다"는 중요한 원칙을 다시 한번 실감했습니다. 앞으로도 적절한 추상화와 구체적인 구현 사이의 "적정선"을 끊임없이 고민하며, 클린하고 안정적인 코드를 지속적으로 작성하는 개발자가 되도록 노력하겠습니다.감사합니다!

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

lch9502

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

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

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

천준민

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

📖 강의 요약 1⃣ 코드 다듬기 주석의 양면성"좋은 주석"우리가 가진 모든 표현 방법을 총 동원해 코드에 의도를 녹여 내고, 그럼에도 불구하고 전달해야 할 정보 가 남았을 때 사용(메서드,변수,클래스) 네이밍에서 의도가 드러나면 주석을 대신 할 수 있을 것 같다. 변수와 메서드의 나열 순서- 변수는 사용하는 순서대로 나열 - 공개 메서드 끼리 기준을 가지고 배치하는 것이 좋음 - 또한 공개 메서드의 배치에 따라서 비공개 메서드의 순서도 고려 해야 한다. - 상태 변경 >> 판별 >= 조회 "결론"나열 순서로도 의도와 정보를 전달 할 수 있다는 것 패키지 나누기"결론"패키지는 문맥으로써 정보를 제공할 수 있다. 2⃣ 기억하면 좋은 조언들 복잡하거나 엉망인 코드를 이해하려 할때 리팩토링 하면서 읽기공백으로 단락 구분하기필요한 적정 수준보다 더 높은 수준의 엔지니어링이 아닌지 생각해보기 Practical Testing: 실용적인 테스트 가이드 1⃣ 테스트는 왜 필요할까커버할 수 없는 영역 발생, 경험과 감에 의존, 늦은 피드백, 유지보수 어려움"올바른 테스트 코드"자동화 테스트로 비교적 빠른 시간안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있다.가까이서 보면 느리지만 멀리서 보면 빠르다. 2⃣ 단위 테스트 작은 코드 단위를 독립적으로 검증하는 테스트프레임 워크 : Junit / 라이브러리 : AsserJ테스트 코드를 작성 할 때는 해피 케이스만 하는 것이 아니라 예외케이스도 함께 작성 해야한다. 3⃣ TDD : Test Driven Development4⃣"TDD"프로덕션 코드 보다 테스트 코드를 먼저 작성해 테스트가 구현 과정을 주독하도록 하는 방법론 = 테스트 주도 개발선 테스트 - > 기능 구현 💡 미션 [섹션 7. 리팩토링 연습]의 "연습 프로젝트 소개" 강의를 보고, '스터디 카페 이용권 선택 시스템 프로젝트에서 지금까지 배운 내용을 기반으로 리팩토링을 진행해 봅시다' 미션 제출 url : https://github.com/2unmini/readable-code/tree/mission/day7 이번 미션에 대한 목표는 배운 내용+ 내가 알고 있는 내용을 가지고 생각해 봤을 때 이유가 충분 한지 판단하여 리팩토링을 해보았지만 다음 날 결과 비교를 해보았을 때는 많이 달랐지만 리팩토링 과제를 하는 동안 이유에 대해 많이 생각해보는 시간이였다. 💬 회고 👍 잘한 점인프런 가입 이후로 처음으로 강의 완료율 100% 달성일급 컬렉션을 적용하여 리팩토링 한점😮‍💨 아쉬웠거나 보완하고 싶은 점System.out.println()을 메소드를 추출할려고 생각했던 점이 아쉬웠다.배운 내용을 다 적용하지 않고 상황에 맞게 적용해 보고 싶다. 📎출처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기backend워밍업클럽인프런

채널톡 아이콘