[Wecode] PIL (프로젝트에서 내가 배운 것) - 작성중

김택수·2022년 10월 30일
0

프로젝트 회고와는 다르게 프로젝트에서 내가 기존에 사용하지 않았던, 그리고 2차 프로젝트를 대비해서 새롭게 알고 넘어갈 수 있는 개념들에 대해 정리해보려고 한다. 지극히 기술적인 내용만 담긴 내용이다보니 회고와 따로 작성하게 되었다.

1. Query문 작성법

1. JSON_ARRAY, JSON_OBJECT

기존에 Layered Pattern으로 나눠놓은 프로젝트에서 데이터구조를 변경해야할 때, Dao단으로 모든 정보를 SELECT문으로 가져온 뒤 Service단에서 로직을 수행하는 식으로 변경을 해왔다.
지금처럼 작은 프로젝트에서는 데이터의 양도 적을 것이기에 상관이 없지만, 만약 데이터가 100만개가 넘어간다고 했을 땐 SELECT문으로 모든 데이터를 불러오는 행위 자체가 응답속도에 엄청난 영향을 미칠것이다.

그래서 로직 자체를 Query문으로 수행하면 이런 문제를 해결 할 수 있다는 것은 잘 알고 있었으나, 방법을 알지 못해 많은 고민을 하고 있었다. 첫째로는 가령 하나의 상품 안에 있는 여러 댓글들을 조회하는 API를 만든다고 하면 하단과 같은 형태로 내보낼 수 있을 것이다.

{
  "data": [
    {
      "productId": 1,
      "review": [
        {
          "reviewId": 1,
          "userName": "김택수",
          "reviewTitle": "안녕하세요",
          "reviewContent": "잘 사용하고 있습니다.",
          "reviewHelpCount": 1
        },
        {
          "reviewId": 2,
          "userName": "김택수",
          "reviewTitle": "안녕하세요",
          "reviewContent": "잘 사용하고 있습니다.",
          "reviewHelpCount": 1
        },
        {
          "reviewId": 3,
          "userName": "김택수",
          "reviewTitle": "안녕하세요",
          "reviewContent": "잘 사용하고 있습니다.",
          "reviewHelpCount": 1
        },
        {
          "reviewId": 4,
          "userName": "김택수",
          "reviewTitle": "안녕하세요",
          "reviewContent": "잘 사용하고 있습니다.",
          "reviewHelpCount": 1
        }
      ]
    }
  ]
}

이 때 사용하게 된 것이 JSON_ARRAY, JSON_OBJECT 와 이것들을 묶어줄 수 있는 GROUP BY였다.

1) 어려웠던 점

TypeORM을 이용해 DB와 Connection 후에 Query문을 작성할 때, Query문을 ``(Back tick) 에 감싸서 사용하는데, 사실 알게 모르게 나는 VSCode의 Prettier 등을 통해 문법적 오류나, 가독성 좋게 코드를 정리해주는 기능을 제공받고 있는데 백틱에 감싸져 있는 Query문은 오류 내용이 near라고 하는, 그러니까 정확하게 짚어주지 않고 이 근처에서 Syntax Error가 발생했다는 식의 Error Message를 반환하기에 어려움이 컸다. 콤마 하나에도 Error를 발생시키니 정말 어려웠고, 처음보는 함수식의 구조를 Query문에 적용하려니 눈에 익지도 않아 구조자체가 한 눈에 파악되지 않았다.

또한 GROUP BY 사용 시, 어떤 기준으로 데이터가 묶이는지 몰랐다.

2) 정리한 내용

JSON_ARRAY, JSON_OBJECT는 이름 그대로 어떠한 데이터들을 배열과 객체로 묶어줄 수 있는 함수이며, 위와 같은 결과에서는 필수적으로 들어가야하고, 프론트에서 데이터를 받아보기에도 편하다는 장점이 있다.
그리고 이것을 어떤 데이터를 기준으로 묶어줄 건지에 대한 코드로 GROUP BY를 써줘야하는데, 방식은 이렇다.
위의 결과값 같은 경우는 where절을 통해 ProductId를 기준으로 데이터를 정리했다. 그러면 데이터의 모양은 하나의 열에 하나의 데이터만을 가지게 되고, 댓글이 10개라면 10개의 열이 생성될것이다.

그럴거면 사실 GROUP을 지을 필요가 없다. 그저 where절로 데이터를 찾으면 될 뿐인데, 이 열의 데이터 자체를 배열 또는 객체로 생성하여 넣고, 그것들을 묶는 기준을 GROUP BY로 명시해주면 된다.

가령 상단의 Query문은 이런 구조로 작성되었다.

SELECT
  product_id AS product_id,
  JSON_ARRAYAGG(
    JSON_OBJECT(
    "reviewId", r.id,
    "reviewTitle", r.title,
    "reviewContent", r.contant,
    "reviewHelpCount", r.help_count,
    "userName", u.name  
    )
  ) AS review
  FROM reviews AS r
  LEFT JOIN users AS u ON r.user_id = u.id
  WHERE product_id = ${productId}
  GROUP BY productId
  1. productId를 기준으로 할 것이기 때문에 productId를 가져오고, 여러 댓글 데이터를 받아올 것이기 때문에 JSON_ARRAY로 배열을 만들어 주고, 그 안에 Key값과 Value값으로 이뤄진 데이터를 JSON_OBJECT를 통해 객체로 만들어 줄 수 있다.
  2. 여기서 중요한 것은 ARRAY가 끝나는 괄호에서 review라는 alias를 줬는데, 이것이 곧 데이터의 반환되는 key값이자, GROUP BY로 만드는 새로운 Table의 행의 이름이 된다.
  3. where 절로 프론트에서 전달되는 productId와 맞는 것들만 찾게 되고
  4. 이것들을 productId를 기준으로 GROUP BY를 써주어 하나의 데이터로 만들어 줄 수 있다.

