Nest.js CRUD로 알아보는 Clean Architecture
BACKEND
NESTJS
설계 흐름
Input Adapter(ex. Controller - use-case 사용) => Input Port(interface - usecase가 구현해야할 메서드 정의) => usecase/domain - 비즈니스 로직 => output port(interface - output adapter가 구현해야할 메서드 정의) => output adapter(ex) typeorm repository)
코드
Input Adapter
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from "@nestjs/common";
import { RBAC } from "../../../auth/decorator/rbac.decorator";
import { Role } from "../../../user/entities/user.entity";
import { ManageTechUseCase } from "../../application/manage-tech.use-case";
import { TechDomainMapper } from "../../dto/mapper/tech.domain.mapper";
import { CreateTechRequestDto } from "../../dto/request/create-tech.request.dto";
import { UpdateTechRequestDto } from "../../dto/request/update-tech.request.dto";
import { TechDomainResponseDto } from "../../dto/response/tech.domain.response.dto";
@Controller("tech")
export class TechRestApiAdapter {
constructor(
private readonly manageTechUseCase: ManageTechUseCase,
) {}
@Get()
async findAll(): Promise<TechDomainResponseDto[]> {
const techDomains =
await this.manageTechUseCase.findAll();
return TechDomainMapper.toDtos(techDomains);
}
@Post()
@RBAC(Role.admin)
async create(
@Body()
createTechRequestDto: CreateTechRequestDto,
): Promise<TechDomainResponseDto> {
const createdTech = await this.manageTechUseCase.create(
createTechRequestDto,
);
return TechDomainMapper.toDto(createdTech);
}
@Patch(":id")
@RBAC(Role.admin)
async update(
@Param("id", ParseIntPipe) techId: number,
@Body()
updateTechRequestDto: UpdateTechRequestDto,
): Promise<TechDomainResponseDto> {
const updatedTech = await this.manageTechUseCase.update(
techId,
updateTechRequestDto,
);
return TechDomainMapper.toDto(updatedTech);
}
@Delete(":id")
@RBAC(Role.admin)
async remove(
@Param("id", ParseIntPipe) techId: number,
): Promise<TechDomainResponseDto> {
const removedTech =
await this.manageTechUseCase.remove(techId);
return TechDomainMapper.toDto(removedTech);
}
}
Input Port
import { TechDomain } from "../../domain/tech.domain";
import { CreateTechRequestDto } from "../../dto/request/create-tech.request.dto";
import { UpdateTechRequestDto } from "../../dto/request/update-tech.request.dto";
export interface TechUseCasePort {
findAll(): Promise<TechDomain[]>;
update(
techId: number,
updateTechRequestDto: UpdateTechRequestDto,
): Promise<TechDomain>;
create(
createTechRequestDto: CreateTechRequestDto,
): Promise<TechDomain>;
remove(techId: number): Promise<TechDomain>;
}
use-case
import {
BadRequestException,
Inject,
Injectable,
} from "@nestjs/common";
import { CreateTechRequestDto } from "../dto/request/create-tech.request.dto";
import { UpdateTechRequestDto } from "../dto/request/update-tech.request.dto";
import { TechUseCasePort } from "../port/input/tech-use-case.port";
import { ManageTechRepositoryPort } from "../port/output/manage-tech.repository.port";
@Injectable()
export class ManageTechUseCase implements TechUseCasePort {
constructor(
@Inject("ManageTechRepositoryPort")
private readonly manageTechRepositoryPort: ManageTechRepositoryPort,
) {}
async findAll() {
return this.manageTechRepositoryPort.findAll();
}
async create(createTechRequestDto: CreateTechRequestDto) {
const duplicatedOne =
await this.manageTechRepositoryPort.findOneByContent(
createTechRequestDto.content,
);
if (duplicatedOne) {
throw new BadRequestException("존재하는 기술입니다.");
}
return this.manageTechRepositoryPort.create(
createTechRequestDto,
);
}
async update(
techId: number,
updateTechRequestDto: UpdateTechRequestDto,
) {
const tech =
await this.manageTechRepositoryPort.findOneById(
techId,
);
if (!tech) {
throw new BadRequestException(
"존재하지 않는 기술입니다.",
);
}
await this.manageTechRepositoryPort.update(
techId,
updateTechRequestDto,
);
return await this.manageTechRepositoryPort.findOneById(
techId,
);
}
async remove(techId: number) {
const tech =
await this.manageTechRepositoryPort.findOneById(
techId,
);
if (!tech) {
throw new BadRequestException(
"존재하지 않는 기술입니다.",
);
}
await this.manageTechRepositoryPort.remove(techId);
return tech;
}
}
domain
export interface TechDomainProps {
id: number;
content: string;
}
export class TechDomain {
private readonly id: number;
private readonly content: string;
constructor({ id, content }: TechDomainProps) {
this.id = id;
this.content = content;
}
get getId() {
return this.id;
}
get getContent() {
return this.content;
}
}
Output Port
import { TechDomain } from "../../domain/tech.domain";
import { CreateTechRequestDto } from "../../dto/request/create-tech.request.dto";
import { UpdateTechRequestDto } from "../../dto/request/update-tech.request.dto";
export interface ManageTechRepositoryPort {
findOneByContent(content: string): Promise<TechDomain>;
findOneById(techId: number): Promise<TechDomain>;
findAll(): Promise<TechDomain[]>;
create(
createTechRequestDto: CreateTechRequestDto,
): Promise<TechDomain>;
update(
techId: number,
updateTechRequestDto: UpdateTechRequestDto,
): Promise<void>;
remove(id: number): Promise<void>;
}
Output Adapter
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TechDomain } from "../../../../domain/tech.domain";
import { CreateTechRequestDto } from "../../../../dto/request/create-tech.request.dto";
import { UpdateTechRequestDto } from "../../../../dto/request/update-tech.request.dto";
import { ManageTechRepositoryPort } from "../../../../port/output/manage-tech.repository.port";
import { Tech } from "../entities/tech.entity";
import { TechMapper } from "../mapper/tech.mapper";
export class ManageTechRepositoryAdapter
implements ManageTechRepositoryPort
{
constructor(
@InjectRepository(Tech)
private readonly techRepository: Repository<Tech>,
) {}
async findAll(): Promise<TechDomain[]> {
const techEntities = await this.techRepository.find();
return techEntities.map(TechMapper.toDomain);
}
async findOneById(techId: number): Promise<TechDomain> {
const techEntity = await this.techRepository.findOne({
where: {
id: techId,
},
});
return TechMapper.toDomain(techEntity);
}
async findOneByContent(
content: string,
): Promise<TechDomain> {
const techEntity = await this.techRepository.findOne({
where: {
content,
},
});
return TechMapper.toDomain(techEntity);
}
async create(
createTechRequestDto: CreateTechRequestDto,
): Promise<TechDomain> {
const techEntity = await this.techRepository.save(
createTechRequestDto,
);
return TechMapper.toDomain(techEntity);
}
async update(
techId: number,
updateTechRequestDto: UpdateTechRequestDto,
) {
await this.techRepository.update(
techId,
updateTechRequestDto,
);
}
async remove(id: number) {
await this.techRepository.delete(id);
}
}