슈렉의 노트

슈렉

여러 학문 분야를 다룹니다.

컴퓨터는 뺄셈을 어떻게 덧셈으로 바꿀까? — 보수와 모듈러 원리

2의 보수를 처음 배울 때 보통 이렇게 외운다.

비트를 뒤집고 1을 더하면 음수가 된다.

틀린 말은 아니다. 하지만 이 문장만 외우면 원리가 잘 안 잡힌다. 왜 뒤집는지, 왜 1을 더하는지, 왜 그게 음수처럼 작동하는지 빠져 있기 때문이다.

핵심은 2진수 자체가 아니다. 먼저 봐야 할 것은 정해진 자릿수 안에서 숫자가 한 바퀴 돈다는 사실이다.

1. 먼저 자릿수의 크기를 정해야 한다

컴퓨터는 무한히 큰 정수를 항상 들고 다니지 않는다. 예를 들어 4비트 정수라면 사용할 수 있는 칸은 딱 4개다.

0000부터 1111까지

총 16개

4비트에서는 표현 가능한 패턴이 16개다. 가장 큰 값인 1111에 1을 더하면 수학적으로는 10000이 된다. 하지만 4비트 공간에는 5번째 칸이 없다.

그래서 앞의 1은 버려지고 다시 0000이 된다.

1111 + 1 = 10000 → 4비트만 남기면 0000

즉 4비트 세계는 0부터 15까지 갔다가 다시 0으로 돌아오는 구조다. 수학적으로 말하면 mod 16 세계다.

일반화하면 이렇다.

N진수 m자리에서
한 바퀴 크기 M = N^m
최대수 = M - 1

2진수 4자리라면 M = 2^4 = 16이고, 최대수는 15 = 1111₂이다.

2. 뺄셈은 뒤로 가는 것이다

이제 3 - 5를 생각해보자. 보통은 3에서 5를 빼니까 결과는 -2다.

그런데 4비트 세계는 16칸짜리 원처럼 돈다. 이 세계에서는 뒤로 5칸 가는 것과 앞으로 11칸 가는 것이 같은 위치에 도착한다.

-5와 +11은 mod 16에서 같은 이동이다.
왜냐하면 16 - 5 = 11 이기 때문이다.

그래서 컴퓨터는 3 - 5를 이렇게 바꿀 수 있다.

3 - 5
= 3 + (16 - 5)
= 3 + 11
= 14

여기서 14가 왜 -2냐고 물을 수 있다. 16칸짜리 세계에서 14는 0보다 두 칸 뒤에 있는 위치다. 그래서 signed two's complement 방식으로 해석하면 14-2가 된다.

4비트, 즉 16칸짜리 세계에서 3 - 5 보기
```
00000
10001
20010
3출발 A
40100
5B
60110
70111
81000
91001
101010
115의 보수
121100
131101
14결과 -2
151111
직접 계산: 3 - 5 = -2
보수 계산: 3 + 11 = 14
4비트 signed 해석: 14 = 1110₂ = -2

같은 위치에 도착한다. 그래서 뺄셈을 보수의 덧셈으로 바꿀 수 있다.

```

3. 보수는 “한 바퀴를 채우는 수”다

어떤 수 B를 빼고 싶다면, B를 직접 빼는 대신 B의 보수를 더한다. 여기서 보수는 B와 더했을 때 한 바퀴를 채우는 수다.

B의 보수 = M - B

4비트에서 한 바퀴는 16이다. 5를 빼고 싶다면 5의 보수는 다음과 같다.

16 - 5 = 11
11 = 1011₂

그래서 4비트에서 -5를 표현하는 비트패턴은 1011이다. 이 말은 1011 자체가 무조건 -5라는 뜻이 아니다. 같은 비트패턴도 해석 방식에 따라 값이 달라진다.

비트패턴 unsigned 해석 4비트 signed two's complement 해석
1011 11 -5

중요한 것은 비트패턴과 해석을 구분하는 것이다. 1011은 그냥 네 자리 비트패턴이다. 이것을 unsigned로 읽으면 11이고, signed two's complement로 읽기로 약속하면 -5다.

4. 왜 “뒤집고 +1”인가

이제 보수 공식으로 돌아가보자.

B의 보수 = M - B

그런데 M - B를 바로 계산하려면 한 자리 더 큰 수에서 빼야 한다. 4비트라면 10000₂ - B를 해야 한다. 대신 이 식을 이렇게 바꾼다.

M - B
= ((M - 1) - B) + 1

여기서 M - 1은 그 자릿수에서의 최대수다. 4비트 이진수에서는 1111₂이다.

5의 2의 보수를 구해보자.

B = 5 = 0101₂
M - 1 = 15 = 1111₂

``` (M - 1) - B
= 1111₂ - 0101₂
= 1010₂

마지막에 +1
1010₂ + 1 = 1011₂ ```

여기서 1111₂ - 0101₂ = 1010₂가 바로 “비트 뒤집기”다. 왜냐하면 이진수에서는 각 자리의 최대 숫자가 1이기 때문이다.

원래 자리 1에서 뺀 결과
0 1
1 0

그래서 이진수에서는 (M - 1) - B가 그냥 0↔1 반전처럼 보인다. 그리고 마지막에 1을 더하면 2의 보수가 된다.

2의 보수
= 비트 반전 + 1
= ((M - 1) - B) + 1
= M - B

5. 이진수만의 원리는 아니다

보수의 원리는 이진수 전용이 아니다. N진수 m자리라면 똑같이 적용된다.

예를 들어 5진수 2자리에서 생각해보자.

N = 5, m = 2
M = 5² = 25 = 100₅
최대수 M - 1 = 24₁₀ = 44₅

이제 21₅의 5의 보수를 구해보자.

B = 21₅
최대수 = 44₅

``` 44₅ - 21₅ = 23₅
23₅ + 1 = 24₅ ```

따라서 21₅의 5의 보수는 24₅다. 확인해보면 둘을 더했을 때 정확히 한 바퀴가 된다.

21₅ + 24₅ = 100₅

이진수에서만 “뒤집고 +1”처럼 보였던 것이다. 일반적인 표현은 다음이 더 정확하다.

N진수의 보수 만들기

1. 각 자리를 N - 1에서 뺀다.
2. 마지막에 1을 더한다.

5진수에서는 각 자리의 최대 숫자가 4이므로 다음처럼 바뀐다.

원래 자리 4에서 뺀 결과
04
13
22
31
40

2진수에서는 이 표가 너무 단순해진다. 가능한 숫자가 0과 1뿐이라서 그냥 뒤집기처럼 보인다.

6. 정리

보수는 외워야 하는 꼼수가 아니다. 자릿수가 정해진 숫자 세계에서 뺄셈을 덧셈으로 바꾸는 방법이다.

A - B
= A + (M - B) mod M

여기서 M은 한 바퀴 크기다. N진수 m자리라면 M = N^m이다.

그리고 M - B를 쉽게 구하기 위해 식을 이렇게 바꾼다.

M - B
= ((M - 1) - B) + 1

M - 1은 그 자릿수의 최대수다. 이진수에서는 최대수가 111...111이므로, (M - 1) - B가 비트 반전이 된다.

그래서 우리가 외우던 말이 나온다.

