티스토리 뷰

Github Repo

https://github.com/wanted-wecode-subjects/deer-subject

개요

  • 대여/반납 요금 계산 API 구현

기간

  • 2021.11.18 - 2021.11.20

개발 환경

  • TypeScript
  • NestJS
  • MySQL

사용 라이브러리

  • typeorm
  • class-validator
  • class-transformer
  • moment-timezone
  • passport
  • jwt
  • bcrypt

구현 사항

요구사항

  • 유저 API
  • 요금 계산 API
    • 지역별 기본 요금, 분당 요금 적용
    • 고장으로 인한 1분 이내 반납시 요금 미청구
    • 벌금 조건 적용
    • 할인 적용

DB 모델링

MySQL spatial data

MySQL은 OpenGIS 클래스에 해당하는 공간 데이터 타입을 제공합니다.

타입 정의 예시
Point 공간에서 좌표 한 점을 표시 POINT (10 10)
LineString 공간에서 다수의 Point를 연결하는 선분을 표시 LINESTRING (10 10, 20 25, 15 40)
Polygon 공간에서 다수의 선분이 연결되어 닫힌 상태의 다각형을 표시 POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))
Multi-Point 공간에서 다수의 Point 집합 MULTIPOINT (10 10, 30 20, 30 10, 10, 20)
Multi-LineString 공간에서 다수의 LineString 집합 MULTILINESTRING ((10 10, 20 20), (20 15, 30 40))
Multi-Polygon 공간에서 다수의 Polygon 집합 MULTIPOLYGON ( (( 10 10, 15 10, 20 15, 20 25, 15 20, 10 10 )) , (( 40 25, 50 40, 35 35, 25 10, 40 25 )) )
GeomCollection 모든 공간 데이터들의 집합 GEOMETRYCOLLECTION ( POINT (10 10), LINESTRING (20 20, 30 40), POINT (30 15) )

출처: https://sparkdia.tistory.com/24
https://dev.mysql.com/doc/refman/8.0/en/spatial-type-overview.html

typeOrm에서의 spatial data

typeOrm에서 엔티티의 column에 spatial 타입을 적용하려면 다음과 같이 @Column() 데코레이터에 옵션을 부여합니다.
area.entity.ts

  ...
  @Column({ type: 'polygon', srid: 4326 })
  area_boundary: string;
  ...

MySQL spatial data 내장 함수

ST_AsText

공간 데이터 형태의 값을 WKT 형식으로 변환하고 문자열로 반환합니다.

SET @g = 'LineString(1 1,4 4,6 6)';

SELECT ST_AsText(ST_GeomFromText(@g));

결과

+--------------------------------+
| ST_AsText(ST_GeomFromText(@g)) |
+--------------------------------+
| LINESTRING(1 1,4 4,6 6)        |
+--------------------------------+

ST_Contains

첫번째 인자 g1이 두번째 인자 g2에 포함되면 1, 포함되지 않으면 0을 반환합니다.

예제1

SET @g1 = ST_GEOMFROMTEXT('POLYGON((175 150, 20 40, 50 60, 125 100, 175 150))');
SET @g2 = ST_GEOMFROMTEXT('POINT(174 149)');

SELECT ST_CONTAINS(@g1,@g2);

결과

+----------------------+
| ST_CONTAINS(@g1,@g2) |
+----------------------+
|                    1 |
+----------------------+

예제2

SET @g1 = ST_GEOMFROMTEXT('POLYGON((175 150, 20 40, 50 60, 125 100, 175 150))');
SET @g2 = ST_GEOMFROMTEXT('POINT(175 151)');

SELECT ST_CONTAINS(@g1,@g2);

결과

+----------------------+
| ST_CONTAINS(@g1,@g2) |
+----------------------+
|                    0 |
+----------------------+

ST_Distance

인자로 받은 두 공간 데이터의 거리를 반환합니다.
인자값이 유효하지 않으면 null을 반환합니다.

SELECT ST_Distance(POINT(1,2),POINT(2,2));

결과

+------------------------------------+
| ST_Distance(POINT(1,2),POINT(2,2)) |
+------------------------------------+
|                                  1 |
+------------------------------------+

ST_GeomFromText

WKT와 SRID를 사용하여 모든 유형의 공간 데이터 값을 구성합니다.

SET @g = ST_GEOMFROMTEXT('POINT(1 1)');
SELECT @g;

결과

+--------------------------------------------------+
|                    ST_GEOMFROMTEXT('POINT(1 1)') |
+--------------------------------------------------+
| 0x0000000001010000000000000000003F0000000000003F |
+--------------------------------------------------+

출처: https://kibua20.tistory.com/189
https://mariadb.com/kb/en/st_astext/
https://mariadb.com/kb/en/st-contains/
https://mariadb.com/kb/en/st_distance/
https://mariadb.com/kb/en/st_geomfromtext/

bcrypt mocking

bcrypt를 직접 import 하여 jest.spyOn으로 bcrypt의 함수를 mocking 한 뒤, 테스트에 사용할 수 있습니다.

user.service.spec.ts

...
import * as bcrypt from 'bcrypt';
...
      jest.spyOn(bcrypt, 'compare').mockResolvedValue(true);
      ...

참고: https://stackoverflow.com/questions/62646538/jest-how-to-test-a-function-within-a-method

이슈

MySQL Version:AsText() is deprecated as of MySQL 5.7.6

ERROR [ExceptionsHandler] ER_SP_DOES_NOT_EXIST: FUNCTION deer.AsText does not exist
QueryFailedError: ER_SP_DOES_NOT_EXIST: FUNCTION deer.AsText does not exist
    at QueryFailedError.TypeORMError [as constructor] (/Users/hak/code/deer-subject/src/error/TypeORMError.ts:7:9)

서버 실행시 상기 에러가 발생하여 찾아본 결과 AsText() 함수가 deprecate 되어 발생한 에러였습니다.
이를 해결하기 위해 TypeOrmModule 설정에서 legacySpatialSupport: false 를 추가하여 해결하였습니다.

app.module.ts

    ...
    TypeOrmModule.forRoot({
      type: 'mysql',
      ...
      legacySpatialSupport: false,
    }),
    ...

참고: https://github.com/mysqljs/mysql/issues/2127

회고

Fact(사실)

  • 저번 프로젝트에서 Heroku의 sqlite 데이터가 사라지는 이슈가 있었다. 이는 dyno가 일시적인 파일시스템을 사용하므로 잠자기 상태에 들어가면 파일들이 삭제되는 현상이다.
  • 오픈 API에 요청을 보낼 때 응답하지 않는 경우 서버는 이를 계속 기다릴 수 있으므로 timeout을 걸어놔야 한다.

Feeling(느낀점)

  • dyno의 파일시스템에 알지 못한 상태로 데이터가 사라지는 현상이 발생하여 당황하였다.
  • 시스템 구축 당시 무한으로 대기하는 경우에 대해 고려하지 못한 것이 아쉬웠다.

Finding(교훈)

  • 서버를 띄우는 환경에 대해 정확히 파악한 후 이에 맞는 환경을 구축해야겠다.
  • 다양한 상황에 대해 대처할 수 있도록 고려하여 시스템을 구축해야겠다.

Future action(향후 계획)

  • Heroku에 서버를 띄울 경우 sqlite 대신 다른 DB를 사용
  • Heroku에 sqlite로 서버를 띄울 경우 서버가 잠들지 않도록 구현(예: cron, 외부 서비스(https://kaffeine.herokuapp.com/)를 사용)
  • axios 사용시 요청에 대한 timeout을 설정

Feedback(피드백)

-

Comments