본문 바로가기
Experience

코딩 금쪽이의 첫 리팩토링 후기(DB편)

by Robinkim93 2023. 2. 18.

현재 진행 중인 선물 추천 서비스 제작 프로젝트에서 나는 선물 추천 API 작성과 기획단계에서 미리 정리해놓은 상품데이터들을 DB로 Migration하는 역할을 담당하게 됐다.

 

먼저 선물 추천 API를 작성해보려 했으나, 데이터베이스에 데이터가 들어있지 않으니 쉽지 않은 부분들이 있어 프론트엔드 개발자 분들께 양해를 구하고, API작성이 완료되기 전까지의 모든 작업은 Mock data를 사용해서 진행하기로 하고 먼저 데이터베이스에 데이터를 어떤 식으로 Migration 할 것 인가를 고민하게 됐다.

 

같이 프로젝트를 하게 된 분들과 기획 단계에서 ERD를 작성하였고, ERD를 토대로 DB를 구성하였다. 그 이후 데이터를 넣는 작업을 해야하는데, Nest.js로 진행하게 된 프로젝트다 보니 Nest.js에 조금 더 익숙해지고자 데이터를 넣는 과정을 하나의 API처럼 작성해보기로 했다. (끝나고 나니 사실 Nest.js의 어떠한 특성이나 기능을 사용한 것이 아니기 때문에 의미는 없었던 것 같다..)

 

여튼 처음 계획했던 데이터 Import 방식은 이렇다.

 

  1. 팀원들이 카테고리 별로 정리해놓은 구글 스프레드시트의 내용을 csv파일로 가져온다.
  2. csv 파일을 DB에 Import 한다.

 

이 계획의 문제점은 바로 발견되었는데, 아래 표는 특정 상품에 대한 필터링 항목을 엑셀로 정리한 표이다.

 

한 row에 여러가지 데이터가 담겨있음.

현재 프로젝트에서는 RDBMS인 MySQL을 사용하고 있다. 관계형 데이터베이스에서는 보통 하나의 row에 여러가지 값을 담지 않는다. 이유는 관계를 지정해주는 값이 여러가지라면, 데이터를 갱신하거나 삭제하는 과정에서 하나의 값이 아닌 여러가지 값을 모두 수정하게 될 가능성이 있고, 보통 참조 값이 많이 들어가있기 때문에 어떤 값을 참조할 것인지에 대해 알기가 용이하지 않기 때문이다.

 

특히, csv파일에서 column을 구분하는 방법을 쉼표(,)로써 하게 되는데 데이터 자체에 쉼표가 붙어있기 때문에 다른 column으로 인식하게 되어 데이터를 Import 했을 때, 맞는 상품에 맞는 필터링 항목이 들어갔는지 일일히 확인해보는 수 밖에 없기 때문에 적절치 않다고 판단했다.

 

두 번째 계획은 Node.js의 대표적인 Built-in Module인 File System(fs)를 이용하는 방법이었는데, 생각보다 사용이 용이하지 않아 다른 방법을 강구하게 되었다.

 

세 번째 계획이자 현재 적용되어있는 방식은 npm에 있는 xlsx 라이브러리를 이용한 방식이다.

아래와 같은 절차를 거쳐 데이터 Import를 계획했는데,

 

  1. HTTP Method 중 하나인 POST를 사용해서, Excel 파일을 Form-data화하여 API로 요청한다.
  2. API는 받아온 Form-data의 buffer를 받아 API가 Excel 파일을 Read 할 수 있도록한다.
  3. xlsx 라이브러리를 이용해 Excel 파일의 Sheet 및 column, row에 접근해서 데이터를 JSON화 한다.
  4. JSON화 된 내용을 가공하여 DB에 Import한다.

 

이렇게 큰 틀을 짜놓고 구현을 해놓고 xlsx로 받아온 데이터의 형태는 아래 사진과 같다.

