블로그

Depth

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

 Section7) Mock을 마주하는 자세Test Double의 종류1) Dummy : 아무것도 하지 않는 깡통 객체2) Fake : 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체 (ex, FakeRepository)3) Stub : 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체, 그 외에는 응답하지 않는다. //상태 검증 (State Verification)4) Spy : Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체, 일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있다,5) Mock : 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체 //행위 검증 (Behavior Verification).BDDMockitoBDDMockito는 Mockito에서 BDD 스타일에 맞추어 모든것의 이름만 바꾼 것이다.일례로 given 구역에 .when()을 쓰면 이질감이 들기 떄문에 BDDMockito가 생긴 것이다..Classicist VS Mockist1) Classicist- 꼭 필요한 경우에만 mocking을 쓰고 왠만하면 진짜 객체로 테스트 하자는 주의이다.2) Mockist- 모든 것을 Mocking 위주로 하여 테스트 하자는 주의이다..Section8) 더 나은 테스트를 작성하기 위한 구체적 조언1) 한 문단에 한 주제!- DisplayName을 한 문장으로 구성할 수 있어야 한다~!2) 완벽하게 제어하기3) 테스트 환경의 독립성을 보장하자- 테스트에서는 팩토리 메서드를 지양하고 Buider나 생성자를 통해 최대한 독립성을 보장해서 given절을 구성하는게 좋다.4) 테스트 간 독립성을 보장하자- 테스트간의 공유자원 사용하지 않기.- 테스트간의 독립성 보장하기~!5) 한 눈에 들어오는 Test Fixture 구성하기- Fixture : 고정룰, 고정되어 있는 물체- Test Fixture : 테스트를 위해 원하는 상태로 고정시킨 일련의 객체6) Test Fixture 클렌징- @Transactional, @rollback 방식과 deleteAllInBatch(), deleteAll() 등을 잘 알고 사용해야 한다.7) @ParameterizedTest8) @DynamicTest9) 테스트 수행도 비용이다. 환경 통합하기- @DataJPATest 보다 @SpringbootTest 사용을 권장한다.- 컨트롤러(통합) 테스트시 @WebMvcTest의 Controllers 옵션을 통해 특정 컨트롤러들만 지정하여 테스트해줌으로써 테스트 수행 비용을 줄여준다.10) private 메서드는 테스트할 필요가 없다.11) 테스트에서만 필요한 메서드 중 생성자 등은 프로덕션 코드에 생성해도 좋다..Section9) AppendIx1) 학습 테스트- 잘 모르는 기능, 라이브러리, 프레임워크를 학습하기 위해 작성하는 테스트- 여러 테스트 케이스를 스스로 정의하고 검증하는 과정을 통해 보다 구체적인 동작과 기능을 학습할 수 있다.- 관련 문서만 읽는 것보다 훨씬 재미있게 학습할 수 있다.2) Spring Rest Docs- 테스트 코드를 통한 API 문서 자동화 도구- API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원활하게 한다.- 장점→ 테스트를 통과해야 문서가 만들어진다 (신뢰도가 높다)→ 프로덕션 코드에 비침투적이다.- 단점→ 코드 양이 많다.→ 설정이 어렵다..Day-15 Mission) 레이어드 아키텍처에서의 테스트 코드 작성강의로 배운 레이어드 아키텍처 관련 내용을 확장시켜 정리할 수 있어 좋았다.과제 Link.Day-18 Mission) Test Double 어노테이션 이해 및 테스트 항목 배치@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks의 차이점을 정리하고 given-when-then 전략으로 테스트 코드 배치 방법을 고민해 볼 수 있어 좋았다.과제 Link..회고오늘로써 인프런 워밍업 클럽 스터디 2기 대장정이 마무리 되었다.클린코드와 테스트코드는 누구나 중요성을 알지만, 혼자서 공부하기 쉽지 않은 부분이다.강의를 완벽하게 이해하지는 못했지만, 강의를 통해 Clean Code와 Test Code에 한 발자국 더 다가갈 수 있었고, 실무에도 점차적으로 적용 해보면서 뜻 깊은 경험을 할 수 있었다.다음에 인프런 스터디 3기가 열린다면 재참여를 고려해 봐야겠다마지막으로 강의와 스터디를 만들어주신 박우빈님과 스터디 관리에 힘써주신 셰리 매니저님께 감사의 마음을 전합니다..출처https://inf.run/jsvaAhttps://inf.run/kHiWM   

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