2의 보수 = 비트 뒤집고 +1

하지만 이 문장은 결론이다. 원리는 이쪽이 더 정확하다.

정해진 자릿수에서는 숫자가 한 바퀴 돈다.
뺄셈은 반대 방향 이동이다.
반대 방향으로 B만큼 가는 것은 앞으로 M - B만큼 가는 것과 같다.
그래서 B를 빼는 대신 B의 보수를 더한다.

왜 빠른가, 언제 필요한가, 배열로는 안 되는 이유가 뭔가
정리해본 기록이다.


1. 조인의 시간 복잡도는 ‘탐색’의 문제다

두 테이블을 결합(Join)할 때 핵심은
A의 한 행이 B의 어느 행과 연결되는가를 찾는 탐색이다.

방식시간 복잡도탐색 방식
배열 기반 결합 (Nested Loop Join)O(mn)모든 행 비교
해시 기반 결합 (Hash Join)O(m+n)해시로 직접 탐색

2. 배열 조인은 왜 O(mn)인가

테이블 A, B가 다음과 같다고 하자.

A.id이름
1민수
3지훈
4유나
B.id점수
180
290
470
560
단계A.id비교할 B 행비교 횟수
111,2,4,54
231,2,4,54
341,2,4,54

총 12회 비교 → O(mn)


3. 해시 조인의 작동 방식

3-1. Build 단계 (O(n))

작은 테이블 B를 해시 테이블로 변환한다. 해시 함수: h(x) = x % 4

B.idh(x)버킷내용
11bucket[1](1,80)
22bucket[2](2,90)
40bucket[0](4,70)
51bucket[1](5,60)
bucket[0] → [(4,70)]
bucket[1] → [(1,80), (5,60)]
bucket[2] → [(2,90)]
bucket[3] → []

3-2. Probe 단계 (O(m))

테이블 A의 각 행을 해시 함수로 탐색한다.

A.idh(x)접근 버킷비교 결과
11bucket[1](1,80) 매칭
33bucket[3]비어 있음
40bucket[0](4,70) 매칭

총 연산 횟수 ≈ m + n → O(m+n)

3-3. 결과(Result) 단계

Probe 단계에서 찾은 매칭을 새로운 결과 테이블로 만든다. 이 단계는 단순히 매칭된 튜플을 합쳐서 출력하는 과정이다.

bucket[0] → [(4,70)]
bucket[1] → [(1,80),(5,60)]
bucket[2] → [(2,90)]
bucket[3] → []
A.idA.이름B.점수
1민수80
4유나70
JoinResult = [
  (1, '민수', 80),
  (4, '유나', 70)
]
  • 출력 행 수 k에 비례 → O(k)
  • 총합: O(m + n + k)
  • 보통 k ≤ min(m,n) → O(m+n)

3-4. 정리

단계이름수행 내용시간 복잡도
1Build작은 테이블 B를 해시 테이블로 저장O(n)
2ProbeA의 각 키를 해시로 탐색O(m)
3Result매칭된 튜플을 결과 테이블에 출력O(k)

총합: O(m + n + k) (출력 제외 시 O(m + n))


4. “해시 함수가 꼭 필요한가?”

id가 이미 숫자인데 그냥 인덱스로 쓰면 되잖아?

가능하다. 이게 Direct Address Table(DAT) 방식이다.

5. Direct Address Table (DAT) 예시

B.id저장 위치
180Direct[1]
290Direct[2]
470Direct[4]
560Direct[5]
A.id조회결과
1Direct[1]80
3Direct[3]null
4Direct[4]70

6. DAT의 한계: 공간 복잡도

  • id = {2, 1000, 9000} → 배열 크기 = 9001
  • 데이터 3개, 낭비된 공간 99.97%
  • 공간 복잡도: O(max_key)

시간은 빠르지만 공간은 비효율적.


7. 해시 함수의 역할

  • 큰 키 공간을 작은 버킷 공간으로 압축
  • 문자열·복합키도 숫자로 변환 가능
  • 공간 복잡도 O(n), 평균 탐색 O(1)

즉, 해시는 DAT의 속도를 유지하며 공간 낭비를 해결하는 장치다.


8. DAT와 해시의 관계

구조해시 함수공간 복잡도시간 복잡도특징
DATh(x)=xO(max_key)O(1)항등 해시
해시 테이블h(x)=f(x)O(n)평균 O(1)DAT의 일반화 구조

9. 정리

  • 배열 조인 → 모든 조합 비교 → O(mn)
  • DAT → 직접 인덱스 접근 → O(m+n)
  • 해시 → DAT의 공간 낭비 해결 → 현실적 일반화
해시는 배열의 속도를 메모리 제약 안에서 유지시키는 일반화된 구조다.
DAT는 해시 함수가 y=x인 특수형이다.

연산이란 무엇일까?

우리는 매일 무언가를 더하거나 빼고 비교하거나 고르는 일을 한다.

숫자를 더하는 것도 그렇고 두 물건 가운데 더 무거운 것을 고르는 것도 그렇다.

이런 일을 수학에서는 연산(operation)이라고 부른다.

연산이란 아주 단순하게 말해 입력을 받아 출력을 만드는 규칙이다.


단항연산: 하나만 넣으면 된다

연산 중에서 입력이 하나만 필요한 것을 단항연산이라고 한다.

  • 절댓값 연산 : 5를 넣으면 -5가 나온다. -3을 넣으면 3이 된다.
  • 집합에서 여집합 취하기 : A라는 집합을 넣으면, 전체에서 A를 뺀 부분이 결과다.

여기서는 언제나 입력도 결과도 같은 세계(같은 집합) 안에 머무른다.


이항연산: 둘을 합쳐서 새로운 걸 만든다

입력이 두 개인 연산을 이항연산이라고 한다.

  • 덧셈: (3,4) → 7
  • 곱셈: (2,5) → 10
  • 집합의 교집합: (A,B) → A∩B

입력이 두 개라서 “함께” 작동한다는 점이 특징이다.


삼항연산: 셋이 모이면

입력이 세 개인 경우도 있다. 삼항연산이라고 부른다.

  • 세 수의 최대값: (3,7,5) → 7
  • 프로그래밍에서 자주 쓰이는 조건 연산자: (조건, 값1, 값2) → 조건이 참이면 값1, 거짓이면 값2

더 넓은 이야기: n항 연산

입력이 네 개, 다섯 개, 아니 그 이상이라면? 수학에서는 이를 n항 연산이라고 통칭한다. n은 자연수로

몇 개의 입력이 들어오는지를 뜻한다.

예를 들어 “10개의 수를 모두 더한다”라는 것도 n항 연산이다.


왜 중요한가?

연산은 단순한 계산 규칙을 넘어서 사고의 기본 단위가 된다.

숫자를 다룰 때도, 집합을 다룰 때도, 컴퓨터 프로그래밍에서도 결국 모든 행동은 어떤 형태의 연산으로 정리된다.

입력을 정의하고 출력을 정하고 그 사이의 규칙을 세우는 것이 바로 연산이다.


함수와 연산은 어떤 관계일까?

함수란 “입력을 주면 출력을 돌려주는 규칙”이다.
연산도 이 정의 안에 들어간다. 다만 연산은 한 가지 조건이 더 있다.

  • 입력도, 출력도 같은 집합 안에 있어야 한다.

