지난 글에서 FFT 심화 분석을 마치며, 신호를 강제로 잘라낼 때 발생하는 스펙트럴 누설(Spectral Leakage)과 이를 막아주는 윈도우 함수를 예고한 바 있습니다.
신호를 분석할 때, 우리는 무한히 이어지는 신호 전체를 한 번에 처리할 수 없습니다. 실제로는 유한한 길이의 데이터 블록을 잘라내어 FFT에 입력합니다. 그런데 이 "잘라내기" 행위 자체가 문제를 일으킵니다.
원래 신호가 아무리 깔끔한 순수 정현파라 하더라도, 특정 구간을 강제로 잘라내면 양 끝단에서 불연속이 발생합니다. FFT는 이 잘린 신호가 주기적으로 반복된다고 가정하기 때문에, 잘린 경계에서의 불연속은 실제로는 존재하지 않는 허구의 고주파 성분을 만들어 냅니다. 이것이 바로 스펙트럴 누설(Spectral Leakage)입니다.
이를 부드럽게 막아주는 방패가 바로 '윈도우 함수(Window Function)'입니다. 윈도우 함수는 신호의 양 끝을 부드럽게 0으로 수렴시켜 불연속을 제거함으로써, 주파수 분석의 정밀도를 크게 끌어올립니다. 이번 글에서는 다양한 윈도우 함수의 원리와 특성을 파악하고, Python 실습을 통해 각 윈도우가 스펙트럼에 어떤 영향을 미치는지 직접 비교해 보겠습니다.
1. 스펙트럴 누설(Spectral Leakage)이란?
컴퓨터로 신호의 일부 구간만 잘라서 가져오는 것은, 수학적으로 무한한 길이의 신호에 **직사각형 윈도우(Rectangular Window)**를 곱하는 것과 같습니다.
직사각형 윈도우 $w[n]$은 구간 내에서는 1, 구간 밖에서는 0인 함수입니다. 주파수 영역에서 이것은 원본 신호의 스펙트럼과 직사각형 윈도우의 스펙트럼(Sinc 함수 형태)이 컨볼루션(Convolution)되는 결과를 낳습니다. 이 Sinc 함수는 중심 주파수 외에도 양옆으로 출렁이는 꼬리를 가지고 있기 때문에, 원래라면 날카로운 하나의 피크(Peak)로 나타나야 할 주파수 성분이 넓게 퍼지면서 주변 주파수 대역을 오염시킵니다. 이를 스펙트럴 누설이라고 합니다.
누설이 심해지면, 크기가 작은 중요한 주파수 성분이 크기가 큰 주파수 성분의 누설 에너지에 파묻혀 보이지 않게 되는 치명적인 문제가 발생합니다.
2. 윈도우 함수의 원리와 트레이드오프
스펙트럴 누설의 근본적인 원인은 잘린 신호의 양끝이 '0'으로 부드럽게 이어지지 않고 갑자기 끊어지기 때문입니다. 따라서 신호의 양끝단으로 갈수록 값을 부드럽게 0으로 감소시키는 종 모양의 곡선을 신호에 곱해주면 불연속성이 완화됩니다.
하지만 세상에 공짜는 없습니다. 윈도우 함수를 적용하면 스펙트럼의 형태가 변하면서 다음 두 지표에 의한 트레이드오프(Trade-off)가 발생합니다.
- 메인 로브(Main Lobe) 폭: 신호의 실제 주파수 성분이 집중되는 중심 봉우리의 너비입니다. 좁을수록 주파수 분해능(Frequency Resolution)이 높습니다.
- 사이드 로브(Side Lobe) 레벨: 메인 로브 주변에 퍼지는 부수적인 봉우리의 크기입니다. 낮을수록 누설 억제 성능이 좋습니다.
메인 로브를 좁게 유지하면 사이드 로브가 커지고(누설 감소), 사이드 로브를 억제하면 메인 로브가 넓어집니다(분해능 감소). 윈도우 함수 선택의 핵심은 바로 이 균형점을 용도에 맞게 찾는 것입니다.
3. 대표적인 윈도우 함수의 종류
주요 윈도우 함수를 수식과 함께 정리합니다. 여기서 $n$은 샘플 인덱스 $(0 \le n \le N - 1)$이고 $N$은 윈도우 길이입니다.
3-1. 직사각형 윈도우 (Rectangular)
$$w[n]=1$$
가장 단순한 형태로, 신호를 그대로 자르는 것과 같습니다. 메인 로브가 가장 좁아 이론적 주파수 분해능은 최고이지만, 사이드 로브 레벨이 약 -13 dB로 누설이 심합니다. 누설에 민감하지 않은 상황이거나, 분석 구간의 신호가 정확히 주기적으로 맞아떨어지는 경우에만 적합합니다.
3-2. 한(Hann) 윈도우
코사인 형태로 양 끝을 부드럽게 0으로 수렴시킵니다. 사이드 로브를 약 -31 dB로 억제하며, 분해능과 누설 억제 사이의 균형이 좋아 범용적으로 가장 많이 사용됩니다. 음악 분석, 오디오 처리, STFT 등 대부분의 상황에서 기본 선택지입니다.
3-3. 해밍(Hamming) 윈도우
Hann 윈도우와 수식이 유사하지만, 양 끝이 0이 아닌 0.08로 끝납니다. 이 차이로 인해 첫 번째 사이드 로브는 약 -43 dB로 Hann보다 낮게 억제되지만, 멀리 있는 사이드 로브의 감쇠는 상대적으로 완만합니다. 음성 처리(MFCC, 스펙트로그램)에서 전통적으로 널리 사용됩니다.
3-4. 블랙만(Blackman) 윈도우
코사인 항을 하나 더 추가하여 사이드 로브를 약 -58 dB까지 강하게 억제합니다. 그 대가로 메인 로브 폭이 Hann/Hamming보다 넓어 분해능이 낮습니다. 근접한 두 주파수 성분을 구별하기 어렵지만, 강한 성분 옆의 약한 성분을 탐지해야 할 때 유리합니다.
3-5. 카이저(Kaiser) 윈도우
($I_0$: 0차 변형 베셀 함수)
$\alpha$ 파라미터 하나로 메인 로브 폭과 사이드 로브 레벨 사이의 트레이드오프를 연속적으로 조절할 수 있는 파라미터화된 윈도우입니다. $\alpha = 0$이면 직사각형 윈도우, $\alpha$가 클수록 사이드 로브가 강하게 억제됩니다. FIR 필터 설계에도 활용되는 강력한 윈도우입니다.
3-6. 플랫탑(Flat-Top) 윈도우
메인 로브의 꼭대기가 평탄(flat)하도록 설계된 윈도우입니다. 사이드 로브 억제 성능은 강하지만 메인 로브 폭이 매우 넓습니다. 분해능보다는 진폭의 정확한 측정이 중요한 계측(instrumentation) 분야에서 사용됩니다.
4. 윈도우 함수 특성 비교표
아래 표는 각 윈도우의 특징을 정리한 내용입니다.
| 윈도우 | 메인 로브 폭 | 최대 사이드 로브 | 누설 억제 | 주 용도 |
|---|---|---|---|---|
| 직사각형 | 좁음 | -13 dB | 낮음 | 정현파 주기 정렬 시 |
| Hann | 중간 | -31 dB | 중-고 | 범용, 오디오, STFT |
| Hamming | 중간 | -43 dB | 중-고 | 음성 처리 |
| Blackman | 넓음 | -58 dB | 높음 | 약한 성분 탐지 |
| Kaiser (α=8) | 넓음 | ~-80 dB | 매우 높음 | FIR 설계, 정밀 분석 |
| Flat-Top | 매우 넓음 | -93 dB | 매우 높음 | 진폭 계측 |
5. Python 실습: 윈도우별 형태 및 주파수 응답 비교
scipy.signal 라이브러리를 활용하여 각 윈도우의 시간 영역 형태와 주파수 영역에서의 응답(메인 로브와 사이드 로브)을 직접 시각화해 보겠습니다.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
from scipy.fft import fft
# 한국어 폰트 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# 기본 파라미터
FS = 1000
N = 512
FREQ = 100.0
n = np.arange(N)
windows = {
'Rectangular': np.ones(N),
'Hann': signal.windows.hann(N),
'Hamming': signal.windows.hamming(N),
'Blackman': signal.windows.blackman(N),
'Kaiser (α=8)': signal.windows.kaiser(N, beta=8.0),
}
colors = ['gray', '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']
# ── 시간 영역 ────────────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(12, 4))
for (name, w), color in zip(windows.items(), colors):
ax.plot(n, w, label=name, color=color, linewidth=1.5)
ax.set_xlabel('샘플', fontsize=11)
ax.set_ylabel('진폭', fontsize=11)
ax.set_title('윈도우 함수 시간 영역 비교', fontsize=13, fontweight='bold')
ax.set_ylim(-0.1, 1.1)
ax.legend(loc='upper right', fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('window_time_domain.png', dpi=150, bbox_inches='tight')
plt.show()
# ── 주파수 영역 ──────────────────────────────────────────────────
NFFT = 8192
fig, ax = plt.subplots(figsize=(12, 6))
for (name, w), color in zip(windows.items(), colors):
W = np.abs(fft(w, n=NFFT))
W /= W.max()
W_dB = 20 * np.log10(W + 1e-12)
freq_bins = np.arange(NFFT) - NFFT // 2
W_dB_shifted = np.roll(W_dB, NFFT // 2)
ax.plot(freq_bins[:NFFT//2+200], W_dB_shifted[:NFFT//2+200],
label=name, color=color, linewidth=1.5)
ax.set_xlim(-10, 200)
ax.set_ylim(-120, 5)
ax.set_xlabel('주파수 빈 (메인 로브 중심 기준)', fontsize=11)
ax.set_ylabel('크기 [dB]', fontsize=11)
ax.set_title('윈도우 함수별 주파수 응답 비교 (정규화)', fontsize=13, fontweight='bold')
ax.axhline(-3, color='k', linestyle=':', linewidth=0.8, alpha=0.5)
ax.axhline(-60, color='k', linestyle=':', linewidth=0.8, alpha=0.5)
ax.text(180, -2, '-3 dB', fontsize=8, color='k', alpha=0.7)
ax.text(180, -59, '-60 dB', fontsize=8, color='k', alpha=0.7)
ax.legend(loc='upper right', fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('window_freq_domain.png', dpi=150, bbox_inches='tight')
plt.show()


그래프의 내용을 아래와 같이 두 축으로 살펴보았습니다.
- 시간 영역(위 그래프): Rectangular는 값이 항상 1이며, Hamming은 양 끝이 약 0.08까지 완만하게 감소하고, Hann은 완전히 0으로 수렴합니다. Blackman은 더 넓고 깊은 종 모양으로 양 끝이 0에 가깝게 수렴합니다. Kaiser는 $\alpha$값에 의해 조절될 수 있으며, 본 실험에서는 8로 설정하였습니다.
- 주파수 응답(아래 그래프): 회색(Rectangular)은 중심축(메인 로브)이 가장 좁지만, 양옆의 출렁임(사이드 로브)이 고작 -13 dB에서 시작합니다.
- 파란색(Hann)과 주황색(Hamming)은 메인 로브 폭이 넓어지는 대신 사이드 로브가 훨씬 낮아집니다. (특히 Hamming의 첫 번째 사이드 로브 억제력이 눈에 띕니다.)
- 초록색(Blackman)은 메인 로브가 가장 두껍지만, 사이드 로브가 바닥으로 뚝 떨어지는 강력한 누설 억제력을 보여줍니다.
6. 윈도우 함수의 응용
6-1. 스펙트럴 누설 비교: 실제 신호에 적용
이제 실제 신호(비정수 주파수 성분 포함)에 각 윈도우를 적용하고 스펙트럼 차이를 비교합니다. 비정수 주파수는 FFT 그리드와 어긋나 누설이 가장 잘 드러나는 상황입니다. 이번 실험에서는 각 윈도우의 누설 범위를 독립적으로 확인하기 위해 서브플롯으로 표시합니다.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
from scipy.fft import fft, fftfreq
# 한국어 폰트 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# 기본 파라미터
FS = 1000 # 샘플링 주파수 (Hz)
N = 512 # FFT 포인트 수 (윈도우 길이)
windows = {
'Rectangular': np.ones(N),
'Hann': signal.windows.hann(N),
'Hamming': signal.windows.hamming(N),
'Blackman': signal.windows.blackman(N),
'Kaiser (α=8)':signal.windows.kaiser(N, beta=8.0),
}
# 비정수 주파수를 가진 순수 정현파 생성 (의도적으로 FFT 빈 경계와 어긋나도록 설정)
t = np.arange(N) / FS
freq_non_integer = 100.3 # Hz - 정수 빈에 맞지 않는 주파수
x = np.sin(2 * np.pi * freq_non_integer * t)
freqs = fftfreq(N, 1/FS)
pos_mask = freqs >= 0
fig, axes = plt.subplots(len(windows), 1, figsize=(12, 14), sharex=True)
fig.suptitle(f'스펙트럴 누설 비교 (신호: {freq_non_integer} Hz, N={N})',
fontsize=13, fontweight='bold')
for ax, (name, w) in zip(axes, windows.items()):
x_win = x * w
X = np.abs(fft(x_win, n=N))
X /= (np.sum(w) / 2) # 윈도우 진폭 보정 (일관된 진폭 비교를 위해)
X_dB = 20 * np.log10(X[pos_mask] + 1e-12)
ax.plot(freqs[pos_mask], X_dB, linewidth=1.2)
ax.set_ylabel('크기 [dB]', fontsize=9)
ax.set_title(name, fontsize=10, loc='left')
ax.set_ylim(-100, 10)
ax.axvline(freq_non_integer, color='r', linestyle='--', linewidth=0.8, alpha=0.6)
ax.set_xlim(0, FS / 2)
ax.grid(True, alpha=0.3)
axes[-1].set_xlabel('주파수 [Hz]', fontsize=11)
plt.tight_layout()
plt.savefig('window_leakage_comparison_freq_domain.png', dpi=150, bbox_inches='tight')
plt.show()

위 그림에서 보듯이 Blackman과 Kaiser가 강력한 누설 억제력을 보여주고 있습니다. 물론 Kaiser는 $\alpha$의 설정에 따라 조금 결과가 다를 수 있습니다.
6-2. 분해능 비교: 근접한 두 주파수 성분 분리
이번에는 윈도우 선택이 근접 주파수 분리 능력(주파수 분해능)에 어떤 영향을 주는지 확인합니다.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
from scipy.fft import fft, fftfreq
# 한국어 폰트 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# 기본 파라미터
FS = 1000
N = 512
windows = {
'Rectangular': np.ones(N),
'Hann': signal.windows.hann(N),
'Hamming': signal.windows.hamming(N),
'Blackman': signal.windows.blackman(N),
'Kaiser (α=8)':signal.windows.kaiser(N, beta=8.0),
}
# 주파수 간격이 매우 좁은 두 성분
f1, f2 = 100.0, 105.0
A1, A2 = 1.0, 0.1
t = np.arange(N) / FS
x_two = A1 * np.sin(2 * np.pi * f1 * t) + A2 * np.sin(2 * np.pi * f2 * t)
freqs = fftfreq(N, 1/FS)
pos_mask = (freqs >= 80) & (freqs <= 125)
colors = ['gray', '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']
fig, ax = plt.subplots(figsize=(10, 5))
fig.suptitle(f'근접 주파수 분리 능력 비교 ({f1} Hz + {f2} Hz, 진폭비 {A1}:{A2})',
fontsize=13, fontweight='bold')
for (name, w), color in zip(windows.items(), colors):
x_win = x_two * w
X = np.abs(fft(x_win, n=N))
X_dB = 20 * np.log10(X[pos_mask] + 1e-12)
ax.plot(freqs[pos_mask], X_dB, label=name, color=color, linewidth=1.5)
ax.axvline(f1, color='blue', linestyle='--', linewidth=0.9, alpha=0.4, label=f'f1 = {f1} Hz')
ax.axvline(f2, color='red', linestyle='--', linewidth=0.9, alpha=0.4, label=f'f2 = {f2} Hz')
ax.set_xlabel('주파수 [Hz]', fontsize=11)
ax.set_ylabel('크기 [dB]', fontsize=11)
ax.legend(loc='upper right', fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('window_frequency_resolution_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

Rectangular 윈도우는 두 피크를 가장 뾰족하게 분리하지만, 누설로 인해 약한 성분(f2)이 강한 성분(f1)의 사이드 로브에 묻혀 버리는 경우가 발생합니다. Hann/Hamming은 누설을 억제하면서도 두 피크를 합리적으로 분리합니다. Blackman/Kaiser는 누설 억제가 탁월해 약한 성분을 더 뚜렷하게 드러내지만, 메인 로브가 넓어 두 피크가 겹쳐 보일 수 있습니다.
7. 윈도우 함수 선택 가이드
위의 내용을 참고하여 다음과 같이 상황에 따른 윈도우를 추천합니다.
| 상황 | 추천 윈도우 |
|---|---|
| 일반적인 스펙트럼 분석, 오디오 분석 | Hann |
| 음성 처리, MFCC 추출 | Hamming |
| 강한 성분 옆의 약한 성분 탐지 | Blackman 또는 Kaiser (α ≥ 6) |
| 진폭의 정확한 크기 측정 (계측) | Flat-Top |
| 메인 로브/사이드 로브를 직접 제어하고 싶을 때 | Kaiser (α로 조절) |
| 신호가 FFT 창에 정확히 정수 주기로 맞아 떨어질 때 | Rectangular |
[Insight]
윈도우 함수는 "신호를 어떻게 잘라보느냐"에 대한 질문에 대한 수학적 해답입니다. 단순히 스펙트럼을 보기 좋게 만드는 후처리 도구가 아니라, 분석의 목적에 따라 무엇을 희생하고 무엇을 얻을지를 결정하는 능동적 선택입니다. 핵심은 메인 로브 폭(분해능)과 사이드 로브 레벨(누설 억제)의 트레이드오프를 이해하는 것입니다. "이 두 주파수 성분이 얼마나 가까이 붙어 있는가"와 "강한 성분 옆에 약한 성분이 있는가"라는 두 가지 질문에 답하면 윈도우 선택의 방향이 결정됩니다. 그렇다면 실무에서는 어떤 윈도우를 선택해야 할까요? 완벽한 만능 윈도우는 없으며, 분석 목적에 따라 선택해야 합니다.
실무에서는 Hann 윈도우가 대부분의 상황을 커버합니다. 만약 모르겠다면 Hann을 먼저 쓰고, 결과를 보면서 필요에 따라 Blackman이나 Kaiser로 이동하는 전략이 합리적입니다. 반대로 Rectangular 윈도우는 아무것도 안 하는 것이 아니라, "누설을 감수하고 최고의 분해능을 선택하겠다"는 명확한 의도가 있을 때만 사용하는 것이 바람직합니다.
이러한 Kaiser 윈도우의 유연성은 실무에서도 빛을 발합니다. 필자는 H.264관련 프로젝트를 수행하며 2차원 영상 데이터를 다룰 때, YUV420, YUV422, YUV444 간의 크로마 서브샘플링을 업샘플링하거나 다운샘플링할 때 적용되는 필터와 같이 Kaiser 윈도우를 사용했던 경험이 있습니다.
다음 글 예고
지금까지 배운 푸리에 변환(FFT)과 윈도우 함수를 이용하면 신호에 어떤 주파수가 들어있는지 명확하게 알 수 있습니다. 하지만 이 주파수 스펙트럼은 치명적인 단점이 있습니다. 바로 "그 주파수가 시간상 '언제' 발생했는지"를 알 수 없다는 것입니다. 악보에 '도, 미, 솔'이 있다는 것은 알지만, 순서대로 친 것인지 동시에 친 화음인지 알 길이 없는 것이죠.
이를 해결하기 위해 윈도우 함수를 짧게 잘라가며 시간의 흐름에 따라 주파수 변화를 추적하는 기술이 등장합니다. 다음 시간에는 오디오 및 음성 분석의 꽃이라 불리는 [12. STFT와 스펙트로그램: 시간에 따라 변하는 주파수 분석 (오디오 분석)] 편을 통해 시간과 주파수를 동시에 꿰뚫어 보는 방법에 대해 다루어 보겠습니다.
📌 이전 연재 글 보기
- [08] 양자화(Quantization): 비트 깊이(Bit-depth)에 따른 오차 분석
- [09] 푸리에 변환(DFT/FFT): 시간에서 주파수로, 물리적 의미의 재해석
- [10] FFT 심화: 진폭/위상 스펙트럼 및 PSD(Power Spectral Density) 해석
