내 코드에 관한 고민 그리고 앞으로 해야 할 것들 (#NestJS Code Review)
소중한 경험
최근에 달려오면서 짠 코드의 전반적인 코드 리뷰를 받을 수 있는 좋은 기회가 있었다. 사실 NestJS + Prisma가 흔한 조합은 아니라서 레퍼런스도 부족했고 성장하면서 짰기 때문에 잘 활용하지도 못했다. 해결하지 못한 의문들은 걸림돌로 마음속에 남았있었는데 많은 것을 해결할 수 있었던 소중한 기회였다.
2시간이 너무 짧게 느껴졌고 정말 많은 것을 느꼈다.
더 시간이 지나면 리뷰한 내용을 많이 까먹을 것 같아서 미리 피드백 복기한 것과 느낀점을 적어본다.
코드 리뷰 방식
처음부터 끝까지 내.. 방대한 코드를 남에게 소개를 하는 것은 처음이어서 긴장을 엄청했다.
코드 리뷰에 앞서서 미리 방어적으로 (변명을 하며) 밑밥 깔았다. 지금 생각해보면 코드로 바로 들어가면 되는데 괜히 소중한 시간을 조금 허비해버린 것은 아닌가 싶다. ㅠㅠ
막상 코드를 보여주자니 어디서부터 시작해야 할지 모르겠어서 어떻게 하면 좋을지 도움을 요청했다.
보여주고 싶은 부분의 모듈, 서비스, 컨트롤러, 엔티티를 보여달라고 하셨다.
간단하게 main 파일부터 시작해서 지목하신 user module부터 시작해서 쭉 컨트롤러->서비스->다른 이어지는 부분으로 내려갔다.
다음 코드리뷰는 이렇게 해보자..!
너무 긴장하지 말쟉!
거의 모든 말에 반사적으로 "네!"라고 답한 것 같다 ㅎㅎ... 필요 이상의 "네!"를 줄이자.
리뷰를 해주는 분을 조금 더 신경 쓰고 말씀하시는 것을 경청하고 말하자!
생각해보면 리뷰하시는 분이 설명하실 때 내가 조금씩 끊어먹은 것 같다. 이건 조금 무례했던 것 같다..
의도를 설명할 때 조금 더 침착하고 명확하게 말하자
(그래도 끝까지 잘 들어주셔서 너무 감사했습니다...)
긴장을 너무하니깐 어떤 질문에 대해 필요 이상으로 길게 대답을 한 것 같다.
시간이 충분하다면 한 (도메인?) 모듈을 집어서 보여주면 전체적으로 설명해주는 것도 방법이고 내가 고민이 된 부분을 문맥과 함께 제시하며 받아도 좋은 것 같다.
코드리뷰 방식은 코드 리뷰의 목적과 내가 어떤 부분을 리뷰를 받고 싶냐에 따라 다를 것이다. 이 부분은 내 스스로도 다른 사람에게 코드도 설명해가며 나름의 경험과 연구가 필요할 것 같다ㅎㅎ..
코드 리뷰 이전에 가지고 있던 고민
1. 객체 지향 공부를 했지만 정작 내 코드에서는 보이지가 않는다.
- 거의 단순 함수의 집합체였다.
2. 함수형을 공부했지만 부수 효과를 잘 분리해서 다루고 있다는 생각은 안 들었다.
3. 근본적으로 내가 이 기술을 왜 쓰는지에 대한 고민이 부족했다.
- ORM은 왜 쓸까? 쿼리와 DB 관리를 비즈니스 로직에 맞추어 편하게 하기 위함인가?
- (코드 리뷰에서 받은 피드백) Prisma와 같은 ORM을 쓰지만 Helper를 써서 의미가 퇴색되었다
- Helper를 쓰는 사람이 결국 모든 것을 알아서 맞춰줘야 한다
- 엔티티가 있었다면..!
- 인증을 JWT를 한 이유?
- (코드리뷰 중) 보편적인 세션이 보안적으로 안전하지 않은가?
- NestJS에 DI가 핵심인 것 같은데 내가 잘 활용하고 있는 것인가?
- 에러 처리를 너무 대충 한 것 같다
- 코드 스멜이 나는 것 같지만 정확히 어디서 나는지 모르겠다
- 테스트 코드는 어떻게 짜야 잘 짜는 것일까/
- Validation 테스트는 어디에?
등 정말 많은 고민이 있었다.
코드 리뷰를 하며 받은 질문과 피드백
인증
인증을 JWT로 처리한 이유가 있습니까? 보편적인 세션이 보안적으로 안전하지 않은가?
Database 및 ORM
Database Column명은 카멜 케이스를 쓴 이유가 있는가?
autoincrement를 쓸 것이면 테이블 데이터는 bigint로 한다
- node는 bigint가 있지만 json에는 없다
- orm에서는 bigint 직렬화 과정에서 문자열로 변환
- 이런 불일치도 ORM에 위임
application의 컨벤션과 db의 컨벤션을 해결해주는 것은 ORM이다.
helper를 쓰면 자연스럽게 내가 이 모든 것을 컨트롤하게 되기 때문에 ORM의 장점을 퇴색시킨다
argon을 쓴 이유?
컨벤션
DB의 컬럼들 github? google? kakaoid? 이 3개의 컬럼은 social_id와 type 컬럼으로 교체 가능하다.
const SECRET = config.get("SECRET")와 const payload = {a:"b"}의 컨벤션이 다른 이유가 있나요?
함수형은 3가지로 분리해 봐야 한다.
- data
- pure 함수
- side-effect 함수
좋은 코드? (OOP + FP)
FP
- pure, side-effect, data를 어떻게 핸들링할 것이냐?
OOP
- 객체 단위로 순수
- 객체 안에서는 데이터가 오가도 괜찮지만 밖에서는 최소화
데이터는 보통 불변을 지향한다
payload는 data
- FP는 새 값으로 복제한다
- OOP는 담당 클래스가 내부 로직을 추상화시켜서 관리한다
payload나 data는 목적이 불분명해서 모호한 단어이다.
클래스와 오브젝트 리터럴의 차이점
- 밖의 사람은 모르게 데이터 제공 가능
- getter setter 제어 가능
OOP TDA (tell dont ask)
- 요청한 쪽이 만든다
OOP는 "상태와 행위를 모은다"보다는 "어떤 객체에 어떤 책임을 주냐 + 어떤 형태로 메시지를 주고받을 것인가"가 핵심이다
책임 + 메시지
추상화를 어떤 레벨까지? 책임자는 누구? 이런 관점을 가지고 접근해보자
let이 필요하면 코드 스멜일 확률이 높다.
린트 규칙 : "console.log를 금지한다"
console.log를 금지하면 서비스, 글로벌 핸들러 등의 부수효과 애들로 자연스럽게 로깅하는 애들이 빠진다.
console.log는 side-effect 함수이기 때문에 순수 함수랑 OOP에서는 도메인 객체 쪽에서 없어야 한다!
도메인 객체에 console.log가 들어가면 표준 입출력에 문제가 생긴다면 어쩔 것인가!
도메인 객체에서는 throw만 던지고 글로벌 핸들러에서 캐치하고 생각한 로깅 방식으로 처리한다.
console.log 잘 찍었냐는 테스트하기 어렵지만 throw 잘됐냐는 테스트하기 쉽다.
자연스럽게 pure해진 부분은 100% 단위 테스트를 짠다.
부수효과 있는 애들은 e2e, 통합 테스트 or mocking한다.
악취를 알 수 있도록 규칙을 짜라!
예를 들어서, 테스트는 mocking 없이 한다. (의견이 많이 갈리는 부분)
TEST 방식
classist("의존성 디자인 잘못했구나!" 탐지에 유리) vs mockist(외부상황 통제됨 = 내 것만 잘 짜면 돼)
mock을 많이 하는 이유는 Jest가 너무 강력해서 그렇다. 외부 모듈까지 모킹 가능하다.
ex. react hook도 대체 가능, 패키지 바꿔치기 가능 => "의존성 관리 내가 왜 해!"
그래서 mock으로 짜면 의존성은 신경 안 쓰고 내 것만 잘 짜면 OK라는 식으로 흘러가기 쉽다. 의존성에 대한 악취가 느껴지지 않게 된다.
그리고 테스트는 일단 빨리 짠다.
Logger Test
logger는 출력을 한다. logger가 의도대로 출력 되는 것을 어떻게 검증할까?
C#에서 사용하는 DI Logger
외부 검증 어려운 애들은 Stub + DI로!
DI로 Logger를 관리하자!
-> console.log(""+e) vs console.log("", e)
교체하기 쉬운 형태 logger 자체를 주입 -> stublogger
stubLogger는 바로 출력을 하지 (console.log를 찍지) 않고 객체 안에 값을 넣는다.
이렇게 stub을 사용하면 테스트 코드는 stubLogger에 값이 의도한 대로 들어갔는지 검증하면 된다.
stub의 다른 예시로는, email(html+css)이 만들어내는 json 적절하게 치환됐는지 검증할 수 있다.
발송하는 애를 stub으로 바꾸면 이메일 출력물도 테스트하기 쉬워진다.
Error 처리
에러를 모든 경우에 대해 처리하자!
console.log 뒤에 stack trace를 위해 , e를 넣자!
unique 값에 대한 insert 후 AlreadyExist catch하기 VS 미리 중복 값있는지 체크하고 insert 안 때리기
[postgres+mysql] "unique에 대해 insert 중복 쿼리를 날리고 튕기면 잡겠다"는 별로다!
- 값 중복으로 인해 튕겨도 값은 increase 된다
- read용이 아닌 master DB에 쓸데없이 부하를 일으키게 된다
미리 중복 값이 있는지 체크하고 insert를 바로 때리지 말자. 최후의 수단이 되어야 할 것이다.
Repository
repository 계층 있어요? 없어요 ㅠㅠ
서비스에 쿼리가 노출되어 있나요?
TS 팁
isPublic== undefined? null : ""
함수 인자의 기본값을 지정해주자.
구조 분해의 장단점
구조 분해를 사용하면 무조건 채워져야 한다.
필수 값이랑 필수값 아닌 것을 구분하지 못한다.
차라리 DTO를 받아서 처리하자.
디렉터리 구조
gateways/class/{redisSession, redisStore} 이상하다!
인터페이스는 어디에..?
gateway/store/interface를 두고/{redis, mongodb}
interface는 호출과 제공자 사이에 계약이니까 보통 상단에 있다.
구현체(redis, mongodb)는 하위 디렉터리에 둔다.
FE, BE 입장 차이
같은 언어지만 백이 바라보는 TS와 프론트가 바라보는 TS는 다르다.
백 : treeshaking 상관없어, 좀 무거워도 돼(한번 깔면 그만), 기능 많은 게 좋아, 불변 객체가 더 좋아!
프론트 : 가벼워야 해! 기능 좀 부족해도 돼, treeshaking 중요
BE와 FE는 서로 다름을 인정하자.
예를 들어서, dayjs(FE) vs js-joda(BE)
https://inventi.studio/en/blog/why-you-shouldnt-use-moment-js
한국 출판 시장(노드 쪽?)은 초보 레벨에 집중되어 있다ㅠㅠ.
Docker
docker-compose.yaml은 root에 하나만 통합해서 만들어둬도 된다.
한번 docker compose up 하면 끝!
AWS SDK Test
"localstack"
AWS SDK 로컬 테스트용으로 사용하면 좋다.
AWS SDK를 CI 환경에서도 테스트 가능해진다.
E2E Test
validation 전부 e2e에 검증 class-validator의 validate로 단위 테스트를 짜면 좋다.
validate가 json 직렬화 역직렬화가 될 때 호출되는 놈이다!
그냥 임의로 dto 만들어서 validate dto로 터트려서 전부 단위 테스트 가능하다.
Post / Delete Method Test
검증을 할 때 생성되면 length == 1보다는 콘텐츠가 진짜 의도대로 박힌 대로 됐는지 체크하자.
length == 1은 prisma 쪽의 query가 다 테스트되어있으면 괜찮다.
prisma쪽의 query가 잘되는지는 모르니까 나중에 query 튜닝 같은 것을 하면 자꾸 테스트 코드를 신뢰를 못하고 스스로 확인하게 될 것이다.
prisma에서 만들어주는 쿼리들을 orm대로 값이 진짜 잘 들어갔는지 체크하자.
어색 != 안 좋다
Test code를 짜는 숙련도를 늘리자. 많은 연습을 하자.
Postman 실행보다 더 빠른 날이 올 것이다.
Test Code로 소통하기
테스트 코드를 다른 개발자가 아닌 분과 소통하기 좋은 수단이다.
테스트 코드만으로 답변이 될 수 있다.
프론트엔드의 Test
프론트 e2e는 dom이 너무 자주 바뀐다..! 대신 inputbox와 같은 애들의 validation 테스트, 백에서 준 데이터에 대한 모델 쪽 테스트
코드 리뷰를 받고...
프레임워크 안에서 설명하고 추구하는 것에 벗어나서 여러 기술이 추구하는 바를 학습해보며 안목을 좀 길러야 되겠다는 생각을 했다.
다시 재정비하며 기본기부터 채우자!!
댓글
이 글 공유하기
다른 글
-
2023년도 상반기 인턴십 후기!
2023년도 상반기 인턴십 후기!
2023.07.04 -
2022년 회고 (소프트웨어 마에스트로 회고)
2022년 회고 (소프트웨어 마에스트로 회고)
2023.01.13 -
2022년 3분기 + 요즘 생긴 관심사 정리 (#모도코_프로젝트)
2022년 3분기 + 요즘 생긴 관심사 정리 (#모도코_프로젝트)
2022.10.04 -
2022년 1~2분기 + 요즘 생긴 관심사 정리 (#군대_전역)
2022년 1~2분기 + 요즘 생긴 관심사 정리 (#군대_전역)
2022.07.11