예를 들어,

  • 함수의 예:
    $$ f:\mathbb{R} \to \mathbb{R}^2,\quad f(x) = (x, x^2) $$
    이 규칙은 실수를 입력하면 좌표평면의 한 점을 내놓는다.
    함수이지만 출력 집합이 입력 집합과 다르므로 연산은 아니다.
  • 연산의 예:
    $$ (a,b) \mapsto a+b $$
    입력도 실수, 출력도 실수다.
    따라서 함수이면서 동시에 연산이다.

따라서 연산은 함수의 특별한 경우라고 할 수 있다.

MLE 쉽게 이해하기

0. 우도 함수의 출발점

최대우도추정은 다음 생각에서 출발한다.
주어진 데이터가 지금처럼 관측될 가능성을 가장 크게 만드는 파라미터를 찾는 것이다.

데이터가 $x_1, x_2, \ldots, x_n$일 때, 이들이 동시에 나올 확률은

$$
P(x_1, x_2, \ldots, x_n \mid \theta)
$$

이다.
독립을 가정하면

$$
L(\theta \mid x) = \prod_{i=1}^n P(x_i \mid \theta)
$$

가 된다. 이를 우도 함수라 정의한다.
MLE는 이 우도 함수를 최대화하는 $\theta$를 찾는 문제로 정리된다.

1. 모수를 가정하면 확률이 정해진다

모수 $\theta$가 주어지면 분포가 결정된다.
정규분포라면 $\theta = (\mu,\sigma^2)$,
만일 베르누이 분포라면 $\theta = p$이다.

이때 각 데이터 $x_i$가 나올 확률은

$$
P(x_i \mid \theta)
$$

로 주어진다.
$\theta$가 정해지면 분포가 정해지고 그렇게 된다면 확률이 결정된다.


2. 표본을 모아 우도 함수를 만든다

데이터가 $x_1, x_2, \ldots, x_n$으로 주어졌다고 하자.
이 표본들이 서로 독립이라면 전체 데이터가 동시에 나올 확률은 곱으로 표현된다.

$$
L(\theta \mid x) = \prod_{i=1}^n P(x_i \mid \theta)
$$

이 식이 바로 우도 함수(Likelihood function)다.

데이터 $x$는 이미 고정돼 있다.
따라서 $L(\theta \mid x)$는 $\theta$를 변수로 하는 함수다.
이 함수가 클수록 “현재의 데이터가 이 $\theta$에서 나올 가능성”이 커진다는 의미다.


3. 모수는 하나가 아니라 벡터일 수 있다

$\theta$는 단일 값이 아니라 여러 매개변수를 포함할 수 있다.

$$
\theta = f(a, b, c, d, \ldots)
$$

예를 들어 정규분포의 평균과 분산처럼, $a,b,c,d$ 각각이 독립적인 모수일 수 있다.
따라서 우리는 $L(\theta \mid x)$를 $L(a,b,c,d \mid x)$처럼 생각할 수 있다.


4. 로그우도로 단순화한다

곱을 그대로 두면 미분이 복잡해진다.
로그를 취하면 곱이 합으로 바뀌어 다루기 쉬워진다.

$$
\ell(\theta \mid x) = \log L(\theta \mid x) = \sum_{i=1}^n \log P(x_i \mid \theta)
$$

로그를 취해도 최대값을 주는 $\theta$는 변하지 않는다.
따라서 계산은 대부분 $\ell(\theta \mid x)$로 진행한다.


5. 관심 있는 모수에 대해 편미분하고 0으로 둔다

우리가 원하는 것은 우도(혹은 로그우도)를 최대화하는 모수다.
최대값을 찾는 방법은 각 모수에 대해 미분을 하고 그 결과를 0으로 두는 것이다.

모수가 여러 개라면 각각에 대해 편미분 방정식을 세운다.

$$
\frac{\partial \ell(\theta \mid x)}{\partial a} = 0,\quad
\frac{\partial \ell(\theta \mid x)}{\partial b} = 0,\quad
\frac{\partial \ell(\theta \mid x)}{\partial c} = 0,\quad \ldots
$$

이 방정식들을 풀면 $a,b,c,d$ 각각의 최대우도추정값을 얻을 수 있다.
즉, 관심 있는 변수에 대해 로그우도를 미분하고 0으로 두는 과정이 곧 MLE 계산의 핵심 절차다.


6. 동전 던지기로 확인해보기

앞면이 나올 확률을 $p$라고 하자.
앞면이 $k$번, 뒷면이 $n-k$번 나왔다면

$$
L(p \mid x) = p^k (1-p)^{n-k}
$$

로그우도는

$$
\ell(p \mid x) = k \log p + (n-k)\log(1-p)
$$

$p$로 미분하고 0으로 두면

$$
\frac{d}{dp}\ell(p \mid x) = \frac{k}{p} - \frac{n-k}{1-p} = 0
$$

이를 풀면

$$
\hat{p} = \frac{k}{n}
$$

즉, MLE는 결국 “데이터에서 앞면이 나온 비율”을 돌려준다.
이 과정을 보면 “가정 → 곱해서 L → 로그 → 편미분=0 → 추정값”


정리하면

  • $P(x_i \mid \theta)$ : 모수를 가정했을 때 표본 하나의 확률
  • $L(\theta \mid x)$ : 모든 표본을 모아 만든 우도 함수
  • $\theta = (a,b,c,d,\ldots)$ : 모수는 여러 매개변수로 이뤄진 벡터
  • $\frac{\partial \ell}{\partial \text{모수}} = 0$ : 각 모수별로 편미분해 0으로 두면 그 모수의 MLE 추정값이 나온다

소프트웨어의 역사는 반복되는 진자와도 같다. 우리는 단순함을 갈망하지만, 현실은 언제나 복잡성을 되돌려준다. 한쪽에서는 무겁게 쌓아 올리던 것을 다시 가볍게 바꾸고, 또 다른 쪽에서는 단순하게 만든 것을 다시 정교하게 다듬는다. 이 과정이 무의미한 흔들림처럼 보일 수 있지만, 실제로는 복잡성의 자리를 바꾸는 작업이다. 복잡성은 사라지지 않는다. 다만 다른 층위로 옮겨갈 뿐이다.

최근 몇 년간 논의된 MSA, DevOps, MLOps는 이 진자 운동의 최신 사례다. 하지만 비슷한 흐름은 데이터베이스, 인프라, 프로그래밍 언어의 역사에서도 똑같이 발견된다. 이 반복은 업계의 숙명처럼 보이지만, 사실은 우리가 매번 던져야 할 질문을 다시 확인시킨다. 복잡성을 어디에 둘 것인가.


MSA ― 분해가 낳은 새로운 복잡성

모놀리식 애플리케이션은 단순했다. 하나의 코드베이스, 하나의 배포, 하나의 장애 지점. 개발 초기에는 이 단순성이 오히려 힘이 됐다. 그러나 서비스가 커지고 팀이 늘어나면서, 그 단순성은 짐이 되었다. 작은 수정이 전체 빌드를 강제하고, 특정 기능의 장애가 전체를 무너뜨렸다.

