들어가며

5월에 종료했던 개인 프로젝트 코드를 다시 열어 보았는데 내가 왜 이렇게 코드를 작성했었지? 라는 생각이 제일 먼저 들었습니다. 대표적으로 기존에 작성했던 회원가입 API 코드를 다시 보니 다음의 두 가지 문제점을 발견했습니다.

  • 회원 정보 저장 쿼리에 대해 트랜잭션 처리가 되어 있지 않았습니다. 이는 회원가입이 실패하더라도 회원 정보가 저장되는 사이드 이펙트를 발생시킬 수 있습니다.
  • 컨트롤러에 요청, 응답을 처리하는 로직과 데이터베이스에 데이터를 저장하는 로직이 혼재되어 있어 가독성과 유지 보수성이 떨어집니다.

문제점을 발견했으니 가만히 있을 수 없어 코드 리팩토링을 진행했고 리팩토링 과정을 기록으로 남기고자 합니다.

기존에 작성했던 회원가입 API 코드

class AuthController {
  // 회원가입 컨트롤러
  static createUser = async function (req, res, next) {
    const reqObj = { ...req.body };
    const { name, image_path, email, password } = reqObj;

    const connection = await pool.getConnection();

    try {
      const queryString =
        'insert into users (name, profile_image_path, email, password) values (?,?,?,?)';
      const queryParams = [name, image_path, email, password];
      const result = await connection.execute(queryString, queryParams);
      if (result[0].affectedRows === 0) {
        throw new InternalServerError('User register fail');
      }
      logger.info('User register success');
      return res.sendStatus(successCode.CREATED);
    } catch (err) {
      throw err;
    } finally {
      connection.release();
    }
  };
}

우선 컨트롤러 안에 응답 처리 로직과 DB 통신 요청 로직이 혼재되어 있어 이를 분리하기로 결정했습니다.

컨트롤러, 서비스로 로직 분리

DB 통신 요청 로직은 AuthService 클래스의 메서드로 추출했습니다.

 

auth.service.js

class AuthService {
  // @param userInfo: {name, image_path, email, password}
  static saveNewUser = async userInfo => {
    let result = 0;
    const connection = await pool.getConnection(async conn => conn);

    try {
      const {name, image_path, email, password} = userInfo;
      const queryString =
      'insert into users (name, profile_image_path, email, password) values (?,?,?,?)';
    const queryParams = [name, image_path, email, password];
    const resultOfQuery = await connection.execute(queryString, queryParams);
    if (resultOfQuery[0].affectedRows === 0) result = 500;
    else result = 201;

    return result;
    } catch (err) {
      result = 500;
      return result;
    } finally {
      connection.release();
    }
  }
}

컨트롤러에서 서비스 메서드의 리턴값에 따라 쉽게 응답을 처리할 수 있도록 리턴값을 상태코드로 지정했습니다.

- 201 : 회원가입 성공

- 500 : 회원가입 실패 및 서버에서 내부적으로 처리하는 과정에서 문제가 있음

 

auth.controller.js

class AuthController {
  // 회원가입 컨트롤러
  static createUser = async (req, res, next) => {
    const res = await AuthService.saveNewUser(req.body);
    if (res === 500) next(new InternalServerError(messages[500]));
    return res.sendStatus(successCode.CREATED);
  };
}

리팩토링 전과 비교했을 때 가독성뿐만 아니라 관심사 분리가 확실하게 됐음을 알 수 있습니다.

- 컨트롤러 : 클라이언트의 요청 및 응답 처리 역할 수행

- 서비스 : 비즈니스 로직 처리

객체 프로퍼티로 에러 메세지 관리를 통한 유지 보수성 및 재사용성 향상

추가적으로, API 별로 모두 500 에러 처리를 하는데 이때 500 에러 메시지가 중복 사용되는 경우가 많았습니다.

재사용성을 높이기 위해서 key: 상태 코드 값, value: 에러 메세지의 구조를 가지는 messages 객체를 만들었습니다.

 

errors/messages.js

const messages = {
  500: '서버 내부 오류 발생',
  404: '찾고자 하는 리소스가 존재하지 않음',
};

module.exports = messages;

트랜잭션 처리

서비스 메서드를 보면 회원 정보 저장 쿼리에 대해 트랜잭션 처리가 되어 있지 않았습니다. 이는 회원가입이 실패하더라도 회원 정보가 저장되는 사이드 이펙트를 발생시킬 수 있습니다.

회원 정보 저장 실패 시 이전 상태로 롤백시킬 수 있도록 반드시 트랜잭션 처리를 해줘야 합니다.

class AuthService {
  // @param userInfo: {name, image_path, email, password}
  static saveNewUser = async userInfo => {
    let result = 0;
    const connection = await pool.getConnection(async conn => conn);

    try {
      await connection.beginTransaction();

      const {name, image_path, email, password} = userInfo;
      const queryString =
      'insert into users (name, profile_image_path, email, password) values (?,?,?,?)';
      const queryParams = [name, image_path, email, password];
      const resultOfQuery = await connection.execute(queryString, queryParams);
      if (resultOfQuery[0].affectedRows === 0) result = 500;
      else result = 201;
    
      await connection.commit();
      return result;
    } catch (err) {
      await connection.rollback();
      result = 500;
      return result;
    } finally {
      connection.release();
    }
  }
}
  • connection.beginTransaction( ) : 트랜잭션 적용 시작
  • connection.commit( ) : 트랜잭션 반영
  • connection.rollback( ) : 트랜잭션 롤백(이전 상태로 돌림)
  • connection.release( ) : connection 해제

트랜잭션 처리 시 주의사항은 반드시 컨트롤러에게 result 리턴 전 커밋 처리를 해줘야 합니다.

만약 커밋 처리를 해주지 않을 시 회원가입이 성공하더라도 사용자 정보가 유실될 수 있습니다.

처음에 커밋 처리를 해주지 않아 고생했던 기억이 있습니다...

마치며

회원가입 API 와 마찬가지로 위와 같이 다른 API 들도 리팩토링 해나갈 예정입니다.

 

복사했습니다!