인프런 커뮤니티 질문&답변

춘몽님의 프로필 이미지

작성한 질문수

[코드캠프] 부트캠프에서 만든 고농축 백엔드 코스

12-01 결제 API

레퍼지토리 주입 기준이 궁금합니다.

해결된 질문

24.04.24 18:35 작성

·

179

·

수정됨

0

안녕하세요.

개인프로젝트하면서 필요한부분들 다시 보는중인데 궁금한점이 생겨서 질문드립니다.

 

이전 1:1 관계등록 API 강의에서는 상품판매위치 레퍼지토리를 상품의 service에서 주입받지 않고 따로 service를 만들어서 거기서 주입받고 해당 로직을 처리했었습니다.

검증로직을 통일시키기 위해서라고 하셨는데, 이번 강의에서는 포인트 레퍼지토리가 주입되어있는 상태에서 추가적으로 유저 레퍼지토리를 주입받아서 유저정보를 가져오시는데, 차이나 기준이 궁금합니다.

 

더불어 나중 강의인 트랜젝션에서도 현재 주입받은 유저 레퍼지토리를 기준으로 진행되는데, 만약 레퍼지토리를 service별로 분리해야된다면, 쿼리러너를 해당 service로 던졌다가 받던지, 아니면 해당 service에서 xxRepository.create()로 만든걸 받던지 하는 식으로 해야할듯 싶은데 그런 불편함보다는 그냥 한곳에서 처리하는게 낫지 않나 하는 생각이 들기도합니다. 그래서 트랜젝션 사용할때의 레퍼지토리 기준과 만약 service별로 나뉘게 된다면 사용 방법이 궁금합니다.

 

마지막으로 제가 트렌젝션을 써봤는데, 다른 테이블의 레퍼지토리를 한 개의 service에서 레퍼지토리 주입은 안받고, 모듈에서만 import의 TypeOrmModule.forFreature([]) 부분에 넣어줘도 잘 동작하던데 이렇게 써도 되는게 맞는지 궁금합니다.

 혹시 몰라 아래에 해당 코드 첨부해봅니다.

// restAPI입니다.

// auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersService } from '../02.Users/users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../02.Users/entities/users.entity';
import { Auth } from './entities/auth.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Auth, User])],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}


// auth.service.ts
import {
  BadRequestException,
  ConflictException,
  Injectable,
  InternalServerErrorException,
} from '@nestjs/common';
import { UsersService } from '../02.Users/users.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Auth } from './entities/auth.entity';
import { DataSource, Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import {
  IAuthServiceCheckInput,
  IAuthServiceCreate,
  IAuthServiceLogin,
} from './interfaces/auth-service.interface';
import { User } from '../02.Users/entities/users.entity';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(Auth)
    private readonly authRepository: Repository<Auth>,
    private readonly configService: ConfigService,
    private readonly dataSource: DataSource,
  ) {}

// 생략

async create({ createUserInput }: IAuthServiceCreate): Promise<User> {
    const { user_id, user_pw, id, ...userData } = createUserInput;
    const { user_email, user_nick } = userData;
    if (!id) {
      const checkUserInput = { user_id, user_email, user_nick };
      await this.checkUser({ checkUserInput });
    }
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      const user = await queryRunner.manager.save(
        User,
        id ? { id: id, ...userData } : userData,
      );
      const hashedPw = await this.hashPw({ user_pw });
      await queryRunner.manager.save(Auth, {
        user_id,
        user_pw: hashedPw,
        user,
      });
      await queryRunner.commitTransaction();
      return user;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw new InternalServerErrorException('회원 가입 실패(DB)');
    } finally {
      queryRunner.release();
    }
  }

// 생략

 

답변 2

0

노원두님의 프로필 이미지
노원두
지식공유자

2024. 04. 27. 14:17

안녕하세요! 춘몽님!

최초 상품 강의시부터 로그인강의까지 레포지토리 주입 방법이 모두 달랐어요!
레포지토리 직접 주입 => 레포지토리가 주입된 모듈 주입
이유는 가장 쉬운 방법부터 실무적인 방법까지 차근차근 설명드리기 위함이었는데요!

가장 쉬운 방법은 다이렉트로 필요한 레포지토리를 직접 주입하는 방법이었지만,
해당 방법은 서비스의 규모가 커지면서 비효율적인 모습을 보여주게 됩니다!
이유는, 재사용 가능한 서비스를 만들어서 사용하게 되면 유지보수가 좋아지기 때문이에요!

 

따라서, 후반부 강의에서는 레포지토리를 직접 주입하지 않고, 재사용 가능한 서비스가 내장된 상태의 모듈을 통째로 떼었다 붙였다 하는 방식으로 코딩하고 있답니다!

 