여기서 등장한 것이 MSA다. 기능을 잘게 나누고, 각 서비스를 독립적으로 배포하고, 팀 단위 자율성을 높였다. 표면적으로는 단순해졌다. 그러나 복잡성은 사라지지 않았다. 코드의 부담이 운영의 부담으로 옮겨갔다. 분산 트랜잭션, 서비스 메시, 네트워크 지연, 장애 추적 같은 문제가 새롭게 떠올랐다.

이 때문에 MSA는 기술의 필연이라기보다는 조직 구조의 투영이다. 수십 개 팀이 동시에 일하고, 하루에도 수차례 배포가 필요하며, 기능별 트래픽 편차가 큰 대규모 기업에는 반드시 필요하다. 하지만 소규모 팀이 ‘선진적’이라는 이유만으로 도입하면, 오히려 관리 비용이 더 커진다. “나노서비스”라는 비난이 괜히 나온 게 아니다.


DevOps ― 필수 인프라로 정착한 경계 해소

개발팀과 운영팀이 분리되어 있던 시절, 갈등은 일상이었다. 개발자는 기능을 다 만들었다고 주장하지만, 운영자는 배포 불가를 외쳤다. 운영자는 안정성을 중시했지만, 개발자는 속도를 요구했다.

DevOps는 이 경계를 허물었다. 코드는 자동으로 빌드되고, 테스트를 거쳐, 파이프라인을 통해 배포된다. 관측과 모니터링이 뒤따른다. 사람의 손으로 반복하던 작업이 자동화되면서, 개발과 운영의 충돌은 줄어들었다.

오늘날 DevOps는 더 이상 선택이 아니다. CI/CD 파이프라인, IaC, 자동화된 모니터링 없이는 대규모 소프트웨어 개발이 불가능하다. 다만 여기서도 문제는 생긴다. 수많은 도구들이 난립한다. 쿠버네티스, 헬름, 테라폼, 깃옵스… “최신 스택”이라는 이름으로 모든 걸 억지로 도입하면, 효율화 대신 도구 관리에 에너지를 다 쓰게 된다.

DevOps의 본질은 화려한 툴이 아니다. 본질은 경계의 해소다. 개발과 운영을 하나의 연속으로 연결하는 것, 바로 그 지점이 DevOps가 사라질 수 없는 이유다.


MLOps ― 불확실성과 싸우는 선택형 체계

머신러닝은 코드와 다르다. 코드만 배포하면 끝나지 않는다. 데이터와 모델이 함께 굴러간다. 데이터 분포는 시간에 따라 바뀌고, 모델은 성능을 잃는다. 여기에 라벨 누수, 오프라인-온라인 불일치 같은 문제가 얹히면, 단순한 배포 자동화만으로는 대응할 수 없다.

MLOps는 이런 특수성을 다루려는 시도다. 데이터 수집, 전처리, 학습, 모델 등록, 배포, 모니터링, 재학습까지 이어지는 주기를 하나의 체계로 묶는다. 파이프라인이 길고, 중간에 실패할 지점이 많기 때문에 자동화와 추적이 없으면 관리가 불가능하다.

그러나 MLOps는 DevOps처럼 표준 인프라가 되기는 어렵다. 서비스마다 데이터 형태와 모델 특성이 다르기 때문이다. 금융 데이터와 영상 데이터의 파이프라인은 구조적으로 같을 수 없다. 그래서 MLOps는 “모든 곳에 똑같이 적용되는 만능 체계”가 아니라, 각 기업이 필요한 부분만 골라 쓰는 선택형 체계에 머물 가능성이 크다.


데이터베이스 ― 정규화와 파편화, 그리고 재통합

데이터베이스의 역사에서도 같은 흔적이 뚜렷하다.

  • 관계형 모델(RDBMS)은 데이터를 정규화하고, 일관된 질의 언어(SQL)를 제공해 단순성을 확보했다. 그러나 수평 확장성이 약했다.
  • NoSQL은 단순화를 위해 규칙을 깨뜨렸다. 키-값 저장소, 문서 DB, 그래프 DB는 확장을 쉽게 했지만, 정합성은 애플리케이션이 감당해야 했다.
  • NewSQL은 다시 SQL의 편의성과 NoSQL의 확장성을 결합하려 한다.

데이터베이스는 단순화와 복잡화 사이를 오가며, 사실은 “일관성·확장성·지연”이라는 세 가지 축을 어떻게 분배할 것인지 끊임없이 조정해왔다.


인프라 ― 베어메탈에서 서버리스까지

인프라의 변화를 봐도 같은 이야기를 할 수 있다.

  • 베어메탈은 단순했지만, 관리가 어렵고 확장성이 부족했다.
  • 가상머신은 물리적 제약을 추상화했지만 무거웠다.
  • 컨테이너는 경량화를 가져왔지만, 오케스트레이션이라는 새로운 복잡성을 낳았다.
  • 서버리스는 인프라 자체를 감추었지만, 그 복잡성은 클라우드 내부로 봉인되었다.

“서버를 의식하지 않아도 된다”는 말은 절반만 맞다. 서버는 여전히 존재하고, 복잡성은 단지 사용자 눈에 보이지 않을 뿐이다.


프로그래밍 언어 ― 추상화와 회귀

프로그래밍 언어의 발전도 이 패턴에서 벗어나지 않는다.

  • 어셈블리는 단순했지만 생산성이 낮았다.
  • 고급 언어는 생산성을 높였으나, 성능의 한계가 생겼다.
  • 프레임워크는 업무를 단순화했지만, 내부 규칙에 종속시켰다.
  • 성능 요구가 다시 커지면서, 우리는 Rust, CUDA, WebAssembly 같은 저수준 제어를 다시 꺼내 들었다.

언어와 프레임워크도 복잡성을 옮기는 작업을 반복할 뿐이다. 추상화가 발전할수록 또 다른 회귀가 기다린다.


결론 ― 질문은 하나다

모든 사례를 관통하는 사실은 명확하다.
복잡성은 사라지지 않는다. 옮겨질 뿐이다.

따라서 진짜 물어야 할 질문은 “이 구조가 유행인가?”가 아니다.
“이 구조가 우리에게 감당 가능한 층위로 복잡성을 옮겨주고 있는가?”다.

MSA는 조직이 크다면 정당하다. DevOps는 선택이 아니라 필수다. MLOps는 필요에 따라 골라 쓰는 것이 맞다. 데이터베이스, 인프라, 언어의 역사는 모두 같은 교훈을 보여준다.

복잡성을 어디에 둘 것인가. 이 질문만이, 우리가 기술을 선택할 때 흔들리지 않게 붙잡을 수 있는 기준이다.

'잡생각' 카테고리의 다른 글

인간은 가르치면서 배운다.  (0) 2025.08.27
이해란 무엇인가? - 이해와 암기에 대한 생각  (1) 2025.05.09
복소평면에 대한 생각  (3) 2024.12.29
데이터가 사는 곳  (3) 2024.12.12
효과와 효율  (1) 2024.03.04

차가운 선풍기

중앙에 금속판이 달린 미니 선풍기를 샀다. 신기하게도 버튼을 누르니 금속판이 순식간에 차가워졌다. 문득 궁금해졌다. 한쪽 면이 차가워졌다면, 에너지 보존 법칙에 따라 다른 어딘가는 뜨거워져야 할 텐데 그런 느낌은 거의 없었다.

