TypeScript 프로젝트에서 Value Object 적용하기

Yi suho·2024년 1월 14일
1

Value Object

목록 보기
1/2
post-thumbnail

평소 TypeScript에서 데이터 구조를 정의할 때 주로 인터페이스를 활용했습니다.인터페이스는 타입의 형태를 정의하고 객체가 특정 구조를 따르도록 강제하는 강력한 도구입니다.
하지만 이번 프로젝트에서는 도메인 주도 설계(DDD)의 핵심 개념중 하나인 Value Object에 대해 배우면서 단순한 형태 정의를 넘어서는 여러 강점을 알게 되어 Value Object를 적용해 보기로 하였습니다.

Value Object는 도메인 주도 설계(DDD)에서 중요한 개념으로 불변성을 갖는 객체를 말합니다.
이러한 객체들은 주로 값을 표현하는데 사용되며 객체의 동등성은 그 값에 의해 결정됩니다.

Value Object 란?(Entity와 Value Object의 차이점)

이번 포스팅에서는 Value Object가 타입스크립트의 인터페이스와 어떻게 다르며, 왜 이 차이가 중요한지 살펴보겠습니다.

인터페이스와 차이점

  • 의미 있는 설계 : Value Object는 단순히 타입을 지정해주는 것 이상의 의미를 가집니다.
    특정 도메인의 규칙과 의미를 코드에 반영하여 잘못된 사용을 방지하고 의도를 명확히 표현할 수 있습니다.
  • 불변성 : Value Object는 일반적으로 변경 불가능(immutable)하게 설계된다. 이는 데이터의 안정성과 예측 가능성을 높이는데 도움이 됩니다.
  • 검증 로직 : 올바른 값 객체 구현에는 생성 시점에서 값의 유효성을 검증하는 로직이 포함되어야합니다.
    이를 통해 더 강력한 도메인 규칙을 적용할 수 있습니다.

적용 예시

프로젝트 간단소개

이번 프로젝트의 목적은 사용자가 제공한 정보를 활용해 맞춤형 문제를 생성하고, 사용자가 이에 대해 답변을 제출하면 그에 대한 피드백을 제공하는 것입니다.
구체적으로 사용자가 특정 포지션과 스킬을 선택하거나,원티드 채용공고의 URL을 제공하면 이를 바탕으로 적합한 면접 질문 10개를 생성합니다.
사용자가 문제에 답변하면 피드백을 제공하는 서비스 입니다.

Value Object를 통한 데이터 유효성 검증

사용자가 입력한 채용공고의 URL을 분석하여 회사명,채용공고의 상세정보를 데이터베이스에 저장할때 Value Object를 활용하여 데이터의 유효성을 검증하는 방법입니다.

인터페이스를 활용하여 데이터를 저장하였다면


interface UrlInterview{
  URL:string;
  companyName:string;
  UrlContents:string;
}


const urlInterview:UrlInterview = {
  URL:'https://www.wanted.co.kr/',
  companyName:'velog',
  UrlContents:'채용 공고 내용'
}

이렇게 할 수 있을 것 입니다.

이때 URL,companyName를 잘못 배치한다면 데이터베이스에 잘못된 정보가 저장 될 것입니다.
둘 다 문자열 타입이기 때문에 컴파일러는 이 오류를 잡아 내지 못하여 발생하는 문제입니다.

Value Object를 활용하여 단순한 값을 좀 더 구조화하여 오류 가능성을 줄이고 코드의 명확성을 높일 수 있습니다.
각 값에 특정 타입을 명시함으로써 잘못된 값이 할당되는 것을 방지 할 수 있습니다.
또 비즈니스 로직에서 사용되는 데이터를 캡슐화하고,필요한 검증 로직을 포함할 수 있어 더 안전하고 효율적인 코드 작성을 가능하게 합니다.

export class UrlValue {
  constructor(private readonly value: string) {
    if (value === '' || !value) {
      throw new Error('URL이 없습니다.');
    }
    try {
      new URL(value);
      this.value = value;
    } catch (error) {
      throw new Error('유효하지 않은 URL 형식입니다.');
    }
  }
  
   getValue() {
    return this.value;
  }
}

export class CompanyName {
  constructor(private readonly value: string) {
    if (value === '' || !value) {
      throw new Error('회사명이 없습니다.');
    }
  }
  
   getValue() {
    return this.value;
  }
}

export class UrlContents {
  constructor(private readonly value: string) {
    if (value === '' || !value) {
      throw new Error('URL Content가 없습니다.');
    }
  }
  
   getValue() {
    return this.value;
  }
 }

  
  
export class UrlInterview {
  constructor(
    private readonly urlValue: UrlValue,
    private readonly companyName: CompanyName,
    private readonly urlContent: UrlContents,
  ) {}...
}  
const urlValue = new UrlValue('https://www.wanted.co.kr/')
const companyName = new CompanyName('velog')
const urlContents = new UrlContents('채용 공고 내용')

