게시글 update Api 만들기

·2022년 6월 9일
0

팀 프로젝트

목록 보기
29/34

코드를 볼 수 있는 곳으로 이동합니다! 클릭시 이동

502번째 줄
608번째 줄
624번째 줄
669번째 줄


흔히 CRUD라고 부르는 api의 기본이 있다.

C : create
R : read
U : update
D : delete

그런데 게시글에 정보를 이것저것 꾸겨넣어놨더니 업데이트를 손을 댈 수가 없는 지경에 와버렸다.

fetchBoard를 하면 볼 수 있는 정보들

그래서 도대체 어떻게 해야할까 하다가 엉망진창이였던 구조부터 엎었다.

과거의 createBoard api

@UseGuards(GqlAuthAccessGuard)
  @Mutation(() => Board)
  createBoard(
    @CurrentUser() currentUser: ICurrentUser,
    @Args('createBoardInput') createBoardInput: CreateBoardInput,
    @Args('boardTagsInput') boardTagsInput: BoardTagsInput,
  ) {
    return this.boardService.create({
      createBoardInput,
      boardTagsInput,
      currentUser,
    });
  }

BoardTagsInput DTO

@InputType()
export class BoardTagsInput {
  @Field(() => [String], { nullable: true })
  boardTagMenu: string[];

  @Field(() => [String], { nullable: true })
  boardTagRegion: string[];

  @Field(() => [String], { nullable: true })
  boardTagMood: string[];
}

과거의 CreateBoardInput DTO

@InputType()
export class CreateBoardInput {
  @Field(() => String)
  boardTitle: string;

  @Field(() => String, { nullable: true })
  boardSugar: string;

  @Field(() => String, { nullable: true })
  boardSalt: string;

  @Field(() => String)
  boardContents: string;

  @Field(() => BOARD_SUB_CATEGORY_NAME_ENUM)
  subCategoryName: string;

  @Field(() => PlaceInput)
  place: { PlaceInput: string };
}

DTO는 최대한 한개로 압축해서 처리하기 위해 사용하는 것인데
글 생성에 DTO가 두개가 있다는 것이 뭔가 이상해보인다.

더불어서 이 상태의 create 코드는 Promise.all에 map이 5개나 돌아가는 구조였고
글을 생성하는데 130줄가량이나 되는 문제가 있었다.

그래서 업데이트가 불가능한 것이 말이 안되기에, 고민을 계속 해봤는데
태그를 보면 메뉴,지역,분위기 3개로 나눴는데
유저가 태그를 새로 생성할 수 있는 것이 아니라, 그냥 선택만 할 수 있는 조건이였다.

태그때문에 수정에 에러가 발생하고 있는 것이였는데
어짜피 선택 만 할 수 있다면 굳이 분리를 할 필요가 없다는 것을 자각했다.

그래서 모조리 날려버리고 [tags]의 형태로 받기로 했더니 문제가 있던 것들이 다 풀렸다.

물론 프론트와 대화를 한 후에 api 수정이 이루어졌다. 엄마한테 허락받는 기분

그래서 이러한 형식으로 변하게 되었다.

현재의 CreateBoardInput DTO

@InputType()
export class CreateBoardInput {
  @Field(() => String)
  boardTitle: string;

  @Field(() => String)
  boardSugar: string;

  @Field(() => String)
  boardSalt: string;

  @Field(() => String)
  boardContents: string;

  @Field(() => BOARD_SUB_CATEGORY_NAME_ENUM)
  subCategoryName: string;

  @Field(() => [String])
  tags: string[];

  @Field(() => PlaceInput)
  place: { PlaceInput: string };
}

현재의 createBoard api

  @UseGuards(GqlAuthAccessGuard)
  @Mutation(() => Board)
  createBoard(
    @CurrentUser() currentUser: ICurrentUser,
    @Args('createBoardInput') createBoardInput: CreateBoardInput,
  ) {
    return this.boardService.create({createBoardInput,currentUser})
  }

그리고 로직에 대한 변화도 바로 확인을 할 수 있었다.

각 태그별로 반복문이 돌아가던 것을 한개로 합치면서 코드의 복잡도가 한결 나아졌다.
또 명확해지면서 업데이트를 할 수 있는 방향을 잡게 되었다.

update api

일단 업데이트를 제일 쉽게 하는 방법은 아래와 같다.

  1. 원래 생성되어있는 것을 불러온다.
  2. 업데이트 된 내용을 덮어씌운다.

