Service Layer를 분리하면서 생기는 일

민경찬·2024년 4월 18일
2

백엔드

목록 보기
15/22
post-thumbnail

Javascript, Typescript 기반으로 작성된 글입니다.

express를 처음 배우는 여러분들의 코드는 아래와 비슷할 것입니다. router내부에 많은 코드들이 담겨있죠.

router.get('/all', async (req, res) => {
  //to FE
  const result = {};
  let statusCode = 200;

  //main
  try {
    //SELECT
    const selectUnivSql = 'SELECT university_idx, university_name FROM university_tb LIMIT 500';
    const selectUnivResult = await pgPool.query(selectUnivSql);

    result.data = selectUnivResult.rows;
  } catch (err) {
    console.log(err);

    result.message = '예상하지 못한 에러가 발생했습니다.';
    statusCode = 409;
  }

  //send result
  res.status(statusCode).send(result);
});

제가 배포했던 첫 프로젝트의 코드입니다. ( Gameuniv )

대학 목록을 가져오는 간단한 API입니다. 지금 보면 리팩토링이 너무 하고 싶어지는 코드죠.

오늘 글에서는 router의 책임에 대해서, 그리고 나머지 책임을 지어질 누군가에 대해서 알아볼 것입니다.

❗️ 무엇이 문제일까요?

위 코드의 문제점은 다음과 같습니다.

  1. SQL이 router내부에 존재
  2. Query결과를 바로 응답

위 두 가지 문제점이 정말 문제점인지에 대한 글을 따로 작성하겠습니다.

이 문제점들이 문제점이 된 원인을 한 마디로 요약하면 모든 책임이 하나로 몰려있다 입니다. 그렇다면 모든 책임이 하나로 몰려있다는 것은 왜 안 좋은 것일까요?

그 이유는 변경에 대한 유연성이 떨어지기 때문입니다. 예시를 하나들어 이해해보겠습니다.

?🤔?: 단순히 대학 이름만 보이는게 아니라 대학교 로고까지도 볼 수 있게 해주세요!

충분히 변경 가능한 영역입니다. 그러나 위 코드에서 이 기능 변경을 반영하기 위해서는 어떤 작업을 해야할까요?

router하나만 봐도 될까요? 정말요?

당연히 모든 router파일을 돌아가며 university_tb테이블을 사용하는 SQL을 찾아야합니다.
그러나 단순히 router가 아닙니다. 지금 router에 모든 내용이 들어가 있죠. 즉, 모든 코드를 훑어봐야된다는 것입니다.

문제점을 알았으니 리팩토링해봅시다.

💡 어떻게 해결할까요?

서비스 레이어를 만듦으로써 어느정도 해결할 수 있습니다.

그렇다면 서비스 레이어란 무엇일까요? http통신에 관한 책임 이외의 비즈니스 로직에 대한 책임을 담당할 누군가가 필요합니다. 그 책임을 담당해줄 녀석을 서비스 레이어라고 합니다.

그렇다면 http통신을 담당해줄 녀석은 누구일까요? 바로 router입니다. 이와 관련해서는 컨트롤러 레이어라고 흔하게 부르지만 이 글에서는 쉽게 router라고 부르겠습니다.

const getUnivAll = async () => {
	const queryResult = await pgPool.query(
      `SELECT 
			university_idx, 
			university_name 
		FROM 
			university_tb 
		LIMIT 
			500`);
 
  	return queryResult.rows;
}

이렇게 대학 목록을 가져오는 로직을 따로 추상화할 수 있습니다. router는 어떻게 될까요?

router.get('/all', async (req, res) => {
  try {
	const univList = await getUnivAll();
    
    res.status(200).send({
      data: univList
    });
  } catch (err) {
	next({status: 409, message: '예상하지 못한 에러가 발생했습니다.'});
  }
});

깔끔하게 변경되었습니다. 그러나 정말 이게 좋은 구조인걸까요?

express가 점점 익숙해지고 3 Layerd Architecture를 조금씩 받아들이는 과정에서하는 흔히 실수를 길게 풀어봤습니다.

❗️❗️ 진짜 문제를 알아봅시다.

팀원분들이 서비스 레이어를 분리할 때 흔히하는 실수에 대해서 길게 풀어봤습니다.
뭐가 문제라는걸까요?

1. 쓸모없는 추상화와 어떻게 쓰는지 모르겠는 함수

단순히 router내부에서 추상화된 함수를 사용한다해서 코드구조가 정말 깔끔해지고 유지보수하기 좋아지는 것일까요? 저의 대답은 절대 아니라고 할 수 있습니다.

어떤 값을 어떤 형식으로 return하는지에대한 그 어떤 정보도 담고있지 않습니다. 함수가 어떤 값을 받아 어떤 값을 리턴하는지 모른다면 재사용이 힘들 뿐더러 협업에 있어서는 크나큰 장애물이됩니다.