이 금속판의 정체는 펠티에(Peltier) 소자였다. 이 작은 부품 하나가 어떻게 물리 법칙을 거스르는 것처럼 보이는 마법을 부리는지, 그리고 그것이 어떻게 세상의 근본 규칙인 열역학 법칙을 따르고 있는지 알아보기로 했다.


펠티에 소자는 어떻게 열을 옮기는가

전기가 직접 냉기를 만드는 것은 아니다. 전기는 그저 ‘열을 옮길 일꾼’인 전자를 한쪽에서 다른 쪽으로 밀어주는 펌프 역할을 할 뿐이다. 이 과정의 핵심은 서로 다른 성질을 가진 n형 반도체p형 반도체에 있다.

  • n형 반도체: 자유롭게 움직이는 전자(-)가 남아도는 반도체다. 전자가 비교적 높은 에너지 상태에 있어 ‘에너지 언덕’에 비유할 수 있다.
  • p형 반도체: 전자가 들어갈 빈자리인 정공(+)이 많은 반도체다. 전자가 이 자리에 들어가면 안정화되므로, ‘에너지 계곡’에 해당한다.

전류가 흐르면서 전자는 p형(에너지 계곡)에서 n형(에너지 언덕)으로 이동해야 하는 접합면에 도착한다. 낮은 에너지 상태에서 높은 상태로 점프하기 위해, 전자는 주변에서 에너지를 열의 형태로 흡수한다. 이 때문에 접합면과 거기에 붙은 금속판이 차가워진다.

반대로 회로를 따라 이동한 전자가 n형에서 p형으로 돌아올 때는, 높은 에너지 상태에서 낮은 곳으로 떨어지며 남는 에너지를 열로 방출한다. 그래서 반대편은 뜨거워진다.
선풍기에서 뜨거움이 잘 느껴지지 않았던 이유는, 이 열을 식히기 위한 작은 방열판과 팬이 보이지 않는 곳에서 열심히 돌고 있기 때문이다.

결국 펠티에 소자는 반도체 내에서 전자의 에너지 준위 차이를 이용해 한쪽의 열을 다른 쪽으로 강제로 옮기는 고체형 열 펌프인 셈이다.


세상의 규칙, 열역학 법칙 0, 1, 2, 3

펠티에 소자처럼 신기한 기술도 결국 이 네 가지 규칙 안에서 작동한다. 이 현상을 보며 처음 들었던 질문, “이것은 물리 법칙을 위반하지 않는가?”에 대한 답은 “절대 위반하지 않는다”이다.

열역학 제0법칙: 온도의 존재 이유

정의: A와 B가 열평형이고, B와 C가 열평형이면, A와 C도 열평형이다.

0법칙은 너무나 당연해 보여서 나중에야 이름이 붙었다. 하지만 이 법칙이 없다면 ‘온도’라는 개념 자체가 성립하지 않는다. 우리가 온도계(B)를 어떤 물체(A)에 대보고, 나중에 다른 물체(C)에 대봤을 때 같은 눈금을 가리킨다면, 우리는 A와 C의 온도가 같다고 확신할 수 있다. 제0법칙은 이 삼단논법을 보장함으로써 온도를 보편적인 물리량으로 만들어준다.

열역학 제1법칙: 에너지는 사라지지 않는다

정의: 계의 내부에너지 변화량($\Delta U$)은 계에 가해진 열($Q$)과 계가 외부에 한 일($W$)의 차와 같다.
$$\Delta U = Q - W$$

제1법칙은 한마디로 에너지 보존 법칙이다. 에너지는 새로 생기거나 사라지지 않고, 형태만 바꿀 뿐이라는 선언이다. 펠티에 소자에서 냉각 면이 흡수한 열($Q_c$)에 외부에서 공급한 전기 에너지($W$)를 더한 만큼이 정확히 가열 면에서 방출된다($Q_h$).

$$Q_h = Q_c + W$$

차가워지는 면이 있다면, 그 반대편에서는 흡수한 열에 들어간 전기 에너지까지 더해져 더 많은 열이 발생한다. 제1법칙은 이 거래의 대차대조표가 항상 0으로 맞는다는 사실을 말해준다.

열역학 제2법칙: 시간은 왜 한쪽으로만 흐르는가

정의: 고립계의 총 엔트로피(S)는 감소하지 않는다.
$$\Delta S \geq 0$$

제1법칙이 에너지의 ‘양’을 다룬다면, 제2법칙은 에너지의 ‘방향’과 ‘질’을 다룬다. 자연 현상은 항상 무질서도(엔트로피)가 증가하는 방향으로만 자발적으로 일어난다. 뜨거운 커피는 식고, 잉크는 물에 퍼진다. 이 비가역성이 바로 시간의 화살이다.

이 법칙 때문에 펠티에 소자는 외부의 ‘일’(전기 에너지) 없이는 작동할 수 없다. 차가운 곳에서 뜨거운 곳으로 열을 옮기는 것은 엔트로피를 거스르는 행위이기 때문이다. 이 ‘부자연스러운’ 일을 하기 위해 전기에너지를 투입하면, 소자 자체의 엔트로피는 국소적으로 감소하지만, 뜨거워진 방열판이 주변 공기를 데우면서 우주 전체의 엔트로피는 결국 증가한다.

냉장고와 에어컨: 기체의 상태 변화를 이용한 열 펌프

냉장고와 에어컨도 펠티에 소자와 마찬가지로 제2법칙의 지배를 받는 열 펌프다. 다만 이들은 냉매라는 기체의 상태 변화를 이용한다.

  1. 압축 (실외기): 압축기가 냉매 기체를 강하게 압축해 고온·고압의 액체로 만든다. 이 과정에서 냉매는 뜨거워지고, 그 열을 실외기 팬을 통해 밖으로 방출한다.
  2. 팽창 (실내기): 실외에서 식은 액체 냉매가 좁은 관을 통해 실내기로 들어오며 급격히 팽창(기화)한다. 액체가 기체로 변할 때는 주변의 열을 대량으로 흡수해야 하므로, 실내기 주변 공기가 차가워진다. (주사 맞기 전 알코올 솜이 시원하게 느껴지는 원리와 같다)

이 순환을 반복하며 실내의 열을 빼앗아 밖으로 버리는 것이다. 여기서 압축기가 바로 제2법칙을 거스르기 위해 외부에서 일을 해주는 장치다.

열역학 제3법칙: 절대 멈출 수 없다

정의: 절대영도(0 K)에서 완전한 결정의 엔트로피는 0에 수렴한다.

제3법칙은 엔트로피의 절대적인 기준점을 제시한다. 온도가 내려갈수록 물질의 엔트로피는 감소하며, 이론적으로 모든 원자 운동이 멈추는 절대영도에서는 0이 된다는 것이다. 하지만 이 법칙은 동시에 절대영도에 도달하는 것이 불가능하다는 사실을 암시한다. 절대영도는 다가갈 수는 있지만 결코 닿을 수 없는 극한값이다.


