
플립플롭(Flip-Flop): 컴퓨터가 기억하는 법
전기가 흐르면 1, 안 흐르면 0. 그런데 어떻게 전원이 꺼져도 정보를 기억할까? 1비트 메모리의 탄생.

전기가 흐르면 1, 안 흐르면 0. 그런데 어떻게 전원이 꺼져도 정보를 기억할까? 1비트 메모리의 탄생.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

논리 게이트(AND, OR, NOT)를 공부하면서 뭔가 허전함을 느꼈다. 이 게이트들은 입력이 들어올 때만 반응하고, 입력이 사라지면 즉시 출력도 사라진다. 마치 금붕어처럼 3초마다 기억을 잃는 회로들이었다.
그런데 컴퓨터는 변수를 저장하고, 프로그램 카운터를 기억하고, 캐시에 데이터를 담아둔다. 나는 매일 let count = 0이라고 쓰지만, CPU 안에서 0이라는 값이 어떻게 '머물러 있는지' 한 번도 생각해본 적이 없었다. 전기는 흐르는 건데, 어떻게 정보를 '가둬두는' 걸까?
이 질문이 나를 플립플롭으로 이끌었다.
처음에 가장 혼란스러웠던 건 래치(Latch)와 플립플롭(Flip-Flop)이 다르다는 거였다. 둘 다 '1비트를 기억하는 회로'인데, 왜 이름이 둘일까?
검색해보니 "래치는 레벨 트리거(level-triggered), 플립플롭은 에지 트리거(edge-triggered)"라는 설명이 나왔다. 그래서 뭐가 다르냐고? 더 찾아보니 "래치는 투명(transparent)하고 플립플롭은 불투명(opaque)"이라는 설명도 나왔다. 뭔 소리야.
한참을 헤매다가 결국 이렇게 이해했다:
래치는 너무 민감해서 타이밍을 맞추기 어렵다. 그래서 실제 CPU에서는 클락 신호에 맞춰 움직이는 플립플롭을 주로 쓴다. 이 클락 신호가 바로 우리가 아는 CPU 클락(3GHz = 초당 30억 번 신호가 뛴다)이다.
플립플롭의 핵심은 되먹임 회로라는 걸 이해하는 순간, 모든 게 와닿았다.
보통 회로는 입력 → 논리 게이트 → 출력으로 흐른다. 한 방향이다. 그런데 플립플롭은 출력을 다시 입력으로 집어넣는다. 마치 뱀이 자기 꼬리를 무는 것처럼.
이걸 처음 본 순간, "아, 이게 무한 루프구나"라는 생각이 들었다. 프로그래밍으로 치면:
# 의사 코드 (Pseudocode)
state = 0 # 초기 상태
while True: # 되먹임 루프
if set_signal:
state = 1
elif reset_signal:
state = 0
# 아무 신호도 없으면 state는 그대로 유지됨
output = state # 출력
한 번 state = 1이 되면, 외부에서 reset_signal을 보내지 않는 한 계속 1로 남아있다. 이게 바로 기억이다. 외부 입력이 사라져도 내부에서 계속 순환하면서 값을 유지하는 것.
이 원리를 받아들이고 나니, "아, 메모리는 전기를 도망가지 못하게 루프 안에 가둬두는 거구나"라고 이해할 수 있었다.
플립플롭을 이해하려면 먼저 SR 래치(Set-Reset Latch)를 봐야 한다. 이게 가장 원시적인 형태의 메모리 회로다.
두 개의 NOR 게이트가 서로의 출력을 상대의 입력으로 연결한 구조다. ASCII로 그려보면:
S ----[NOR]---- Q
| ^
v |
R ----[NOR]---- Q'
진리표를 정리해보면:
| S | R | Q (다음 상태) | 설명 |
|---|---|---|---|
| 0 | 0 | Q (유지) | 아무 일도 안 일어남. 기존 값 유지. |
| 1 | 0 | 1 | Set. Q를 1로 강제 세팅. |
| 0 | 1 | 0 | Reset. Q를 0으로 초기화. |
| 1 | 1 | X (금지) | 둘 다 1이면 회로가 혼란에 빠짐. |
여기서 중요한 건 S=0, R=0일 때 Q가 그대로 유지된다는 점이다. 이게 바로 기억이다. 외부에서 아무 신호도 안 보내도, 내부의 되먹임 루프가 계속 돌면서 값을 보존한다.
나는 이걸 전등 스위치 비유로 받아들였다. 전등 스위치를 켜면(Set), 손을 떼도 전등은 계속 켜져 있다. 끄기 전까지는(Reset). 스위치의 '위치'가 바로 1비트 메모리인 셈이다.
SR 래치는 S=1, R=1일 때 문제가 생긴다. 그래서 실제로는 D 플립플롭(Data/Delay Flip-Flop)을 더 많이 쓴다.
D 플립플롭은 입력이 딱 하나다. D (Data). 그리고 클락 신호가 상승 에지(0→1로 바뀌는 순간)일 때만 D 값을 Q에 저장한다.
D ----[D FF]---- Q
|
CLK ---+
| CLK 상승 에지 | D | Q (다음 상태) |
|---|---|---|
| ↑ (0→1) | 0 | 0 |
| ↑ (0→1) | 1 | 1 |
| - (그 외) | X | Q (유지) |
클락이 뛸 때만 값을 받아들인다. 마치 택배 기사가 정해진 시간에만 문을 두드리는 것과 같다. 나머지 시간에는 누가 초인종을 눌러도 문이 안 열린다.
이걸 이해하고 나니, CPU가 왜 클락 신호에 맞춰 움직이는지 와닿았다. 모든 연산이 클락에 맞춰 '틱, 틱, 틱' 박자를 맞추며 진행되는 거다. 3GHz CPU는 초당 30억 번 "지금이야!" 신호를 보내는 셈이다.
코드로 비유하면:
class DFlipFlop {
constructor() {
this.Q = 0; // 저장된 값
}
// 클락 상승 에지에서만 호출됨
onClockRisingEdge(D) {
this.Q = D; // D 값을 저장
}
getOutput() {
return this.Q; // 저장된 값 반환
}
}
// 사용 예시
const ff = new DFlipFlop();
ff.onClockRisingEdge(1); // 클락 ↑, D=1 → Q=1
console.log(ff.getOutput()); // 1
// 클락이 안 뛰는 동안에는 Q가 유지됨
console.log(ff.getOutput()); // 여전히 1
ff.onClockRisingEdge(0); // 클락 ↑, D=0 → Q=0
console.log(ff.getOutput()); // 0
플립플롭에는 여러 변형이 있다. 나는 처음에 "왜 이렇게 종류가 많아?"라고 짜증 났는데, 결국 이거였다: 용도에 따라 입력 방식을 바꾼 거다.
SR 래치의 "S=1, R=1 금지" 문제를 해결한 버전이다.
| J | K | Q (다음 상태) |
|---|---|---|
| 0 | 0 | Q (유지) |
| 1 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 1 | Q' (반전) |
J=1, K=1일 때 Q가 반전된다. 이게 SR 래치와의 차이다. 0이면 1로, 1이면 0으로 뒤집힌다.
JK의 특수 케이스다. J와 K를 묶어서 하나의 입력 T로 만든 것.
| T | Q (다음 상태) |
|---|---|
| 0 | Q (유지) |
| 1 | Q' (반전) |
T=1이면 값이 뒤집힌다. 마치 전등 스위치를 누를 때마다 켜짐/꺼짐이 토글되는 것처럼.
나는 T 플립플롭을 보고 "아, 이게 카운터 회로의 기본이구나"라고 이해했다. 클락이 뛸 때마다 0→1→0→1 반복하니까, 클락을 세는(counting) 용도로 쓸 수 있다.
# T 플립플롭으로 0~3 카운터 만들기
class TFlipFlop:
def __init__(self):
self.Q = 0
def toggle(self):
self.Q = 1 - self.Q # 0이면 1로, 1이면 0로
return self.Q
# 2비트 카운터 (0, 1, 2, 3 반복)
bit0 = TFlipFlop() # LSB
bit1 = TFlipFlop() # MSB
for clock in range(8):
b0 = bit0.toggle() # bit0은 매번 토글
if b0 == 0: # bit0이 1→0으로 넘어갈 때
b1 = bit1.toggle() # bit1도 토글 (캐리)
else:
b1 = bit1.Q
count = b1 * 2 + b0
print(f"Clock {clock}: {count} (이진수: {b1}{b0})")
출력:
Clock 0: 1 (이진수: 01)
Clock 1: 0 (이진수: 10)
Clock 2: 3 (이진수: 11)
Clock 3: 0 (이진수: 00)
...
이게 바로 CPU의 프로그램 카운터(PC, Program Counter) 원리다. 명령어를 하나씩 실행할 때마다 PC를 1씩 증가시키는데, 내부적으로는 T 플립플롭들이 연쇄적으로 토글하면서 카운트를 올린다.
플립플롭을 이해하면서 가장 중요하게 받아들인 개념이 에지 트리거(Edge-Triggered)다.
왜 에지 트리거가 필요할까? 클락이 1인 동안에도 입력 D가 계속 바뀔 수 있다면, 출력 Q도 덩달아 막 바뀐다. 이러면 다음 회로가 혼란에 빠진다. "지금 읽은 값이 맞는 거야, 아니야?"
에지 트리거는 딱 한 순간(0→1 전환 시점)에만 값을 캡처하고, 나머지 시간에는 입력이 어떻게 바뀌든 무시한다. 이게 바로 동기화(Synchronization)의 핵심이다.
나는 이걸 사진 찍기 비유로 이해했다. 레벨 트리거는 카메라 셔터를 누르는 동안 계속 찍히는 거고(흔들림 발생), 에지 트리거는 셔터를 누르는 순간 딱 한 번만 찍히는 거다(선명함).
플립플롭 하나는 1비트만 저장한다. 그럼 8비트를 저장하려면? 8개의 플립플롭을 나란히 배치하면 된다.
D7 D6 D5 D4 D3 D2 D1 D0 ← 입력 (8비트)
| | | | | | | |
[FF][FF][FF][FF][FF][FF][FF][FF] ← 8개의 D 플립플롭
| | | | | | | |
Q7 Q6 Q5 Q4 Q3 Q2 Q1 Q0 ← 출력 (8비트)
↑
공통 CLK (모두 같은 클락 신호)
이게 바로 8비트 레지스터(Register)다. CPU 안에 있는 EAX, EBX 같은 레지스터들이 이런 식으로 만들어진다.
레지스터를 수천, 수만 개 모으면? SRAM (Static RAM)이 된다. CPU 캐시 메모리가 바로 SRAM으로 만들어진다.
| 항목 | SRAM | DRAM |
|---|---|---|
| 구조 | 플립플롭 (6개 트랜지스터) | 축전기 + 트랜지스터 (1개) |
| 속도 | 빠름 | 느림 |
| 전력 | 많이 먹음 | 적게 먹음 |
| 가격 | 비쌈 | 쌈 |
| 리프레시 | 불필요 | 필요 (축전기 방전됨) |
| 용도 | CPU 캐시 (L1, L2, L3) | 메인 메모리 (RAM) |
DRAM은 축전기(Capacitor)에 전하를 저장하는 방식이라 구조가 단순하고 저렴하다. 하지만 축전기는 시간이 지나면 전하가 새서 사라지기 때문에, 몇 밀리초마다 다시 충전해줘야 한다(리프레시). 이게 번거롭고 느리다.
반면 SRAM은 플립플롭으로 만들어져서 전원만 공급되면 값이 영구히 유지된다. 리프레시가 필요 없어서 빠르다. 대신 트랜지스터를 많이 써서 비싸고 전력도 많이 먹는다.
나는 이걸 노트(SRAM) vs 포스트잇(DRAM) 비유로 받아들였다. 노트는 내용이 오래 가지만 무겁고 비싸다. 포스트잇은 가볍고 저렴하지만 떨어질 수 있어서 계속 확인해야 한다.
플립플롭이 CPU에서 어떻게 쓰이는지 보면 더 와닿는다.
현재 실행 중인 명령어의 주소를 저장한다. 16비트 PC라면 16개의 플립플롭으로 구성된다.
명령어 실행 순서:
1. PC에서 주소 읽기 (예: 0x0100)
2. 메모리[0x0100]에서 명령어 가져오기
3. 명령어 실행
4. PC를 1 증가 (0x0101)
5. 1번으로 돌아가기
여기서 PC를 증가시키는 게 바로 앞에서 본 T 플립플롭 카운터다.
메모리에서 읽어온 명령어를 임시로 저장한다. 8비트 명령어라면 8개의 D 플립플롭.
연산 결과를 저장한다. ADD 명령어를 실행하면 그 결과가 ACC에 저장된다.
이 모든 레지스터들이 같은 클락 신호에 맞춰 동기화되어 움직인다. 한 클락 사이클에 하나의 동작이 일어나는 거다.
나는 이걸 정리해보면서, "아, CPU는 거대한 플립플롭 군단이 클락에 맞춰 춤추는 거구나"라고 이해했다.
플립플롭의 되먹임 회로는 전기가 계속 공급되어야만 작동한다. 전원을 끄는 순간 되먹임 루프가 끊기고, 갇혀있던 전기가 사라지면서 저장된 정보도 함께 날아간다.
이게 바로 휘발성 메모리(Volatile Memory)의 본질이다.
그럼 왜 굳이 휘발성 메모리를 쓸까? 속도 때문이다.
비휘발성 메모리(플래시 메모리)는 전자를 절연체 안에 가둬서 저장한다. 이 과정이 느리다. 반면 SRAM은 단순히 전기 신호를 루프 안에서 빙글빙글 돌리기만 하면 되니까 엄청 빠르다.
| 저장 장치 | 접근 속도 | 휘발성 여부 |
|---|---|---|
| L1 캐시 (SRAM) | ~1ns | 휘발성 |
| RAM (DRAM) | ~100ns | 휘발성 |
| SSD | ~100μs | 비휘발성 |
| HDD | ~10ms | 비휘발성 |
L1 캐시는 SSD보다 10만 배 빠르다. 이 속도를 포기할 수 없어서, 우리는 휘발성이라는 단점을 감수한다.
나는 이걸 메모장 vs 돌판 비유로 받아들였다. 메모장에 쓰면 빠르지만 물에 젖으면 지워진다(휘발성). 돌판에 새기면 영구적이지만 시간이 오래 걸린다(비휘발성). 용도에 따라 골라 쓰는 거다.
플립플롭을 이해하고 나니, 평소에 당연하게 여겼던 것들이 다르게 보였다.
Redis는 메모리(DRAM)에 데이터를 저장한다. 플립플롭 원리상, 전원이 꺼지면 데이터가 날아간다. 그래서 Redis는 주기적으로 디스크에 스냅샷을 저장(RDB)하거나 AOF(Append-Only File)로 백업한다.
만약 이걸 안 하면? 서버가 재부팅되는 순간 모든 세션 정보가 사라진다. 사용자들이 전부 로그아웃된다.
let count = 0을 선언할 때, CPU는 레지스터에 플립플롭 여러 개를 할당해서 0을 저장한다. 변수가 많을수록 레지스터가 부족해지고, 스택 메모리(DRAM)로 넘어간다. 레지스터→DRAM으로 가는 순간 속도가 100배 느려진다.
그래서 최적화된 컴파일러는 변수를 최대한 레지스터에 넣으려고 한다(레지스터 할당 최적화).
CPU 클락이 빠를수록 플립플롭이 초당 더 많이 상태를 바꾼다. 상태를 바꿀 때마다 전력을 소비한다. 그래서 고성능 CPU는 열이 많이 나고, 쿨러가 필요하다.
모바일 CPU(ARM)는 클락을 낮춰서 전력을 아낀다. 대신 코어를 많이 붙여서 성능을 보완한다.
플립플롭은 출력을 입력으로 되먹이는 단순한 아이디어에서 시작했다. 이 루프 구조가 전기를 가둬두고, 1비트의 기억을 만들어냈다.
이것들이 모여서 레지스터가 되고, 캐시가 되고, RAM이 된다. 우리가 쓰는 모든 변수, 모든 데이터는 결국 어딘가의 플립플롭에 전기 신호로 갇혀 있다.
전원을 끄면 루프가 끊기고, 전기는 도망가고, 기억은 사라진다. 그래서 우리는 디스크에 저장하고, 데이터베이스를 쓰고, 백업을 한다.
나는 이 원리를 이해하고 나서, 하드웨어와 소프트웨어가 분리된 세계가 아니라 전기로 이어진 하나의 시스템이라는 걸 받아들였다. 코드 한 줄이 실행될 때마다, 어딘가의 플립플롭이 상태를 바꾸고 있다. 그게 바로 컴퓨터가 '계산하고 기억하는' 방법이었다.