Developer with Cat
© 2021. All rights reserved.
nextTickQueue는 process.nextTick() API의 콜백들을 가지고 있으며, microTaskQueue는 Resolve된 프로미스의 콜백을 가지고 있습니다.
이 두 개의 큐는 이벤트루프에 포함되어있지 않으며 이벤트루프에 앞서 실행하기 위한 것을 목적으로한다.
따라서 libUV 라이브러리에 포함되어 있지 않고 Node.js 에 포함되어있다.
tick 은 반영구적인 event loop 에서 하나의 loop 를 의미합니다.
tick
process.nextTick() 은 비동기 API 이지만 위 다이어그램의 어떠한 이벤트루프에도 속해있지 않습니다.
process.nextTick() 의 목적은 사용자의 동기코드 이후, 이벤트루프가 진행되기 이전 의 시간을 목적으로 합니다.
다음과 같은 상황을 보면
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log("bar", bar); // 1 }); bar = 1;
사용자의 동기코드인 bar = 1; 이 수행된 이후에 someAsycnApiCall 에 전달된 callback 이 수행됩니다. 만약 process.nextTick(callback) 을 callback() 으로 바꾸게 된다면 bar = 1; 이 실행되기 이전에 callback 이 수행될 수 있습니다.
bar = 1;
someAsycnApiCall
process.nextTick(callback)
callback()
callback
process.nextTick() 은 이벤트루프 이전에 수행되기 때문에 반복적으로 호출하게 되면 poll 단계가 수행되지 않을 수 있으며 이는 I/O 단계를 Starving 상태로 만들 수 있습니다.
사실 두 함수의 이름은 바뀌어야 합니다. process.nextTick()이 setImmediate()보다 더 즉시 실행되지만 이미 수많은 코드들이 작성되어 있기 때문에 두 이름을 바꾼다면 npm 패키지에 잠재적으로 많은 문제가 발생할 수 있기 때문에 이름을 바꾸지 않는다고 합니다.
process.nextTick() 을 쓰는 이유는 단순합니다.
사용자가 이벤트루프를 진행하기 전에 수행할 작업이 필요하기 때문입니다.
다음 예를 봅시다.
const server = net.createServer(); server.on("connection", (conn) => {}); server.listen(8080); server.on("listening", () => {});
server.listen(8080) 은 이벤트루프 시작 부분에서 수행될 것 입니다. server.on 으로 등록한 listening 콜백은 setImmedate() 로 수행이 됩니다. 따라서 server.listen(8080) 보다 먼저 적용될 수 있습니다.
server.listen(8080)
server.on
listening
setImmedate()
피드백은 항상 환영입니다
출처
이전 포스트에서 Event loop 는 epoll_wait loop 로서 커널에 관심사를 알려주고 커널에서 알림이 blocking 상태를 해제하고 적절한 javascript API 로 변환하는 과정이라고 했습니다. 쉽게 말하자면 Node.js 가 None Blocking I/O 작업을 수행하도록 해주는 과정입니다.
Event loop 더 고수준에서 다이어그램으로 보자면 아래와 같습니다.
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
이벤트루프는 저수준 c 코드에서 일어나는 일이며 사실 더 복잡한 과정을 가지고 있습니다만 Node.js 공식 문서에서는 위 과정이 Event loop 의 가장 중요한 단계 를 의미한다고 합니다.
모든 단계는 특징을 가지고 있습니다.
타이머는 제공된 콜백이 일정 시간 후 실행되어야 하는 시간을 지정합니다. 이 시간은 운영체제 스케쥴링이나 다른 콜백 실행 때문에 지연될 수 있으므로 정확한 시간이 아닙니다.
예를 들어 100ms 이후 실행되도록 시간을 지정하고 95ms 가 걸리는 파읽 읽기를 비동기로 수행한다고 가정합니다.
const fs = require("fs"); function someAsyncOperation(callback) { // 이 작업이 완료되는데 95ms가 걸린다고 가정합니다. fs.readFile("/path/to/file", callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // 완료하는데 95ms가 걸리는 someAsyncOperation를 실행합니다. someAsyncOperation(() => { const startCallback = Date.now(); // 10ms가 걸릴 어떤 작업을 합니다. while (Date.now() - startCallback < 10) { // 아무것도 하지 않습니다. } });
someAsyncOperation 에서 95ms 이 걸리는 fs.readFile 을 수행하고 10ms 를 소요하는 콜백을 다시 수행합니다.
someAsyncOperation
fs.readFile
해당 과정은 poll 과정에서 일어나고, poll 임계점에 다다르지 않았다고 가정합니다.
콜백이 완료되면 105ms 를 소요하게 되고 poll 큐에 아무것도 없기 때문에 poll 단계는 종료됩니다.
이제 이벤트루프는 check -> close callback 단계를 거쳐 다시 timers 단계로 돌아가며 100ms 로 지정한 콜백은 105ms 이후 실행되었기에 timers 의 시간은 정확한 시간이 아니라 콜백을 실항할 때 까지 걸리는 최소시간 이라는 것을 알 수 있습니다.
이름에서 알 수 있다시피 콜백의 완전히 완료하지 못하고 연기된 콜백을 실행하는 단계 입니다.
다음 두 가지 기능을 합니다.
poll 단계에서는
poll 큐가 다음 단계로 간다는 말은 하나 이상의 timers 가 준비되어있거나 setImmediate() 가 스케쥴링 되어 있다는 뜻 입니다.
poll 단계가 수행된 후에 콜백을 수행하기 위한 단계입니다. setImmediate() 를 통해 이 단계를 수행할 수 있습니다.
setImmediate() 는 setTimeout() 와 다른 단계에서 수행이 되며 동일한 I/O 주기 내에서 둘을 같이 호출 한다면 setImmidate() 가 항상 먼저 실행됩니다.
소켓 또는 핸들이 닫힌 경우와 같이 close 이벤트를 처리하기 위한 단계입니다.
Bert Belder 의 강연 에서는 다음과 같은 그림을 통해 설명하고 있습니다.
Node.js 는 이벤트루프를 libUV 라이브러리를 통해 수행하고 있습니다. libUV 에 대해 더 알고 싶다면 libUV Doc 를 참고해주세요.
피드백은 환영입니다.
시작하기 앞서 이 포스트는 Node.js 유튜브 영상 을 참조하고 만든 것을 알려드립니다.!!
Node.js 의 Event Loop 가 Scale 측면에서 우수하다 라는 말을 알기 위해서는 기존에는 요청을 어떻게 처리 했는지를 이해하면 도움이 많이 됩니다.
TCP Connection 과정을 기존에 어떻게 처리 했는지 봅시다.
int server = socket(); bind(server, 80) listen(server) while(int connection = accept(server)) { do_something(connection); }
위의 의사코드 작동을 보면
TCP connection 요청을 받고 상태는 accept connection 됩니다. accept connection 은 system call 이고 system call 은 프로그램을 block 할 수 있습니다. 즉 do_something 이 끝날 때까지 다른 무언가를 할 수 없는 상태가 됩니다. 예를 들어 10 초가 걸리는 요청이 있다면 그 요청을 끝내기 전까지는 다른 요청을 처리하지 못하게 될 수 있습니다.
자 이제 Multi Thread 개념을 생각해봅시다. 만약 새로운 Connection 마다 새로운 Thread 를 생성해서 할당하고 그 Thread 에게 요청을 맡기고 요청이 끝나면 다시 Thread 를 회수한다고 생각해봅시다.
Main Thread 에서는 Connection 을 기다리고 Connection 요청이 오면 새로운 Thread 를 생성하고 요청을 할당한 후 다시 Connection 을 기다린다면 요청이 끝나기 전 다른 요청을 받을 수 있습니다.
int server = socket(); bind(server, 80) listen(server) while(int connection = accept(server)) { pthread_create(echo, connection); } void echo(int connection) { char buf[4096]; while(int size = read(connection, buffer, sizeof buf)) { write(conection, buf, size); } }
Multi Thread 환경에서도 문제가 있습니다. Thread 를 생성하고 할당하는 과정은 우리가 다룰 데이터에 비하면 상대적으로 상당히 무거운 과정입니다.
예를 들어 정말 간단한 작업을 요청을 하더라도 Thread 를 생성해야합니다. Thread 를 관리하기 위한 메모리 할당, Thread 간 Context switching 과정 등에서 들어갈 작업 등..을 생각해보면 비효율적일 수도 있습니다. 임계 Thread 양보다 많은 요청이 들어온다면 요청을 처리할 수도 없게 됩니다.
Scale 관점의 문제를 해결하기 위해서 Epoll 이라는 것을 알아봅시다. Epoll 은 I/O 통지 모델로서 커널 수준에서 file descriptor를 관리하게 됩니다.
epoll 이 하는 역할은 epoll descriptor 를 만들고 커널에 우리가 어떤 이벤트에 관심이 있는지 말해줄게!! 그 이벤트가 발생하면 나에게 알려줘!! 하는 역할을 하게 됩니다. 다시 말해 커널에게 작업을 맡기겠다는 말입니다. 커널에 정보를 알려주고 다시 요청을 받을 준비를 하게 됩니다. Epoll 을 이용하면 cpu 자원 또한 아끼게 됩니다.
Epoll 에 대해서는 다음 블로그를 참고해주세요 . Epoll 알아보기
이를 Event loop 라 하며 각각의 루프를 tick 이라 합니다. 아래와 같은 식이라 생각하면 됩니다. (사실 더 복잡하지만 간략화 했습니다.)
struct epoll_event_events[10]; while((int max = epoll_wait(eventfd, events, 10))) { for(n = 0; n < max; n++) { if(events[n].data.fd.fd == server) { //Server socket has connection!! int connection = accept(server); ev.events = EPOLLIN; ev.data.fd = connection; epoll_ctl(eventfd,EPOLL_CTL_ADD, connection, &ev); } else { //Connection socket has data char buf[4096]; int size = read(connection, buffer, sizeof buf); write(connection, buffer, size); } } }
이 반영구적인 loop 가 event loop 의 정체입니다. 커널에 관심사를 알려주고 관심사가 일어날 때까지 blocking 하며 대기상태로 진입한 후 관심사가 일어나면 Node.js 가 그 관심사를 자바스크립트 api 로 변환하게 되는 겁니다.
이 loop 는 더이상 event 를 기다려도 되지 않을 때 까지 반복됩니다.
충격적이겠지만 사실 Node.js 에도 Thread poll 이라는 것이 존재합니다. Epoll 을 통해 관리할 수 없는 유형의 작업은 이 Thread poll 의 Thread 에 할당하여 처리하게 됩니다.
File System
Dns.Lookup calls
crypto
any c++ addons that use it
이외에도 여러 유형이 있습니다. 매우 cpu intensive 한 작업에서 None pollable 한 경우가 많았습니다. 만약 None Pollable 한 작업을 많이 요구하는 Node.js 어플리케이션의 경우는 Thread poll size 를 늘리는 것도 생각해야합니다.
또한 Event Loop Time 시간을 잘 고려해서 내 어플리케이션이 어디서 Blocking 이 많이 되는가를 모니터링 해야 한다는군요.
Node.js 의 이벤트 루프를 공부하다가 발견한 영상을 토대로 정리했습니다. 미흡한 정보는 이후 포스트를 업데이트 하며 추가/수정 하겠습니다. 피드백은 항상 환영입니다.
이후에는 고수준에서 Event loop 를 살펴보겠습니다.
기본 Redux 는 간단하게 작용합니다. 중앙화 된 상태저장소가 있고 각각의 컴포넌트들이 그 상태저장소에 접근해서 원하는 것을 가져올 수 있습니다. 자식들에게 상태(state)와 자산(props)을 물려줄 필요는 없습니다.
Redux 를 구성하는 3가지 요소가 있습니다.
간단하게 Actions 는 이벤트 데이터 입니다. 데이터는 심플한 Javascript Object 입니다. 다음처럼요.
{ type: REQUEST_ROBOTS_SUCCESS, isPending : false, payload: { robots : [some api call results] }
이 Actions 를 dispatch 라는 함수를 통해 Redux Store 로 보내게 됩니다.
dispatch({ type: REQUEST_ROBOTS_SUCCESS, isPending : false, payload: { robots : [some api call results] } });
Reducers 는 순수함수입니다. 이전 상태 값을 받아 새로운 상태 값을 리턴하는 함수입니다.
순수함수란 값을 받아서 어떤 값을 다시 리턴 해주는 함수이며, 이때 동일한 값을 받으면 그 값에 대해서 항상 같은 값을 리턴해준다면 이를 순수함수라고 합니다..
dispatch 를 통해 action 을 받은 Reducer 는 action 의 타입에 따라 다른 행동을 합니다.
reqeustBots 라는 Reducer 를 예를 들면,
export const requestRobots = (state=initialStateRobots, action={}) => { switch (action.type) { case REQUEST_ROBOTS_PENDING: return Object.assign({}, state, {isPending: true}) case REQUEST_ROBOTS_SUCCESS: return Object.assign({}, state, {robots: action.payload, isPending: false}) case REQUEST_ROBOTS_FAILED: return Object.assign({}, state, {error: action.payload}) default: return state }
이처럼 생겼으며 순수함수 이므로 state 를 변화시킨다기 보다는 새로운 state 를 만들어 리턴하게 됩니다.
store 는 앱의 state 를 가지고 있는 저장소입니다.
const store = createStore(Reducers);
이 store 를 Redux 의 Provider 를 통해 앱으로 전달하게 됩니다.
ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
App 컴포넌트에서는 이 store 에 접근하기 위해 react-redux 라이브러리의 connect 함수를 통해 연결하게 됩니다.
const mapStateToProps = (state) => { return { searchField: state.searchRobots.searchField, robots: state.requestRobots.robots, isPending: state.requestRobots.isPending, }; }; // dispatch the DOM changes to call an action. note mapStateToProps returns object, mapDispatchToProps returns function // the function returns an object then uses connect to change the data from redecers. const mapDispatchToProps = (dispatch) => { return { onSearchChange: (event) => dispatch(setSearchField(event.target.value)), onRequestRobots: () => dispatch(requestRobots()), }; }; export default connect(mapStateToProps, mapDispatchToProps)(App);
Redux 에서는 Action 이 dispatch 되기 전 이를 가로채 가공할 수 있는 middleware 기능이 있습니다.
다음과 같이 사용 가능합니다.
import thunkMiddleware from "redux-thunk"; import { createLogger } from "redux-logger"; const store = createStore( rootReducers, applyMiddleware(thunkMiddleware, logger) );
redux-thunk 는 비동기 수행에, redux-logger 는 로그를 남기기 위해 사용하는 middleware 입니다.
React 의 Context hook 또한 상태 관리를 위한 목적을 가지고 있습니다. Redux 에서와 비슷하게 Provider / Consumer 패턴으로 state 를 제공/ 소비를 할 수 있습니다.
Redux 와 비슷하게 state 를 관리할 수 있습니다. 그럼 왜 Redux 를 아직 사용할까요??
Redux 의 장점
위의 장점이 필요 없다면 React hooks 만 써도 됩니다. 필요 이상의 일을 할 필요가 없어지는 것 입니다.
그래서 모든 개발자가 React hooks 가 Redux 를 대체할 수 있나요? 라는 질문에 “아니요” 라고 대답하는 것 같습니다.
다만 React hooks 또는 React 관련 기능이 더 성장한다면 Redux 를 대체할 수도 있지 않을까 생각합니다.
참고 :
참고 예제 :
이번에는 이전 포스트에서 다뤘던 내용에 대해서 Refactoring 을 하고 테스트도 해보겠습니다.
└─modules └─user │ user.controller.ts │ user.module.ts │ user.repository.ts │ user.service.ts │ user.spec.ts │ ├─dto │ create-user.dto.ts │ index.ts │ update-user.dto.ts │ ├─entities │ user.entity.ts │ ├─exceptions email-already-exist-exception.ts user-not-found.exception.ts username-already-exist-exception.ts
계속 리팩토링 하다보니 위와 같은 구조가 만들어졌습니다. 파일 이름이 바뀐 것도 있고, 내부 구현사항이 바뀐 것도 있습니다. 리팩토링 하면서 설계의 중요성을 다시 느낍니다.
Custom Exception 을 만들기 위해 user 폴더 안에 exceptions 폴더를 만듭시다!
이메일과 유저네임이 중복 되었을 때 그리고 유저를 찾지 못했을 때 발생 시킬 exception 을 각각 만들어 줍시다.
username-already-exist-exception.ts
import { BadRequestException } from "@nestjs/common"; export class UsernameAlreadyExistException extends BadRequestException { constructor(error?: string) { super("username already exist", error); } }
email-already-exist-exception.ts
import { BadRequestException } from "@nestjs/common"; export class EmailAlreadyExistException extends BadRequestException { constructor(error?: string) { super("email already exist", error); } }
user-not-found.exception.ts
import { BadRequestException } from "@nestjs/common"; export class UserNotFoundException extends BadRequestException { constructor(error?: string) { super("user not found", error); } }
각각의 클래스는 BadRequestException 을 상속받고 있습니다. 해당 Exception 을 발생시키면 Status Code 는 400(BadRequest) 이 발생됩니다.
매우 간단하게 Custom Exception 을 만들었습니다!!
create 메소드 안에서 이전에 만들었던 QueryBuilder 부분을 삭제하고 바로 위에서 만들었던 Custom Exceptions 들을 사용하여 간단하게 만들어줍니다.
UserService.create
const thisUser = await this.userRepository.findOne({ username: username }); if (thisUser) { const error = "UserName is already exist"; throw new UsernameAlreadyExistException(error); } const thisEmail = await this.userRepository.findOne({ email: email }); if (thisEmail) { const error = "Email is already exist"; throw new EmailAlreadyExistException(error); }
그리고 유저를 저장하여 반환할 때 단순 number 값이 아니라 Object 를 반환해줍시다.
const userId = await this.userRepository.save(newUser).then((v) => v.id); return { userId: userId };
테스트를 위해 remove 메소드 안의 내용을 다음과 같이 만들어 줍시다.
이후 Auth 관련 기능을 만들면서 remove 기능은 사라질겁니다…
UserService.remove
async remove(email: string): Promise<DeleteResult> { return await this.userRepository.delete({ email: email }); }
remove 와 관련된 controller 부분을 다음으로 변경해주세요!
UsersController.remove
@Delete('') remove(@Body('email') email: string) { return this.usersService.remove(email); }
@Body annotation 은 response body 안에서 해당하는 필드를 가져와 변수에 입력해주는 기능을 합니다!!
@Body
먼저 유닛테스트가 아닌, e2e 테스트를 진행하기 위한 코드임을 알려드립니다. 테스트 관련 기술이 많이 부족해서 계속 공부중입니다. ㅜㅜ 더 알아가면서 포스트를 업데이트 하도록 하겠습니다!!
테스트는 해당 url 로 요청 수행시 적절한 응답이 오는 확인합니다.
e2e 테스트 방법에 정석이 있겠지만 진행 도중 많은 오류가 발생하여.. 일단 야매로 진행하겠습니다.
서버실행 -> url로 요청
요청을 수행하기 위해서 supertest 라이브러리를 먼저 설치합시댜!!
$ npm install --save-dev supertest
supertest 관한 내용은 다음링크에서 더 확인 가능합니다!! npm supertest
그리고 테스트 하기 위한 nest 라이브러리도 설치합시다 !!
$ npm i --save-dev @nestjs/testing
이제 user.spec.ts 라는 파일을 user 폴더 안에 생성하여 다음과 같은 코드를 작성합시다!
import * as request from "supertest"; const app = "http://localhost:3000"; describe("User create 테스트", () => { beforeEach(async () => { await request(app).delete("/users").send({ email: "test1@example.com" }); await request(app).delete("/users").send({ email: "test2@example.com" }); }); it("email 중복 확인", async () => { await request(app) .post("/users") .send({ email: "test1@example.com", username: "testuser", password: "12345", }) .expect(201); const res = await request(app) .post("/users") .send({ email: "test1@example.com", username: "abcd", password: "12345", }) .expect(400); expect(res.body.error).toBe("Email is already exist"); }); it("username 중복 확인", async () => { await request(app) .post("/users") .send({ email: "test2@example.com", username: "testuser", password: "12345", }) .expect(201); const res = await request(app) .post("/users") .send({ email: "test1@example.com", username: "testuser", password: "12345", }) .expect(400); expect(res.body.error).toBe("UserName is already exist"); }); });
describe
beforeEach
it
test
자 이제 테스트를 시작해봅시다!!
먼저 서버실행!!
$ npm run start
그리고 테스트도 실행 !!
$ npm run test
> nestjs-practice@0.0.1 test C:\Users\jiyoung\nestjs-practice > jest PASS src/modules/user/user.spec.ts User create 테스트 √ email 중복 확인 (173 ms) √ username 중복 확인 (24 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 1.392 s, estimated 5 s Ran all test suites.
다음과 같이 통과하면 성공입니다!!
package.json 에서 jest 관련 설정을 spec 또는 test 가 붙은 ts 파일로 설정했기 때문에 해당하는 파일은 모두 테스트하게 됩니다!!
테스트 관련해서는 아직 공부하고 있습니다 !! e2e 테스트를 먼저 해보기 위해 시도는 많이 했으나 그만큼 오류가 많이 나더군요 ㅜㅜ 위와 같은 야매 방법을 통해 테스트를 했으나, 이후에 해결방법을 알아와서 포스트를 업데이트 하겠습니다!!
또한 이후에는 Unit Test 관련 기능도 알아보겠습니다!
다음에는 post 모듈을 만들어봅시다!! 감사합니다.
피드백은 항상 환영입니다.