Stack
NodeJs
,Typescript
,node-json-db
Preview
이번 글은 프로그래머스 과제 테스트 란에 있는 우리 싸인 API 개발 과제에 대한 분석 및 공부를 위해 작성되었습니다. 과제 대략적인 요건은 이렇습니다. 회원가입/로그인/참가자 인증을 하고 문서에 서명하여 데이터베이스에 저장 및 조회 등의 처리를 위한 Back end 로직을 개발합니다. 자세한 요건은 아래 더 보기를 클릭하시면 볼 수 있습니다.
https://school.programmers.co.kr/skill_check_assignments/233
프로젝트 구조
src 폴더 안 디렉터리를 보면 다음과 같습니다.
types : 데이터 객체의 타입에 대한 정의
api : api 요청에 대한 응답을 제공
common : 공통 exceptions와 interfaces가 정의
lib : database(sqlite-better3) 연결과 json web token 처리
middlewares : 인증, csrf, error 처리를 위한 middleware
app.ts : 라우팅 처리와 서버 기동 부분
server.ts : 서버의 시작(starting point)
config.ts : 설정 파일
api 폴더를 뜯어보면 다음과 같습니다.
세 개의 API 라우팅으로 나뉩니다. documents(문서 처리), participant(참여자 처리), users(사용자 처리)
각 서비스는 dto, entities, controller, service, repository, spec.ts(testing)의 아키텍처로 구성되어 있습니다.
dto : Data Transfer Object 데이터 전송 객체. 각 서비스에서 controller와 service 가 통신할 때 주고받는 데이터의 타입을 정의
entities : 데이터베이스와 직접적으로 통신하는 데이터의 타입에 대한 정의
controller : DTO로 변환된 요청 body를 매개변수로 적절한 Service의 메서드를 호출
- 클라이언트의 요청(Request)을 수신합니다.
- 요청(Request)에 들어온 데이터 및 내용을 검증합니다.
- 서버에서 수행된 결과를 클라이언트에게 반환(Response)합니다.
- 즉, 컨트롤러는 클라이언트의 요청(Request)을 서비스 계층으로 데이터를 전달
Service : DTO를 비즈니스 로직을 통해 Entity로 변환된 객체를 repository를 이용해서 저장.
아키텍처의 가장 핵심적인 비즈니스 로직을 수행하고 실제 사용자(클라이언트)가 원하는 요구사항을 구현한다.
repository : 데이터베이스와 직접 연결. query가 작성되어 있음.
이를 계층형 아키텍처라 합니다. 계층형 아키텍쳐는 각 계층은 자신이 맡은 역할만을 수행하며 다른 계층으로부터 분리되어 동작합니다.
하지만 상위 계층에서 하위 계층으로 로직이 흘러감에 따라 상위 계층은 하위 계층의 속성 및 객체에 접근이 가능한 데에 비해 하위 객체는 그렇지 못합니다. 하지만 개발을 하다 보면 하위 계층에서 상위 계층의 객체에 접근이 불가피하게 되기도 합니다. 그러다 보니 상위 계층을 한 단계 내려 하위 계층에 위치하게 되면 서로 참조를 할 수 있어 제약점은 해결할 수 있지만 하위 계층이 비대해져 좋은 아키텍처가 되지 못합니다.
관련 개념.
OOP : Object Oriented Programming
TDD : Test Driven Development
소스 파헤치기
프로세스가 어떻게 흘러가는지 순서도를 그려보면 아래와 같습니다.
초기 프로젝트 소스를 보면 document api 기능은 구현되어 있지 않아 이를 구현하는 게 요건인데 participant와 user와 같은 구조를 따르도록 구현을 해야 합니다.
app.ts
class App {
private app: express.Application;
constructor(controllers: Controller[]) {
this.app = express();
this.initializeMiddlewares();
this.initializeControllers(controllers);
this.initializeErrorHandling();
}
public listen() {
const port = process.env.PORT ?? 3000;
this.app.listen(port, () => {
console.log(`App listening on the port ${port}`);
});
}
public getServer() {
return this.app;
}
private initializeMiddlewares() {
this.app.use(express.json());
this.app.use(
session({
name: 'prgrms.sid',
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
}),
);
this.app.use(csrf());
this.app.use(verifyJWT);
}
private initializeErrorHandling() {
this.app.use(errorMiddleware);
}
private initializeControllers(controllers: Controller[]) {
const router = Router();
router.get('/', (req, res) => res.send('OK'));
controllers.forEach((controller) => {
router.use(controller.router);
});
this.app.use('/api', router);
}
}
*controllers 객체를 server.ts 로부터 매개변수로 받아 json, jwt, csrf, controller, error-middleware를 순차적으로 실행합니다.
서비스의 진입단은 이렇고 요건 중 이미 구현되어 있는 공개용 API 중 sign 과정을 들여다봅시다.
Part of user.controller.ts
export default class UserController implements Controller {
path = "/users";
router = Router();
userService = new UserService(new UserRepository()); //user.service.ts의 UserService 클래스
constructor() {
this.initializeRoutes();
}
initializeRoutes() {
const router = Router();
router
.post("/signup", wrap(this.signUp))
.post("/login", wrap(this.login))
.get("/me", wrap(this.me));
this.router.use(this.path, router);
}
signUp: Handler = async (req): Promise<SignUpResponse> => {
const { email, password, name } = req.body as SignUpDto;
if (!email) {
throw new BadRequestException("이메일은 필수입니다.");
}
if (!password) {
throw new BadRequestException("비밀번호는 필수입니다.");
} else if (password.length < 8) {
throw new BadRequestException("비밀번호는 최소 8글자 이상입니다.");
}
if (!name) {
throw new BadRequestException("이름은 필수입니다.");
}
const { count: hasEmail } = this.userService.countByEmail(email);
if (hasEmail) {
throw new BadRequestException("이미 가입된 이메일입니다.");
}
await this.userService.signUp({ //실제 로그인 프로세스는 user.service.ts 에 넘긴다.
email,
password,
name,
});
return true;
};
}
*/signup, /login, /me 는 다 wrap이라는 메서드로 감싸져 있습니다.
lib/request-handler.ts의 wrap은 다음과 같이 생겼습니다.
export const wrap = (handler: Handler) =>
async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await handler(req, res, next);
res.json(response);
next();
} catch (err) {
next(err);
}
};
singup 함수의 return 값을 받아 response(응답)을 전송하고 다음 프로세스를 진행하는 미들웨어 역할을 하는 함수입니다.
user.controller.ts 는 클라이언트로부터 받은 데이터를 검증하고 실제 프로세스 처리는 user.service.ts 에 넘깁니다.
회원가입 절차이니 이메일과 비밀번호 등의 입력값에 대한 검증 및 기존 유저인지 체크를 합니다.
Part of user.service.ts
async signUp({ name, email, password }: SignUpDto): Promise<UUID> {
const { count: hasEmail } = this.countByEmail(email);
if (hasEmail) {
throw new BadRequestException("중복된 이메일이 있습니다.");
}
const encreyptedPassword = await hash(password, hashRounds);
const id: UUID = uuidv4();
const now = new Date().toISOString();
this.userRepository.create({ //user.repository.ts 에서 데이터베이스에 삽입
id,
email,
name,
password: encreyptedPassword,
created_at: now,
updated_at: now,
});
return id;
}
*특이점이 있다면 앞서 user.controller.ts 에서 중복이메일 체크를 했는데 user.service.ts 에서 한번 더 한다는 겁니다. 굳이 user.service.ts 에선 없어도 될 것 같은데 말이죠..
아무튼 user.service.ts 에선 controller로부터 받은 데이터(패스워드)를 알맞게 변경한 다음(암호화) 데이터베이스와 통신을 담당하는 user.repository.ts 에 데이터를 전달합니다.
Part of user.repository.ts
create(raw: UserRaw) {
const result = db
.prepare(
[
"INSERT INTO",
this.tableName,
"(id, email, name, password, created_at, updated_at)",
"VALUES",
"($id, $email, $name, $password, $created_at, $updated_at)",
].join(" ")
)
.run(raw);
return result;
}
직접 데이터베이스에 INSERT문을 전송합니다.
다음글에서 과제에서 주어진 요건을 보면서 한 문제씩 해결해 보도록 합시다.
'Back-End' 카테고리의 다른 글
[Nodejs] 프로그래머스 우리싸인 API 서버 개발 과제분석[3] (2) | 2023.08.06 |
---|---|
[Nodejs] 프로그래머스 우리싸인 API 서버 개발 과제분석[2] (0) | 2023.08.05 |
es6 문법으로 refactoring하기 (0) | 2023.07.30 |
[Linux Centos] offline 환경에서 redis 설치하기 (0) | 2023.06.17 |
Installation of Yarn Berry on Network Disconnected Environment (0) | 2023.06.14 |