Depth

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

 3주차부터 Practical Testing (실용적인 테스트 가이드) 강의로 넘어가게 되었다.  Section1) Intro강의에서 학습하게 될 것1) 테스트 코드가 필요한 이유2) 좋은 테스트 코드란?3) 실제 실무에서 진행하는 방식 그대로 테스트를 작성해가면서 API를 설계하고 개발하는 방법4) 정답은 없지만, 오답은 있다! 구체적인 이유에 근거한 상세한 테스트 작성 팁 Section2) 테스트는 왜 필요할까?1) 테스트는 왜 필요할까?- 테스트 코드를 작성하지 않는다면 코드의 변화가 생기는 매 순간마다 발생할 수 있는 모든 Case를 고려해야 한다..2) 테스트 코드가 병목이 된다면?- 테스트코드 자체가 유지보수하기 어려워진다.- 잘못된 검증이 이루어질 가능성이 생긴다..3) 올바른 테스트 코드는?- 자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있다. Section3) 단위 테스트Junit5로 테스트하기1) 단위 테스트- 작은 (ex, 클래스 or 메서드) 코드 단위를 독립적으로 검증하는 테스트- 검증속도가 빠르고, 안정적이다.2) Junit5- 단위 테스트를 위한 테스트 프레임워크- XUnit - Kent Back, - Sunit (Samltalk), Junit(Java), NUnit(.NET)- Spring-boot-starter-test 라이브러리를 통해 사용 가능하다..3) AssertJ- 테스트코드 작성을 원활하게 돕는 테스트 라이브러리- 풍부한 API, 메서드 체이닝 지원- Spring-boot-starter-test 라이브러리를 통해 사용 가능하다.테스트 케이스 세분화1) 해피 케이스- 요구사항을 그대로 만족하는 케이스- 경계값 테스트 : 범위 (이상, 이하, 초과, 미만), 구간, 날짜 등이 중요.2) 예외 케이스- 경계값 테스트 : 범위 (이상, 이하, 초과, 미만), 구간, 날짜 등이 중요.테스트하기 어려운 영역 분리하기1) 테스트하기 어려운 영역- 관측할때마다 다른 값에 의존하는 코드→ 현재 날짜/시간, 랜덤 값, 전역변수/함수, 사용자 입력 등- 외부 세계에 영향을 주는 코드→ 표준 출력, 메시지 발송, 데이터베이스에 기록하기 등.2) 순수 함수 - 테스트하기 쉬운 코드- 같은 입력에는 항상 같은 결과- 외부 세상과 단절된 형태- 테스트하기 쉬운 코드  Section4) TDDTDD (Test Driven Development)1) TDD- 프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론.2) TDD의 핵심 가치-기존) 선 기능 구현, 후 테스트 작성의 문제점→ 테스트 자체의 누락 가능성→ 특정 테스트(==해피 케이스) 케이스만 검증할 가능성→ 잘못된 구현을 다소 늦게 발견할 가능성- TDD) 선 테스트 작성, 후 기능 구현→ 복잡도가 낮은(==유연하며 유지보수가 쉬운), 테스트 가능한 코드로 구현할 수 있게 한다.→ 쉽게 발견하기 어려운 엣지(Edge) 케이스를 놓치지 않게 해준다.→ 구현에 대한 빠른 피드백을 받을 수 있다.→ 과감한 리팩토링이 가능해진다.Section5) 테스트는 [문서]다테스트는 [문서]다1) 테스트 == 문서- 프로덕션 기능을 설명하는 테스트 코드 문서- 다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완- 어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서, 모두의 자산으로 공유.DisplayName을 섬세하게1) DisplayNamed을 섬세하게 적기- 메서드 자체의 관점보다 도메인 정책 관점으로, 도메인 용어를 사용하여 한층 추상화된 내용을 담기- 테스트의 현상(ex, 성공한다… 실패한다 등)을 중점으로 기술하지 말 것.BDD 스타일로 작성하기1) BDD (Behavior Driven Development)- TDD에서 파생된 개발 방법으로- 함수 단위의 테스트에 집중하기보다, 시나리오에 기반한 테스트케이스(TC) 자체에 집중하여 테스트하는 기법- Given / When / Then 방식 사용→ Given : 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등)→ When : 시나리오 행동 진행→ Then : 시나리오 진행에 대한 결과 명시, 검증.Mission 12) 단위 테스트 작성studycafe 프로젝트에서 InputHandler, StudyCafePassOrder, StudyCafeSeatPass 클래스에 대한 테스트 코드를 작성하였다.고민했던 점으로는 Scanner를 통해 사용자 입력을 받는 로직을 테스트하기 위해 Mock을 사용할 수 있지만, Mock 대신 InputStream과 System.setIn()을 통해 입력해주는 방식을 선택했다.작은 메서드 단위로 단위 테스트를 진행하니 코드도 복잡해지지 않고 간결하여 테스트 하기가 수월하였다.Github Code. Section6) Spring & JPA 기반 테스트레이어드 아키텍처(Layered Architecture)1) Layered Architecture- Presentation Layer, Business Layer, Persistence Layer로 구분- 아키텍처를 분리하는 이유 : 관심사의 분리.Spring / JPA 훑어보기 & 기본 엔티티 설계1) Library vs Framework- Library : 외부에서 이미 개발된 코드를 가져온다. (내 코드가 주체가 되어 동작)- Framework : 이미 동작하는 환경이 구성되어 있고, 내 코드는 프레임안에서 수동적으로 동작.2) Spring- IoC (inversion of Control) : 객체의 생성과 의존성 관리를 프레임워크에서 대신 수행- DI (Dependency Injection) : 외부에서 객체 간의 의존성 주입을 통해 결합도를 낮춰줌- AOP (Aspect Oriented Programming) : 공통 관심사를 별도의 로직으로 분리해 코드 중복을 줄여주고 모듈화.3) JPA-Java Persistance API- Java 진영의 ORM 기술 표준- 반복적인 CRUD SQL을 생성 및 실행해주고, 여러 부가 기능들을 제공- Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA 제공- 주로 사용되는 어노테이션들→ @Entity, @Id, @Column→ @ManyToOne, @OneToMany, @OneToOne, @ManyToMany. Layer별 테스트1) Persistence Layer- Data Access의 역할- 비즈니스 가공 로직이 포함되어서는 안됨, Data에 대한 CURD에만 집중한 레이어.2) Business Layer- 비즈니스 로직을 구현하는 역할- Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)를 통해 비즈니스 로직을 전개- 트랜잭션을 보장.3) Presentation Layer- 외부 세계의 요청을 가장 먼저 받는 계층- 파라미터에 대한 최소한의 검증을 수행.회고- 테스트 코드 작성을 의무적으로 작성 하지 않는 환경에서 일을 해왔는데, 스터디를 진행하면서 단위 테스트 코드 작성의 중요성과 필요성을 깨달았다. 모든 예외 케이스를 조기에 발견해서 대처할 순 없겠지만 내가 작성한 코드 범위내에서 테스트가 필요한 영역들을 분리하여 로직이 의도한 대로 동작하는지 검증하는 작업은 백엔드 개발자의 숙명이라는 생각이 들었다.이제 Mock과 테스트 방법론에 대한 강의가 남아있는데, 마지막 주차도 지금처럼 묵묵히 학습해 나갈 것이다.출처https://inf.run/jsvaAhttps://inf.run/kHiWM      

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

