본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
var는 변수를 선언하기 위한 방식으로 같은 이름의 변수를 재선언 가능합니다.
let 역시 변수를 선언하기 위한 방식이지만 var과 다르게 같은 이름으로 재선언이 불가능합니다.
const는 상수를 선언하기 위한 방식으로 선언된 상수는 변경이 불가합니다.
변수와 상수는 메모리 영역의 수정 여부로 구분합니다.
비동기 작업을 관리하기 위한 자바스크립트 객체입니다.
비동기 작업을 관리하기 위해 사용하던 콜백함수로 인해 콜백 지옥이 발생했습니다.
이러한 콜백 지옥을 해소하고 더 나은 가독성을 위해서 Promise를 사용합니다.
async/await는 자바스크립트의 비동기 작업을 더 쉽고 직관적으로 작성하게 도와주는 문법입니다.
Promise 기반의 코드를 작성할 때 작성하는데, async는 그 함수의 앞에 붙이고 그 함수 내부에서 await 를 사용합니다.
이 때, await가 붙은 비동기 작업은 동기적으로 동작해서 작업이 끝날 때까지 기다립니다.
이를 통해서 비동기 작업을 마치 동기 작업처럼 동작시킬 수 있습니다.
호이스팅은 자바스크립트에서 변수나 함수의 선언 부분이 코드의 최상단으로 올려지는 현상을 말합니다.
호이스팅은 이러한 변수나 함수의 정보를 수집하는 과정을 이해하기 위한 가상 개념입니다.
변수 선언은 호이스팅되지만, 변수 할당은 호이스팅되지 않습니다.
함수 선언은 함수 전체가 호이스팅됩니다.
함수 표현식은 변수와 마찬가지로 선언 부분만 호이스팅되고 할당 부분은 호이스팅되지 않습니다.
화살표 함수는 자바스크립트에서 함수 표현식을 더 간결하게 사용하기 위한 방식입니다.
그리고 this 바인딩과 관련해서 일반적인 함수 표현식과 다르게 적용됩니다.
일반적인 함수 안에서의 this는 전역을 가르키지만 화살표 함수에서의 this는 상위 스코프의 this를 가르킵니다.
Express는 가장 널리 사용되는 Node.js 웹 프레임워크입니다.
Express는 주로 간단한 웹 서버 구축을 통해 빠르게 웹을 개발하기 위해서 사용됩니다.
Express의 경우 별도의 라이프 사이클이 없고 미들웨어로 개발자가 구현해야 하는 문제가 있습니다.
그리고 Express는 자바스크립트 기반이기에 타입에 의한 런타임 에러가 많이 발생합니다.
그래서 이에 대한 대안으로 저는 대표적으로 Nest.js가 있다고 생각합니다.
Nest.js는 타입스크립트를 기반으로 해서 타입에 대한 런타임 에러를 줄일 수 있습니다.
그리고 Nest.js에는 가드, 인터셉터, 파이프 등과 같은 라이프 사이클이 존재하기 때문에 개발자는 그 라이프 사이클에 맞춰서 개발을 진행하면 됩니다.
얕은 복사는 참조형 타입에서 바로 아래 단계의 값만 복사하는 방법입니다.
자바스크립트에서 얕은 복사는 Object.assign()과 스프레드 연산자를 활용해서 구현이 가능합니다.
깊은 복사는 모든 참조를 끊고 값들을 복사하는 방법입니다.
자바스크립트에서 깊은 복사는 JSON.parser(JSON.stringify(obj))와 재귀함수를 활용해서 구현이 가능합니다.
JWT는 클라이언트와 서버 간의 통신에서 인가 수단으로 사용됩니다.
인증된 클라이언트가 로그인하면 서버로부터 발급받습니다.
그리고 인가가 필요한 작업을 요청할 때는 헤더에 JWT를 포함해서 서버에 전달하면 서버에서 그 JWT를 검증하고 유효성을 확인합니다.
JWT는 헤더, 페이로드, 시그니처로 구성되는데 시그니처를 통해서 토큰의 무결성을 확인합니다.
보통 JWT는 Access Token과 Refresh Token으로 나누는데, 이 때 Access Token은 비교적 짧은 유효기간을 가지고, Refresh Token은 긴 유효기간을 가집니다.
만약 Access Token가 만료되면 재발급을 위해서 Refresh Token을 통해서 새로운 토큰을 발급합니다.
다른 API 서비스에서 호출 시 클라이언트가 기존에 발급받은 JWT를 헤더에 포함시켜서 전송합니다.
서버는 헤더를 통해 받은 JWT를 비밀키를 통해서 검증해서 인가 여부를 판단합니다.
Node.js는 자바스크립트 런타임 환경으로, 서버에서 자바스크립트를 실행할 수 있게 해줍니다.
가장 큰 특징 중 하나는 싱글 스레드로 동작한다는 점입니다.
싱글 스레드 기반으로 동작하기 때문에 멀티 스레드 환경에서 발생할 수 있는 데드락같은 동시성 문제를 피할 수 있고,
개발자가 스레드 간의 동기화를 고민할 필요가 없어 코드가 간단해집니다.
그리고 또 다른 큰 특징은 Node.js가 논-블로킹과 비동기 방식을 채택해 비동기적으로 I/O작업을 처리합니다.
이벤트 루프를 통해서 비동기 작업을 처리하며, 이를 통해 I/O작업을 기다리지 않고 다른 작업을 계속 수행할 수 있습니다.
또한, 콜백, 프로미스, async/await 구문을 통해 비동기 작업을 관리하고, 코드의 흐름을 간결하게 유지하면서도 효율성을 높일 수 있습니다.
이벤트 루프는 Node.js에서 비동기 작업을 처리하기 위해 사용하는 구조입니다.
Node.js는 싱글 스레드 기반이지만 이벤트 루프를 통해서 비동기 I/O 작업을 효율적으로 하기 위해서 사용합니다.
이벤트 루프의 구조는 크게 호출 스택, 이벤트 큐, 백그라운드, 이벤트 루프로 나눌 수 있습니다.
만약 비동기 작업이 실행되면 해당 작업의 콜백 함수는 백그라운드로 보내져서 실행되고 그 결과를 이벤트 큐에 추가합니다.
그리고 이벤트 루프는 호출 스택이 비어있는지 확인하고 이벤트 큐에서 대기 중인 콜백 함수를 호출 스택으로 가져갑니다.
빅오 표기법은 알고리즘의 성능을 평가하는 방법 중 하나입니다.
주로 알고리즘의 시간 복잡도, 공간 복잡도를 표현하기 위해서 사용합니다.
시간 복잡도는 알고리즘이 실행되는 데 걸리는 시간을 의미하고, 공간 복잡도는 얼마나 많은 메모리를 사용하는지를 나타냅니다.
시간 복잡도를 예로 O(1)은 언제나 같은 시간이 소요된다는 것을 의미합니다.
그리고 O(n)은 입력의 크기와 비례하게 시간이 늘어난다는 것을 의미하고, O(n^2)은 입력이 2배가 늘어나면 시간이 4배 늘어난다는 것을 의미합니다.
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
# 예시 사용
print(bubble_sort([64, 34, 25, 12, 22, 11, 90]))
def selection_sort(arr):
for i in range(len(arr)):
min_index = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_index]:
min_index = j
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
# 예시 사용
print(selection_sort([64, 25, 12, 22, 11]))
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and key < arr[j]:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
# 예시 사용
print(insertion_sort([12, 11, 13, 5, 6]))
def merge_sort(arr):
# 배열의 길이가 2보다 작으면 이미 정렬된 상태이므로 그대로 반환
if len(arr) < 2:
return arr
# 중간 인덱스 계산
mid = len(arr) // 2
# 배열을 두 개의 하위 배열로 나누고 재귀적으로 정렬
left = merge_sort(arr[:mid]) # 왼쪽 하위 배열
right = merge_sort(arr[mid:]) # 오른쪽 하위 배열
# 두 개의 하위 배열을 병합하기 위한 배열 초기화
merged_arr = []
l = r = 0 # 왼쪽과 오른쪽 하위 배열의 인덱스 초기화
# 두 하위 배열을 비교하여 정렬된 배열을 만듭니다
while l < len(left) and r < len(right):
if left[l] < right[r]:
merged_arr.append(left[l]) # 왼쪽 배열의 요소를 추가
l += 1 # 왼쪽 인덱스 증가
else:
merged_arr.append(right[r]) # 오른쪽 배열의 요소를 추가
r += 1 # 오른쪽 인덱스 증가
# 남은 요소가 있을 경우 추가
merged_arr += left[l:] # 왼쪽 배열에 남은 요소 추가
merged_arr += right[r:] # 오른쪽 배열에 남은 요소 추가
return merged_arr # 병합된 배열 반환
def quick_sort(arr):
# 배열의 길이가 1 이하인 경우, 이미 정렬된 상태이므로 그대로 반환
if len(arr) <= 1:
return arr
# 피벗(pivot) 설정: 배열의 중간 요소를 피벗으로 선택
pivot = arr[len(arr) // 2]
# 피벗보다 작은 요소들로 이루어진 배열 생성
left = [x for x in arr if x < pivot]
# 피벗과 같은 요소들로 이루어진 배열 생성
middle = [x for x in arr if x == pivot]
# 피벗보다 큰 요소들로 이루어진 배열 생성
right = [x for x in arr if x > pivot]
# 재귀적으로 왼쪽, 중간, 오른쪽 배열을 정렬하고 병합하여 반환
return quick_sort(left) + middle + quick_sort(right)
# 예시 사용
print(quick_sort([3, 6, 8, 10, 1, 2, 1])) # 출력: [1, 1, 2, 3, 6, 8, 10]
def heapify(arr, n, i):
largest = i # 현재 노드의 인덱스
left = 2 * i + 1 # 왼쪽 자식 노드의 인덱스
right = 2 * i + 2 # 오른쪽 자식 노드의 인덱스
# 왼쪽 자식이 현재 노드보다 큰 경우
if left < n and arr[i] < arr[left]:
largest = left # largest를 왼쪽 자식으로 업데이트
# 오른쪽 자식이 현재 largest보다 큰 경우
if right < n and arr[largest] < arr[right]:
largest = right # largest를 오른쪽 자식으로 업데이트
# largest가 현재 노드가 아닌 경우
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i] # swap
heapify(arr, n, largest) # 재귀적으로 heapify 호출
def heap_sort(arr):
n = len(arr) # 배열의 길이
# 배열을 힙으로 변환 (최대 힙 생성)
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# 힙에서 요소를 하나씩 꺼내어 정렬
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # 최대 요소를 배열의 끝으로 이동 (swap)
heapify(arr, i, 0) # 남은 힙을 다시 힙화
return arr # 정렬된 배열 반환
# 예시 사용
print(heap_sort([12, 11, 13, 5, 6, 7])) # 출력: [5, 6, 7, 11, 12, 13]
깊이 우선 탐색(Depth First Search, DFS)
너비 우선 탐색(Breath First Search, BFS)
Array와 LinkedList는 모두 데이터를 저장하기 위한 자료구조입니다.
하지만 데이터 접근 방식이나 메모리 상에서의 구조 등에서 차이를 보입니다.
Array는 인덱스를 통해서 데이터에 빠르게 접근이 가능하고, 고정된 크기의 연속된 메모리에 데이터를 저장합니다.
LinkedList는 노드의 포인터를 통해서 삽입과 삭제가 빠르게 이루어지고, 필요에 따라 크기가 변할 수 있는 동적인 크기를 가집니다.
그렇기에 데이터의 접근 속도가 중요하다면 Array를, 동적 크기와 빈번한 삽입과 삭제가 필요하다면 LinkedList를 선택하는 것이 좋습니다.
Stack과 Queue는 모두 데이터를 저장하기 위한 자료구조입니다.
Stack은 FILO(First In, Last Out) 구조로, 가장 먼저 들어간 데이터가 가장 나중에 제거되는 방식입니다.
데이터를 추가할 때는 push() 메서드를, 삭제할 때는 pop() 메서드를 사용합니다.
Stack의 사용 예로 함수의 호출 관리나 Undo/Redo, 웹페이지 히스토리 등이 있습니다.
Queue는 FIFO(First In, First Out) 구조로, 가장 먼저 들어간 데이터가 가장 먼저 제거되는 방식입니다.
데이터를 추가할 때는 enqueue() 메서드를, 삭제할 때는 dequeue() 메서드를 사용합니다.
Queue의 사용 예로 프로세스 관리나 대기열과 같이 순서를 따질 필요가 있을 때 사용합니다.
그래프는 정점과 간선으로 구성된 자료구조 입니다.
사이클이 존재할 수 있고, 정점과 정점을 간선으로 연결해서 연결된 정점 간의 관계를 표현합니다.
트리는 그래프의 일종으로, 하나의 루트 노드가 자식 노드를 가지는 형태의 자료구조 입니다.
계층적 구조를 가지며 사이클 없이 연결된 그래프입니다.
그래프와 트리의 가장 큰 차이점은 사이클의 유무이며 그래프는 복잡한 관계를 표현할 수 있는 구조이고, 트리는 명확한 계층 구조를 가진 구조입니다.
이진 트리는 각 노드가 최대 두 개의 자식 노드를 가질 수 있는 트리입니다.
이진 탐색 트리는 각 노드의 서브 트리는 해당 노드보다 작으면 왼쪽, 크면 오른쪽으로 구성된 트리입니다.
그래서 값의 검색, 삽입, 삭제 등의 연산에서 평균 O(log n)의 시간 복잡도로 수행됩니다.
힙은 완전 이진 트리의 일종으로, 부모 노드의 값을 기준으로 최대 힙, 최소 힙 구조를 가집니다.
최대 힙은 부모 노드의 값이 자식 노드의 값보다 크거나 같은 구조를 의미합니다.
최소 힙은 반대로 부모 노드의 값이 자식 노드의 값보다 작거나 같은 구조를 의미합니다.
즉, 이진 트리는 가장 기본적인 트리 구조고, 이진 탐색 트리는 데이터의 검색 및 정렬이 효율적인 트리 구조고, 힙은 주로 우선순위 큐나 정렬 알고리즘에서 사용되는 트리 구조입니다.
Red-Black Tree
이진 탐색 트리의 일종으로 삽입과 삭제 시 색을 통해 균형을 유지합니다.
각 노드는 레드 또는 블랙이라는 색을 가지고 다음과 같은 규칙을 따릅니다.
B+ Tree
일반적인 이진 탐색 트리와 다르게 하나의 노드가 여러 개의 자식을 가질 수 있습니다.
모든 데이터는 리프 노드에만 저장되고, 나머지 비 리프 노드들은 인덱스 역할을 합니다.
리프 노드끼리 연결되어 있어서 범위 데이터를 찾을 때 용이합니다.
그래서 주로 데이터베이스나 파일 시스템에서 많이 사용됩니다.
해시테이블은 키와 값의 쌍을 저장하는 자료구조로, 해시 함수를 통해 키를 고유한 인덱스로 변환해서 데이터를 저장합니다.
장점으로는 빠른 검색이 가능하고 해시 함수만 있으면 비교적 구현 자체는 간단하다는 점입니다.
단점으로는 해시된 키의 값이 충돌하는 문제가 생길 수도 있고, 정렬된 데이터가 필요할 때는 부적합하다는 점입니다.
이진 탐색 트리는 각 노드가 최대 두 대의 자식 노드를 가지는 트리 구조로, 왼쪽은 부모 노드보다 작고 오른쪽은 부모 노드보다 큰 값들로 구성한다는 특징이 있습니다.
장점으로는 데이터의 수가 바뀌어도 동적으로 크기 조정이 가능하고, 데이터의 순서가 바뀌어도 간단하게 정렬이 가능하다는 점입니다.
단점으로는 편향된 이진 탐색 트리가 될 수도 있고, 그래서 균형 이진 탐색 트리를 만든다면 구현 복잡성이 높아진다는 점입니다.
Primary Key
Foreign Key
ER 모델
정규화는 데이터베이스에서 데이터의 중복을 최소화하고 무결성을 유지하기 위한 일종의 규칙입니다.
정규화를 통해 동일한 데이터가 여러 테이블에 반복 저장되는 것을 방지할 수 있고, 데이터의 일관성을 높여서 데이터 무결성을 유지할 수 있습니다.
정규화에도 단계가 존재하는데, 일반적으로 제 3 정규화까지 진행하는 경우가 많다고 합니다.
제 1 정규화는 도메인이 원자값을 가져야 한다는 것을 의미합니다. 즉, 각 컬럼은 하나의 데이터만 가진다는 것을 의미합니다.
제 2 정규화는 부분적 종속이 없어야 한다는 것을 의미합니다. 즉, 현재 테이블의 주제와 관련없는 컬럼을 다른 테이블로 빼는 작업을 의미합니다.
제 3 정규화는 이행적 중속이 없어야 한다는 것을 의미합니다. 즉, 하나의 기본키에만 의존하도록 테이블을 분리하는 작업을 의미합니다.
무결성은 데이터베이스에서 데이터의 정확성, 일관성, 신뢰성을 보장하는 개념입니다.
무결성의 목적은 데이터의 품질을 유지하고 오류를 최소화하는 것 입니다.
그래서 무결성에도 몇 가지 종류가 있습니다.
참조 무결성은 외래키가 참조하는 기본키가 항상 존재해야 한다는 원칙입니다.
도메인 무결성은 각 필드의 데이터가 허용되는 값인지 확인해야 한다는 것을 의미합니다.
개체 무결성은 테이블의 기본키가 NULL 값을 가질 수 없고 고유해야 한다는 원칙입니다.
이렇게 3개가 가장 많이 사용되는 기본적인 무결성 유형입니다.
JOIN은 두 개 이상의 테이블을 결합해서 하나의 결과로 표현하는 SQL 연산입니다.
다양한 JOIN 유형이 존재하고 상황에 따라 다르게 사용할 수 있습니다.
INNER JOIN은 두 테이블의 공통된 데이터만 포함합니다. 즉, 두 테이블에서 조건에 맞는 행만 결과에 포함됩니다.
OUTER JOIN은 한 개의 테이블에만 데이터가 있어도 결과가 나옵니다. OUTER JOIN에는 LEFT, RIGHT, FULL OUTER JOIN이 존재합니다.
LEFT OUTER JOIN은 오른쪽 테이블에서 일치하는 값을 왼쪽 테이블에 포함시켜서 결과로 출력합니다. 이때 일치하는 값이 없으면 NULL로 표현됩니다.
RIGHT OUTER JOIN은 왼쪽 테이블에서 일치하는 값을 오른쪽 테이블에 포함시켜서 결과로 출력합니다. 이때 일치하는 값이 없으면 NULL로 표현됩니다.
FULL OUTER JOIN은 두 테이블의 모든 행을 포함시키면서 각 테이블에서 일차하는 값이 없으면 NULL로 표현됩니다.
NoSQL은 Not Only SQL
의 약자로, RDBMS와 달리 테이블 간 관계를 정의하지 않습니다.
주로 비정형 데이터 및 대규모 데이터 처리에 많이 사용됩니다.
데이터의 양이 증가할 때, 서버의 성능을 높이지 않고, 여러 서버를 추가해서 확장할 수 있습니다.
그리고 데이터를 여러 서버에 분산해서 저장해서 가용성과 성능을 높입니다. 즉, 하나의 서버에 문제가 생겨도 다른 서버에서 데이터에 접근이 가능합니다.
반면에 RDBMS는 테이블 간의 관계를 통해 테이블 간의 복잡한 관계 처리를 할 때 사용됩니다.
주로 정확성과 정형화된 데이터 관리에 강점을 가지고 있습니다.
RDBMS는 주로 하나의 서버를 사용하기에 데이터의 양이 증가하면 서버의 성능을 높이는 방식이기 때문에 한계가 있습니다.
그래서 서버가 고장나면 데이터에 접근하지 못할 수 있습니다.
트랜잭션은 데이터베이스에서 일련의 작업을 하나의 단위로 묶어서 처리하는 것을 의미합니다.
이렇게 묶어서 처리하는 이유는 데이터의 일관성과 무결성을 유지하기 위해서 이러한 작업들을 모두 성공시키거나, 모두 취소되어야 합니다.
트랜잭션의 성질 (ACID 원칙)
Lock은 트랜잭션 처리의 순차성을 보장하기 위한 기법입니다.
그리고 트랜잭션을 사용할 때 주의할 점이 있습니다.
프로세스는 실행중인 프로그램을 의미합니다.
쓰레드는 프로세스 내에서 실행되는 작업의 단위입니다.
프로세스는 각자 독립적인 메모리 공간을 가지고, 쓰레드는 같은 프로세스 내에서 실행되기에 메모리 공간을 공유합니다.
멀티프로세스는 여러 개의 프로세스를 동시에 실행하는 방식을 의미합니다.
각 프로세스는 독립적인 메모리 공간을 가지기 때문에 안정성과 성능을 제공합니다.
하지만 독립적이기에 자원의 소모가 크고 프로세스 끼리는 IPC라는 복잡한 방법을 통해 통신해야 합니다.
멀티쓰레드는 하나의 프로세스 내에서 여러 개의 쓰레드를 동시에 실행시키는 방식을 의미합니다.
모든 쓰레드가 같은 메모리 공간을 공유해서 빠른 데이터 교환을 통해 효율성은 높습니다.
하지만 하나의 쓰레드에서 오류가 발생하면 같은 프로세스 내의 다른 쓰레드에도 영향을 미칠 수 있습니다.
동기는 작업이 순차적으로 진행되는 것을 의미합니다.
즉, 하나의 작업이 완료되어야만 다음 작업이 시작될 수 있습니다.
동기 방식은 직관적이고 간단하지만, 시간이 오래 걸리는 문제가 발생할 수 있습니다.
비동기는 작업이 독립적으로 진행되는 것을 의미합니다.
즉, 하나의 작업이 완료되지 않아도 다음 작업을 시작할 수 있습니다.
비동기 방식은 응답성을 높이고 병렬 처리를 가능하게 하지만, 구현이 복잡해질 수 있습니다.
Deadlock은 교착 상태를 의미합니다.
교착 상태는 두 개 이상의 프로세스가 서로 자원을 점유한 상태에서 상대방이 점유하고 있는 자원을 기다리며 무한히 대기하는 상태를 의미합니다.
즉, 어떤 프로세스도 다음 과정으로 진행할 수 없는 상황을 말합니다.
교착 상태가 발생하기 위해서는 다음과 같은 조건을 모두 충족하는 경우 발생합니다.
이러한 교착 상태를 해결하기 위해서는 다음과 같은 방법이 있습니다.
TCP와 UDP는 전송 계층 프로토콜입니다.
TCP는 연결 지향 프로토콜로 신뢰성이 높고 패킷 구조가 복잡하며, 오류 검출과 재전송 기능이 있습니다.
UDP는 비연결 지향 프로토콜로 속도가 빠르지만 신뢰성으며, 패킷 구조가 간단합니다.
3-way handshake는 TCP 연결 설정 과정입니다.
클라이언트가 요청 신호(SYN)를 보내면 서버가 응답(SYN-ACK)한 뒤, 클라이언트가 잘 받았다는 의미로 응답(ACK) 신호를 보내면서 연결이 완료됩니다.
사용자가 브라우저 주소창에 URL을 입력합니다.
브라우저는 입력된 도메인 이름을 DNS에 요청해서 해당하는 IP주소를 가져옵니다.
얻은 IP 주소를 사용해서 웹 서버와의 연결을 위해 TCP 연결 설정을 합니다.
HTTP 프로토콜을 사용해서 서버에 요청을 합니다.
웹 서버는 요청을 처리한 후, 요청된 리소스를 포함한 HTTP 응답을 브라우저로 전송합니다.
수신한 HTML 문서를 해석하고 사용자에게 보여지는 페이지를 렌더링합니다.
OSI 7계층
리피터, 허브, 브릿지, 라우터
L2, L3, L4, L7 스위치
REST API는 HTTP Method를 통해 자원에 대한 CRUD를 적용하고 주소를 정하는 방법을 의미합니다.
즉, RESTFul API는 REST API 주소 체계를 이용하는 시스템을 의미합니다.
실제로 완벽한 RESTFul API는 구현하기 어렵기 때문에, 최대한 깔끔하고 이해하기 쉬운 주소 체계를 만드는 것이 중요합니다.
웹에서 데이터를 주고 받는 서버-클라이언트 모델의 프로토콜을 의미합니다.
즉, 웹 브라우저가 서버와 통신하는 규칙을 말합니다.
서버-클라이언트 모델은 클라이언트가 서버에 요청을 보내면, 서버는 요청에 대한 응답을 반환하는 형태를 의미합니다.
그리고 HTTP 프로토콜에는 무상태(stateless)라는 특징이 있는데, 각 요청은 독립적이고 서버는 이전 요청의 상태를 기억하지 않는다는 특징이 있습니다.
HTTP는 데이터를 암호화하지 않고 전송하지만 HTTPS는 SSL/TLS 프로토콜을 사용해서 데이터를 암호화합니다.
HTTP에서는 클라이언트와 서버간의 신회성을 보장하지 않지만, HTTPS는 SSL/TLS 인증서를 통해서 서버의 신원을 인증합니다.
그리고 HTTPS는 검색엔진에서 HTTP보다 더 높은 순위를 부여받는 경우가 많습니다.
GET : 서버 자원을 가져오려고 할 때 사용합니다.
POST : 서버에 자원을 새로 등록하고자 할 때, 또는 뭘 써야할지 애매할 때 사용합니다.
PUT : 서버의 자원의 전체 내용을 수정하려고 할 때 사용합니다.
PATCH : 서버의 자원의 일부만 수정하려고 할 때 사용합니다.
DELETE : 서버의 자원을 삭제하려고 할 때 사용합니다.
GET은 데이터 조회에 적합하고, POST는 데이터 전송 및 생성에 적합합니다.
둘 다 주로 사용자의 정보를 저장하고 관리하는 데 사용되는 기술입니다.
Cookie
Session
좋은 코드에 대한 저의 생각은 간단하게 2가지로 말할 수 있을 것 같습니다.
첫째, 중복이 최소화 되어야 합니다. 중복이 되는 부분은 함수화, 모듈화를 통해 줄여야 합니다.
둘째, 다른 사람들이 봐도 이해하기 쉬워야 합니다. 변수명, 함수명, 코드 구조 등을 나만 알아볼 수 있으면 안됩니다.
셋째, 두 번째와 이어지긴 하지만 핵심 기능에 대해서는 간단한 주석이 필요하다고 생각합니다.
객체 지향
객체지향은 절차를 간소화하는 것이지, 결코 절차를 무시하는 것이 아닙니다.
데이터와 이를 처리하기 위한 기능들을 모듈 내부에 체계적으로 작성합니다.
자료구조와 이를 중심으로 한 모듈들을 먼저 설계한 다음에 이들의 실행 순서와 흐름을 짜는 방식입니다.
데이터의 무결성을 보장하고 코드의 재사용성이 높습니다.
절차 지향
절차지향은 전통적인 프로그래밍 방식입니다.
데이터와 이를 처리하기 위한 기능들을 별도로 분리해서 작성합니다.
프로그램의 순서와 흐름을 먼저 세우고 필요한 자료구조와 함수를 설계하는 방식입니다.
프로젝트의 규모가 커질수록 구성 요소들 간의 결합도가 높아지고 데이터의 무결성을 보장하기 어렵습니다.
객체 지향 프로그래밍
객체지향 프로그래밍은 데이터와 그 데이터를 처리하는 메서드가 같은 모듈 내부에 배치되는 프로그래밍 방식입니다.
코드의 좋은 가독성, 높은 재사용성, 쉬운 유지보수를 위해 사용합니다.
객체지향 프로그래밍 방식은 데이터와 기능이 밀접하게 연결되어 있기 때문에, 코드의 구조와 동작을 직관적으로 파악할 수 있습니다.
만약 어떤 문제가 발생하면 빠르게 문제를 인지하고, 어떤 코드에서 오류가 발생했는지 빠르게 찾아보며, 오류를 빠르게 고쳐 개발하는 시간을 최대한으로 줄이는 것을 위해 사용합니다.
함수형 프로그래밍은 프로그램을 함수의 조합으로 구성하여 문제를 해결하는 방식입니다.
외부 상태를 변경하지 않는 부수 효과가 없어야 합니다.
항상 동일한 입력에 대해서 동일한 결과를 반환해야 합니다.
여기서 함수는 1급 객체로 취급되어 변수에 담거나 매개변수로 사용할 수 있습니다.
여러 개발자가 동시에 작업할 경우 코드 병합 중에 버전 충돌이 발생할 수 있습니다.
그래서 협업이 비효율적으로 진행되어 생산성이 떨어질 수 있습니다.
코드 변경 이력을 제대로 관리하지 못하면, 변경 사항 추적이 어려워 문제가 발생했을 때 원인 파악이 어렵습니다.
이러한 문제들은 프로젝트의 성공에 부정적인 영향을 미칠 수 있습니다.
TDD(테스트 주도 개발, Test-Driven Development)는 소프트웨어 개발 방법론 중 하나로, 코드를 작성하기 전에 먼저 테스트 케이스를 작성하는 방식입니다.
테스트 코드를 통해 예상대로 동작하지 않는 코드를 빠르게 찾을 수 있습니다.
코드 변경 시, 변경 후의 코드도 정상적으로 동작하는지 확인할 수 있습니다.
결과적으로 TDD는 높은 코드 품질, 버그 조기 발견, 유지보수 용이라는 장점이 있습니다.
CI 지속적인 통합, CD 지속적인 배포라는 의미로, 애플리케이션 개발 단계를 자동화하여 애플리케이션을 보다 짧은 주기로 고객에게 제공하는 방법입니다.
CI/CD의 예로는 Gtihub Actions, Jenkins 등의 도구가 있습니다.
CI/CD를 사용할 때의 장점
MVC 패턴은 소프트웨어 디자인 패턴으로, 애플리케이션을 모델(Model), 뷰(View), 컨트롤러(Controller)의 세 가지 구성 요소로 나누어 설계하는 방법입니다.
모델은 어떻게 데이터가 변경되고 조작되는지에 대한 애플리케이션의 비즈니스 로직을 관리하고
뷰는 프론트엔드와 같은 사용자 인터페이스를 담당하며,
컨트롤러는 모델을 통해 받은 데이터를 처리하거나, 결과 값을 뷰에 반환합니다.
HTTP는 단순 요청-응답 방식이지만 HTTP2는 다중 요청을 동시에 처리 가능합니다.
HTTP는 요청-응답이 반복되면서 헤더의 크기가 커질 수 있지만, HTTP2는 HPACK이라는 헤더 압축 기술을 사용해서 헤더 크기를 줄여서 전송이 가능합니다.
HTTP는 기본적으로 보안 기능이 없지만, HTTP2는 TLS를 통해서 보안을 강화합니다.
Apache
Nginx
이번 프로젝트에서 CI/CD 배포 역할을 맡았습니다.
CI/CD 배포는 가장 많이 사용되는 방법 중 하나인 Github Actions을 이용했습니다.
이번 프로젝트의 배포 서버는 AWS EC2 인스턴스를 사용했습니다.
CI/CD가 진행되던 도중 Github Actions에서 CD 과정이 Timeout이 발생했다고 에러가 떴습니다.
확인해보니 EC2 환경에서 실행시킬 프로젝트 서버를 준비하는 과정에서 CPU가 100%로 치솟는 문제가 발생해서 모든 작업들이 멈추고 EC2 인스턴스도 먹통이 되었습니다.
조금 더 정확한 원인을 찾아보니 EC2 SSH 접속을 도와주는 appleboy/ssh-action을 다운로드 하는 과정에서 CPU 점유율이 100%로 치솟고 타임 아웃이 발생해서 EC2 인스턴스 자체가 멈춰버렸습니다.
그래서 appleboy/ssh-action를 사용하는 방법이 아니라 직접 EC2에서 SSH 접속을 위한 설정을 명령어를 CD 파일에서 실행시켜서 SSH에 접속하는 방법을 사용했습니다.
사실 이번 프로젝트에서 구체적인 보안까지 신경쓰기 정말 어려웠습니다.
그래도 소셜 로그인을 구현하면서 보안적인 문제를 보완해서 구현했습니다.
기존에는 카카오 로그인 백엔드 API를 호출하면 생성한 토큰을 URL에 넣어서 리다이렉트 진행했습니다.
이러한 방식은 사용자의 정보가 들어 있는 토큰이 노출된 상태로 클라이언트에게 전달되는 문제가 있습니다.
그래서 고안한 방법이 의미없는 코드를 만들어서 해당 코드를 키로 하는 사용자 ID를 레디스에 저장하는 방법입니다.
이 방법을 사용하면 클라이언트는 URL 상에서 해당 코드만 확인이 가능하기 때문에 정확한 사용자 정보를 확인할 수 없습니다.
그리고 클라이언트가 필요로 하는 토큰은 직접적으로 노출된 형태가 아니라 Body에 담겨서 클라이언트에게 전달되기 때문에 보안적으로 다소 향상되었다고 볼 수 있습니다.
ALB를 통해 대용량 트래픽 처리에 대한 테스트를 진행했습니다.
참고로 EC2 인스턴스가 프리티어 t2.micro를 사용했기 때문에 10초동안 5~6000개 정도의 트래픽으로 테스트 진행했습니다.
실제로 EC2 인스턴스를 늘리면 대용량 트래픽을 분산해서 처리할 수 있을까를 확인하기 위해서 진행했습니다.
하지만 인스턴스를 3개까지 늘리면서 진행했지만, 생각처럼 유의미한 결과가 나오지 않았고 Timeout을 40초까지 설정해야 정상적으로 에러없이 트래픽을 수용했습니다.
이는 사용자가 페이지 로딩까지 30~40까지 기다려한다는 것을 의미하기 때문에 실패한 테스트라고 생각합니다.
그래서 Auto Scaling을 통해서 트래픽 요청 수에 따라서 인스턴스를 자동으로 증가시켜서 클라이언트가 훨씬 원활한 요청을 할 수 있도록 이어서 진행했습니다.
똑같은 환경으로 테스트한 결과 Auto Scaling은 인스턴스를 4개까지 생성했고 이를 통해 사용자가 기다려야 하는 최대 시간이 15~20초로 절반정도 줄어들었습니다.
이 때는 최대 인스턴스를 4개까지라고 설정했지만 5개 이상으로 설정하면 사용자에게 더 나은 환경을 제공할 수 있다는 생각을 했습니다.
저는 이러한 테스트들을 통해서 대용량 트래픽 처리에 있어 인스턴스 관리와 클라이언트 간의 밀접한 관계를 이해하는 데 큰 도움이 되었던것 같습니다.
사실 이러한 인스턴스 서버와 사용자 트래픽에 대해서 간단하게라도 경험해볼 기회가 없었기 때문에 더욱 좋은 경험이었던 것 같습니다.
그리고 Auto Scaling과 같은 시스템의 확장성과 유연성을 높이는 방법을 고민하는 시간을 가져서 좋았습니다.