하지만, 마지막 결제 트랜잭션 강의에서는 하나의 서비스에서 여러 레포지토리를 직접 연결하고 있는데, 이 부분은 트랜잭션을 설명하기 위한 강의로서 레포지토리 주입은 가장 쉬운 직접 주입으로 대체한 것일 뿐이며, 실무용으로 사용하기에는 한단계 더 심화 리팩토링이 필요하답니다!


===============================================================

시간이 되신다면 한 번 도전해 보세요!
결론부터 말씀드리면, 레포지토리를 직접 주입 받는 것이 아닌, 모듈 형태로 주입 받아야 하고, 해당 모듈 내에서 트랜잭션이 연결되도록 설계하셔야 합니다!

ex)
1. TransactionModule 이라는 모듈을 하나 만들어 주시고, 해당 모듈을 주입받습니다.

2. TransactionModule 내에서 TransactionService를 하나 만들고, 여기서 queryRunner 를 관리합니다.
(getQueryRunner 등의 함수를 만드셔서 저장된 queryRunner를 반환하시면 되겠죠?!)

3. nestjs는 기본적으로 싱글톤으로 작동하기 때문에, 이대로 사용하셨다간 모든 요청자들간에 queryRunner를 공유하게 된답니다! 따라서, TransactionService는 RequestScope이 되어야, 요청자들간에 자기만의 TransactionService가 만들어지게 되고, 자기만의 queryRunner가 하나씩 생기게 되는 것이지요!

4. 위와 같이 사용하신다면, 여러 테이블의 트랜잭션. 예를들어 결제 => 상품 => 유저 등의 트랜잭션을 묶을때, 각각에서 모두 TransactionModule을 주입받아 해당 TransactionService내의 queryRunner에 저장함으로써, 모듈 주입 방식을 사용함과 동시에 하나의 queryRunner로 문제를 해결하실 수 있답니다!^^

 

0

인프런 AI 인턴님의 프로필 이미지

2024. 04. 26. 18:42

안녕하세요, 인프런 AI 인턴입니다.

Spring Data JPA 같은 Spring Data 프로젝트를 사용할 때, 레퍼지토리 인터페이스를 정의하고 이를 주입받아 사용하는 방식은 매우 일반적인 패턴입니다. 이 때 어느 서비스에 어떤 레퍼지토리를 주입할지는 애플리케이션의 설계와 요구 사항에 크게 의존합니다. 일반적으로는 공통적인 원칙에 따라 결정되지만, 프로젝트의 특성, 팀의 선호, 특정 기술 요구 사항 등으로 인해 다를 수 있습니다.

먼저, 서비스 간의 결합도를 낮추고 각 서비스의 책임을 명확하게 하기 위해 차별화된 도메인 내에서 레퍼지토리를 분리하여 사용하는 것이 일반적인 접근 방식입니다. 이는 한 서비스에서 다루는 데이터와 로직의 범위를 명확히 하여, 유지 보수와 확장성을 향상시키기 위함입니다.

그러나, 실제 구현 시에는 다양한 요소를 고려해야 합니다. 예를 들어, 트랜잭션 관리의 복잡성, 성능 최적화의 필요성, 의존성 주입의 편의성 등이 이러한 결정에 영향을 줄 수 있습니다. 특히, 여러 테이블이 연관된 작업을 처리할 때는 적절한 레퍼지토리의 선택과 트랜잭션 관리 방법이 중요한 포인트가 됩니다.

Spring Data JPA를 예로 들면, @Transactional 애노테이션을 사용하여 선언적 트랜잭션 관리를 적용할 수 있으며, 이를 통해 서비스 레이어에서 트랜잭션을 관리하는 것이 일반적입니다. 즉, 하나의 트랜잭션 내에서 여러 리포지토리를 사용하여 데이터를 일관성 있게 관리할 수 있습니다.

주어진 코드 예제에서처럼, TypeORM을 사용하는 경우, DataSource 인스턴스를 사용하여 쿼리 러너를 생성하고, 이를 통해 트랜잭션을 시작, 커밋 또는 롤백할 수 있습니다. 이 방식은 구체적인 요구 사항에 따라 트랜잭션이 필요한 논리적 작업 단위를 더 세밀하게 제어할 수 있게 해줍니다.

끝으로, TypeOrmModule.forFeature([]) 부분에 다른 테이블의 레퍼지토리를 등록하여 사용하는 것은 TypeORM 내에서 해당 엔티티에 대한 리포지토리를 생성하고 필요할 때 주입할 수 있도록 함으로써, 모듈 간의 의존성을 명시적으로 관리할 수 있게 해줍니다.

보편적으로, 복잡한 대규모 애플리케이션을 개발할 때는 세분화된 서비스와 레퍼지토리를 통해 구성 요소 간의 응집력을 높이고 결합도를 낮추는 것이 유리합니다. 그러나, 프로젝트의 규모, 팀의 작업 흐름, 기술 스택 등에 따라 최적의 접근 방식은 달라질 수 있음을 인지하는 것이 중요합니다.

춘몽님의 프로필 이미지

작성한 질문수

질문하기