Nestjs DI(container) 의존성 주입에 대하여
기존 테스트 코드를 기펙토링 하는 과정중 아래와 같은 코드를 발견했다.
리펙토링을 위해
기본적으로 프로젝트에서 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