들어가며
이전 시간에는 시간 영역에서 필터가 신호에 작용하는 원리를 컨볼루션(Convolution)이라는 연산을 통해 살펴보았습니다. 출력이 과거 입력값들의 가중 합산이라는 사실, 그리고 임펄스 응답(Impulse Response)이 시스템의 특성을 완전히 기술한다는 사실을 확인했습니다.
하지만 한 가지 근본적인 문제에 직면하게 됩니다. 시간 축에서 두 신호를 일일이 곱하고 더하는 컨볼루션 연산은 직관적이긴 하지만, 시스템이 복잡해질수록 계산량이 기하급수적으로 늘어납니다. 또한, 시간 영역의 신호 모양만으로는 이 필터가 특정 주파수를 어떻게 통과시키는지, 혹은 이 시스템이 안정적인지 불안정한지 한눈에 파악하기가 매우 어렵습니다.
이 복잡한 시간 영역의 컨볼루션을 단순한 주파수 영역의 '곱셈'으로 변환하고, 필터 분석을 체계화하는 강력한 수학적 도구가 필요합니다. 연속 시간 신호에 라플라스 변환(Laplace Transform)이 있다면, 이산 시간 신호(Discrete-time Signal)에는 바로 Z-변환(Z-Transform)이 있습니다.
이번 편에서는 Z-변환의 정의와 수렴 영역(ROC), 역 Z-변환, 주요 성질, 그리고 단위 원(Unit Circle)과 DTFT의 관계를 Python 시각화와 함께 차근차근 살펴보겠습니다.
1. Z-변환의 정의와 물리적 의미
Z-변환은 시간 영역의 이산 신호 $x[n]$을 복소수 평면인 Z-도메인(Z-domain)으로 매핑하는 수학적 연산입니다. 수학적 정의는 다음과 같습니다.
여기서 $z$는 복소수(Complex Number) 변수입니다. 복소수는 크기(magnitude)와 위상(phase)을 가지므로, 극좌표계를 사용하여 다음과 같이 표현할 수 있습니다.
위 수식에서 $r$은 크기(magnitude), $\omega$는 각주파수(angular frequency)입니다. 이를 Z-변환 공식에 대입하면 다음과 같은 형태가 됩니다.
이 수식은 매우 중요한 물리적 의미를 담고 있습니다.
단순한 이산 신호 $x[n]$을 분석하는 것이 아니라, 원래 신호에 $r^{-n}$이라는 지수 함수(Exponential function)를 곱한 새로운 신호를 푸리에 변환하는 것과 같습니다. 지수 함수를 곱함으로써, 진동하거나 발산하여 원래는 푸리에 변환을 할 수 없는 불안정한 신호들까지도 분석할 수 있게 만들어 주는 것이 바로 Z-변환의 진정한 힘입니다.
실제 인과(causal) 시스템을 다룰 때는 $n < 0$인 구간에서 $x[n] = 0$인 단방향(unilateral) Z-변환을 주로 사용합니다.
$$X(z) = \sum_{n=0}^{\infty} x[n] z^{-n}$$
간단한 예시: 단위 임펄스와 단위 계단 신호
가장 기본적인 두 신호의 Z-변환을 직접 계산해 보겠습니다.
단위 임펄스 신호 $\delta[n]$:
$n = 0$일 때만 $\delta[0] = 1$이므로, Z-변환은 단순히 1이 됩니다. 모든 $z$에서 수렴합니다.
단위 계단 신호 $u[n]$:
이 급수는 등비급수 공식으로 계산되며, $|z^{-1}| < 1$, 즉 $|z| > 1$인 조건에서만 수렴합니다.
2. 수렴 영역 (ROC, Region of Convergence)
Z-변환 공식은 $n = -\infty$부터 $\infty$까지의 무한급수입니다. 무한히 더하는 연산이기 때문에, 이 합이 무한대로 발산하지 않고 특정한 유한한 값을 가지기 위한 조건이 반드시 필요합니다.
급수가 발산하지 않고 수렴하도록 하여 유한한 값을 만드는 복소수 $z$의 크기($|z| = r$) 범위를 수렴 영역(Region of Convergence, ROC)이라고 부릅니다.
ROC는 다음과 같은 중요한 특성을 가집니다.
- ROC는 원환(annular region) 형태입니다: $r_1 < |z| < r_2$
- 유한 길이(FIR) 신호의 ROC는 $z = 0$ 또는 $z = \infty$를 제외한 전체 $z$ 평면인 경우가 많습니다.
- 인과적이고 안정적인 시스템의 경우, ROC는 단위 원(Unit Circle, $|z| = 1$)을 포함해야 합니다.
- ROC를 명시하지 않으면 Z-변환만으로는 원래 신호를 유일하게 복원할 수 없습니다.
ROC는 신호의 특성에 따라 다음과 같이 형태가 달라집니다.
- 인과 신호 (Causal Sequence): $n \ge 0$에서만 값이 있는 신호. ROC는 어떤 원의 바깥쪽이 됩니다. ($|z| > a$)
$$x_1[n] = a^n u[n] \quad \Leftrightarrow \quad X(z) = \frac{z}{z - a}, \quad |z| > |a| $$ - 반인과 신호 (Anti-causal Sequence): $n < 0$에서만 값이 있는 신호. ROC는 어떤 원의 안쪽이 됩니다. ($|z| < a$)
$$x_2[n] = -a^n u[-n - 1] \quad \Leftrightarrow \quad X(z) = \frac{z}{z - a}, \quad |z| < |a| $$ - 양방향 신호 (Two-sided Sequence): 양쪽 모두에 값이 있는 신호. ROC는 두 원 사이의 고리(Ring) 형태가 됩니다. ($a < |z| < b$)
이처럼 ROC는 X(z)의 수식만큼이나 중요한 정보입니다. 아래 Python 코드로 복소 평면 위에 ROC를 시각화해 보겠습니다.
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
fig.suptitle('Z-변환의 수렴 영역(ROC) 비교', fontsize=14, fontweight='bold')
a = 0.5
theta = np.linspace(0, 2 * np.pi, 500)
# --- 좌측: 인과 신호 ROC: |z| > 0.5 (바깥 무한 영역) ---
ax = axes[0]
# 전체 배경을 ROC 색으로 채움
ax.set_facecolor('skyblue')
ax.patch.set_alpha(0.3)
# 극점 안쪽 원(|z| ≤ 0.5)을 흰색으로 덮어서 제외 영역 표현
ax.fill(a * np.cos(theta), a * np.sin(theta), color='white')
# 단위 원
ax.plot(np.cos(theta), np.sin(theta), 'k--', linewidth=1.5, label='단위 원 (|z|=1)')
# 극점
ax.plot(a, 0, 'rx', markersize=12, markeredgewidth=2, label=f'극점 (z={a})')
# 경계 원
ax.plot(a * np.cos(theta), a * np.sin(theta), 'b-', linewidth=1.2, label=f'|z|={a} 경계')
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
ax.set_aspect('equal')
ax.axhline(0, color='gray', linewidth=0.5)
ax.axvline(0, color='gray', linewidth=0.5)
ax.set_xlabel('실수축 (Re)')
ax.set_ylabel('허수축 (Im)')
ax.set_title('인과 신호: $x_1[n] = (0.5)^n u[n]$\nROC: |z| > 0.5 (외부 무한 영역)', fontsize=11)
ax.legend(loc='upper right', fontsize=9)
ax.grid(True, linestyle=':', alpha=0.5)
# --- 우측: 반인과 신호 ROC: |z| < 0.5 (안쪽 유한 영역) ---
ax = axes[1]
# 안쪽 원만 ROC 색으로 채움
ax.fill(a * np.cos(theta), a * np.sin(theta), color='salmon', alpha=0.3, label='ROC 영역')
# 단위 원
ax.plot(np.cos(theta), np.sin(theta), 'k--', linewidth=1.5, label='단위 원 (|z|=1)')
# 극점
ax.plot(a, 0, 'rx', markersize=12, markeredgewidth=2, label=f'극점 (z={a})')
# 경계 원
ax.plot(a * np.cos(theta), a * np.sin(theta), 'b-', linewidth=1.2, label=f'|z|={a} 경계')
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
ax.set_aspect('equal')
ax.axhline(0, color='gray', linewidth=0.5)
ax.axvline(0, color='gray', linewidth=0.5)
ax.set_xlabel('실수축 (Re)')
ax.set_ylabel('허수축 (Im)')
ax.set_title('반인과 신호: $x_2[n] = -(0.5)^n u[-n-1]$\nROC: |z| < 0.5 (내부 유한 영역)', fontsize=11)
ax.legend(loc='upper right', fontsize=9)
ax.grid(True, linestyle=':', alpha=0.5)
plt.tight_layout()
plt.savefig('roc_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

좌측 그래프는 인과 신호의 ROC로, 극점 $z = 0.5$ 바깥쪽($|z| > 0.5$) 영역이 ROC이며 단위 원을 포함합니다. 우측은 반인과 신호의 ROC로, 극점 안쪽($|z| < 0.5$) 영역이 ROC이며 단위 원을 포함하지 않습니다.
3. Z-변환의 기본 성질
Z-변환은 선형 변환이므로 디지털 필터 설계와 시스템 분석을 쉽게 만들어주는 강력한 수학적 성질들을 가지고 있습니다.
3.1. 선형성 (Linearity)
시간 영역에서 두 신호를 더하거나 상수를 곱한 것은 Z-도메인에서도 동일하게 유지됩니다. ROC는 두 신호의 ROC의 교집합 이상이 됩니다.
$$\mathcal{Z}\{a \cdot x_1[n] + b \cdot x_2[n]\} = a \cdot X_1(z) + b \cdot X_2(z)$$
3.2. 시간 지연 (Time Delay)
DSP에서 가장 빈번하게 사용되는 성질입니다. 시간 축에서 신호를 1샘플 지연시키는 것은 Z-도메인에서 $z^{-1}$을 곱하는 것과 완벽히 동일합니다.
$$\mathcal{Z}\{x[n - k]\} = z^{-k} X(z)$$
디지털 필터를 소프트웨어로 구현할 때 과거의 데이터($x[n-1]$ 등)를 저장해두는 버퍼 메모리들이 수식에서는 모조리 $z^{-1}$로 치환되어, 복잡한 미적분이나 차분 방정식이 단순한 대수학 연산으로 바뀝니다.
3.3. 컨볼루션과 곱셈의 대응
시간 영역의 컨볼루션은 Z-변환 영역에서 단순 곱셈으로 변환됩니다. 이 성질 덕분에 필터의 동작을 전달함수 $H(z)$로 간결하게 표현할 수 있습니다.
$$\mathcal{Z}\{x[n] * h[n]\} = X(z) \cdot H(z)$$
3.4. z-영역 스케일링
신호에 지수 시퀀스 $a^n$을 곱하면, Z-변환에서는 $z$를 $z/a$로 대체하는 것에 해당합니다.
$$\mathcal{Z}\{a^n x[n]\} = X \left( \frac{z}{a} \right)$$
4. 역 Z-변환 (Inverse Z-Transform)
Z-도메인에서 분석이나 필터 설계를 마쳤다면, 이를 다시 시간 영역의 신호나 소프트웨어 코드로 돌려놓아야 합니다. Z-변환의 역연산, 즉 $X(z)$로부터 원래 신호 $x[n]$을 복원하는 방법을 역 Z-변환이라고 합니다. 수학적인 정의는 복소 평면상의 폐곡선 적분(Contour Integration)을 사용하지만, 실무에서는 거의 계산하지 않습니다.
대신 부분 분수 전개법(Partial Fraction Expansion)을 주로 사용합니다. 복잡한 형태의 $X(z)$를 이미 결과를 알고 있는 단순한 기본 분수들의 합으로 쪼갠 뒤, Z-변환 표(Table)를 보고 역으로 시간 영역 신호 $x[n]$을 조립하는 방식입니다.
예를 들어 다음과 같은 $X(z)$를 생각해 보겠습니다.
이를 부분 분수로 분해하면:
계수를 계산하면 $A = 2, B = -1$이 되고, 따라서:
Python 환경에서는 scipy.signal.residuez 함수를 사용하여 이러한 부분 분수 전개를 단 한 줄의 코드로 쉽게 처리할 수 있습니다.
import numpy as np
from scipy import signal
# X(z) = 1 / [(1 - 0.5z^{-1})(1 - 0.25z^{-1})]
# 분자: 1, 분모: (1 - 0.5z^{-1})(1 - 0.25z^{-1}) 전개
# 분모 = 1 - 0.75z^{-1} + 0.125z^{-2}
b = [1] # 분자 계수
a = [1, -0.75, 0.125] # 분모 계수
r, p, k = signal.residuez(b, a)
print("=== 부분 분수 분해 결과 ===")
for i, (res, pole) in enumerate(zip(r, p)):
print(f" 항 {i+1}: 잔류(residue) = {res:.4f}, 극점(pole) = {pole:.4f}")
print(f" 직접항(k) = {k}")
# 역 Z-변환 결과 검증: x[n] = 2*(0.5)^n*u[n] - 1*(0.25)^n*u[n]
n = np.arange(20)
x_reconstructed = r[0] * (p[0] ** n) + r[1] * (p[1] ** n)
# 임펄스 응답과 비교
impulse_input = np.zeros(20)
impulse_input[0] = 1
x_impulse = signal.lfilter(b, a, impulse_input)
print("\n=== 역 Z-변환 검증 (처음 10샘플) ===")
print(f" 부분 분수 복원: {x_reconstructed[:10].round(4)}")
print(f" 임펄스 응답: {x_impulse[:10].round(4)}")
=== 부분 분수 분해 결과 ===
항 1: 잔류(residue) = -1.0000, 극점(pole) = 0.2500
항 2: 잔류(residue) = 2.0000, 극점(pole) = 0.5000
직접항(k) = []
=== 역 Z-변환 검증 (처음 10샘플) ===
부분 분수 복원: [1. 0.75 0.4375 0.2344 0.1211 0.0615 0.031 0.0156 0.0078 0.0039]
임펄스 응답: [1. 0.75 0.4375 0.2344 0.1211 0.0615 0.031 0.0156 0.0078 0.0039]
실행 결과로 얻은 부분 분수 분해 결과로부터 역변환된 임펄스 응답은 다음과 같습니다.
여기서 $\color{red}{R_1 = 2}, \color{red}{R_2 = -1}$이 바로 잔류(residue)입니다.
각 항의 역 Z-변환 공식은 다음과 같습니다.
따라서 최종 역 Z-변환 결과는:
즉 잔류는 각 지수 성분의 가중치(크기와 부호)를 결정합니다.
또한, 위 코드의 실행 결과로부터 부분 분수로 복원한 값과 임펄스 응답이 완전히 일치하는 것을 확인할 수 있습니다.
5. 주요 Z-변환 쌍 정리
자주 사용하는 Z-변환 쌍을 표로 정리합니다. 역 Z-변환 시 참고 기준으로 활용하시기 바랍니다.
| 시간 영역 $x[n]$ | Z-변환 $X(z)$ | ROC |
|---|---|---|
| $\delta[n]$ | $1$ | 전체 $z$ 평면 |
| $u[n]$ | $\displaystyle \frac{z}{z - 1}$ | $|z| > 1$ |
| $a^n u[n]$ | $\displaystyle \frac{z}{z - a}$ | $|z| > |a|$ |
| $n \cdot a^n u[n]$ | $\displaystyle \frac{az^{-1}}{(1 - az^{-1})^2}$ | $|z| > |a|$ |
| $\cos(\omega_0 n) u[n]$ | $\displaystyle \frac{1 - \cos(\omega_0)z^{-1}}{1 - 2\cos(\omega_0)z^{-1} + z^{-2}}$ | $|z| > 1$ |
| $r^n \cos(\omega_0 n) u[n]$ | $\displaystyle \frac{1 - r \cos(\omega_0)z^{-1}}{1 - 2r \cos(\omega_0)z^{-1} + r^2 z^{-2}}$ | $|z| > r$ |
6. Z-변환과 단위원, 그리고 DTFT
앞서 복소수 $z = r e^{j\omega}$라고 정의했습니다. 만약 크기 $r$이 1이라면 어떻게 될까요?
복소 평면에서 원점에서 거리가 1인 점들의 모임을 **단위원(Unit Circle, $|z|=1$)**이라고 합니다.
Z-변환 공식에 $z = e^{j\omega}$를 대입해 보겠습니다.
이 수식은 신호 처리에서 주파수 분석을 위해 사용하는 이산 시간 푸리에 변환(DTFT, Discrete-Time Fourier Transform) 공식과 완전히 동일합니다.
즉, DTFT는 Z-변환을 단위원($|z|=1$) 위에서만 평가한 특수한 경우입니다. 시스템의 주파수 응답을 알고 싶다면 Z-도메인의 단위원 위를 한 바퀴 걸어 다니며 그 값을 읽어내기만 하면 됩니다. 단, 이 주파수 응답이 유효하려면 시스템의 ROC 안에 단위원이 반드시 포함되어 있어야만 합니다. 이 관계는 다음과 같은 중요한 의미를 가집니다.
- 단위 원(Unit Circle)이 ROC 안에 있으면 DTFT가 존재하며, 시스템이 안정적(Stable)임을 의미합니다.
- 단위 원 위의 $z$ 값은 각각의 정규화 주파수 $\omega \in [-\pi, \pi]$에 대응됩니다.
- 복소 평면에서 극점(pole)이 단위 원에 가까울수록 해당 주파수 근방에서 주파수 응답의 크기가 커집니다.
- 반대로 영점(zero)이 단위 원 위에 있으면 해당 주파수 성분이 완전히 제거됩니다.
아래 Python 코드로 단위 원과 주파수 응답의 관계를 함께 시각화해 보겠습니다.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# 간단한 IIR 필터 정의: 극점 r=0.9, 각도 π/4 (π/4 대역 강조 필터)
r = 0.9
omega0 = np.pi / 4
# 켤레 극점 쌍: z = r*e^{±j*omega0}
# 분모: (z - r*e^{jω0})(z - r*e^{-jω0}) = z^2 - 2r*cos(ω0)*z + r^2
a = [1, -2 * r * np.cos(omega0), r**2]
b = [1]
# 주파수 응답
w, h = signal.freqz(b, a, worN=512)
# 극점/영점
zeros, poles, _ = signal.tf2zpk(b, a)
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
fig.suptitle('단위 원과 DTFT(주파수 응답)의 관계', fontsize=14, fontweight='bold')
# --- 좌측: 극점·영점 및 단위 원 ---
ax1 = axes[0]
theta = np.linspace(0, 2 * np.pi, 500)
ax1.plot(np.cos(theta), np.sin(theta), 'k--', linewidth=1.5, label='단위 원')
# 극점
ax1.plot(poles.real, poles.imag, 'rx', markersize=12,
markeredgewidth=2, label=r'극점 ($r=0.9$, $\omega_0=\pi/4$)')
# 영점 (z=0)
ax1.plot(zeros.real, zeros.imag, 'bo', markersize=10,
markerfacecolor='none', markeredgewidth=2, label='영점')
# 단위 원 위의 샘플 주파수 표시
sample_omegas = [0, np.pi/4, np.pi/2, 3*np.pi/4, np.pi]
for om in sample_omegas:
ax1.plot(np.cos(om), np.sin(om), 'g^', markersize=8)
ax1.annotate(f'ω={om/np.pi:.2f}π',
(np.cos(om), np.sin(om)),
textcoords='offset points', xytext=(8, 5), fontsize=8)
ax1.set_xlim(-1.5, 1.5)
ax1.set_ylim(-1.5, 1.5)
ax1.set_aspect('equal')
ax1.axhline(0, color='gray', linewidth=0.5)
ax1.axvline(0, color='gray', linewidth=0.5)
ax1.set_xlabel('실수축 (Re)')
ax1.set_ylabel('허수축 (Im)')
ax1.set_title('복소 z-평면: 극점·영점 분포')
ax1.legend(fontsize=9, loc='lower right')
ax1.grid(True, linestyle=':', alpha=0.5)
# --- 우측: 주파수 응답 (크기) ---
ax2 = axes[1]
ax2.plot(w / np.pi, 20 * np.log10(np.abs(h)), 'b-', linewidth=2)
ax2.axvline(omega0 / np.pi, color='r', linestyle='--',
label=r'극점 주파수 $\omega_0=\pi/4$ (0.25$\pi$)')
ax2.set_xlabel('정규화 주파수 (×π rad/sample)')
ax2.set_ylabel('크기 (dB)')
ax2.set_title('주파수 응답: |H(e^{jω})|')
ax2.legend(fontsize=9)
ax2.grid(True, linestyle=':', alpha=0.5)
ax2.set_xlim(0, 1)
plt.tight_layout()
plt.savefig('unit_circle_dtft.png', dpi=150, bbox_inches='tight')
plt.show()

좌측 그래프에서 극점은 단위 원의 안쪽($r = 0.9$), 정규화 주파수 $\omega_0 = \pi/4$ 방향에 위치합니다. 우측 그래프의 주파수 응답을 보면 $\omega = 0.25\pi$ 부근에서 응답 크기가 최대가 되는 것을 확인할 수 있습니다. 극점이 단위 원에 가까울수록 해당 주파수의 이득(gain)이 증가한다는 원리가 그대로 반영된 결과입니다.
7. 종합 예제: Z-변환으로 차분 방정식 분석하기
실제 필터 설계에서는 Z-변환을 이용하여 차분 방정식(Difference Equation)을 전달함수 $H(z)$로 변환하고, 이를 기반으로 시스템의 특성을 분석합니다.
다음 차분 방정식을 생각해 보겠습니다.
양변에 Z-변환을 적용하고, 시간 지연 성질 $\mathcal{Z}\{y[n - 1]\} = z^{-1} Y(z)$를 이용합니다.
이 시스템의 임펄스 응답은 $h[n] = (0.5)^n u[n]$으로, 지수적으로 감쇠하는 안정적인 시스템입니다. Python으로 전달함수로부터 시스템의 동작을 직접 확인해 보겠습니다.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# 전달함수 H(z) = 1 / (1 - 0.5z^{-1})
b = [1] # 분자: 1
a = [1, -0.5] # 분모: 1 - 0.5z^{-1}
n_samples = 30
n = np.arange(n_samples)
# 이론적 임펄스 응답
h_theory = (0.5) ** n
# scipy로 계산한 임펄스 응답
impulse_input = np.zeros(n_samples)
impulse_input[0] = 1
h_scipy = signal.lfilter(b, a, impulse_input)
# 주파수 응답
w, H = signal.freqz(b, a, worN=512)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
fig.suptitle('H(z) = z/(z-0.5) 시스템 분석', fontsize=13, fontweight='bold')
# 임펄스 응답
ax1 = axes[0]
ax1.stem(n, h_scipy, linefmt='b-', markerfmt='bo', basefmt='gray')
ax1.plot(n, h_theory, 'r--', linewidth=1.5, label='이론값 $(0.5)^n$')
ax1.set_title('임펄스 응답 h[n]')
ax1.set_xlabel('샘플 인덱스 n')
ax1.set_ylabel('진폭')
ax1.legend(fontsize=9)
ax1.grid(True, linestyle=':', alpha=0.5)
# 주파수 응답 (크기)
ax2 = axes[1]
ax2.plot(w / np.pi, 20 * np.log10(np.abs(H) + 1e-10), 'b-', linewidth=2)
ax2.set_title('주파수 응답 크기 |H(e^{jω})|')
ax2.set_xlabel('정규화 주파수 (×π rad/sample)')
ax2.set_ylabel('크기 (dB)')
ax2.grid(True, linestyle=':', alpha=0.5)
ax2.set_xlim(0, 1)
# 극점·영점 및 단위 원
ax3 = axes[2]
theta_c = np.linspace(0, 2 * np.pi, 500)
ax3.plot(np.cos(theta_c), np.sin(theta_c), 'k--', linewidth=1.5, label='단위 원')
zeros_pz, poles_pz, _ = signal.tf2zpk(b, a)
ax3.plot(poles_pz.real, poles_pz.imag, 'rx', markersize=12,
markeredgewidth=2.5, label=f'극점 (z=0.5)')
if len(zeros_pz) > 0:
ax3.plot(zeros_pz.real, zeros_pz.imag, 'bo', markersize=10,
markerfacecolor='none', markeredgewidth=2, label='영점')
ax3.set_xlim(-1.5, 1.5)
ax3.set_ylim(-1.5, 1.5)
ax3.set_aspect('equal')
ax3.axhline(0, color='gray', linewidth=0.5)
ax3.axvline(0, color='gray', linewidth=0.5)
ax3.set_title('극점·영점 분포 (z-평면)')
ax3.set_xlabel('실수축 (Re)')
ax3.set_ylabel('허수축 (Im)')
ax3.legend(fontsize=9)
ax3.grid(True, linestyle=':', alpha=0.5)
plt.tight_layout()
plt.savefig('difference_equation_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
# 안정성 판별
print("=== 시스템 안정성 판별 ===")
for i, pole in enumerate(poles_pz):
mag = np.abs(pole)
status = "안정 (|z| < 1)" if mag < 1 else "불안정 (|z| ≥ 1)"
print(f" 극점 {i+1}: z = {pole:.4f}, |z| = {mag:.4f} → {status}")
그래픽 출력에서는 임펄스 응답과 주파수 응답을 시간축·주파수축으로 각각 도시하였고, 안정성 정보를 시각적으로 보여주기 위해 단위원과 극점을 도시하였습니다. 극점이 단위 원 안쪽($|z| < 1$)에 위치하므로 시스템은 안정적입니다. 임펄스 응답도 지수적으로 0에 수렴하는 것을 확인할 수 있습니다.

다음은 코드 내에서 시스템의 안정성을 판별하기 위해 극점의 위치가 단위원 내부에 있는지 외부에 있는지 판별하여 안정성 결과를 출력한 내용입니다.
=== 시스템 안정성 판별 ===
극점 1: z = 0.5000, |z| = 0.5000 → 안정 (|z| < 1)
[Insight]
시간 영역에서 $y[n] = x[n] * h[n]$ 이라는 무겁고 골치 아픈 컨볼루션 연산은 Z-변환을 거치고 나면 단순한 곱셈이 됩니다.
우리는 시간의 흐름이라는 1차원적인 시야에서 벗어나, Z-평면이라는 2차원 공간 위로 올라오게 되었습니다. 이 2차원 지도 위에서 시스템(필터)은 특정한 위치에 봉우리(극점, Pole)를 세우고 골짜기(영점, Zero)를 파놓은 지형도와 같습니다.
단순히 수식을 계산하기 위해 Z-변환을 도입한 것이 아닙니다. 이 지형도를 내려다봄으로써 우리는 복잡한 계산 없이도 봉우리의 위치만 보고 "이 시스템은 발산하여 터져버리겠군", 혹은 "이 필터는 저주파 대역을 통과시키겠군" 하고 직관적으로 시스템의 운명을 예측할 수 있게 됩니다. 이것이 제어 공학과 신호 처리에서 Z-변환이 가지는 강력한 통찰입니다.
다음 글 예고
이번 편에서는 Z-변환의 정의와 ROC, 역 Z-변환, 그리고 단위 원과 DTFT의 관계를 살펴보았습니다. 특히 극점의 위치가 시스템의 안정성과 주파수 응답을 동시에 결정한다는 점을 확인하였습니다. Z-변환이 제공하는 2차원 지도 위에서 필터의 특성을 파악할 수 있는 기반을 다졌습니다.
다음 시간에는 [16. Z-변환 응용: 전달함수 H(z), 극점·영점과 필터 안정성] 편을 통해, Z-변환을 실제 필터 설계에 적용하는 방법을 더욱 깊이 다루겠습니다. 극점·영점 배치를 직접 설계하여 원하는 주파수 응답을 만들어 내는 과정을 Python 시뮬레이션과 함께 살펴보겠습니다.
