Daily Logs/TIL (Today I Learned)

Nestjs DI(container) 의존성 주입에 대하여

Jcob.moon 2024. 12. 11. 20:51

기존 테스트 코드를 기펙토링 하는 과정중 아래와 같은 코드를 발견했다.

리펙토링을 위해
기본적으로 프로젝트에서 jest.fn()을 통한 모킹메서드들을  다른 파일에 두고 
mockUserEntity 와 같은 파일도 마찬가지로 하나의 파일을 만들어 export하여 다른 파일에서 
가져오는 방식으로 사용하고 있다 
기존 코드
ex ) 1번코드

describe('UserService', () => {
  let userService: UserService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: 'IUserRepository',
          useValue: mockUserRepository,
        },
        {
          provide: 'HashingService',
          useValue: mockHashingService,
        },
      ],
    }).compile();

    userService = module.get<UserService>(UserService);
  });

  describe('findEmail', () => {
    it('사용자를 찾아 이메일을 반환해야 합니다.', async () => {
      mockUserRepository.findUserByNameAndPhoneNumber.mockResolvedValue(mockUserEntity);

      const result = await userService.findEmail(mockCreateUserDto);

      expect(mockUserRepository.findUserByNameAndPhoneNumber).toHaveBeenCalledWith(
        mockCreateUserDto.name,
        mockCreateUserDto.phoneNumber,
      );
      expect(result).toEqual({ email: mockUserEntity.email });
    });
  });
});


2번코드
첫번째 의문은 

describe('UserService', () => {
  let userService: UserService;
  let userRepository: any; // Mock 객체
  let hashingService: any; // Mock 객체

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: 'IUserRepository',
          useValue: mockUserRepository,
        },
        {
          provide: 'HashingService',
          useValue: mockHashingService,
        },
      ],
    }).compile();

    userService = module.get<UserService>(UserService);
    userRepository = module.get('IUserRepository'); // 의존성 가져오기
    hashingService = module.get('HashingService');  // 의존성 가져오기
  });

  describe('findEmail', () => {
    it('사용자를 찾아 이메일을 반환해야 합니다.', async () => {
      userRepository.findUserByNameAndPhoneNumber.mockResolvedValue(mockUserEntity);

      const result = await userService.findEmail(mockCreateUserDto);

      expect(userRepository.findUserByNameAndPhoneNumber).toHaveBeenCalledWith(
        mockCreateUserDto.name,
        mockCreateUserDto.phoneNumber,
      );
      expect(result).toEqual({ email: mockUserEntity.email });
    });
  });
});


주석 친부분처럼 Nest의 Di(Container)를 활용하여 실제 서비스 동작과 유사한 환경에서 테스트를 하는 것이 더 
적절하지 않나에 대한 궁금증이 생겼다.
왜냐하면 기존 코드에선  예시 1번 코드에서 beforeEach 에서 const moduler:TestingModule 을 하여
실제 서비스와 유사한 환경을 구성하였음에도 
 모킹을 직접적으로 활용하고 있는것이 
일관성이 떨어지지 않나 하는 생각이였다.
그리하여 2번 처럼 사용해야하지않을까 생각을 하였다.

그리하여 Nest의 Di(Container) 에 대해서 조사하게되었다.

의존성과 의존성 주입이란?

의존성(Dependency):

클래스가 다른 클래스나 서비스의 기능에 의존하는 경우, 해당 클래스는 의존성을 가지고 있다고 말합니다.

예: AuthService가 UserRepository의 메서드를 호출한다면, AuthService는 UserRepository에 의존성을 가집니다.


의존성 주입(Dependency Injection):

  • 의존성을 클래스 내부에서 직접 생성하지 않고, 외부에서 주입받아 사용하는 설계 패턴.
  • NestJS에서는 DI 컨테이너가 클래스의 의존성을 자동으로 관리하고 주입합니다.

AuthModule: 인증 로직의 캡슐화

 