2. Query 결과를 그대로 리턴하는 상황은 그대로

어찌됐건 함수에서 쿼리 결과를 그대로 반환하고 그걸 다시 router에서 그대로 응답해주는 구조는 동일합니다.

사실상 문제점을 제대로 해결했다고 보기는 어렵겠네요

💡💡 진짜 진짜 해결해봅시다.

서론이 정말정말 길었습니다. 서비스 레이어를 서비스 레이어 답게 분리하는 방법을 알아봅시다.

  1. 서비스 레이어에서 사용하는 객체를 정의한다.
  2. 가공하는 무언가가 만든다.

위 두 가지가 반드시 필요합니다.

구조적 타이핑으로 해결하기

/**
 * @returns {Promise<{ idx: number, name: string, createdAt: Date }[]>}
 */
const getUnivAll = async () => {
    const queryResult = await pgPool.query(
        `SELECT 
			university_idx, 
			university_name,
            created_at
		FROM 
			university_tb 
		LIMIT 
			500`
    );

    return queryResult.rows.map((univ) => ({
        idx: univ.university_idx,
        name: univ.university_name,
        createdAt: new Date(univ.created_at),
    }));
};

jsDoc을 통해 어떤 값을 return하는지 명시해주고 값을 가공하는 과정을 거쳐야합니다.

덕 타이핑으로 해결하기

서비스 레이어에서 사용하는 객체를 만듭니다.

class Univ {
    /**
     * @type {number}
     */
    idx;

    /**
     * @type {string}
     */
    name;

    /**
     * @type {Date}
     */
    createdAt;

    constructor(data) {
        this.idx = data.idx;
        this.name = data.name;
        this.createdAt = data.createdAt;
    }
}

서비스 레이어에서 객체 배열을 반환하도록 만듭니다.

/**
 * @returns {Promise<Univ[]>}
 */
const getUnivAll = async () => {
    const queryResult = await pgPool.query(
        `SELECT 
			university_idx, 
			university_name,
            created_at
		FROM 
			university_tb 
		LIMIT 
			500`
    );

    return queryResult.rows.map((univ) => new Univ({
        idx: univ.university_idx,
        name: univ.university_name,
        createdAt: new Date(univ.created_at),
    }));
};

두 방법 모두 핵심은 서비스 레이어에서 사용하는 객체의 분리와 가공 주체의 분리입니다.

😎 이 방법의 핵심은?

javascript에서 지원하는 구조적 타이핑 방식과 class문법을 사용한 덕 타이핑 방식 모두 해결하는 방법을 알아봤습니다.

그런데 왜 이렇게 하는 것일까요?

핵심은 책임의 분리입니다.

기존의 코드에서는 query의 책임이 응답의 결과까지로 이어지고 있습니다. 그렇다는 말은 query의 작은 수정이 응답 형태를 변경할 것이고 프론트엔드 코드의 변화까지 이어진다는 것입니다.

책임을 분리할 경우 query를 변경하더라도 가공의 주체만을 변경해주면됩니다. 응답에 대한 책임을 더 이상query가 가지지 않는 것이죠. 우리는 이제 응답에 대한 작은 수정도, query에 대한 작은 수정도 유연하게 대처할 수 있습니다. 책임소재만 파악하고 변경해주면 되는 것이죠.

🧐 근데 정말 좋을까요?

레이어를 만드는 것 또한 추상화 비용이 발생합니다.

그러나 우리는 추상화 비용을 감당하면서 다음의 이점을 얻었습니다.

  1. 작은 변경점에 유연하게 대처할 수 있는 코드
  2. 높은 코드 가독성

그러나 우리는 레이어를 분리하면서 잃는 것들 또한 생깁니다. 예를 들어볼까요? 위에서 "작은" query의 변경을 유연하게 대처할 수 있다고 했습니다. 그러나 요구되는 기능이 변경되고 query부터 서비스 객체, 응답 값 모두가 바뀌어야하면 어떻게 될까요? 만들었던 Service, Entity, Dto 모두를 변경해야합니다.

즉, 높은 추상화비용이 발생한다는 것입니다.

절대적으로 나쁜 구조는 없습니다. 요구되는 기능이 급하고 바쁘다면, 추상화를 할 시간이 없다면 레이어를 나누지 않는 것도 방법입니다.

결론

레이어를 분리한다는 것은 책임을 분리한다는 것입니다. 어떤 책임이 있는지, 그리고 책임이 정말 분리되었는지 꼭 의심하며 개발합시다.

그리고 jsDoc쓰면서 느끼실 겁니다. 웬만하면 Typescript를 쓰도록 합시다.

0개의 댓글