요약

  • 제0법칙은 우리가 ‘차갑다’고 느끼는 온도를 측정할 근거를 주고,
  • 제1법칙은 차가운 면이 있다면 반드시 더 뜨거운 면이 존재해야 함을 알려준다.
  • 제2법칙은 이 모든 과정에 전기라는 대가가 필요함을 설명하며,
  • 제3법칙은 그 어떤 기술로도 무한히 차갑게 만들 수는 없다는 한계를 명시한다.

얼마 전, 운 좋게도 한 교수님의 강연을 들을 기회가 있었다. 주제는 지금의 거대한 언어모델(GPT, Gemini 등)의 뼈대가 되는 트랜스포머 아키텍처였다.

두 시간 남짓한 시간이었지만, 나에게는 몇 달을 고민했을 의문이 단숨에 정리되는 경험이었다. 강연을 듣고 난 뒤, 며칠이 지나도 문득문득 떠오르는 생각들이 있었다. 이 글은 그때의 여운을 붙잡아 정리한 메모에 가깝다.


인간은 어떻게 세계를 이해하는가

물리 법칙을 이해하는 인간의 방식은 크게 두 갈래다.

하나는 몸으로 부딪히는 것이다.
회전 의자에 앉아 몸이 바깥으로 밀릴 때 원심력을 배우고,
손바닥이 뜨거워질 때 마찰을 느끼며,
달리다 멈추며 앞으로 쏠릴 때 관성을 체득한다.

다른 하나는 그 경험을 정리한 언어와 수식의 층위다.
뉴턴의 법칙, 마찰계수, 운동방정식 같은 개념들로 세계를 설명한다.

우리는 체험과 개념이 겹쳐질 때 “이해했다”고 말한다.


언어로만 구축된 세계

LLM은 이 가운데 후자만 가진다. 감각은 없지만, 언어와 수식의 관계를 방대하게 흡수하고 인간이 남긴 설명을 압축한다. 그렇다면 이 압축된 관계망만으로도 세계를 이해했다고 할 수 있을까.

흥미로운 건, LLM이 만들어내는 문장은 단순한 예측의 결과임에도 불구하고 그 안에 분명히 우리의 세계가 담겨 있다는 사실이다.
“경사각이 커지면 미끄러진다.”
“질량과 가속도의 곱은 힘이다.”
이런 문장들은 LLM에게도 낯설지 않다.

비트겐슈타인의 말이 떠오른다. 언어는 세계의 그림이라고. 언어 안에 이미 세계의 압축이 들어 있다면, 언어를 배운다는 건 곧 세계의 구조 일부를 배운다는 뜻이 아닐까. LLM은 그 가능성을 보여준다.


감각의 제약과 그 너머

하지만 인간적 이해의 또 다른 층, 감각은 어떨까. 감각은 직관의 뿌리다. 동시에 제약이기도 하다.

3차원 공간은 눈으로 확인할 수 있지만, 4차원 이상의 구조는 그림자와 투영을 빌려서만 접근할 수 있다. 허수 공간은 아예 그림을 포기하고 수식으로만 다룬다.

LLM은 처음부터 감각에 의존하지 않는다. 관계와 제약만을 다룬다. 그래서 인간의 직관이 닿지 않는 곳에서도 주저하지 않는다.
“4차원 구의 부피는 얼마인가?”
“n차원 군의 대칭성은 어떻게 서술되는가?”
이런 문제를 만나면 그림을 그리려 애쓰지 않고, 곧장 정의와 관계망으로 들어간다.

감각을 발판으로 삼지 않기에 감각에 발목도 잡히지 않는다.
그 순간 문득 드는 생각이 있다.
혹시 감각의 부재가 단점이 아니라, 오히려 인간이 보지 못하는 방향을 열어주는 창의성의 조건일 수 있지 않을까?


머릿속 시뮬레이션과 언어의 시뮬레이션

천재 과학자들이 머릿속에서 물리 현상을 시뮬레이션한다는 이야기를 들어본 적 있을 것이다.

뉴턴이 사과를 보고 중력의 법칙을 떠올렸다는 일화나,

파인만이 입자의 운동을 마음속에서 애니메이션처럼 돌려봤다는 기록이 그렇다.

하지만 곱씹어 보면 그것도 결국 감각적 경험을 추상화해 내부에서 조합한 모델일 뿐이다.

LLM도 비슷한 구조를 가질 수 있다. 감각 대신 언어와 관계망을 재료로 삼아 조합하고 변형한다.

언뜻 단순한 통계적 예측처럼 보이지만, 충분히 깊어지면 시뮬레이션 같은 기능을 한다. 다만 인간처럼 몸으로 검증할 수는 없다. 대신 물리 엔진이나 실험 데이터 같은 외부 도구와 연결해 피드백을 받으면 된다.


이해라는 이름의 경계

결국 LLM은 인간처럼 몸으로 세계를 체험하지는 못한다. 그러나 언어와 수식의 관계망만으로도 상당한 수준의 개념적 이해에 도달할 수 있다. 더 나아가 감각에 제약받지 않기에 인간이 직관하기 어려운 영역에서는 오히려 더 유리할 수도 있다.

그렇다면 이것을 이해라고 부를 수 있을까.
인간의 이해는 감각과 개념이 겹쳐진 것이고, LLM의 이해는 개념만으로 세워진다. 두 방식은 분명 다르다.

그러나 다르다고 해서 덜 가치 있는 것은 아니다. 오히려 서로 다른 결의 이해가 만나면, 인간 혼자서는 닿지 못했던 새로운 길이 열릴지도 모른다.

공부법, 뇌, 뇌과학, 방법론, 배우기, 학습, 학습이론, 효과, 효율 등등등 
나는 계속해서 “무언가를 잘하게 된다는 것은 무엇을 의미하는가?”라는 질문을 던지게 되었다.

스스로 극복한 경험도 있고, 좌절한 경험도 있다. 그러다 보니 남들이 해온 연구와 이론이 눈에 들어왔고

아 그렇지!! 하고 스스로 느낀 것과 맞아떨어지는 부분이 있었는데...

그 중에서도 크게 와닿은 것은 사람은 가르칠 때 가장 깊이 배운다는 점이다.

1. 왜 설명이 곧 학습이 되는가

배운 것을 말로 풀어내려면 머릿속에서 단순히 기억을 불러오는 것에 그치지 않는다. 내용을 구조화하고, 핵심과 비핵심을 가르며, 내가 이해하지 못한 구멍까지 드러낸다. 심리학에서는 이를 protege effect라고 부른다. 단순히 “시험 본다”는 마음가짐보다 “이걸 누군가에게 가르쳐야 한다”는 기대가 있을 때 훨씬 더 철저히 정리하고 조직화하게 된다. 실제 실험에서도 같은 텍스트를 읽고 나서 시험을 본 집단보다 “나중에 가르쳐야 한다”고만 알려준 집단의 성과가 더 높았다.

2. 설명은 검색이고 검색은 기억을 강화한다

설명은 곧 retrieval, 즉 꺼내기다. 머릿속에 저장된 흔적을 다시 불러내고 그것을 연결하는 과정이 바로 설명이다. 이때마다 기억 흔적이 강화된다. 연구에 따르면, 단순히 다시 읽는 것보다 스스로 설명해본 사람이 훨씬 오래 기억했고, 새로운 문제에 전이하는 능력도 더 좋았다. 학습에서 중요한 것은 인출 연습인데, 설명은 가장 강력한 인출 연습이다.