이 데이터를 가지고 쭉 기능을 구현하다보니, 큰 문제점이 생겼는데

 

gender나 age처럼 라벨링 데이터들이 들어있는 테이블과 어떤 상품이 어떤 라벨링을 가지고 있는지 들어있는 product와 gender, age 등의 중간테이블이 존재하는데 구현할 당시에는 이것들을 어떤 식으로 구현해야 할지를 몰랐다.

 

그래서 내가 선택한 방법은... 창피하지만 하드코딩과 복붙이었다.

 

무려.... 740줄에 달하는 복붙을 하게 됐는데..

모든 로직이 형식과 과정이 똑같지만 접근하는 테이블의 이름과, xlsx로 받아온 데이터의 key값만 달라진다.

그치만 구현 당시에는 이것을 어떻게 처리할지 모르는 점과 팀원분들께 양해를 구한 상태였기 때문에 최대한 빠르게 구현할 수 있는 방법을 선택할 수 밖에 없었고, 그것에 대한 결과물은 이렇듯이 처참하다.

 

어찌저찌 구현은 했지만, 심각한 문제점들이 또 다시 발생하게 됐는데,

 

  • 분기처리를 제대로 하지 않아 DB 내부의 있는 값을 그저 길이로만 판단하도록 구현했기 때문에 데이터가 Import 되다가 실패했을 때, Import가 성공된 데이터들 때문에 DB의 값이 0이 아닐 때 데이터를 Import 하는 쿼리문에 닿지 못하고 계속 남아있는 데이터를 Update 하는 쿼리문만 실행하게 되어 계속 테이블을 TRUNCATE 해줘야 하는 문제.
  • 해당 내용이 전부 성공했을 때, DB에 적용하고 하나라도 실패했을 때는 모두 Roll-back 시키는 Transaction을 사용하였는데, 하나의 쿼리문을 쓸 때 마다 createQueryRunner로 만들어놓은 QueryRunner를 사용해서 Transaction 안에서 잠시 메모리에 저장해놓는 방식을 사용했어야 했는데, 바보같이 그냥 dataSource를 사용함으로써 결론적으로는 Transaction의 기능을 사용한 것이 아니게 됐다. (추후 리팩토링에서는 Transaction을 사용하지 않았음)
  • 하나의 테이블에 대한 과정이 묶여 있어야 테이블마다 적용되고 다음 테이블로 넘어가는 프로세스로 로직이 진행됐을 것이다. 하지만 나는 DB내부의 값을 쭉 불러오고, 순서를 보장받지 못한채로 계속 DB에 Import 했기 때문에 값이 일정하게 들어가지 못했다. (예를 들면, 1,2,3,4,5가 들어가야하는데 1,1,1,1,1 처럼 들어가는 경우가 더러 발생했다.)

해당 문제를 해결하기 위한 방안으로 생각한 것들은 다음과 같다.

  • Transaction 미적용
    • 개념만 알고 있는 사실이고 2번에서 설명할테지만 Excel에서 불러온 데이터를 일일히 다 DB안에 있는 데이터와 대조해서 Import를 하면 굳이 Transaction을 사용할 필요가 없다고 생각했다.
  • 필요한 일련의 과정을 하나의 함수로 만든다.
    • 모든 과정이 하나의 함수에 들어가 있다보니 예상하지 못하는 부작용(Side-Effect)들이 존재했다. 문제점에서 언급했던 3번 항목이 그 예인데, 함수형 프로그래밍을 통해 일련의 과정을 함수 내부에 넣어두고, 입력값만 다르게 호출을 해준다면, 굳이 전처럼 모든 과정을 key값만 다르게 하드코딩 할 필요가 없이 호출 시 입력값을 다르게 해주고 함수 내부에서 동적으로 받아올 수 있는 로직을 사용하면 될 것이라 생각했고, 모든 과정들이 들어있는 함수에서의 문제가 없다면 예상하지 못하는 부작용을 예방할 수 있다고 생각했다.
  • 테이블 단위의 확인이 아닌 데이터 단위로 확인한다.
    • 문제점의 1번 항목같이 테이블에 데이터가 있는지 없는지로 분기처리를 하다보니 정확히 데이터 Import가 이루어졌는지 알 수 없고, 어디에서 문제가 일어났는지 알 수 없었다. xlsx를 사용해 받아온 Excel 파일의 데이터를 일일히 테이블 내의 데이터와 비교하면서 존재하지 않는다면 추가하고, 존재한다면 그 존재하는 항목의 id값을 받아와 그 id값을 Excel 파일의 데이터로 수정하는 식의 로직을 구현하면 되겠다고 생각했다.

 

