HTTP가 RFC를 엄격히 준수하는 이유
HTTP가 RFC(Requests for Comments)라는 엄격한 표준을 따르는 이유는 상호운용성(interoperability) 때문이다.
파편화 방지
만약 구글 크롬이 A라는 방식으로 요청을 보내고, 네이버 서버는 B라는 방식만 이해한다면 웹은 동작하지 않는다.
전 세계 수억 개의 서버와 클라이언트가 문제 없이 통신하려면 공용어(Standard)가 필수적이다.
중간 매개체의 존재
사용자가 웹사이트에 접속할 때 브라우저와 서버만 있는 게 아니다.
그 사이에는 수많은 프록시(Proxy), 캐시 서버, CDN, 방화벽 등이 존재한다.
이 모든 장비가 RFC 표준을 따르고 있어야만 데이터가 중간에 왜곡되지 않고 전달된다.
그 외 사유
| 구분 | 내용 |
| 확장성 | HTTP/1.1에서 HTTP/2, HTTP/3로 발전하면서도 하위 호환성을 유지하기 위해 엄격한 규칙을 유지했다. |
| 보안 | RFC에는 보안 취약점을 방지하기 위한 헤더 처리 방식이 명시되어 있다. 이를 무시하면 해킹 (예: HTTP Request Smuggling)에 노출될 수 있다. |
| 예측 가능성 | 개발자가 웹 애플리케이션을 만들 때, 서버가 어떤 상태 코드(예: 200 OK, 404 NOT FOUND)를 보낼지 예측할 수 있어야 한다. |
equals() & hashcode() -- Java vs. Kotlin
Java의 클래스 생성
Java에서 클래스를 만들 때 반드시 equals()와 hashcode()를 구현해야만 컴파일이 되는 것은 아니다.
구현하지 않으면 부모인 Object 클래스의 기본 메서드(객체의 메모리 주소 비교)를 사용하게 된다.
하지만 DTO(Data Transfer Object)처럼 객체 안의 '데이터'가 같은지를 비교해야 하거나, HashMap, HashSet 같은 Collection에 담으려면 반드시 이 두 메서드를 쌍으로 재정의해야 한다.
그렇지 않으면 데이터는 같은데 주소가 다르다는 이유로 '다른 객체'로 취급되는 참사가 벌어진다.
최신 Java(Java 14 이상) 부터 record 키워드를 도입하여 Kotlin처럼 자동으로 이 메서드들을 생성해 준다.
// 1. 전통적인 방식 (IDE의 도움 없이는 고통스러움)
public class JavaUser {
private final String name;
public JavaUser(String name) { this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.size()) return false;
JavaUser user = (JavaUser) o;
return Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
// 2. 현대적인 방식 (Java 14+)
public record JavaRecordUser(String name) { }
// 단 한 줄로 equals, hashCode, getter 자동 생성!
Kotlin의 클래스 ('data class') 생성
Kotlin은 반드시 data class로 선언해야만 컴파일러가 equals(), hashCode(), toString(), copy() 등을 자동으로 생성한다.
// 1. 일반 클래스: equals/hashCode 자동 생성 안 됨 (주소 비교)
class Person(val name: String)
// 2. 데이터 클래스: equals/hashCode 자동 생성 됨 (값 비교)
data class DataPerson(val name: String)
fun main() {
val p1 = Person("Gemini")
val p2 = Person("Gemini")
println(p1 == p2) // false (주소가 다름)
val d1 = DataPerson("Gemini")
val d2 = DataPerson("Gemini")
println(d1 == d2) // true (안의 데이터가 같음)
}
Kotlin coroutine : "가벼운 쓰레드"
Coroutine을 흔히 "경량 쓰레드(Lightweight thread)"라고 부르지만, 엄밀히 말하면 "비선점형 다중 작업(Non-preemptive Multitasking)을 위한 상태 제어 장치"에 가깝다.
OS가 관리하는 쓰레드는 시분할(Time-sharing)로 강제 전환(Preemptive)되지만, 코루틴은 코드가 스스로 "나 여기서 잠시 쉴게(suspend)"라고 양보해야만 제어권이 넘어간다.
Kotlin 키워드 : suspend
// Kotlin: 비차단 방식
suspend fun loadUserDetail() = withContext(Dispatchers.IO) {
// 여기서 64개 쓰레드 중 하나를 사용합니다.
val user = api.fetchUser()
user // 결과 반환 시 쓰레드는 즉시 풀로 반환됨
}
// Java (전통적 방식): 쓰레드 차단 방식
public User loadUserDetail() {
// 쓰레드가 응답을 받을 때까지 아무것도 못하고 기다립니다 (Blocking)
return api.fetchUser().execute().body();
}
비차단(Non-blocking)
suspend 키워드는 실행 중에 잠시 멈출 수 있고, 이때 자신을 실행하던 쓰레드를 놓아준다(yield).
이 덕분에 해당 쓰레드는 다른 작업에 사용할 수 있게 된다.
상태 머신 (State Machine)
Kotlin 컴파일러는 suspend 지점을 기준으로 코드를 분리하여 '상태'로 관리한다.
작업이 재개될 때 Continuation 객체를 통해 이전의 지역 변수와 실행 상태를 복원한다.
플랫폼별 코루틴 동작 양상
Kotlin은 "Multi-platform"을 지향하므로, coroutine이 동작하는 환경에 따라 물리적 구현이 다르다.
| 환경 | 역할 | 주요 특징 | 사용하는 Dispatcher |
| PC / Server (JVM) | 병렬성 극대화 | 수많은 쓰레드 풀을 사용해 대규모 요청을 효율적으로 처리한다. | Dispatchers.Default, IO |
| Android | UI 응답성 유지 | 메인 쓰레드를 멈추지 않고 비동기 작업 (네트워크, DB)을 처리한 뒤 결과를 UI에 반영한다. |
Dispatchers.Main (UI 전용) |
| Web Browser (JS) | callback loop 탈출 | JS 엔진은 단일 쓰레드이므로, 실제 병렬 실행보다 비동기 코드의 가독성을 높이는 데 집중한다. | Dispatchers.Main (Event Loop) |
Dispatchers.IO의 한계
Dispatchers.IO는 내부적으로 공유된 쓰레드 풀을 사용한다.
Kotlin의 coroutine 라이브러리는 기본적으로 이 풀의 최대 크기를 64개 또는 CPU 코어 수 중 더 큰 값으로 제한한다.
따라서, 만약 64개의 쓰레드가 모두 Thread.sleep()이나 복잡한 블로킹 I/O 작업으로 꽉 차 버린다면, 새로운 Dispatchers.IO coroutine은 가용한 쓰레드가 생길 때까지 대기하게 된다.
만약 대규모 서버 환경에서 더 많은 동시 I/O 작업이 필요하다면 아래와 같은 시스템 설정을 통해 제한을 늘릴 수 있다.
// 시스템 프로퍼티를 통해 기본값 수정 (애플리케이션 시작 시)
System.setProperty(IO_PARALLELISM_PROPERTY_NAME, "100")
// 또는 특정 작업에 대해서만 개별적으로 제한을 넓힌 디스패처 생성
val customIoDispatcher = Dispatchers.IO.limitedParallelism(100)
Kotlin의 DB 연결 방식 : JDBC (Java Database Connector)
Kotlin (JDBC)
├─ mybatis
├─ jooq < 거의 안씀
├─ exposed < kotlin 전용
├─ Spring Data JPA
└─ hibernate(JPA)
├─ Kotlin DSL
└─ Query DSL
RDBMS 유형
RDBMS
├─ MySQL (흔함)
├─ PostgreSQL (고급 기능)
├─ MSSQL server (대기업, 게임회사)
├─ Oracle (대기업, 금융계)
└─ DB2 (은행)
데이터베이스가 비싼 이유와 발전 서사
DB가 비쌀 수 밖에 없는 3가지 기술적 이유
1. ACID 원칙 준수를 위한 '자원 소모'
DB의 존재 이유는 데이터의 신뢰성이다.
- Atomicity (원자성)
- 정의 : 트랜잭션이 성공하거나 실패하거나 둘 중 하나여야 한다.
- 이를 위해 DB는 모든 변경사항을 WAL(Write-Ahead Loggin) 로그에 기록한다.
- 결과적으로 디스크를 두 번 사용하는 것이다.
- Isolation (고립성)
- 정의 : 여러 명이 동시에 데이터를 고칠 때 엉키지 않도록 '잠금(Lock)'을 건다.
- 이 과정에서 CPU는 누가 먼저 왔는지 계산하고 관리하느라 바빠진다.
2. CPU 및 메모리 집약적 연산
DB는 단순히 데이터를 읽고 쓰는 기계가 아니다.
- 인덱스(Index) 관리 : 데이터를 빨리 찾기 위해 B-Tree 같은 복잡한 자료구조를 메모리 위에 유지해야 한다.
- 쿼리 최적화 : SQL을 던지면 어떻게 자료를 가장 빨리 가져올지 경로를 계산하는데, 이때 CPU 연산량이 폭증한다.
3. 백업과 고가용성(HA) 비용
데이터는 사라지면 끝이다. 따라서 실시간 복제(Replication)와 스냅샷 백업이 필수인데, 이는 단순 저장 공간 비용 뿐만 아니라 데이터 전송 네트워크 비용과 이를 관리할 고성능 스토리지 비용을 수반한다.
데이터베이스의 역사 : 파일에서 클라우드까지
| 세대 | 명칭 | 특징 | 비고 |
| 1세대 | File System | .txt나 .dat 파일에 직접 기록 | 중복 데이터 및 무결성 문제 발생 |
| 2세대 | Data Language | dBASE, FoxPro 등 전용 언어 등장 | 데이터 구조화 시작, 로컬 중심 |
| 3세대 | RDBMS | Oracle, MySQL 등 관계형 DB의 시대 | SQL 표준화 및 데이터 무결성 보장 |
3-1세대(Thin DB) : 기능은 S/W, 저장만 DB
DB는 저장만 담당하고, 모든 복잡한 계산은 Java나 Python 같은 서버 코드(S/W)에서 처리한다. (초기 웹 개발 스타일)
3-2세대(Thick DB) : 기능도 DB에 구현
Stored Procedure나 Trigger를 써서 DB 내부에서 비즈니스 로직을 돌린다.
"DB가 제일 빠르니 DB에 다 넣자"던 90~2000년대 Enterprise 스타일이다.
3-3세대(Logic-less DB) : DB 기능 사용 안함
마이크로서비스(MSA)나 NoSQL이 부상하면서 "DB는 무거우면 안 된다"는 주의이다.
DB 기능을 최소화하고 확장이 쉬운 단순 저장소 형태로 사용한다.
3-4세대(Hybrid / Modern) : DB 기능 일부 사용
성능이 필요한 부분(예: JSON 처리, 지리 정보 연산)은 DB 기능을 적극 활용하고,
비즈니스 로직은 애플리케이션에 두는 형태다.
마치며 : 왜 3-3세대에서 3-4세대로 이동하는 이유
한때 DB 기능을 아예 안 쓰는 게(3-3세대) 유행이었지만, 최근에는 다시 적절히 활용하는 추세(3-4세대)다.
그 이유는 DB가 직접 하는 게 훨씬 빠르기 때문이다.
네트워크를 통해 데이터를 가져와 서버에서 계산하는 것보다, 데이터가 있는 곳(DB)에서 계산해 결과만 받는 게 비용 효율적이기 때문이다.
결국 DB 내용이 비싼 이유는 그만큼 우리(=사용자) 대신 어려운 일을 정확하고 빠르게 처리하기 때문이다.
Powered By. Gemini
'Backend' 카테고리의 다른 글
| [Backend] Kotlin 기반 HTTP 서버 구현 (요약) (1) | 2026.01.19 |
|---|---|
| [Backend] Kotlin 기반 HTTP 서버 구현 (상세) (0) | 2026.01.16 |
| [Backend] 고성능 시스템에서 전송 속도를 통제하는 방법 (0) | 2025.12.31 |
| [Backend] BitTorrent Protocol : 거대 파일을 조각 내어 공유·분산하기 (1) | 2025.12.29 |
| [Backend] TCP 스트림 기반 파일 전송 기능 구현 - CHAT/FILE multiplexing, 단일 Writer, backpressure 구현 기록 (1) | 2025.12.19 |