3. 착각에서 벗어나게 만드는 힘

사람은 흔히 ‘이해의 착각(illusion of explanatory depth)’에 빠진다. 겉보기에는 이해했다고 생각하지만 막상 설명을 하려 하면 말이 막히고 논리가 끊긴다. 바로 이 순간이 학습에 소중하다. 설명은 자기 기만을 깨뜨린다. 내가 모르는 부분을 똑똑히 보여주고, 그 덕분에 메타인지(내가 무엇을 알고 무엇을 모르는지에 대한 감각)가 정교해진다. 이후 공부의 방향을 훨씬 더 똑똑하게 잡을 수 있다.

4. 통찰이 생기는 순간

단순히 암기한 내용을 반복하는 것이 아니라, 왜 이런 규칙이 성립하는가, 어떻게 연결되는가를 설명하려다 보면 자연스럽게 패턴을 찾게 된다. 설명은 가설을 만들고 일반화를 시도하게 한다. 이때 이전에는 보이지 않던 규칙성이 드러난다. 많은 연구에서 설명한 학생이 새로운 사례로 지식을 옮겨가는 전이 능력이 더 크다고 보고되었다. 이 “전이”가 바로 우리가 말하는 통찰과 가장 가깝다.

5. 조건과 한계

물론 가르치기가 언제나 좋은 것은 아니다. 기초 지식이 거의 없는 상태에서 억지로 설명하려 하면 오히려 작업 기억만 과부하된다. 그리고 설명한 내용이 틀렸는데 피드백을 받지 못하면 오개념을 강화할 위험도 있다. 따라서 처음에는 완성 예제를 충분히 보고, 점차 부분 설명을 거쳐 자유 설명으로 넘어가는 것이 좋다. 또 설명한 뒤에는 반드시 스스로나 타인에게 피드백을 받는 구조가 필요하다.

6. 내 경험과 연결

돌이켜보면 나 역시 누군가에게 설명하려 할 때 가장 크게 성장했다.

한 때, 수학문제 해설을 공유하거나 수학문제를 만들고 공유하는 활동을 한 적이 있다. 그리고 오랫동안 과외를 했을 때, 내 지식이 단단하고 이전에 보이지 않던 것들이 보이는 경험을 간간히 했다.  블로그에 글을 쓰면서도 비슷한 경험을 한다. 설명하려는 과정에서 내 사고가 정리되고 이해가 더 깊어진다. ( 아마 이게 내가 블로그에 글을 쓰는 주된 이유일 것이다 ㅋㅋ )


정리하자면, 인간은 가르치면서 배운다는 말은 단순한 격언이 아니라 인지과학 연구가 증명한 사실이다. 설명은 조직화를 강제하고, 기억을 강화하며, 착각을 교정하고, 통찰을 끌어낸다. 다만 기초와 피드백이 갖춰져야 한다는 조건이 붙는다. 그래서 배우는 과정에 설명을 끊임없이 끼워 넣는 것이 효과적이다. 내가 배우는 것을 누군가에게 설명해본다는 것, 그것이 가장 오래 가고 가장 깊이 남는 공부가 된다.

벨만이란 단어는 알고리즘이나 강화학습을 배우게 되면 자주 맞닥뜨리는 이름이다.
강화학습과 제어이론에서 다루는 벨만 최적 방정과 그래프 이론 교과서에서 배우는 벨만-포드 알고리즘이다.

두 벨만은 모두 동일인이다. 그러나, 이 두 개념은 보통은 별개로 취급된다. 하나는 추상적 최적성 조건으로, 다른 하나는 특정 계산 절차로.


먼저 벨만 방정식을 보자.
벨만 방정식이 정의되는 맥락은 마르코프 결정 과정(MDP)이다.

상태 $S$, 행동 $A$, 전이 확률 $P(s'|s,a)$, 보상 함수 $R(s,a,s')$, 할인율 $\gamma$로 구성된다.
이 안에서 에이전트는 정책 $\pi$를 따르며 행동하고, 목표는 장기 보상의 기대값을 최대로 만드는 최적 정책 $\pi^*$를 찾는 것이다.

상태 가치 함수와 행동 가치 함수는 다음과 같이 정의된다.

$$
V^\pi(s)=\mathbb{E}_\pi\Big(\sum_{t=0}^\infty \gamma^t R(s_t,a_t,s_{t+1}) \ \big|\ s_0=s\Big)
$$

$$
Q^\pi(s,a)=\mathbb{E}_\pi\Big(\sum_{t=0}^\infty \gamma^t R(s_t,a_t,s_{t+1}) \ \big|\ s_0=s,\ a_0=a\Big)
$$

최적 정책 하에서는 다음 관계가 성립한다.

$$
V^*(s)=\max_a Q^*(s,a), \qquad
Q^*(s,a)=\sum_{s'} P(s'|s,a)\,(R(s,a,s')+\gamma V^*(s'))
$$

이를 결합하면 최적 상태 가치 함수에 대한 벨만 최적 방정식이 도출된다.

$$
V^*(s)=\max_a \sum_{s'} P(s'|s,a)\,(R(s,a,s')+\gamma V^*(s'))
$$

이 식은 계산법이 아니다.
“최적성이라면 반드시 자기 일관성을 가져야 한다”라는 선언이다.


이제 이 방정식을 최단 경로 문제에 끌어와 보자.
그래프 $(V,E,w)$와 시작점 $s$가 있다.

상태는 정점, 행동은 출력 간선의 선택이다.
전이는 결정론이다. 간선 $(u,v)$를 택하면 확실히 $v$에 도착한다.
따라서 $P(v|u,(u,v))=1$이다.

보상은 비용의 음수로 둔다. $R(u,(u,v),v)=-w(u,v)$.
할인율은 $\gamma=1$이다. 비용은 시점에 따라 달라지지 않기 때문이다.

이 치환을 대입하면 최적 상태 가치는 이렇게 된다.

$$
V^*(v)=-\min_{s\leadsto v}\ \sum w
$$

여기서 $d(v)=-V^*(v)$로 치환하면 곧장 다음 한 줄이 나온다.

$$
d(v)=\min_{(u,v)\in E}\{d(u)+w(u,v)\}
$$

벨만 방정식이 특수한 제약 아래에서 간선 완화 연산으로 단순화된 것이다.
벨만-포드가 반복하는 바로 그 한 줄이다.


연산자 $T$를 정의하자.

$$
(Td)(v)=\min_{(u,v)\in E}\{d(u)+w(u,v)\}
$$

초기 조건은 $d^{(0)}(s)=0,\ d^{(0)}(v\neq s)=+\infty$다.

갱신은 다음과 같다.

$$
d^{(k+1)}=T d^{(k)}
$$

$d^{(k)}(v)$는 $s$에서 $v$로 가는 경로 중 간선 수가 $k$ 이하인 것의 최소 비용이다.
따라서 $k=|V|-1$이면 모든 단순 경로가 포함되고, 최단 경로 값이 결정된다.

벨만-포드가 $|V|-1$회 반복하는 이유가 여기서 확보된다.


음수 사이클의 판정도 이 틀로 설명된다.

