들어가며
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 들도 리팩토링 해나갈 예정입니다.
'포트폴리오 > JJINCAFE IN SEOUL' 카테고리의 다른 글
[리팩토링] 폴더 구조의 변경 (0) | 2022.11.15 |
---|---|
[Docker 적용] MySQL 서비스 컨테이너에 .sql 파일 Import 후 데이터베이스에 적용하기 (0) | 2022.11.15 |
서버 실행할 때 왜 .env.production 파일이 파싱되지 않지? (0) | 2022.11.14 |