strict 모드를 키고, 서버가 뒈졌다.

·2024년 11월 28일
2

회사이야기

목록 보기
117/118

Null-Safety가 뭐야?

2024년 11월 27일

근 1년 넘게 작업을 하던 Null-Safety 작업이 완료가 됐다.

Null-Safety란 tsconfig.jsonstrict 옵션을 키는 것으로, 엄격한 타입 검사가 이뤄지도록 만드는 옵션이다.

간단한 예시를 들자면 이런걸 다 잡아준다.

TypeORM의 manager.findOne 혹은 queryBuilder.getOne의 경우 조회가 되지 않을 경우 null 을 반환한다.

근데 왜 반환값에는 명시가 안되어있느냐고 에러를 내뱉고 있는 것이다.

이러다보니 코드 베이스가 거대할수록 작업량이 많아지는데….

찔끔찔끔 하다가, 거대 에픽 진행 전 일주일이라는 시간을 받아 전원이 투입되어 빠르게 수정 작업이 이뤄졌다.


감격한 CTO님의 한마디,,,

뭐야 서버가 안켜져요

그렇게 메인 브랜치에 머지가 된 후 로컬서버에서 켰는데?

위와 같은 에러가 뜨면서, 로컬에서 실행이 되지 않는 것이였다.
일단 에러가 뜨니 고쳐야하고…. 퇴근도 해야하니 에러가 뜨는대로 전부 다 고쳐버렸다. (이때가 오후 3시)

근데 Entity 파일이 너무나도 많았고 DB를 확인도 해봐야하고,
작업을 분리하기도 애매해서 결국 혼자 다 고쳤다… (2시간걸림, 파일 85개)

그리고 이후 왜 안되는가에 대해서 찾아봤다. (여기가 본론임)

에러가 뜨는 이유는 무엇인가?

에러 로그를 확인해보면 DataTypeNotSupportedError: Data type "Object" in "OpenApiAuth.expiredAt" is not supported by "mysql" database.라고 적혀있고 아래 위치까지 찍어준다.

그럼 천천히 들어가보자.

typeorm/src/metadata-builder/EntityMetaValidtor.ts 42번째 줄

Entity의 메타데이터를 순회하면서 타입에 대한 검증이 진행된다.

typeorm/src/metadata-builder/EntityMetaValidtor.ts 125~136번째 줄

현재 사용하고 있는 DB는 mysql이기에 해당 조건문에 들어가서 빨간색에서 지원 가능한 컬럼을 확인하고 파란색에서 안맞으면 에러를 발생시키는 트리거가 된다.

typeorm/src/driver/mysql/MysqlDriver.ts 100번째 줄 부터 아래로 쭈욱

확인을 해보면 Object가 없다, 그래서 에러가 발생했다.
~~

이러면 좋겠지만 도대체 왜 Object가 됐는지 확인을 해볼 시간이다.

Object는 어디서 온걸까?

아래의 코드들은 ts 파일과 js로 트랜스파일링 된 코드다.

아래 사진을 보면, Date라고 선언이 되어있으나, Object로 변환이 된 모습을 볼 수 있다.

하지만, 여기서 유니온 타입인 null 을 제외한다면?

정상적으로 Date로 반환되는 모습을 확인 할 수 있다.

그럼 여기서 이해가 안가는 부분이 생기게 된다,
Null-Safety 작업을 해서 null 타입이 추가 됐더라도 결국 Date 타입인데, 왜 Object로 반환이 되는 것인가?

그래서 typeorm의 Column 데코레이터가 어떻게 구현이 되는지 확인을 해보기로 했다.

typeorm/src/decorator/columns 156번째 줄

잘 읽어보면 type이 존재를 하면 그대로 쓰고 정의가 되어있지 않다면 reflectMetadataType를 쓰는 것을 볼 수 있다.

자…. 그럼 한번 더 깊숙하게, Reflect가.... 뭔데?

Reflect가 뭔가요?

타입스크립트에서 메타데이터를 저장하고, 활용하게 만들어주는 고마운 녀석(?)이다.

NestJS는 앵귤러(결국 스프링)의 모습을 본따 사용하기 때문에
데코레이터(@)가 정말로 많이 들어가있다.

근데 이 데코레이터를 파싱되는 과정에서 Reflect를 사용하게 되는데, Reflect.defineMetadata가 사용되어 메타데이터 키와 타입 정보를 저장하게 된다.


아무튼 Ts에서 Js로 컴파일이 이뤄진 후, 메타데이터가 저장되는 과정이 진행되는데
여기서 우리가 알고 싶은 것은, 왜 Date | null 이 Object로 저장되는가 이다.

타입은 단 한개만 지정이 가능하다.

뭐야 유니온 타입도 있는데요?

라는 생각을 했는데, 그게 아니였다;

이것은 "언제?" 라는 관점이 상당히 중요한데, 런타임 상황에서는 타입이 단 한개만 존재할 수 있다.

그렇다면, 여기서 조금 이해가 간다.
Date | null은 왜 Object로 변경이 되었는가?

이것은 Reflect의 라이브러리 코드를 읽어보면 알 수 있다.

reflect-metadata / ReflectLite.ts 1206번째 줄

위 코드를 읽어보면, 유니온에 해당하는 부분이 없다.

타입스크립트의 컴파일러는 타입 안정성을 위해 최대한 넓은 범위의 타입을 추론하려고 하는데
이 때문에 최대한 범위가 넓은 Object 타입으로 처리되는 경향성을 보이게 되는 것이다(...)

글을 맺으며

원인을 찾아가는 과정에서 이게 맞나 라는 생각이 좀 많이 들었다....ㅋㅋ

에러 하나를 찾기 위해 이곳저곳을 둘러보면서 원인이 무엇일까? 에 대해 알아봤는데

예전에 이렇게 뎁스를 깊게 파보면서 뭐가 문제일까~ 하던 시기가 있었다보니
과거의 그 흥미로움이 다시 나를 일깨운 것 같달까.

상당히 재밌었으나, 솔직히 납득이 되...기엔 조금 아직은 어색한 부분이 있지 않나 싶다.
사실 strict 모드를 안키면 에러가 안나니까, 켰을 땐 이렇게 에러를 띄워주는게 맞는 것 같기야 한데..

아마 타입 시스템 그 자체에 대한 지식이 더 있으면 납득이 되지 않을까 싶다.
아무튼 재밌게 알아봤다!

(조만간 알아보던가 나중에 찾아보던가 해야겠다, 프로그래밍에서 타입은 정말 중요하다보니..)

끝.

p.s 아 그래서 어떻게 해결하냐고?

이렇게, 모두 타입을 명시해주면 해결된다.

진짜 끝!

profile
물류 서비스 Backend Software Developer

6개의 댓글

comment-user-thumbnail
2024년 12월 1일

늘 응원합니다 파이팅 ^_^

1개의 답글