const urlInterview = new UrlInterview(urlValue,companyName,urlContents)

Value Object을 활용한다면 아래와 같이 URL 값이 들어가야 하는 곳에 commpanyName의 값이 들어간다면 Value Object 내부의 검증 로직에서 오류를 발생시켜 잘못된 값이 데이터베이스에 저장되는것을 방지 할 수 있습니다.

const urlValue = new UrlValue('velog')
//UrlValue 클래스의 생성자는 입력된 문자열이 유효한 URL 형식인지 검증합니다. 이 경우 생성자에서 
//'유효하지 않은 URL 형식입니다.'라는 오류가 발생합니다.
//이렇게 Value Object를 통해 잘못된 입력에 대한 오류를 빠르게 감지하고 처리할 수 있습니다.

Value Object로 비즈니스 로직 구현

프로젝트를 진행하며 공통적으로 사용되는 비즈니스 로직을 Value Object로 구현하여 서비스간의 의존성을 줄이고 비즈니스 로직 수행 후 유효성 검사를 진행하므로써 전체적인 작업의 효율성을 높였습니다.

프로젝트에서는 OpenAI API를 통해 생성된 10개의 문제가 긴 문자열로 반환되는데, 이를 개별 문제로 분리하여 배열에 저장하는 작업이 필요합니다.
또 한번의 요청으로 동일한 문제가 발생하지 않도록 검증하는 로직이 중요합니다.

이러한 비즈니스 로직이 사용자 설정 문제 서비스와 URL분석 문제 서비스 양쪽에서 필요했기 때문에
한 서비스에서 로직을 정의하고 다른 서비스가 이를 의존하기보다는 Value Object를 활용하여 로직을 구현하고 두 서비스 모두에서 이를 활용하는 것이 더 효율적이라고 판단했습니다.

Value Object를 활용하여 OpenAI API를 통해 생성된

{"1. 질문내용\n2. 질문내용\n .....\n10. 질문내용"}

위와 같은 형식의 긴 문자열을 다루는 questionReplace() 라는 static 함수를 구현했습니다.
이 함수는 긴 문자열을 받아서 적절히 나누고 결과를 OpenAIQuestion 객체의 인스턴스로 반환합니다.
static함수는 클래스의 인스턴스를 생성하지 않고도 클래스이름을 통해 직접 호출 할 수 있습니다.

export class OpenAIQuestion {
  // 전달된 문자열 배열을 받아 중복을 제거하고, 
  // 10개 미만의 문제가 있는 경우 에러를 발생시킵니다.
  constructor(private readonly value: Array<string>) {
    const duplicateCheck = [...new Set(value)];

    if (duplicateCheck.length < 10) {
      throw new Error('10개의 문제가 생성되지 않았습니다.');
    }

    this.value = duplicateCheck;
  }
  
  // questionReplace 함수는 주어진 긴 문자열에서 문제들을 분리하고
  // OpenAIQuestion 객체의 새 인스턴스로 반환합니다.
  // 이 과정에서 문자열에서 숫자와 점을 제거하고, 줄바꿈으로 분리합니다.
  static questionReplace(question: string): OpenAIQuestion {
    const linesWithoutNumbers = question.replace(/^\d+\.\s+/gm, '');
    const questionsArray = linesWithoutNumbers.split('\n');

    return new OpenAIQuestion(questionsArray);
  }

  // getValue 함수는 Value Object의 현재 값을 반환합니다.
  getValue() {
    return this.value;
  }
}
 const questionArray = OpenAIQuestion.questionReplace("1. 질문내용\n2. 질문내용\n .....\n10. 질문내용" );

서비스에서 OpenAIQuestion 클래스의 questionReplace함수를 호출해 긴 문자열을 나누고 결과적으로 생성된 10개의 문제가 중복되지 않았는지 확인합니다.

문제의 중복 여부를 판단하기 위해 Set 객체를 활용했습니다.
Set은 중복된 값을 허용하지 않기 때문에 중복된 문제가 있을 경우 Set에 추가된 문제들의 총 수가 줄어들 것입니다.
이 특성을 이용하여 분리된 문제들을 Set 객체에 저장하고 결과적으로 배열의 길이가 10미만일 경우 오류를 발생시키도록 구현했습니다.

 const duplicateCheck = [...new Set(value)];

    if (duplicateCheck.length < 10) {
      throw new Error('10개의 문제가 생성되지 않았습니다.');
    }

    this.value = duplicateCheck;

이번 프로젝트를 통해 TypeScript에서 Value Object를 활용하는 것의 중요성을 이해 하게 되었습니다.
인터페이스만을 사용할 때와 달리 Value Object를 도입함으로써 데이터의 불변성을 보장하고 강력한 유효성 검증 로직을 통해 코드의 안정성과 효율성을 높일수있었습니다.

이 포스팅이 Value Object 의 개념을 이해하는데 도움이 되었기를 바랍니다.

0개의 댓글