그래서 수정 된 코드는 아래의 사진과 같다.

Controller에서 호출되는 main함수. xlsx을 이용해 JSON형태로 데이터를 받고, 상품추가 함수와 라벨링데이터추가 함수를 실행한다.
특정 상품이 어떤 라벨링 데이터를 가지고 있는지 중간 테이블에 저장하는 함수
라벨링 데이터를 추가하기 위해서는 상품이 먼저 존재해야 하기 때문에 상품 정보를 추가하는 함수를 따로 구현

큰 틀에서의 변화와 로직에서의 변화를 나누어서 설명하자면,

큰 틀에서의 변화

  • 기존에는 테이블들의 정보를 쭉 불러오고 그 아래서 불러온 테이블 정보를 가지고 로직들을 쭉 수행하는 식이었다면, 리팩토링 후에는 하나의 테이블에 적용되어야 할 일련의 과정(테이블 정보 불러오기, xlsx에서 불러온 데이터로 테이블 정보와 비교, 비교 후 정해진 로직 수행)들만 함수내부에 작성하고 DB내의 테이블명과 xlsx로 받아온 Excel 데이터의 접근할 때의 key값만 main 함수에서 동적으로 인자에 할당하였다. 
  • 일련의 과정을 모아놓은 함수를 main 함수에서 호출하는 방식으로 변경

로직에서의 변화

  • 데이터를 하나하나 확인하도록 구현했기 때문에 리팩토링 전에 발생했던 TRUNCATE 해줘야하는 문제나, Data가 제대로 들어갔는지 불안해 하지 않아도 되도록 구현했다.

리팩토링 후의 개인적 Feedback

  • 리팩토링 전의 코드와 후의 코드의 가장 큰 차이점은 데이터를 다루는 방식에 있다고 생각한다. 전에는 단순히 쿼리문을 날리거나 라이브러리로 받아온 데이터를 사용했다면, 리팩토링 후에는 다양한 Method를 사용해서 데이터를 다루기 편한 구조로 바꾸고 로직을 수행했다는 점에서 발전이 있다고 생각한다.
  • 다양한 개념들에 대해 접할 수 있어서 좋았다. 함수형 프로그래밍이라던지 비동기처리에 대한 경험, 리팩토링 등을 접하면서 전과는 프로젝트 및 코드를 대하는 느낌이 달라졌다고 생각한다.
  • 아쉬운 점은 Nest.js의 특성을 잘 사용하지 못한 것과, TypeORM의 기능을 적극적으로 사용하지 않았다는 점이다. 사실 부트캠프에서 쌩쿼리문만 작성했기 때문에 아직까지는 쌩쿼리문에 조금 더 익숙하다보니 ORM 기능을 적극 사용하지 않았다. 다음 프로젝트에서는 조금 더 ORM 기능을 사용한 기능을 구현해보는 것이 좋을 것 같다.

'Experience' 카테고리의 다른 글

AWS S3 업로드 속도를 개선해보자 (feat. webp, loseless)  (0) 2024.03.11
RESTful API  (0) 2023.02.03
[SQL] RAND / OFFSET & LIMIT / alias  (0) 2023.02.03