Depth

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

 강의 수강 Section6) 코드 다듬기좋은 주석인란? -우리가 가진 모든 표현 방법을 총동원해 코드에 의도를 녹여냈음에도 불구하고 전달해야할 정보가 남았을때 주석을 사용해야 함. 주석을 사용하면 코드가 아닌 주석에 의존한다는 것을 알아야 함   변수와 메서드의 나열 순서- 변수는 사용하는 순서대로 나열- 메서드는 1순위가 공개 메서드, 2순위가 비공개 메서드 순으로 나열     Intellij IDE 활용- 코드 포맷 정렬 단축키 : Ctrl + ALT + L- Sonarlint : 오류, 버그, 스타일 등을 알려주어 문제점 개선을 도와주는 Plugin- .editorConfig : 확장자마다 스타일을 다르게 줄수 있게 도와주는 설정파일 Section7) 리팩토링 연습리팩토링 포인트- 추상화 레벨 : 중복 제거, 메서드 추출, 객체에 메시지 보내기- 객체의 책임과 응집도 : IO 통합, 일급 컬렉션, display()의 책임, Order 객체- 관점의 차이로 달라지는 추상화 : 구현에 초점을 맞춘 추상화 VS 도메인 개념에 초점을 맞춘 추상화 Section8) 기억하면 좋은 조언들능동적 읽기- 복잡하거나 엉망인 코드를 읽고 이해하려 할때, 리팩토링하면서 읽기-> 공백으로 단락 구분-> 주석으로 이해한 내용 표기하며 읽기-> 메서드와 객체로 추상화 해보기- 핵심 목표는 우리의 도메인 지식을 늘리는 것이고, 이전 작성자의 의도를 파악하는 것.2. 오버 엔지니어링- 필요한 적정 수준보다 더 높은 수준의 엔지니어링을 말함-> ex) 구현체가 하나인 인터페이스 : 구현체를 수정할 때마다 인터페이스도 수정해야 함.-> ex) 너무 이른 추상화 : 정보가 숨겨지기 때문에 복잡도가 높아지고, 후대 개발자들이 선대의 의도를 파악하기가 어려움  3. 은탄환은 없다- 항상 정답인 기술은 없음.- 오버 엔지니어링이 되더라도 한계까지 리팩토링 연습을 해보고, 적정 수준, 적정 시점을 깨닫기가 필요. 회고)2주차까지 쉼 없이 달려왔는데, readable code 강의가 끝난것이 끝이 아니라, 새로운 시작인 것 같다.이 강의를 통해 클린코드로 가는 발검을을 한 발자국 내딛은것 같다. 클린코드 책도 한번 읽어봐야 겠다는 생각도 들었다.3주차부터 Practical Testing 강의가 시작되는데, 테스트 코드 강의도 빠짐없이 수강하여 포기하지 않고 스터디를 끝까지 이어 나갈 것이다. 미션Day7-Mission3) '스터디 카페 이용권 선택 시스템' 프로젝트 리팩토링 리팩토링 진행 내용코드 중복 제거- if문 내에 중복된 지역 범위 코드들을 전역화메서드로 추출- 동일한 맥락의 코드들을 메서드화 NullPointException 방지 - Null 값 처리를 위한 Optional 사용메서드 오버로딩 추가 - Null 매개변수 사용을 외부에 드러내지 않기 위해 메서드를 오버로딩하여 사용Github Code 회고- 추상화 관점에서의 리팩토링을 어느정도 진행하였는데, 객체의 책임과 응집도 관점에서 좀 더 리팩토링을 진행해야 겠다는 생각과 함께 리팩토링에는 끝이 없다는 것을 느끼고 있다. 어쩌면 내가 진행한 readable한 code가 관점에 따라 누군가에게는 그렇지 않을수도 있다는 생각이 든다.  출처https://inf.run/jsvaAhttps://inf.run/kHiWM