이렇게 할 경우 어지간한 것은 전부 해결이 된다.

그런데 본문같이 한개의 컬럼으로 이루어져있는 것은 이런 방식으로 하는 것이 맞겠지만
관계가 존재하는 컬럼의 경우에는 비효율적이라고 생각했다.

  1. 업데이트 데이터에 추가된 정보가 존재한다 => 새로 데이터를 생성한다.
  2. 업데이트 데이터에 기존에 있던 정보가 사라졌다 => 기존 데이터를 삭제한다.
  3. 업데이트 데이터에 기존에 있던 정보가 남아있다 => 데이터를 유지한다.

그래서 이것을 구현하기 위해서 Array.prototype.filter와 includes를 사용하여 차집합을 구했다.

아래는 이렇게 차집합을 구해서 데이터를 추가 및 삭제를 하는 현재 쓰고 있는 코드다.

계속 조회하는 로직이 들어가있어서, 아쉬움이 많은데 어떻게 개선을 할 수 있는지는 조금 더 확인을 해봐야할 것 같다.

ManyToMany인 것을 일부로 풀어서 현재 테이블의 구조가 아래와 같은 형식인데

저렇게 조회를 하지 않고서는 값을 가져오는 것이 불가능한 것 같아서 로직은 저렇게 되어있다.

맨 위부터 순서를 적어보면

  1. 기존에 게시글에 달려있는 태그들의 태그 이름들을 tag에 저장한다
  2. 기존 태그의 모임인 tag를 map으로 돌려서 객체 구조에서 이름만 있는 배열을 구성한다.
  3. 새롭게 들어온 태그와 기존의 태그를 비교하여 추가될 태그를 add_tag에 저장한다.
  4. 기존의 태그와 새롭게 들어온 태그를 비교하여 삭제될 태그를 delete_tag에 저장한다.
  5. 한번에 작업을 하기 위하여 Promise.all을 사용했다.
    5-1. add_tag를 map을 돌려서 이름으로 BoardTag Entity에서 찾은 값을 tag에 저장한다.
    5-2. 새롭게 추가된 태그를 가지고 있는 게시글 ID와 태그 ID를 중간 테이블인 boardSide에 입력한다.
    5-1. delete_tag를 map을 돌려서 이름으로 BoardTag Entity에서 찾은 값을 tag에 저장한다.
    5-2. 게시글 ID와 태그 ID를 조건으로 삭제되어야하는 BoardSide Entity를 tagSide에 저장한다.
  6. 이것은 캐시로 남겨놓을 이유가 없는 것 같아서 저장된 tagSide의 PK로 하드딜리트한다.

이러한 구조로 3개의 테이블의 조인되어있는 테이블들이 수정된다.

updateBoard.service

async update({ currentUser, boardId, updateBoardInput }) {
    const { tags, place, ...inputData } = updateBoardInput;
    const board = await this.boardRepository.findOne({
      where: { boardId },
      relations: ['user'],
    });

    if (board.user.userId !== currentUser.userId)
      throw new UnauthorizedException('해당 글의 작성자가 아닙니다.');

    if (place) {
      const newPlace = await this.isPlace({ place });
      await this.boardRepository.update({ boardId }, { place: newPlace });
    }

    if (updateBoardInput.boardContents) {
      this.imageUpdate({ updateBoardInput, board, boardId });
    }

    if (tags) {
      this.tagUpdate({ board, tags, boardId });
    }

    const newBoard = {
      ...board,
      ...inputData,
    };

    return await this.boardRepository.save(newBoard);
  }

그런데 조금 고민인 것은 조인된 테이블을 추가 및 삭제하는 코드가 상당히 길어서
퍼사드패턴을 사용하여 클래스 내부에 선언을 해놓고 this로 불러오고 있는데 코드가 깔끔해보인다는 장점은 있지만
역시나 내부 로직이 돌아가는 것을 확인하려면 이리저리 왔다갔다 해야한다는 점이 있어서
이 부분이 현명한 선택인 건지, 아쉬운 선택인 것인지 알 길이 없어서 조금 답답하다.

아무튼 업데이트를 못해서 반푼만도 못한 개발자라고 좀 슬퍼하고 있었는데 해결해서 넘 다행이다!

끝!

profile
물류 서비스 Backend Software Developer

0개의 댓글