Provider에 대한 공부 기록이다.
이전 기록에서 Provider(서비스 파일)에 대해 간단하게 다뤄봤다.
본격적인 Provider(서비스 파일)에 대한 기록을 남기려고 한다.
개요
프로바이더에서의 키워드는 의존성 주입이다.
의존성 주입이란 OOP에서 많이 활용하는 기법으로 ,
특정 객체가 의존하는 또 다른 객체를 외부에서 선언하고 이를 주입받아 사용하는 것이다.
프로바이더의 주요 아이디어인 의존성 주입은 개체 사이의 다양한 관계를 만들지만
개체 인스턴스를 연결하는 작업은 Nest의 런타임 시스템에 위임된다.
Nest에서 프로바이더는 서비스 파일로 구현된다.
이전 컨트롤러 기록에서 코드 중 일부를 보자.
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
생성자를 통해 UserService 클래스를 주입하고, userService라는 객체 멤버 변수에 할당하고 있다.
그렇다면 서비스 파일을 보자.
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
Injectable 데커레이터를 이용해 다른 Nest 컴포넌트에서 주입할 수 있는 프로바이더가 됐다.
위처럼 의존성을 주입함으로써 얻을 수 있는 효과가 무엇일까?
의존성 주입의 장점은 아래와 같다.
- 의존성이 줄어든다 : 주입받는 객체가 변한다고 하더라도 주입되는 과정에 대한 수정이 필요 없다.
- 재사용성이 높다 : 객체를 주입해서 사용함으로써 다른 Nest 컴포넌트에서도 사용 가능하다.
- 테스트하기 좋다 : 분리된 객체이므로 따로 테스트할 수 있다.
- 가독성이 좋다 : 위의 예로는 비즈니스 로직을 분리한 것이므로, 가독성이 훨씬 좋아졌다.
이제 서비스 파일에서 유저 서비스의 회원 가입 로직을 구현해 보자.
DB에 저장하는 것에 관련된 기록은 추후 공부 후에 남길 예정이다.
Injectable 데커레이터는 해당 서비스 파일이
Nest IOC 컨테이너에서 관리할 수 있는 클래스임을 선언하는 메타데이터를 첨부한다.
IOC 컨테이너는 공급자 간의 관계를 해결하는 장치이며,
종속성 주입 기능의 기초가 된다.
회원가입 기능 구현
회원가입 기능에서는 유저 생성 기능을 구현해야 한다.
유저를 생성하려면, 아래와 같은 기능이 있어야 한다.
- 가입하려는 유저가 존재하는지 검사
- 회원 가입 인증 이메일 발송
- 유저를 DB에 저장
1, 2번 모두 DB에 대한 공부가 필요하므로
3번 기능을 구현하고 Provider에 대한 전체적인 구조만 그려보도록 하자.
Controller 수정
위에서도 언급했듯 서비스 파일에서 비즈니스 로직을 구현하려면,
컨트롤러 주입하고 객체 멤버 변수를 이용해 서비스 파일의 메서드에 요청을 전달해야 한다.
때문에 User Controller를 수정해 보자.
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() dto: CreateUserDto): Promise<void> {
const { name, email, password } = dto;
await this.usersService.createUser(name, email, password);
}
Controller를 위와 같이 수정했다.
생성자를 기반으로 UserService를 주입하고,
dto에서 얻은 정보를 UserService에 전달했다.
속성 기반 주입
위의 예시에서는 Injectable 데커레이터를 이용해 생성된 서비스 파일(프로바이더)을 생성자를 기반으로 주입했다.
Nest에서는 속성 수준에서 프로바이더를 주입할 수 있도록 지원한다.
예를 들어 최상위 클래스가 하나 또는 여러 공급자에 의존하는 경우
생성자의 하위 클래스에서 super()를 호출하여 끝까지 전달하는 것은 매우 비효율적이다.
이를 방지하기 위해 속성 수준에서 Inject 데커레이터를 사용할 수 있다.
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
위의 코드가 속성 수준에서 프로바이더를 주입한 예시이다.
하지만 꼭 필요한 경우가 아니라면 생성자 기반 주입이 권장된다.
UserService 구현
서비스 파일에서는 앞서 말한 기능들을 구현하는데,
회원가입 인증 메일을 보내는 로직은 따로 이메일 서비스 파일을 만들어 구현할 것이다.
그리고 두 개의 라이브러리를 설치하는데
이메일 검증 시 필요한 토큰 형식을 uuid로 쓸 것이므로 uuid 라이브러리와
무료로 이메일 전송을 해주는 nodemailer 라이브러리를 사용할 것이다.
$npm i uuid
$npm i -D @types/uuid
$npm i nodemailer
$npm i -D @types/nodemailer
이제 서비스 파일을 구현해 보자.
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { EmailService } from 'src/email/email.service';
import * as uuid from 'uuid';
@Injectable()
export class UsersService {
constructor(private emailService: EmailService) {}
async createUser(name: string, email: string, password: string) {
await this.checkUserExists(email);
const signupVerifyToken = uuid.v1();
//토큰 생성
await this.saveUser(name, email, password, signupVerifyToken);
await this.sendMemberJoinEmail(email, signupVerifyToken);
}
async checkUserExists(email: string) {
return false;
//DB 공부 이후 추가 예정
}
async saveUser(
name: string,
email: string,
password: string,
signupVerifyToken: string,
) {
return;
//DB 공부 이후 추가 예정
}
async sendMemberJoinEmail(email: string, signupVerifyToken: string) {
await this.emailService.sendMemberJoinVerification(
email,
signupVerifyToken,
);
//이메일 기능은 따로 서비스 파일로 분리
//회원가입 인증 이메일 발송
}
}
Email Service 구현
분리된 이메일 기능을 구현해 보자.
nodemailer 라이브러리를 이용해 gmail로 이메일을 전송할 것이다.
nodemailer 라이브러리는 간단하게 테스트할 무료 라이브러리이므로
서비스를 만들 때는 사용해서는 안 된다.
import { Injectable } from '@nestjs/common';
import Mail = require('nodeMailer/lib/mailer');
import * as nodemailer from 'nodemailer';
interface EmailOptions {
to: string;
subject: string;
html: string;
}
//메일 옵션 타입
@Injectable()
export class EmailService {
private transporter: Mail;
constructor() {
this.transporter = nodemailer.createTransport({
//nodemailer에서 제공하는 Transporter 객체 생성
service: 'Gmail',
auth: {
user: 'MY_GMAIL',
pass: 'MY_PASSWORD',
},
});
}
async sendMemberJoinVerification(
emailAddress: string,
signupVerifyToken: string,
) {
const baseUrl = 'http://localhost:3000';
const url = `${baseUrl}/users/email-verify?signupVerifyToken=${signupVerifyToken}`;
//유저가 버튼을 눌렀을 때 가질 링크, 이 링크를 통해 이메일 인증 요청이 들어온다.
const mailOptions: EmailOptions = {
to: emailAddress,
subject: '가입 인증 메일',
html: `
가입확인 버튼을 누르시면 가입 인증이 완료됩니다.<br/>
<form action="${url}" method="POST">
<button>가입확인</button>
</form>
`,
};
return await this.transporter.sendMail(mailOptions);
//이메일 전송
}
}
여기서 MY_GMAIL, MY_PASSWORD, URL들을 하드코딩하고 있지만,
환경 변수로 관리하는 것이 일반적이다.
이제 /users로 POST 요청을 보내보면,
body에 기입한 이메일로 가입 인증 메일이 가게 된다.
해당 메일에 가입 확인 버튼을 누르면,
위와 같이 쿼리로 받은 dto 객체를 로그에서 볼 수 있다.
이외의 기능
이제 이메일 인증과 로그인, 유저 정보 조회 기능이 남았다.
이는 DB 공부가 선행되어야 하므로 컨트롤러에서 서비스 파일로 로직을 위임하는 작업만 해보자.
@Post('/email-verify')
//이메일 인증
async verifyEmail(@Query() dto: VerifyEmailDto): Promise<string> {
const { signupVerifyToken } = dto;
await this.usersService.verifyEmail(signupVerifyToken);
}
@Post('/login')
//로그인
async login(@Body() dto: UserLoginDto): Promise<string> {
const { email, password } = dto;
await this.usersService.login(email, password);
}
@Get('/:id')
//회원 조회
async getUserInfo(@Param('id') id: string): Promise<string> {
return await this.usersService.getUserInfo(id);
}
참고
마치며
잘못된 정보에 대한 피드백은 환영입니다.
감사합니다.
'Backend > NestJS' 카테고리의 다른 글
[NestJS] User API 만들기(3) (파이프 Pipe 유효성 검사) (0) | 2022.12.30 |
---|---|
[NestJS] Nest 모듈 설계 (0) | 2022.12.29 |
[NestJS] User API 만들기(1)(Controller, 컨트롤러) (1) | 2022.12.28 |
[NestJS] 데커레이터 Decorator(합성, 클래스, 메서드, 매개변수) (0) | 2022.12.25 |
[NestJS] 프레임워크와 라이브러리의 차이, 필수 기능 (0) | 2022.12.20 |