AuthModule.ts

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),
    JwtModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        secret: config.get<string>('JWT_SECRET_KEY'),
      }),
      inject: [ConfigService],
    }),
    TypeOrmModule.forFeature([UserEntity]),
  ],
  controllers: [AuthController],
  providers: [
    AuthService,
    JwtStrategy,
    GoogleStrategy,
    NaverStrategy,
    {
      provide: 'HashingService',
      useClass: BcryptHashingService, // Bcrypt 해시 구현체 주입
    },
    {
      provide: 'IUserRepository',
      useClass: UserRepository, // UserRepository 주입
    },
  ],
  exports: [AuthService, JwtStrategy], // 외부에서 AuthService를 사용할 수 있도록 내보냄
})
export class AuthModule {}

 


AuthService는 UserRepository, HashingService를 DI 컨테이너에서 주입받습니다.

 

AuthService.ts

@Injectable()
export class AuthService {
  constructor(
    @Inject('IUserRepository') private readonly userRepository: IUserRepository,
    @Inject('HashingService') private readonly hashingService: HashingService,
  ) {}

  async validateUser(email: string, password: string): Promise<boolean> {
    const user = await this.userRepository.findByEmail(email);
    if (!user) return false;

    return this.hashingService.compare(password, user.password);
  }
}


UserModule에서 AuthService를 사용하려면 AuthModule을 imports에 추가합니다.

UserModule.ts

@Module({
  imports: [AuthModule],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}


UserService에서 AuthService를 주입받아 사용

UserService.ts

@Injectable()
export class UserService {
  constructor(private readonly authService: AuthService) {}

  async authenticateUser(email: string, password: string) {
    const isValid = await this.authService.validateUser(email, password);
    if (!isValid) throw new UnauthorizedException();
  }
}


테스트 예 Mock 객체를 사용하여 DI를 주입하면 단위 테스트가 간단해집니다.

const mockUserRepository = {
  findByEmail: jest.fn(),
};

const mockHashingService = {
  compare: jest.fn(),
};

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    providers: [
      AuthService,
      { provide: 'IUserRepository', useValue: mockUserRepository },
      { provide: 'HashingService', useValue: mockHashingService },
    ],
  }).compile();

  authService = module.get<AuthService>(AuthService);
});

 

it('should validate user successfully', async () => {
  mockUserRepository.findByEmail.mockResolvedValue({ email: 'test@test.com', password: 'hashed' });
  mockHashingService.compare.mockResolvedValue(true);

  const result = await authService.validateUser('test@test.com', 'password');
  expect(result).toBe(true);
  expect(mockUserRepository.findByEmail).toHaveBeenCalledWith('test@test.com');
});

 

NestJS의 DI 패턴은 프로젝트 구조의 모듈화를 돕고, 의존성을 효율적으로 관리하여 유지보수성과 테스트 용이성을 극대화합니다.

특히  AuthModule과 같은 인증 모듈을 설계할 때, 의존성을 캡슐화하고 필요에 따라 exports를 활용하여 다른 모듈에서 재사용할 수 있도록 설계해야 합니다.

추가적으로, 테스트 시 Mock 객체를 활용한 DI 주입을 통해 테스트 효율성을 높일 수 있습니다.

NestJS의 DI는 모듈 간의 결합도를 낮추고, 코드의 확장성과 재사용성을 보장하는 핵심 개념입니다. 이를 적절히 활용하여 안정적이고 유지보수 가능한 애플리케이션을 설계할 수 있습니다.

같이 읽어보면 좋은 자료 : 
https://velog.io/@ferrari_roma/Nest%EC%9D%98-DI-container
https://hou27.tistory.com/entry/NestJS-Provider%EC%99%80-Nest-IoC-Container


 

NestJS의 DI Container

Nest와 DI Container, 맨땅에 헤딩할 때는 몰랐던 것들

velog.io

 

 

NestJS - Provider와 Nest IoC Container

왜 그랬는지는 모르겠지만 Typescript Type Challenge를 하다가 갑자기 Provider에 대한 궁금증이 생겼다. Provider NestJS의 Provider는 Providers are a fundamental concept in Nest. Many of the basic Nest classes may be treated as a p

hou27.tistory.com