Kotlin은 기본적으로 Call by Value이다. 이는 Int, Boolean 같은 Jvm이 primitives 타입을 사용하는 클래스에 해당한다. 반면에, 우리가 직접 생성하는 class를 사용할 때는 주의해야 한다. Kotlin은 이를 Call By Value of Reference 형태로 가져오며, 이는 variable인 class 내 프로퍼티를 변경을 가능하게 한다. 하지만 새로 할당하는 것은 불가능하다.
data class Person(var name: String)
fun changeName(p: Person) {
p.name = "Alice"
}
fun newPerson(p: Person) {
p = Person("Alice")
}
fun main() {
val person = Person("Bob")
changeName(person)
println(person.name) // Alice
val person2 = Person("Jake")
newPerson(person2)
println(person2.name) // Jake
}
따라서, 불변성을 유지하기 위해서는 class 내 프로퍼티를 value로 정의하고, 변경 시 객체 자체를 변경하는 것이 좋다.
추상 클래스와 인터페이스 둘 다 추상화 및 상속을 구현할 수 있다. 추상 클래스는 기본적인 기능을 미리 정의해놓을 수 있다. 즉, 구현부와 추상부가 동시에 존재하고 이를 활용하려면 상속하여 추상부를 구체화 해주어야 한다. 반면에 인터페이스는 추상부만 존재하며 구현부가 없다. 따라서, 둘의 차이는 기본 기능 즉, 구현체의 유무이며 추상 클래스는 프로퍼티, 생성자도 가질 수 있다는 것이 큰 차이이다.
RAM (Random Access Memory)
: 전원이 꺼지면 데이터가 지워지는 휘발성이며, 현재 실행 중인 프로그램이나 데이터를 임시로 저장하는 주된 메모리이다.
ROM (Read Only Memory)
: 전원이 꺼져도 데이터가 삭제되지 않으며, 주로 컴퓨터의 부팅 프로그램 등 하드웨어 초기화에 필요한 중요한 데이터를 저장하고 수정이 어렵다.
Flash Memory
: 비휘발성 메모리이며, 필요에 따라 데이터를 쓰고 지울 수 있다.
레지스터
: CPU 내부에 있는 가장 빠른 메모리로, 연산에 필요한 데이터를 일시적으로 저장하여 성능을 높인다.
캐시 메모리
: CPU와 주기억 장치 사이에 위치한 SRAM이며, 둘의 성능 차이를 보완하여 성능을 높인다.
프로세스는 실행 중인 프로그램의 객체이며, 독립된 메모리 영역과 높은 비용을 요구한다. 쓰레드는 프로세스 내에서 실행되는 최소 단위로 공유 메모리로 쓰레드 간 통신이 쉬우며 비교적 적은 성능을 요구한다. 하지만 상호 공유가 쉬다는 것과 동시에 한 쓰레드에서의 오류가 전체에 치명적일 수도 있다는 주의점도 있다. 메모리 영역에서 살펴보면 쓰레드 간에는 Code/Data/Heap 영역을 공유하고 별도의 Stack을 가진다.
프로세스는 서로 독립된 메모리를 갖기 때문에 서로 데이터를 주고 받을 수 없다. 따라서 OS가 제공하는 IPC 메커니즘을 이용해야 한다. 해당 메커니즘은 다음과 같은 것들이 있다.
프로세스나 스레드의 상태를 의미하는 컨텍스트를 잠시 멈추고 다른 컨텍스트로 전환하는 것을 뜻한다. 여기서 상태는 CPU, 메모리의 레지스터, PC, 스택 포인터 등을 의미한다. 따라서, 스위칭한 뒤엔 레지스터 저장/복원, 캐시 미스 증가, 커널 모드 전환 등 상태를 복원하는 과정이 필요하기 때문에 비용이 발생한다.
커널이란?
운영체제 중 항상 메모리에 올라가 있는 운영체제의 핵심 부분으로써 하드웨어와 응용 프로그램 사이에서 인터페이스를 제공하는 역할을 하며 컴퓨터 자원들을 관리하는 역할을 한다. 즉, 하드웨어를 제어하기 위해 응용 프로그램에 제공되는 인터페이스이다.
보통 OS의 커널에 의해서 수행되며 주어진 Time Slice 소진, 리소스 요구, 인터럽트 등에 의해 발생한다. 종류는 프로세스 to 프로세스(P2P)와 쓰레드 to 쓰레드(T2T) 두 가지가 있다. 둘 모두 커널 모드에서 실행되고 CPU의 레지스터 상태를 교체한다. 이 때, 쓰레드는 네가지 메모리 영역 중 STACK만 독립적으로 갖기 때문에 이 곳만 상태를 복구하면 된다. 그 말은 즉, P2P의 경우엔 MMU와 TLB도 관리해야 한다는 뜻이다. 또한 다른 작업을 하다 돌아온 것이기 때문에 캐시 메모리가 오염되어있을 가능성이 있다.
데드락은 두 개 이상의 프로세스(또는 스레드)가 서로 자원을 점유한 채, 상대방의 자원을 기다리며 무한 대기 상태에 빠지는 현상이다. 상호 배제, 점유 대기, 비선점, 순환 대기가 조건이며, 네 가지 중 하나라도 깨뜨리면 예방이 가능하다.
임계 영역은 두 개 이상의 스레드가 동시에 접근하면 문제가 생기는 코드 영역을 뜻하며, 읽기 과정에서 다른 스레드가 끼어들면 예상치 못한 값을 얻을 수 있다.
스케줄러는 CPU, IO 등 시스템 자원을 어떤 프로세스(또는 스레드)에 언제 할당할지 결정하는 OS 구성 요소이다. 운영체제 관점에서 크게 3가지 스케줄러가 존재하다.
장기 스케줄러: 프로세스 생성 시 메모리에 진입할 프로세스를 결정한다.중기 스케줄러: 메모리에서 프로세스를 일시 중지하거나 재개한다.단기 스케줄러: CPU를 어떤 프로세스에 할당한지 결정한다.단기 스케줄러가 CPU 프로세스를 선택하는 방법은 두 가지가 있다.
Android는 Linux 커널에서 Completely Fair Scheduler (CFS)
1️⃣ 프로세스 준비
: 실행 가능한 프로세스들은 모두 RB-Tree에 삽입되고 각자 vruntime 값을 가집니다.
2️⃣ 스케줄링 결정
: 가장 낮은 vruntime를 갖는 작업 실행
3️⃣ CPU 실행 및 vruntime 업데이트
: 실행 시간만큼 vruntime이 증가, CPU 사용량이 많으면 vruntime이 빨리 증가 → 다음에 덜 실행됨
운영체제가 메모리를 일정한 크기의 블록 단위로 관리하는 기법으로 프로세스의 논리적 주소와 물리적 메모리를 페이지와 프레임으로 나누어 관리한다. 이를 통해 비연속적인 메모리 공간에 프로레스를 배치하고 단편화를 문제를 해결할 수 있다.
| 구분 | 의미 | 예시 |
|---|---|---|
| 페이지(Page) | 프로세스의 논리 메모리를 일정 크기로 나눈 단위 | 프로세스 코드/데이터 일부 |
| 프레임(Frame) | 실제 물리 메모리(RAM)를 동일 크기로 나눈 단위 | 실제 RAM 내의 블록 |
| 페이지 테이블(Page Table) | 논리 주소와 물리 주소 간의 매핑 정보를 관리 | Page 0 → Frame 5, Page 1 → Frame 3 등 |
TLB(Translation Lookaside Buffer)란?
페이징을 하더라도 Page Table에 접근, 물리 주소 접근 총 두 번의 접근이 필요하다. TLB는 Page Table 접근 전에 위치하여, 해당 페이지 정보가 TLB에 존재하면 바로 물리적 주소를 제공한다.
만약, TLB 내에 없다면 Page Table 접근한다. Page Table 엔트리(PTE)가 invalid라면 Page Fault가 발생하고 페이지를 적재한 뒤 Page Table를 갱신한다. valid라면 TLB를 바로 갱신한다.
https://velog.io/@jaeyunn_15/OS-TLB-MMU
프로세스의 실제 물리 메모리(RAM) 크기와 상관없이, 자신만의 연속된 메모리 공간을 사용하는 것처럼 보이게 하는 기술이다. 따라서 페이징이 이를 구현하기 위한 핵심 기술이다.
다음과 같은 원리로 작동한다.
MMU(Memory Management Unit)란?
CPU와 메모리 사이에 위치한 하드웨어로, CPU가 발생시키는 가상(논리) 주소를 물리 주소로 변환하는 역할을 한다. 페이징이나 세그먼테이션과 같은 메모리 관리 기법을 하드웨어 차원에서 지원하며, 이 과정에서 TLB, 페이지 테이블(또는 세그먼트 테이블) 을 사용해 빨른 주소 변환을 지원한다. 따라서 가상 메모리 시스템을 구현하기 위한 핵심 장치이다.
위 과정에서 Page Fault가 발생하면 운영체제가 디스크에서 페이지를 읽어 메모리에 적재한다. 이것을 교체(Replacement)라고 교체 기준을 정하는 알고리즘도 있다.
| 자료구조 | 구조 | 접근 방식 | 삽입/삭제 | 시간 복잡도 |
|---|---|---|---|---|
| 큐(Queue) | 선형 | FIFO | 삽입: 뒤(enqueue), 삭제: 앞(dequeue) | O(1) 삽입/삭제(LinkedList 기준) |
| 스택(Stack) | 선형 | LIFO | 삽입: 뒤(push), 삭제: 뒤(pop) | O(1) 삽입/삭제 |
| 트리(Tree) | 계층 구조 | 부모-자식 | 삽입/삭제 위치 규칙 있음 | O(log n) ~ O(n) (트리 종류에 따라 다름) |
| 힙(Heap) | 완전 이진 트리 | 부모-자식 관계 유지 | 삽입/삭제: 부모-자식 비교 후 재정렬 | O(log n) 삽입/삭제, O(1) 최댓값/최솟값 접근 |
키(Key)를 해시 함수(Hash Function)에 넣어, 저장 위치(Index)를 계산하고 값을 저장/검색하는 자료구조로, 평균적으로 탐색, 삽입, 삭제 모두 O(1)의 시간 복잡도를 가진다.
해시 함수는 매우 큰 범위를 가지지만 충돌 가능성은 여전히 존재한다. 따라서, 충돌을 미리 방지하기 위해 두 가지 방법이 있다. 체이닝(Chaining)는 같은 Index에 연결 리스트를 추가하여 여러 값을 저장하는 방법이다. 구현이 간단하고 크기가 유연하지만 리스트 내부를 모두 순환해야할 수 있다. 오픈 주소법(Open Addressing)은 충돌 시 다른 빈 슬롯을 찾아 저장하는 방법으로, 메모리를 연속적으로 사용하고 캐시 효율이 좋지만 클러스터링(특정 범위 집중 현상)이 발생할 수 있다.
| 알고리즘 | 정의/설명 | 시간 복잡도 (평균/최악) | 특징 |
|---|---|---|---|
| 버블 정렬(Bubble Sort) | 인접한 두 원소를 비교해 자리 교환 | O(n²) / O(n²) | 구현 간단, 안정 정렬 |
| 선택 정렬(Selection Sort) | 최솟값/최댓값을 찾아 순서대로 위치 교환 | O(n²) / O(n²) | 비교적 느림, 불안정 |
| 삽입 정렬(Insertion Sort) | 현재 원소를 정렬된 부분에 삽입 | O(n²) / O(n²) | 소규모/거의 정렬된 데이터에 효율적, 안정 |
| 퀵 정렬(Quick Sort) | Pivot 기준으로 분할 후 재귀 정렬 | O(n log n) / O(n²) | 평균 빠름, 불안정 |
| 병합 정렬(Merge Sort) | 리스트를 반씩 나누어 정렬 후 병합 | O(n log n) / O(n log n) | 안정, 추가 메모리 필요 |
| 힙 정렬(Heap Sort) | 힙 자료구조를 이용해 최댓값/최솟값 순서대로 추출 | O(n log n) / O(n log n) | 불안정, 공간 효율 좋음 |
| 계수 정렬(Counting Sort) | 정수 키 범위를 카운트하여 정렬 | O(n+k) / O(n+k) | 안정, 키 범위 제한 |
| 기수 정렬(Radix Sort) | 자리수 단위로 정렬 후 병합 | O(nk) / O(nk) | 안정, 자리수 기반 |
| 버킷 정렬(Bucket Sort) | 데이터를 균등 분할 후 각 버킷 정렬 | O(n+k) / O(n²) | 평균적 효율 좋음 |
큰 문제를 작은 부분 문제로 나누어 해결하고, 그 결과를 재사용하여 전체 문제를 효율적으로 해결하는 알고리즘 설계 기법이다. 특히, 재사용하는 방법에는 Top-Down 방식인 Memoization과 Bottom-Up 방식인 Tabulation이 있다.
HTTP 1.0과 1.1로 나누어 살펴보겠다. 공통적으론 다음 특징을 갖는다.
1.0에서는 한 번의 요청과 한 번의 응답 이후 연결을 해제하는 비연결성 특징을 가졌다. 하지만 연속적인 통신에선 3-way handshake에 대한 오버로드가 커져 오히려 비효율적이였다. 이를 해결하기 위해 HTTP 1.1 버전은 다음과 같은 차이를 갖는다.
TCP는 연결 지향으로 신뢰성 있는 데이터 전송를 목표로 한다. 따라서, 3-way Handshake로 연결 수립, 데이터 순서 보장 → Segment 번호 기반 재조립, 오류 검출 및 재전송, 흐름 제어와 혼잡 제어 제공 등의 기능을 제공한다.
UDP는 비연결(Connectionless)으로 신뢰성 없는 데이터 전송을 수행한다. 간단하고 빠르며 헤더 오버헤드 적다. 하지만 전송된 데이터의 순서를 보장하지 않고 손실 가능성도 있다. 따라서, 실시간 전송, 스트리밍, 게임 등에서 유리한 통신 프로토콜이다.
TCP/IP프로토콜을 이용해서 통신을 하는 응용프로그램이 데이터를 전송하기 전에 먼저 정확한 전송을 보장하기 위해 상대방 컴퓨터와 사전에 세션을 수립하는 과정을 의미한다. 다음과 같은 세 과정을 거친다.
세션을 종료하기 위해 수행되는 절차이다.
REST 아키텍처 원칙을 따르는 웹 API로, 자원과 HTTP 메서드를 경로로 표현하여 쉽게 사용할 수 있다. 또한, 무상태(Stateless) 구조, 일관된 표현 방식(JSON, XML)을 특징으로 하며, 서버는 클라이언트 요청에 대해 상태 코드(2xx, 4xx, 5xx)로 결과를 알린다.