본문 바로가기
개발

더 나은 코드를 고민 해보기 - Mapper & Builder 패턴

by 최승환 2024. 3. 6.

사이드 프로젝트 작업을 하면서 Service 레이어에서 결과값을 return 하는 과정에서 불필요한/불편한 코드 작업이 반복되는 경우가 있었다.

 

예를 들어, 아래의 코드는 어떤 모듈 Service 의 수많은 함수 중 하나의 예이다.

commitment 라는 키워드는 내가 작업 중인 사이드 프로젝트에서 사용되는 용어이므로, 읽기 복잡하다면, 단순히 item 이라는 용어로 치환해서 읽어도 무방하다.

async completeCommitment(commitmentId: string, user: User): Promise<CommitmentInfo> {
    try {
      const commitmentActivity = await this.commitmentActivityRepo.findOne({
        where: {
          commitment: { id: commitmentId },
          user: { id: user.id },
        },
        relations: ['commitment'],
      });

            ...

            const userInfo = userInfoMapper(user);
            const commitmentActivityInfo = commitmentActivityInfoMapper(commitmentActivity);
            const commitmentInfo = commitmentInfoMapper(commitment, this.userInfo);
            commitmentInfo.activity = commitmentActivityInfo;

      return commitmentInfo;
    } catch (e) {
            ...
    }
  }

 

코드를 보면, 데이터베이스에서 가져온 모델 데이터를 그대로 Client 에 전달 할 때 발생할 수 있는 문제들을 방지하기 위해 (User의 Password 등등… Client 에서 불필요한 정보)
~~~Info 라는 형태로 데이터를 Wrapping 해주는 Mapper 를 사용하고 있다.

아래는 여러 Mapper 중, userInfoMapper 예시이다.

// 예시
const userInfoMapper = (user: User): UserInfo => {
  return {
    id: user?.id,
    nickname: user?.nickname,
    createDate: user?.createDate,
  };
};

 

위와 같이 단순히 한 차례 필요한 데이터만 가져오도록 하는 기능이 구현되어 있다.

 

문제점

여기서 문제는, Service 레이어의 여러 수많은 함수들에서, commitmentInforeturn 하는 과정에서

// 코드 반복
const userInfo = userInfoMapper(user);
const commitmentActivityInfo = commitmentActivityInfoMapper(commitmentActivity);
const commitmentInfo = commitmentInfoMapper(commitment, this.userInfo);
commitmentInfo.activity = commitmentActivityInfo;

return commitmentInfo;

이러한 코드가 반복 되고 있다는 것이다.

 

commitmentInfo 를 반환하기 위해 코드가 반복 되고 있을 뿐만 아니라, commitmentInfo 의 형태가 바뀌는 때에는 프로젝트의 모든 commitmentInfo 를 생성하고 있는 곳에서 일일이 코드를 전부 수정 해야만 하는 사태가 벌어지게 된다.

 

더군다나 commitmentInfo 객체를 생성한 후,

commitmentInfo.activity = commitmentActivityInfo; 이런 식으로 activity 를 연결해 주는 코드 순서 역시 조잡해 보이고 혼동하기 쉬워 보인다.

 

Builder 패턴을 사용하자

 

Builder 패턴은 이렇게 객체의 생성 절차가 복잡할 때 유용하게 적용해 볼 수 있는 패턴이다.

아래는 CommitmentInfoBuilder 코드이다.

/**
 * @description 사용 예시
 * const commitmentInfo = new CommitmentInfoBuilder().setUserData(user).setCommitmentData(commitment).build();
 */
export class CommitmentInfoBuilder {
  private commitmentInfo: CommitmentInfo;
  private userInfo: UserInfo;
  private commitmentActivityInfo: CommitmentActivityInfo;

  setUserData(user: User): CommitmentInfoBuilder {
    this.userInfo = userInfoMapper(user);
    return this;
  }

  setCommitmentActivityData(commitmentActivity: CommitmentActivity): CommitmentInfoBuilder {
    this.commitmentActivityInfo = commitmentActivityInfoMapper(commitmentActivity);
    return this;
  }

  setCommitmentData(commitment: Commitment): CommitmentInfoBuilder {
    this.commitmentInfo = commitmentInfoMapper(commitment);
    return this;
  }

  build(): CommitmentInfo {
        if (this.userInfo) this.commitmentInfo.user = this.userInfo;
    if (this.commitmentActivityInfo) this.commitmentInfo.activity = this.commitmentActivityInfo;
    return this.commitmentInfo;
  }
}

 

필요한 데이터를 set 하는 set~~~~~Data() 함수에서 자기 자신 (this) 를 return 하고 있다.

각종 set 함수에서는, 객체에 해당 데이터를 set 할 때 필요한 로직들을 수행하고 있다. 여기선 단순히 데이터에 맞는 Mapper 함수를 실행하고 있다.

 

아래는 Builder 패턴을 적용하여 코드를 수정한 내용이다.

async completeCommitment(commitmentId: string, user: User): Promise<CommitmentInfo> {
    try {
      const commitmentActivity = await this.commitmentActivityRepo.findOne({
        where: {
          commitment: { id: commitmentId },
          user: { id: user.id },
        },
        relations: ['commitment'],
      });

            ...

      const commitmentInfo = new CommitmentInfoBuilder()
        .setUserData(user)
        .setCommitmentActivityData(commitmentActivity)
        .setCommitmentData(commitmentActivity.commitment)
        .build();

      return commitmentInfo;
    } catch (e) {
            ...
    }
  }

 

Builder 의 각종 Set 함수 동작 후에, build() 를 호출하는 방식으로 코드를 수정하였다.

각 데이터를 Set 하는 부분에서 해당 로직들을 추상화 해, 데이터를 Set 하는 로직이 변경 되더라도 Builder 에서 일괄적으로 핸들링 할 수 있도록 하였고,

build() 함수에서 Set 한 데이터들을 연결해 주는 로직을 만들어서 commitmentInfo 생성 시의 로직을 핸들링 할 수 있도록 하였다.

 

디자인 패턴은 개발자들이 프로그래밍을 하다 만나는 어떠한 비슷한 유형의 문제들을 좀 더 잘 해결할 수 있도록 ‘패턴’ 이라는 형태로 형식화 한 것이다.

현업에서는, 코딩하다가 문제점을 파악하고 디자인 패턴을 적용해 보는 경우가 있고, 이미 알고 있는 디자인 패턴을 토대로 코드를 고민해 가는 경우도 있는 것 같다.