TIL

24/01/11 TIL __ 트렐로 만들기 회고

GABOJOK 2024. 1. 11. 19:51

 

 

 

팀프로젝트가 끝났다.

트렐로 와 같은 칸반보드를 만드는게 과제로 주어졌고,

금요일부터 목요일 오전 제출이니깐 6일정도 된거 같다. 

 

프로젝트가 끝날때마다 항상 아쉬움이 남았지만

이번 팀프로젝트에서는 좀더 많은 아쉬움이 남았던거 같다. 

 

이유를 생각해 봤는데  이 프로젝트를 통해 어떤걸 얻어가고 싶은지 명확하게 정하지 않아서 아쉬움이 발생한 것 같다. 

 

명확하게 하지 않고, 마냥 주어진 과제를 따라가다 보면, 할게 많기때문에

선택과 집중이 필요한 짧은 기간의 프로젝트에서 흐지부지 될 수 있는것 같다.

언제나 그랬던것 처럼 최선을 다했지만, 시간은 제한적이기 때문에 선택과 집중이 매우 필요하다고 느꼈다. 

 

카드 정렬 부분을 고민을 많이 했었는데 

혼자 고민할게 아니다 보니(카드 뿐만 아니라 리스트도 이동을 해야했기 때문)

그래서 팀에 이야기를 꺼내 함께 회의를 진행하게 되었다. 

정말 오랜 회의끝에 나온 결론은 프론트에서 처리하는걸로 정리되었다. 

짧은 기간에 비해 다 못할것 같다는 결론이였다.  

그래서 이부분에 대해 고민을 끝까지 하지 않았는데

다시 생각해 보면 이부분에 대한 고민이 필요했다. 😂

 

 

물론 좋았던 부분도 있다. 

문제가 생겼을 때 함께 공유하고 찾아보는 부분과,

다들 금방금방 하시는 편이여서 진행에 크게 차질이 없었다. 

그랬기 때문에 오히려 아쉬웠던거 같다. 

이 멤버라면 더 좋은 결과물을 뽑아낼 수 있었을것 같았는데 말이다. 

 

 

 

이번 과제를 진행하며 신경썻던 부분은,

네스트 환경에서 에러처리를 꼼꼼히 하려고 했었고,

트랜젝션이나 락처리로 데이터를 안전하게 처리하기 위해 노력했다.

락은 정말 다양하게 있었고 실제로 그것이 어떻게 락이 동작하는지 깊게 공부하진 못했지만, 

공부한 내용을 간략하게 기록한다. 

 

READ COMMITTED는 커밋된 데이터만 읽으며, 읽기 기능이 잠금이 된다

수정 작업도 있는데 읽기 기능에만 국한된 READ COMMITTED를 사용한 이유는 이렇다. 

 

처음에는 격리 수준이 높은게 짱이지~ 라고 외치며 빡센 격리를 찾았지만,

이내 단점을 보고야 말았다..

 

🤨 격리 수준이 높다 !== 좋은 데이터 관리

Serializable 이라는 격리 수준은 가장 엄격한 격리 수준이라 일관성 있는 데이터 관리가 가능하다. 

Phantom Reads(한 트랜젝션이 범위 내에서 새로운 데이터가 추가되거나 삭제되는것) 를 방지해 주기까지 한다. 

그런데 성능적 문제가 있었다. 

많은 잠금을 사용한다는건 동시성이 저하된다는 것이고, 성능이 떨어질 수 있으며, 데드락에 대한 가능성까지 높아진다고 한다.

(데드락은 여러 트렌젝션이 서로 잠금을 기다리는 경우 발생할 수 있다.)

격리수준이 높다보니 트랜젝션의 완료까지 시간이 많이 걸려서 지연 발생할 확률이 높아 보였고,

자원의 이용도 또한 많이 가져가기 때문에 성능적 이슈를 고려 할 수 밖에 없었다.

그렇다고 추가 시스템자원을 사용할  이유도 없었기에 다른 락을 찾아보았다. 

 

그렇게 READ COMMITTED를 사용하게 되었다. 

  • 커밋된 안전한 데이터만 읽을수 있고,
  • 여러 트렌젝션이 동시에 읽을수 있을 뿐더러
  • 일관성도 어느정도 보장되는 특성이 있었기에 사용하기로 했다. 

 

 

 

