ET네 만물상 - GitHub Repository / 배포 링크
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.1",
"@nestjs/core": "^8.0.0",
"@nestjs/elasticsearch": "^8.0.0",
"@nestjs/jwt": "^8.0.0",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/typeorm": "^8.0.2",
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
},
elasticsearch라던가 typeorm이라던가 nest가 내부적으로 지원하는 것들이 많은 거 같다.
설정은 내가 한게 아니기도 하고, 주로 다룰 내용은 nest를 사용한 구조와 방법?들 이기 때문이 요정도만하고 넘어가자
src
|-- cart
| |-- application
| |-- domain
| |-- dto
| |-- entity
| `-- presentation
|-- config
| |-- filter
| `-- properties
|-- destination
| |-- application
| |-- domain
| |-- dto
| |-- entity
| `-- presentation
|-- infra
| `-- mysql
|-- order
| |-- application
| |-- domain
| |-- dto
| |-- entity
| `-- presentation
|-- payment
| `-- presentation
|-- product
| |-- application
| |-- domain
| |-- dto
| |-- entity
| |-- infrastructure
| `-- presentation
`-- user
|-- application
|-- domain
|-- dto
|-- entity
|-- infrastructure
`-- presentation
core-module.ts
jwt-middleware.ts
main.ts
이 프로젝트에는 DDD(Domain Driven Design) 패턴을 적용했기 때문에 각 디렉토리가 하나의 도메인 단위로 구분된다. 물론 완전한 DDD는 아니다. 각 도메인이 서로 연결되어 있기 때문에...
각 도메인은 디렉토리는 5~6개의 하위 리덱토리로 구성되는데, 하나씩 살펴보자
const jwtConfig = properties.auth;
@Module({
imports: [
MysqlConfig,
JwtModule.register({
secret: jwtConfig.secret,
signOptions: { expiresIn: jwtConfig.expiresIn },
}),
ProductModule,
DestinationModule,
CartModule,
OrderModule,
UserModule,
PaymentModule,
],
})
export class AppModule {
configure(consumer: MiddlewareConsumer): any {
consumer
.apply(LoggerMiddleware)
.exclude("/auth")
.exclude("/auth/*")
.exclude("/users")
.exclude("/users/*")
.forRoutes("*");
}
}
Module 데코레이터로 NestJs로 만들 전체 APP 모듈을 만든다.
각 도메인별 모듈과 MySQL, jwt관련 모듈을 import해서 합친다
로그인 관련 처리를 하는 jwt middleware도 apply한다.
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
constructor(private readonly jwtService: JwtService) {}
use(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) {
try {
const token = req.cookies[properties.auth.tokenKey];
if (token) {
const result = this.jwtService.verify(token)["userId"];
if (!result) throw Error("token expired");
req.body.userId = this.jwtService.decode(token)["userId"];
}
next();
} catch (e) {
res.clearCookie(properties.auth.tokenKey);
res.status(HttpStatus.PRECONDITION_FAILED);
res.send(messages.failed.EXPIRED_TOKEN);
}
}
}
Injectable데코레이터로 Provider를 만들고, NestMiddleware를 implements해서 NestJS에서 사용할 미들웨어로 만든다
우리 프로젝트는 쿠키에 accessToken이 담겨있기 때문에 이 미들웨에를 거치면 해당 토큰의 유효성을 검사해서 Body에 userId를 넣어주거나, 에러처리를 한다.
이후 컨트롤러에서 Body 데코레이터를 사용한 파라미터에서 userId를 추출할 수 있다!
const serverPort = properties.server.port;
const nestApplication = async () => {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: [properties.client],
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: true,
});
app.useGlobalFilters(new HttpExceptionFilter());
app.use(cookieParser());
await app.listen(serverPort);
};
nestApplication();
서버 앱을 만드는 메인 파일.
cors 옵션, 필터, 각 미들웨어 등을 등록한다!
디렉토리 구조를 살펴보기 전 데코레이터에 대해 간략히 알아보자
데코레이터란 새 함수를 반환해서, 전달된 함수/메서드 동작을 수정하는 함수이다.
@
접두사와 함께 사용하면 decorating 하고자 하는 class, 함수 등의 위에 적용해서 nestJS에서 사용하는 형태로 변경시킬 수 있다.
자세한 내용은 더 깊게 따로 아티클로 작성해보도록 하자. 지금 이 단계에서는 내가 정의할 함수/ 클래스를 nestJS라는 프레임워크의 형태에 맞게 변형시켜준다 정도로만 이해하자
DB 테이블 구조를 정의한다.
typeorm 모듈에서 필요한 데코레이터 함수를 import해서 사용한다.
@Entity(product_option)
class ProductOption
@PrimaryGeneratedColumn()
id: number;
@PrimaryColumn({ type: "char", length: 32 })
id: string;
@ManyToOne(() => User, (user) => user.wishes, { lazy: true })
@OneToMany(() => Wish, (wish) => wish.user)
wishes: Wish[];
@OneToOne(() => Review, (review) => review.order)
review: Review;
@ManyToOne(() => Product, (product) => product.images, {
nullable: false,
onDelete: "CASCADE",
})
@JoinColumn({ name: "product_id" })
product: Product;
@Column({ type: "char", length: 32, nullable: true })
image: string;
@CreateDateColumn({
type: "timestamp",
name: "created_at",
default: () => "CURRENT_TIMESTAMP(6)",
})
createdAt: Date;
export class ReviewResponse {
averageRate: number;
rates: ReviewRate[];
reviews: ReviewDTO[];
static of(productReviews: Review[]): ReviewResponse {
const RATES: ReviewRate[] = [
{ rate: 1, count: 0 },
{ rate: 2, count: 0 },
{ rate: 3, count: 0 },
{ rate: 4, count: 0 },
{ rate: 5, count: 0 },
];
const averageRate = Number(
(
productReviews.reduce((result, review) => {
return result + review.rate;
}, 0) / productReviews.length
).toFixed(1)
);
const rates = productReviews.reduce(
(result: ReviewRate[], review): ReviewRate[] => {
result[review.rate - 1].count++;
return result;
},
RATES
);
const reviews: ReviewDTO[] = productReviews.map((review): ReviewDTO => {
return {
id: review.id,
rate: review.rate,
content: review.content,
image: review.image,
authorName: review.order.user.name,
createdAt: review.createdAt,
};
});
return {
averageRate,
rates,
reviews,
};
}
}
@Controller("/my")
export class MyController {
constructor(private readonly myService: MyService) {}
@Get("/info")
async getMyInfo(@Body("userId") userId: number): Promise<MyInfoResponse> {
return await this.myService.getMyInfo(userId);
}
...
}
3 layer architecture에서 controller의 역할을 한다.
@Controller 데코레이터를 사용한다. 해당 컨트롤러가 사용될 path를 입력한다.
constructor의 인자에 내부에서 사용할 Service를 전달한다.
컨트롤러 클래스의 메소드에 HTTP 메소드에 해당하는 데코레이터를 붙이고, 해당 요청과 맞물릴 path도 입력!
@Injectable()
export class MyService {
constructor(
private readonly carts: Carts,
private readonly reviews: Reviews,
private readonly destinations: Destinations,
private readonly questions: Questions,
private readonly users: Users,
private readonly orders: Orders,
private readonly wishes: Wishes
) {}
async getMyInfo(userId) {
try {
const user = await this.users.findUserById(userId);
return MyInfoResponse.of(user);
} catch (e) {
throw new ETException(400, messages.failed.FAILTED_TO_FIND_MY_INFO);
}
}
...
3 layer architecture에서 service 역할을 한다.
Injectable는 Nest의 Provider로 만드는 데코레이터다. 즉, Service를 Provider로 만드는 것
Provider의 핵심은 dependencies를 주입할 수 있다는 것!
constructor의 인자에 내부에서 사용할 domain을 전달한다.
@Injectable()
export class Wishes {
constructor(
@InjectRepository(Wish)
private readonly wishRepository: Repository<Wish>,
@InjectRepository(Product)
private readonly productRepository: Repository<Product>
) {}
...
3 layer architecture에서 Repository 역할을 한다.
마찬가지로 Injectable 데코레이터를 사용해서 Provider로 만든다.
constructor의 인자에 InjectRepository 데코레이터를 사용해서 내부에서 사용할 엔티티 Repository를 전달한다.
Repository는 엔티티에 해당하는 DB에 접근할 수 있는 객체라고 생각하면 된다.
async findMyWishesByUserId(userId: number): Promise<Wish[]> {
return await this.wishRepository.find({
relations: ["product", "product.images", "user"],
where: { user_id: userId },
});
}
Repositoty.find({ where, relations);
Repositoty.findOne(where, relations);
Repositoty.findOne({ id });
Repositoty.insert(newRow);
Repositoty.delete({ id });
Repositoty.update({ id }, newRow);
Repositoty.count({ id }, newRow);
Repositoty.query(query);
s3Repository.putObject(key, value);
s3Repository.deleteObject(id);
this.productRepository.findOne(wish.productId).then((product) => {
if (product.wishCount > 0) {
product.wishCount++;
}
this.productRepository.save(product);
});