백엔드워밍업클럽클린코드2기depthmy

Depth

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

 Section2) 추상Clean Code를 추구해야 하는 이유나를 포함해 다른 개발자가 코드를 읽고 이해하는데 드는 시간이 절약된다.프로그램의 정의프로그램 = (데이터가 담긴) 변수 + (변수를 사용하는) 메서드의 집합추상중요한 정보는가려내어 남기고, 덜 중요한 정보는생략하여 버리는 것적절한 추상화 : 도메인 안에서, 정말 중요한 핵심 개념만 남겨서 표현하는 것이다. 추상화의 가장 대표적인 행위는 이름을 짓는 것이다.(변수) 이름 짓기 Tip1) 단수와 복수 구분- 끝에 '(e)s'를 붙여 구분2) 이름 줄이지 않기- 일반적으로 무엇이든 이름을 줄여서 사용하는 것은 가독성을 제물로 바쳐 효율성을 얻는 것인데,유지보수 관점에서 득보다 실이 크다.- 관용어 처럼 자주 사용하는 것은 줄여도 괜찮다.=> ex, column -> col, latitude -> lat=> count -> cnt (추천X)3) 은어/방언 사용X- 현재의 팀만 아는 용어 사용 금지4) 좋은 코드를 보고 습득하기메서드와 추상화한 메서드는 반드시 한가지 일만 해야 한다.2가지 일을 하게된다면 추상화된 내용(==메서드 선언부)을 보고 구체적인 내용(메서드 구현부)의 유추가 어렵다.추상화 레벨외부 세계(==추상화 레벨이 높은 세게)와 내부세계(==추상화 레벨이 낮은 구체)를 나누었을때 추상화 레벨이 달라지는데, 하나의 세계 안에서 각 로직의 추상화 레벨은 동등해야 한다.매직 넘버, 매직 스트링매직 넘버(스트링) : 의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등을 말한다.Mission 1) 생각나는 추상과 구체의 예시Execute Login1) 클라이언트가 서버에 암호화된 ID와 Password를 HTTP Body에 담아 Request를 보낸다.2) 서버는 ID와 Password를 복호화하고 각 필드에 대한 유효성 검사를 한다.3) 유효하다면, 복호화된 ID와 Password를 DB에 보내 가입된 유저인지 확인한다.4) 가입된 유저라면, Session Cookie 혹은 JWT 등에 사용자 정보를 담아 클라이언트에 Response로 보낸다.Section3) 논리, 사고의 흐름뇌 메모리 적게 쓰기최소의 인지적 노력으로 (뇌 메모리를 줄여) 최대의 정보를 제공해야 한다,Early returnelse (if) 대신 return을 사용하는 것을 권장한다,.사고의 Depth 줄이기1) 중첩 반복문을 메서드 혹은 Stream을 통해 개선하면 좋다.2) 사용할 변수는 가깝게 선언하기공백라인도 의미를 가진다.부정어if문에서 부정어(!) 사용시 메서드화하여 분리하기해피케이스와 예외처리1) 예외가 발생할 가능성을 낮추는게 좋다. (ex, 사용자 입력, 객체 생성자, 외부 서버의 요청 등)2) 의도한 예외(ex, Custom Exception)와 예상하지 못한 예외 구분하기3) NullPointException은 항상 발생하지 않게 해야 한다.- 메서드 설계시 return null을 자제하고, Optional 사용을 고려하기 - Optional의 orElse(), orElsGet(), orElseThrow() 메서드의 차이 이해 필요.Section4) 객체지향 패러다임객체 설계1) 새로운 객체 생성시 주의사항- 1개의 관심사로 명확하게 책임이 정의되었는지 확인 필요- 생성자, 정적 팩토리 메서드에서 유효성 검증이 가능함을 인지- setter 사용 자제- getter도 사용 자제하고, 반드시 필요한 경우에만 추가 - 필드의 수는 적을수록 좋다2) 도메인 지식은 만드는 것이 아니라 발견하는 것Mission 2-1) 읽기 좋은 코드로 리팩토링#중요하게 생각한 점1) order 객체 Null 체크 필요2) if-else문들 if문으로 개선3) 부정어구(!) 없애기4) getter 제거 → 객체에 의미가 담긴 메서드를 별도 생성5) 공백라인 사용public boolean validateOrder(Order order) { //1) Null Check if (order == null) { log.info("주문을 확인할 수 없습니다."); return false; } //2) remove (getter) if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } //3) remove (! + if-else + getter) if (order.isInvalidTotalPrice()) { log.info("올바르지 않은 총 가격입니다."); return false; } //4) remove (! + if-else) if (order.hasNoCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false } return true; } public class Order { private List<Item> items; private double totalPrice; private CustomerInfo customerInfo; public boolean hasNoItems() { return items == null || items.isEmpty(); } public boolean isInvalidTotalPrice() { return totalPrice <= 0; } public boolean hasNoCustomerInfo() { return customerInfo == null; } }Mission 2-2) 자기만의 언어로 정리한 SOLID1) SRP- Single Responsibility Principle (단일 책임의 원칙)- “하나의 클래스에 변경이 발생한다면 그 이유(==책임)는 반드시 하나여야 한다”는 원칙- ex) 프로그램 실행 부와 실제 실행 로직은 나누어져 있어야 한다.- 높은 응집도, 낮은 결합도와 관련 있음.2) OCP- Open-Closed Principle (개방-폐쇄 원칙)- “기존 코드의 변경 없이, 기능을 확장할 수 있어야 한다”는 원칙3) LSP- Liskov Substitution Principle (리스코프 치환 원칙)- 상속 구조에서, “부모 클래스의 인스턴스는 자식 클래스의 인스턴스로 치환될수 있어야 한다”는 원칙 4) ISP- Interrface Segregation Principle (인터페이스 분리 원칙)- ”하나의 구체 클래스는 자신이 사용하지 않는 인터페이스에 의존해서는 안된다”는 원칙(이때는, 인터페이스를 2개로 분리해야 한다) 5) DIP- Dependency Inversion Principle (의존성 역전 법칙)- “레벨이 높은 모듈(ex, Lv2 카페)은 구체 모듈(ex, Lv0 커피)에 바로 의존해서는 안되고 추상화(ex, Lv1 음료)에 의존해야 한다”는 원칙 미션을 통해 SOLID 원칙을 다시 한번 상기시킬수 있었는데, 실제 업무에 활용하기 위해서는 스스로 좀더 깊은 학습이 필요할것 같다. 그리고 클린코드의 방법론을 미션을 통해 적용해 보면서 코드가 좀 더 잘 읽히고 이해하기 쉬워지는 것을 직접 느낄수 있었다.Section5) 객체지향 적용하기상속과 조합상속은 시멘트처럼 굳어지는 구조이기 때문에 수정이 어려우므로, 상속보다 조합을 사용하는게 좋다.Value Object도메인의 어떤 개념을 추상화하여 표현한 값 객체로, 불변성, 동등성, 유효성 검증 등을 보장해야 한다.VO (Value Object)는 내부의 모든 값이 다 같아야 동등한 객체로 취급한다. 이에 반해 Entity는 식별자만 같으면 동등한 객체로 취급한다.일급 컬렉션컬렉션(List, Set, Map 등)을 포장하면서, 컬렉션만을 유일하게 필드로 가지는 객체로, 단 하나의 컬렉션 필드만을 가진다.만약, getter로 컬렉션을 반환할 일이 생긴다면 외부 조작을 피하기 위해 꼭 새로운 컬렉션(List<Object타입>)으로 만들어 반환하는게 좋다. 스터디를 진행하면서, 처음에는 완벽하게 다 이해하려고 생각해 학습에 시간이 오래걸렸는데, 이제는 내가 당장 적용해 볼수 있는 부분들을 Target으로 하여 배워나가야 겠다는 깨달음을 얻었다. 출처https://inf.run/jsvaAhttps://inf.run/pZXb7

백엔드워밍업클럽클린코드2기depthmy

채널톡 아이콘