@Injectable()
export class CardsService {
  constructor(
    @InjectRepository(Card)
    private cardRepository: Repository<Card>,
    @InjectRepository(User)
    private userRepository: Repository<User>,
    @InjectRepository(List)
    private listRepository: Repository<List>,
    private listService: ListService,
    private dataSource: DataSource,
  ) {}

  //카드 생성하기_ +리스트 테이블에 order배열에 추가하기
  async create(
    createCardDto: CreateCardDto,
    listsId: number,
    user: User,
  ): Promise<CreateCard | CreateCardFail> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('READ COMMITTED');

    try {
      const { allowMembers, cardName, workers } = createCardDto;

      //카드의 이름은 유니크 해야 합니다.
      const findCardName = await queryRunner.manager
        .getRepository(Card)
        .findOne({
          where: { cardName, listsId: listsId },
        });
      if (findCardName) {
        throw new BadRequestException('이미 존재하는 카드의 이름입니다.');
      }

      //컬럼 레포지토리에서 컬럼 아이디에 해당하는 데이터 목록 불러온 후
      const findListData = await this.listService.findOneListData(+listsId);
      if (!findListData) {
        throw new BadRequestException(
          '존재하지 않는 컬럼에 카드를 생성할 수 없습니다.',
        );
      }

      //user 테이블에서 존재하는 사용자 인지 조회하기 _ allowMembers
      const findAllowMembers = await this.userRepository
        .createQueryBuilder('user')
        .select('user.id')
        .andWhere('user.id IN (:...ids)', { ids: allowMembers })
        .getMany();
      if (findAllowMembers.length !== allowMembers.length) {
        throw new BadRequestException('지정하신 사용자는 없는 사용자 입니다. ');
      }

      //workers에 유저들이 실제 존재하는 유저인지 검증_ 꼼꼼히 처리하는게 좋다고 생각해서 넣었습니다.
      const findWorkers = await this.userRepository
        .createQueryBuilder('user')
        .select('user.id')
        .andWhere('user.id IN (:...ids)', { ids: workers })
        .getMany();
      if (findWorkers.length !== workers.length) {
        throw new BadRequestException('지정하신 사용자는 없는 사용자 입니다.');
      }

      const createCard = await queryRunner.manager.getRepository(Card).save({
        ...createCardDto,
        writer: user.name,
        userId: user.id,
        listsId: findListData.id,
      });
      //list테이블에 카드순서 데이터 수정
      const changeCardIdToString = createCard.id.toString();
      findListData.cardOrder.push(changeCardIdToString);
      await queryRunner.manager.getRepository(List).save(findListData);

      await queryRunner.commitTransaction();
      return {
        success: true,
        message: '카드 생성을 완료했습니다.',
        data: createCard,
      };
    } catch (err) {
      await queryRunner.rollbackTransaction();
      return err.response;
    } finally {
      await queryRunner.release();
    }
  }

  //해당 컬럼에 속하는 모든 카드 보기 + 리스트이름 데이터 추가.
  async allCardsInOneList(
    listsId: number,
    user: User,
  ): Promise<AllCardsInOneList | CreateCardFail> {
    try {
      //해당 리스트 아이디가 존재하는지 검증.
      const allCardsInOneList = await this.cardRepository.find({
        where: { listsId },
      });
      if (!allCardsInOneList) {
        throw new BadRequestException('존재하지 않는 리스트 입니다.');
      }

      //현재 접근하는 유저가 카드에 접근을 허용한 allowMember중 한명인가?
      const filterCardList = allCardsInOneList.filter((data) => {
        return data.allowMembers.map((e) => +e).includes(user.id);
      });

      const findListData = await this.listService.findOneListData(+listsId);
      return {
        success: true,
        message:
          '리스트에 속한 모든 카드 중 유저에게 허용된 모든 카드조회를 완료했습니다.',
        data: {
          listTitle: findListData.listTitle,
          cardList: filterCardList,
        },
      };
    } catch (err) {
      console.log(err);
      return err.response;
    }
  }

  //카드 상세보기
  async cardDetail(
    cardId: number,
    user: User,
  ): Promise<CreateCard | CreateCardFail> {
    try {
      const findCardDetail = await this.cardRepository.findOne({
        where: { id: +cardId },
      });
      if (!findCardDetail) {
        throw new BadRequestException('존재하지 않는 카드입니다.');
      }
      const allowMembers = findCardDetail.allowMembers
        .map((e) => +e)
        .includes(user.id);
      if (!allowMembers) {
        throw new UnauthorizedException('접근 권한이 없습니다. ');
      }
      return {
        success: true,
        message: '카드 상세정보 조회를 완료했습니다.',
        data: findCardDetail,
      };
    } catch (err) {
      console.log(err);
      return err.response;
    }
  }

  //카드 수정하기 __ 작업자만 수정이 가능합니다.
  async update(
    cardId: number,
    updateCardDto: UpdateCardDto,
    user: User,
  ): Promise<CreateCard | CreateCardFail> {
    const {
      cardName,
      cardDescription,
      cardColor,
      allowMembers,
      workers,
      startDate,
      endDate,
      endTime,
    } = updateCardDto;

    try {
      const findCard = await this.cardRepository.findOne({
        where: { id: cardId },
      });
      if (!findCard) {
        throw new BadRequestException('존재하지 않는 카드입니다.');
      }

      const availableUser = findCard.workers.map((e) => +e).includes(user.id);
      if (!availableUser) {
        throw new UnauthorizedException('수정 권한이 주어지지 않았습니다.');
      }

      //user 테이블에서 존재하는 사용자 인지 조회하기 _ allowMembers
      const findAllowMembers = await this.userRepository
        .createQueryBuilder('user')
        .select('user.id')
        .andWhere('user.id IN (:...ids)', { ids: allowMembers })
        .getMany();
      console.log('findAllowMembers', findAllowMembers);
      if (findAllowMembers.length !== allowMembers.length) {
        throw new BadRequestException('지정하신 사용자는 없는 사용자 입니다.');
      }

      //workers에 유저들이 실제 존재하는 유저인지 검증_ 꼼꼼히 처리하는게 좋다고 생각해서 넣었습니다.
      const findWorkers = await this.userRepository
        .createQueryBuilder('user')
        .select('user.id')
        .andWhere('user.id IN (:...ids)', { ids: workers })
        .getMany();
      if (findWorkers.length !== workers.length) {
        throw new BadRequestException('지정하신 사용자는 없는 사용자 입니다.');
      }

      const updateCard = await this.cardRepository.save({
        id: findCard.id,
        cardName,
        cardDescription,
        cardColor,
        allowMembers,
        workers,
        startDate,
        endDate,
        endTime,
      });

      return {
        success: true,
        message: '카드 수정을 완료했습니다.',
        data: updateCard,
      };
    } catch (err) {
      return err.response;
    }
  }

  //카드 삭제하기_ 작성자만 카드 삭제가 가능합니다.
  async remove(
    cardId: number,
    user: User,
  ): Promise<DeleteCard | CreateCardFail> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('READ COMMITTED');

    try {
      const findWriter = await queryRunner.manager.getRepository(Card).findOne({
        where: { id: cardId },
      });
      console.log('findWriter', findWriter);
      if (!findWriter) {
        throw new BadRequestException('존재하지 않는 카드입니다.');
      }
      if (findWriter.userId !== user.id) {
        throw new UnauthorizedException(
          '작성자가 아님으로 삭제할 수 없습니다.',
        );
      }

      //리스트 테이블에 cardOrder필드 변경해주기
      const findListData = await this.listService.findOneListData(
        +findWriter.listsId,
      );
      const newCardOrder = findListData.cardOrder.filter(
        (e) => +e !== findWriter.id,
      );
      findListData.cardOrder = newCardOrder;
      await queryRunner.manager.getRepository(List).save(findListData);

      await queryRunner.manager.getRepository(Card).delete({ id: cardId });

      await queryRunner.commitTransaction();
      return {
        success: true,
        message: '해당 카드삭제가 정상적으로 처리되었습니다.',
      };
    } catch (err) {
      await queryRunner.rollbackTransaction();
      return err.response;
    } finally {
      await queryRunner.release();
    }
  }
}

 

 

 

이제 마지막 프로젝트를 남겨두었는데

해당 프로젝트를 통해 어떤걸 얻어낼것인지 명확히 하고 가야겠다.