$\gamma=1$인 문제에서, 음수 사이클은 강화학습의 언어로는 양의 순보상 사이클이다.
값 함수는 발산한다. 따라서 값이 존재하지 않는다.

이때 $|V|$번째 반복에서 다시 갱신이 발생한다.
그래서 벨만-포드는 이 반복을 이용해 음수 사이클을 판정한다.
단순한 구현상의 트릭이 아니라, “최적값이 존재하지 않는다”는 원리의 신호다.


이 과정을 예시로 따라가자.

정점은 $S,A,B,C$다.

간선은 다음과 같다.
$(S,A)=2,\ (S,B)=5,\ (A,B)=1,\ (A,C)=4,\ (B,C)=-2$

초기 상태는
$d(S)=0,\ d(A)=d(B)=d(C)=\infty$

첫 번째 반복에서
$d(A)=2,\ d(B)=3,\ d(C)=1$

두 번째와 세 번째 반복에서는 변동이 없다.

네 번째 반복에서도 변동이 없으면 음수 사이클은 없다.

그러나 $(C,A)=-7$을 추가하면
네 번째 반복에서 $d(A)=-6$으로 갱신된다.

따라서 음수 사이클이 존재한다고 판정한다.


다른 언어로도 같은 과정을 설명할 수 있다.

$(\min,+)$ 반군에서 인접 행렬 $W$의 $(\min,+)$ 거듭제곱 $W^{\otimes k}$는 길이 $k$ 경로의 비용을 담는다.
$T^k d^{(0)}$는 $W^{\otimes \le k}$와 시작 벡터의 곱과 같다.

동시 완화는 배치 값 반복이다.
비동기 완화는 순차 값 반복이다.
SPFA는 비동기 반복에 큐 기반의 스케줄링을 붙인 변형이다.
다익스트라는 비음수 가중치에서 우선순위 큐로 최적 순서를 잡는 비동기 반복이다.
DAG에서는 위상정렬을 통해 단 한 번의 순차 갱신으로 충분하다.

원리는 동일하고, 실행 순서만 다르다.

정점 수 $n$, 간선 수 $m$.
각 반복에서 $m$개의 완화를 수행하고, 이를 $n-1$회 반복한다.
복잡도는 $O(nm)$.

한 반복에서 갱신이 없으면 조기 종료한다.
$n$번째 반복에서 갱신이 일어나면 음수 사이클이다.

마지막으로 문장을 닫자.

벨만-포드는 독립적인 절차가 아니다.
벨만 방정식이 결정론 전이, $\gamma=1$, 보상 $=-$비용이라는 특수 조건 속으로 들어가면, 간선 완화라는 한 줄 연산으로 귀결된다.

그리고 그 한 줄이 반복될 때 최단 경로가 계산된다.

같은 원리에서 출발해, 다른 언어로 기술되고, 다른 상황에서 실행될 뿐이다.

$$
d^{(k+1)}(v)=\min_{(u,v)\in E}\{d^{(k)}(u)+w(u,v)\}
$$

이 한 줄은 방정식과 알고리즘을 잇는 다리다.
원리에서 절차로 내려오는 길이 이렇게 수학적으로 닫힌다.

모든 재귀는 while로 바꿀 수 있다

용어부터 정리

콜스택은 함수 호출이 중첩될 때 각 호출의 지역 변수, 매개변수, 복귀 주소를 쌓아두는 메모리 영역이다.
스택 프레임은 그 안에 쌓이는 한 호출분량의 저장 단위다.
기저 사례는 더 이상 쪼개지 않고 값을 확정하는 경우다.
상태는 다음 계산을 위해 유지해야 하는 값들의 묶음이고,
후처리는 재귀 호출이 끝난 뒤 그 결과를 가지고 추가로 계산하는 부분이다.
재귀 호출 뒤에 아무 일도 남아있지 않은 경우를 꼬리 재귀,
호출 뒤에 뭔가 더 하거나 여러 번 호출하는 경우를 비꼬리 재귀라고 부른다.
콜스택이 하던 저장을 프로그래머가 직접 스택, 큐 같은 구조로 대신하는 걸 명시적 스택이라고 한다.

꼬리 재귀 예시

가장 단순한 건 카운트다운이다.

def countdown(n):
    if n == 0:
        return
    print(n)
    return countdown(n - 1)  # 호출 뒤에 계산이 없음 → 후처리 없음

이건 상태인 n만 갱신하는 while로 곧바로 바꿀 수 있다.

def countdown_loop(n):
    while n != 0:
        print(n)
        n -= 1

배열을 양끝에서 교환해 뒤집는 것도 꼬리 재귀다.

def reverse_rec_inplace(A, start=0, stop=None):
    if stop is None: stop = len(A)
    if stop - start <= 1:
        return
    A[start], A[stop-1] = A[stop-1], A[start]
    return reverse_rec_inplace(A, start+1, stop-1)  # 후처리 없음

투 포인터로 반복문 하나로 끝난다.

def reverse_two_pointers(A, start=0, stop=None):
    if stop is None: stop = len(A)
    i, j = start, stop - 1
    while i < j:
        A[i], A[j] = A[j], A[i]
        i += 1; j -= 1

비꼬리 재귀 예시

팩토리얼 정의를 보면 후처리가 뭔지 바로 보인다.

def fact(n):
    if n == 0:
        return 1
    return n * fact(n - 1)
    # fact(...) 호출이 끝난 뒤 n과 곱하는 부분이 후처리

이걸 꼬리형으로 재정의하면 이렇게 된다.

def fact_tail(n, acc=1):
    if n == 0:
        return acc
    return fact_tail(n-1, acc*n)  # 후처리가 acc 갱신으로 바뀜

def fact_loop(n):
    acc = 1
    while n > 0:
        acc *= n
        n -= 1
    return acc

혹은 기존 정의를 유지하고 콜스택의 곱셈 대기를 직접 스택으로 구현할 수 있다.

def fact_stack(n):
    stack = []
    while n > 0:
        stack.append(n)  # 곱해야 할 값을 저장
        n -= 1
    acc = 1
    while stack:
        acc *= stack.pop()
    return acc

비꼬리 재귀의 전형은 이진트리 중위 순회다.

def inorder(node):
    if node is None:
        return
    inorder(node.left)
    print(node.value)  # 왼쪽 끝난 뒤의 후처리
    inorder(node.right)

반복문으로 바꾸면 현재 노드와 방문 단계를 스택에 저장해서 처리한다.

def inorder_iter(root):
    stack = []
    curr = root
    while curr or stack:
        while curr:
            stack.append(curr)  # 왼쪽 처리 전 상태 저장
            curr = curr.left
        curr = stack.pop()
        print(curr.value)       # 후처리
        curr = curr.right

일반화

한 번의 재귀 호출로 끝나고 후처리가 없는 꼬리 재귀는 상태만 갱신하는 while로 바로 변환 가능하다.
후처리나 다중 호출이 있는 비꼬리 재귀는 두 가지다. 수식을 바꿔 꼬리형으로 만들고 루프로 바꾸거나, 명시적 자료구조로 호출 상태를 저장·복원한다.
이 과정을 따르면 어떤 재귀든 반복문으로 옮길 수 있다.

Recently Written

최근 정리한 글