깊은 복사와 얕은 복사
얕은 복사(Shallow Copy)는 객체의 참조 값(주소 값)을 복사하여 새로운 객체를 생성하지 않고, 기존 객체와 동일한 메모리 주소를 참조하는 복사이며,
반면 깊은 복사(Deep Copy)는 객체의 실제 값을 복사하여 새로운 객체를 새로 생성하는 것이다!
얕은 복사
얕은 복사는 주소를 복사하기 때문에 일반적인 STRING/INT/BOOL 과 같은 형식은 복사된 값을 가져오지만,
배열이나 객체와 같은 경우는 주소를 복사하기 때문에 원본 자체를 가져오게된다!
그로인해 가져온 객체나 배열을 수정하면, 원본도 똑같이 수정이되는 참사가 일어난다..
const Mine = ["noodles", { list: ["eggs", "flour", "water"] }];
const Copy = Array.from(Mine);
console.log(Copy);
// ["noodles",{"list":["eggs","flour","water"]}]
Copy[0] = "bread"
Copy[1].list = ["rice flour", "water"];
console.log(Mine);
// Array [ "noodles", { list: ["rice flour", "water"] } ]
console.log(Copy);
// Array [ "bread", { list: ["rice flour", "water"] } ]
깊은 복사
깊은 복사는 얕은 복사와 달리 객체나 배열도 “값”만 복사하여 원본을 훼손하지 않는다!
(참고로 이 경우엔 기존 객체(o1)와 복사한 객체(o2)는 다르게(o1 !== o2) 취급된다 (주소 값이 다르기에))
const Mine = ["noodles", { list: ["eggs", "flour", "water"] }];
const Copy = JSON.parse(JSON.stringify(Mine));
console.log(Copy);
// ["noodles",{"list":["eggs","flour","water"]}]
Copy[0] = "bread"
Copy[1].list = ["rice flour", "water"];
console.log(Mine);
// Array [ "noodles", { list: ["eggs", "flour", "water"] } ]
console.log(Copy);
// Array [ "bread", { list: ["rice flour", "water"] } ]
JWT(Json Web Token)
JWT (Json Web Token)은 사용자 인증과 식별을 위한 토큰 기반 인증 방식으로, JSON 객체를 사용하여 정보를 전달한다!
토큰은 주로 웹표준을 따라 암호화된 형태로 인증에 필요한 정보를 담고 있다.
위와 같은 구조를 가지고 있으며, 공식 사이트를 통해 해석될 수 있기 때문에 개인정보를 그대로 담아둬선 안됀다!
( 또한 악용될 여지가 있기 때문에 서버에서만 알아볼 수 있는 정보가 좋다! )
JWT의 사용
JWT는 위에서 말했듯이 사용자 인증과 식별을 위해 사용하며, 주로 RESTful API 와 같이 무상태(Stateless) 통신에서 이용하게 된다. JWT가 도입된 웹사이트를 기준으로 설명을 진행해보겠다!
-
사용자가 회원가입된 아이디로 로그인 요청을 보낸다.
-
웹 사이트는 로그인 정보가 DB에 저장된(회원가입된) 정보와 비교한다.
-
성공 시, 유저를 식별할 수 있는 정보를 담은 Access Token(JWT)과 Refresh Token(JWT)을 생성한다!
-
서버는 Refresh Token을 DB에 저장해두고 두 토큰을 사용자에게 전달한다
-
사용자는 로그인이 필요한 서비스들을 Access Token을 보내 인증하고 이용할 수 있게 된다.
-
사용자가 로그아웃 시, 서버에 저장된 Refresh Token은 폐기된다
여기서 잠깐 왜 토큰을 두 개나 발급하나요?
-
바로 토큰을 탈취당했을 경우를 생각하여 만료기간이 짧지만 서비스 접근이 가능한 Access Token과
만료기간이 길며 이러한 Access Token을 발급하는데 사용하는 Refresh Token을 만든 것이다! -
이렇게 분리하여 발급 받으면 탈취당한 Access Token을 다른 사용자가 일정된 시간(만료기간)동안만
사용할 수 있기에 피해가 최소화 된다!
근데 Refresh Token이 탈취되면요..?
-
결론만 말하자면 망한거지만, 그럴 경우는 적다! 왜냐하면 Refresh Token은
Access Token이 만료되었을 때에만 사용되기에 통신 빈도가 매우 적다. -
대책안으로 Refresh Token을 사용할 때마다 Refresh Token도 같이 재발급을 하는 방법도 있다!
Single-Threaded
현재 공부 중이며 사용하는 Node.js는 싱글 스레드 방식이다! 이에 대해 조금 더 자세히 설명을 해보면..
일단 스레드란 프로그램(프로세스) 실행의 단위를 뜻하며 싱글 스레드는 말 그대로 한번에 한 작업만 가능하단 의미이다!
그런데 Node.js에는 비동기로(non-blocking, asynchronous) 작동하던데 도대체 어떻게 싱글 스레드 인거죠..?
사실은 Node.js의 싱글 스레드는 요청을 처리하는 부분(javascript)만 싱글 스레드로 돌아가고,
I/O(입출력) 과 같은 비동기 작업은 libuv를 활용하여 멀티 스레드로 처리하는 것이다!
(아래로 가세요라!)
EventLoop
이벤트 루프는 Node.js의 비동기 작업을 관리하는 무한 루프로 이 루프는 여러 단계(phase)로 구성되어 있으며,
각 단계에서 특정 유형의 콜백 함수를 실행한다.
-
Timers: setTimeout과 setInterval로 예약된 콜백 함수를 실행
-
Pending Callbacks: 시스템 작업(예: TCP 오류)과 같은 이전 루프에서 실행되지 않아 지연된 I/O 콜백들을 실행
-
Idle, Prepare: 내부적으로 사용되는 단계로, 일반적으로 개발자가 직접 사용하지 않음
-
Poll: 새로운 I/O(입/출력) 이벤트를 기다리고, 해당 이벤트의 콜백을 실행함
( 만약 처리할 이벤트가 없으면 다음 단계로 넘어감 ) -
Check: setImmediate로 예약된 콜백 함수를 실행
-
Close Callbacks: 자원을 정리하거나, 종료 하는 이벤트의 콜백을 실행함
파일 읽기, 네트워크 요청, 데이터베이스 쿼리와 같은 I/O 작업은 비동기로 처리되며,
작업이 완료되면 해당 콜백이 EventLoop의 적절한 단계에서 실행되도록 집어넣어 집니다.
위의 순서대로 비동기 작업들이 처리되기에 이를 이해하면, 코드를 더 효율적으로 설계가능해진다!
LibUv
비동기 I/O를 처리하는 핵심 라이브러리로 EventLoop와 Thread Pool을 관리하여 다양한 운영체제에서 일관된 비동기 I/O 인터페이스를 제공합니다.
이로써 Node.js가 단일 스레드로 동작하면서도 비동기 작업을 효율적으로 처리할 수 있게 도와주는 것이죠!
(EventLoop 는 비동기 I/O 작업을 관리하고, Thread Pool은 CPU 집약적인 작업을 병렬로 처리)
-
기본적으로 libuv는 4개의 스레드로 구성된 Thread Pool을 사용함!
(환경 변수 UV_THREADPOOL_SIZE를 통해 조정가능 ) -
스레드 풀의 작동 방식
-
작업 큐: 스레드 풀은 작업 큐를 가지고 있어, 비동기 작업이 요청되면 해당 작업은 작업 큐에 추가됨
-
스레드 할당: 스레드 풀의 스레드 중 하나가 작업 큐에서 작업을 가져와 실행
-
작업 완료: 작업이 완료되면 결과가 이벤트 루프로 전달되고, 해당 콜백이 실행됨
-
Async/Non-Blocking
비동기(async)는 작업이 병렬적으로 실행되어, 한 작업이 완료되기를 기다리지 않고 다른 작업을 실행하는 것을 의미한다!
( 이와 반대되는 개념으로 동기(sync)는 순차적으로 실행하여 앞선 작업이 완료되기 전까지 대기한다 )
Non-Blocking 이란 작업이 완료되지 않아도, 다른 작업도 수행될 수 있도록 제어 권한을 넘겨주는 개념이다.
( 이와 반대되는 개념인 Blocking은 제어 권한을 통제하여 다른 작업이 수행되지 않도록 한다 )
그래서 위와 같은 개념인 비동기와 Non-Blocking은 똑같은 개념인것 같아보이지만.. 차이가 있다!
바로 작업의 완료 여부를 트래킹하는가 이다.
비동기(async)의 경우 작업이 완료되면 콜백/이벤트를 통해 결과가 처리되기에 작업완료 여부를 확인할 필요가 없으나,
Non-Blocking의 경우 각 작업들이 완료되었는지 지속적으로 트래킹 하여 다음 처리를 직접 실행해주어야 한다!
이러한 차이로 아래와 같이 용도가 다르다!
-
비동기: I/O 작업, 네트워크 요청, 타이머 등과 같이 작업 완료를 시스템이 자동으로 처리할 수 있는 경우에 적합
-
Non-Blocking: 실시간 시스템, 게임 엔진, 저수준 네트워크 프로그래밍 등에서 작업 상태를 직접 확인하고 즉시 처리해야 하는 경우에 적합