프로젝트의 '루틴 완료' 기능은 'userId'를 사용하나, 다른 핵심 기능인 '챌린지'는 'nickName'을 사용하는 데이터 처리 패턴을 가지고 있었습니다. 이를 통일하기 위한 작업을 진행하며 문제가 생긴 부분을 정리했습니다.

순수하지 않은 UseCase

기존 '루틴 완료' 기능의 가장 큰 문제는 UseCase 계층이 너무 많은 책임을 지고 있다는 것이었습니다.

// 기존 UseCase (복잡한 구조)
constructor(
  // 💥 문제: UseCase가 두 개의 다른 도메인 Repository에 의존하고 있다.
  private readonly routineCompletionsRepository: IRoutineCompletionsRepository,
  private readonly userRepository: IUserRepository 
) {}

async execute(request) {
  // 1. nickname으로 User 정보를 조회하고, (User 도메인의 책임)
  const user = await this.userRepository.findByNickname(request.nickname);
  // 2. userId로 변환한 뒤, (데이터 변환 책임)
  // 3. RoutineCompletion을 생성한다. (RoutineCompletion 도메인의 책임)
  const createdCompletion = await this.routineCompletionsRepository.create({
    userId: user.id,
    // ...
  });
}

이 구조는 클린 아키텍처 원칙을 위반합니다. AddRoutineCompletionUseCase User 도메인의 내부 사정(userRepository)까지 알아야만 했습니다. 이는 결합도(Coupling)를 높이고, 응집도(Cohesion)를 낮추는 전형적인 안티 패턴이었습니다.

책임의 재분배 

해결책은 간단했습니다. "nickname userId로 변환하고 데이터를 생성하는 책임" UseCase에서 떼어내어, 그 일의 책임을 Infrastructure 계층의 Repository에게 위임하는 것이었습니다.

먼저, IRoutineCompletionsRepository 인터페이스에 nickname을 직접 받아 처리할 수 있는 새로운 '창구'를 열었습니다.

// IRoutineCompletionsRepository.ts (개선)

// UseCase는 이제 이 메소드만 호출
createByNickname(request: {
  nickname: string;
  routineId: number;
  // ...
}): Promise<RoutineCompletion>;

그리고 PrRoutineCompletionsRepository 구현체에서, 기존 '챌린지' 기능이 사용하던 효율적인 Prisma 쿼리 패턴을 그대로 적용했습니다.

// PrRoutineCompletionsRepository.ts (개선)

async createByNickname(request: CreateByNicknameRequest): Promise<RoutineCompletion> {
  // 1. nickname으로 user를 먼저 찾고,
  const user = await prisma.user.findUnique({
    where: { nickname: request.nickname }
  });

  if (!user) {
    throw new Error(`사용자를 찾을 수 없습니다: ${request.nickname}`);
  }

  // 2. 찾아낸 user.id를 사용하여 한 번의 쿼리로 데이터를 생성한다.
  const createdCompletion = await prisma.routineCompletion.create({
    data: {
      userId: user.id, // 💥 책임이 이곳으로 이동했다!
      routineId: request.routineId,
      // ...
    },
  });

  return { /* ... */ };
}

 

 

결과

이 리팩토링으로 유즈케이스는 순수한 본인의 역할에만 집중하게 되었습니다.

// 개선된 UseCase (단순한 구조)
constructor(
  // 이제 자신의 도메인 Repository에만 의존한다.
  private readonly routineCompletionsRepository: IRoutineCompletionsRepository
) {}

async execute(request) {
  // 데이터 변환의 복잡한 과정은 모두 숨겨졌다.
  // UseCase는 그저 "nickname으로 완료 기록을 생성해줘" 라고 말할 뿐이다.
  return this.routineCompletionsRepository.createByNickname({
    nickname: request.nickname,
    // ...
  });
}

 

  •  구조 일관성 확보 : 이제 '챌린지'와 '루틴 완료' 기능은 완전히 동일한 패턴으로 동작합니다.
  •  클린 아키텍처 준수 : 각 계층이 자신의 책임만을 수행하게 되었습니다. UseCase는 더 이상 다른 도메인의 데이터베이스 접근 방식에 대해 알 필요가 없습니다.
  •  코드 간결화 : 복잡했던 UseCase의 의존성과 로직이 사라지면서, 전체 코드 라인 수가 12줄 감소했습니다. 더 적은 코드로 더 명확한 의도를 표현하게 된 것입니다.

결과

이번 리팩토링으로 코드가 동작하는 것 이상인 "올바른 위치에서 올바르게 동작한다"는 것에 대해 고민하였습니다. UseCase의 불필요한 책임을 덜어내고 Repository에게 위임하여 더 견고하고 더 일관성 있으며 더 유지보수하기 쉬운 아키텍처를 구축했습니다. 

728x90