2. Transaction 사용법

Transaction이란 한가지의 논리적 작업을 완료하기 위해서 데이터베이스를 변환하는 일련의 과정들을 하나의 단위로 합쳐놓은 것이다. 쉽게 말하면 '하나의 유저가 탈퇴 시, 그 유저가 작성한 모든 게시글을 지운다' 라는 과정에는 우리의 유저가 맞는지 확인, 해당 유저가 작성한 글 확인, 글 삭제, 유저 데이터 삭제 등의 과정들이 있을 것이며, 이것을 하나로 합쳐놓은 하나의 단위가 Transaction이다.

Transaction의 중요한 특징 중 하나는 All or Nothing 즉, 전부 완료되어서 데이터베이스를 변화시키거나 중간에 완료처리 되지 않으면 데이터베이스를 변화시키지 않고 지금까지 했던 과정들을 전부 Rollback 해야한다.

현재 프로젝트에서는 TypeORM을 이용해서 Transaction을 생성하고 관리할 수 있는데, QueryRunner를 사용할 수 있고, 그 안에서 데이터베이스와 연결 하고, startTransaction을 통해 Transaction을 시작하고, 여러 과정들을 거친 후 commitTransaction을 통해 Transaction을 커밋(종료)할 수 있다. 커밋되면 데이터베이스를 로직에 맞게 변경하고, rollbackTransaction을 통해 에러상황 시 모든 과정을 다시 Rollback 할 수 있다.

아직 써보지 않아 자세한 설명이 어렵기 때문에, 2차 프로젝트 시 다시 내용을 정리하는 것으로 마무리한다.

참고자료 : Transaction

3. Wrapper Class 사용하여 TypeORM 사용

해당 내용은 멘토님께 코드리뷰를 받는 시간에 코드리뷰 대신 현재의 코드를 어떻게 더 확장하여 좋은 코드로 만들 것인가에 대해 이야기 하던 도중 받은 피드백으로 전혀 생각지도 못한 방식이었으나, 생각지도 못한 확장성을 가지고 올 수 있는 방식이여서 2차 프로젝트에는 꼭 적용해보려 하는 방식 중 하나라 내용을 정리한다.

쉽게 정리하자면, 현재 TypeORM로 API와 DB를 연결하는데, 이 때 TypeORM에서 제공하는 DataSource를 통해 연결하게된다. 여기까진 문제가 없으나, DataSource를 통해 Query문을 작성하게 되고, 그것으로 데이터를 받아오는 과정을 거치는데 이런 Query문이 100만개가 있다고 가정했을 때, DB가 바뀌거나 TypeORM과 같은 라이브러리가 바뀐다면 100만개의 Query문에서 사용된 DataSource를 전부 변경해야하는 큰 이슈가 생긴다.

이런 것들을 방지 하기 위해 Class를 만들고, 그것 안에서 연결하는 것들을 정의하고 그 안에서 Query문을 사용할 수 있게끔 감싸주는 역할을 하는 Wrapper Class를 만들어 줄 수 있다.

const { DataSource } = require("typeorm");

class Database {
  constructor(dataSource) {
    this.dataSource = dataSource;
  }

  async query(sql, params) {
    return await this.dataSource.query(sql, params);
  }

}

const typeOrmDataSource = new DataSource({
  type: process.env.TYPEORM_CONNECTION,
  host: process.env.TYPEORM_HOST,
  port: process.env.TYPEORM_PORT,
  username: process.env.TYPEORM_USERNAME,
  password: process.env.TYPEORM_PASSWORD,
  database: process.env.TYPEORM_DATABASE,
});

const database = new Database(typeOrmDataSource)


module.exports = database;
  1. Database라는 Class를 선언하고 constructor의 인자로 dataSource를 받아 선언해준다.

  2. Class 안에서 사용할 Method를 정의할 수 있기 때문에 query라는 함수를 만들고, 그 안엔 내가 사용하던 TypeORM의 query 함수를 가져오고, 그 안에 인자로 sql문과 params를 받게 만들어준다.

  3. database를 생성해주고, new Database 로 복사 해준 후, 그 안에 우리가 사용하던 typeORM의 DataSource를 넣어준다. (이렇게 해줘야 TypeORMDataSource를 Class안에 넣어줄 수 있고, 그것을 받아 우리는 query method를 사용할 수 있게 된다.)

  4. 마지막에 모듈로써 exports 해주면 API의 어디에서든 사용이 가능하다.

이렇게 해주면 알수 있는 부분은 typeORMDataSource와 같은 다른 Sequelize와 같은 라이브러리에서도 API와 DB를 연결하는 방식이 있을테고, 만약 변경이 일어났을 때 이 Class 내에 전달해주는 객체를 다르게 해주고, Class 안에서 사용되는 Method만 다시 정의해주면 100만개의 Query문을 수정할 필요없이 이 Class만 수정해주면 전체 코드의 수정을 최소화 할 수 있다.

4. Query Parameter를 받을 때 객체형식으로 분기처리

5. GlobalErrorHandler

6. jwt 발급 및 검증 모듈

profile
개발자 키우기 Lv1

0개의 댓글