Profile picture

[Python] 데코레이터(Decorator) 실습 - Feat. args, kwargs, JWT

JaehyoJJAng2025년 12월 15일

개요

최근 웹 개발(Flask)을 공부하면서 로그인 기능을 구현하다가 벽에 부딪혔습니다.


JWT 개념도 낯선데, 그걸 감싸고 있는 데코레이터의 구조는 더 이해가 안갔거든요.


특히 wrapper 함수가 *args, **kwargs를 받는데, 어떻게 원래 함수의 인자와 딱 맞물려 돌아가는지 그 원리가 헷갈렸습니다.


데코레이터의 구조를 파악하기 위해 실습 내용을 정리해보겠습니다.


데코레이터(Decorator) ?

파이썬에서의 데코레이터는 기존 함수를 수정하지 않고 그 기능을 확장하는 방법을 제공합니다.


기존 함수 위에 "포장지"를 감싼다고 생각하면 됩니다.


데코레이터는 @ 기호를 사용하여 정의되며, 함수나 메소드 앞에 위치합니다!


한 가지 예시를 들어보겠습니다.

def greet_decorator(func):
    def wrapper():
        print("함수 실행 전에 무언가 로직을 실행합니다..")
        func() # 인자로 받은 함수(func)를 실행합니다.
        print("함수 실행 후 무언가 로직을 실행합니다..")
    return wrapper

@greet_decorator
def greet(name: str) -> str:
    return f"안녕하세요 {name}님!"

greet(name="Sarah")

위 예제에서 greet_decorator는 decorator이며, greet 함수 앞에 @ 기호와 함께 위치하고 있습니다.


이를 통해 greet 함수는 greet_decorator에 인자로 넘겨지게 되며, 함수가 호출 전후에 docorator 내에 정의된 추가 기능이 실행됩니다.


위에서는 func 전후에 print 문이 실행이 되겠네요.


단계별로 풀어서 데코레이터 실행 흐름을 살펴보도록 하겠습니다.


1. 호출: 실제로는 wrapper가 호출됨.

# 사용자가 함수를 호출함
result = greet(name="Sarah")

이때, 데코레이터에 의해 실제로는 greet라는 이름표를 달고 있는 wrapper 함수가 호출됩니다.


2. wrapper 내부의 '포장 (Packing)'

wrapper는 정의될 때 (*args, **kwargs)를 인자로 받습니다.

이건 뭐가 들어오든지 다 받아주겠다 는 뜻입니다.

파이썬은 들어온 인자들을 정리해서 변수에 싹 담습니다(packing)

  • args: 위치 인자들을 튜플로 묶음 -> 위 예제에서는 위치 인자들이 정의되지 않았기에 없겠죠? -> ()
  • kwargs: 키워드 인자들을 딕셔너리로 묶습니다 -> {'name': 'Sarah'}

3. 원본 함수 호출 시 '풀기 (Unpacking)'

이제 wrapper 안에서 원본 함수(func)를 호출할 차례입니다.

func(*args, **kwargs) 코드를 만나면 파이썬은 묶어놨던 보따리를 풉니다.


  • args: 튜플을 풀어서 쉼표로 구분된 값으로 만듬. (빈 튜플이라 변화없음)
  • *kwargs: 딕셔너리 {'name': 'Sarah'}를 풀어서 Key=Value 형태로 만듭니다.

즉, 실제로 실행되는 코드는 다음과 같이 변환되겠네요.

# 코드 상: func(*args, **kwargs)
# 실제 실행: func(name="재효")  <-- 원본 함수가 원하던 형태 그대로!

데코레이터 활용 사례

문법적으로는 대충 알겠는데, "굳이 이걸 왜 써야 하는지" 와닿지 않죠?


JWT(Json Web Token) 기반의 로그인 인증 시스템을 구현해보면서,

데코레이터가 없는 코드와 데코레이터를 적용한 코드를 비교해보고, 데코레이터의 가치를 알아보겠습니다.


1. 데코레이터가 없다면?

만약 우리 서비스에 로그인이 필요한 기능이 3개(비밀 데이터 조회, 내 정보 수정, 회원 탈퇴)가 있다고 가정해 봅시다.

데코레이터 없이 구현하면 모든 함수마다 인증 로직을 복사/붙여넣기 해야 합니다.


데코레이터를 도입하지 않았을 때 어떻게 되는지 코드로 살펴보시죠.

@app.route('/secret-data')
def get_secret_data():
    # === [반복 구간] 인증 로직 시작 ===
    token = request.headers.get('Authorization')
    if not token: return jsonify({'msg': '로그인 하세요'}), 401
    try:
        jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    except:
        return jsonify({'msg': '유효하지 않은 토큰'}), 401
    # === [반복 구간] 인증 로직 끝 ===

    return jsonify({'data': '비밀 데이터'})

@app.route('/update-profile')
def update_profile():
    # === [또 복사] 인증 로직 ===
    # ... (위와 똑같은 10줄의 코드) ...
    return jsonify({'msg': '수정 완료'})

위 코드의 문제점이 뭘까요?

  • 코드 중복: 보안이 필요한 API가 100개라면? 똑같은 코드를 100번 복사해야 합니다.
  • 유지보수 지옥: 보안 정책이 바뀌면 100군데를 일일이 찾아다니며 수정해야 합니다.
  • 가독성 저하: 함수는 '자기 할 일(비즈니스 로직)'에 집중해야 하는데, 정작 중요한 코드는 보안 검사에 밀려 보이지 않게 됩니다.

2. 데코레이터가 있다면?

데코레이터를 사용하면 이 문제를 단숨에 해결할 수 있습니다.

인증 로직을 딱 한 번만 정의해두고 필요한 함수 위에 스티커처럼 붙이기만 하면 됩니다.

@app.route("/secret-data")
@token_required # <-- "야, 이 방(get_secret_dat) 들어가려면 검문부터 받고와!"
def get_secret_data():
    # 인증 코드는 싹 사라지고, 진짜 할 일만 남음!
    return jsonify({"data": "비밀 데이터"})

비유하자면 건물 1층 로비에 통합 보안 검색대를 하나만 설치한 것입니다.

모든 사람은 로비를 통과해야만 사무실로 갈 수 있고, 각 사무실은 "신분증 검사"는 신경 끄고 "자기 업무"에만 집중할 수 있습니다.


실전 구현: Flask + JWT로 만드는 인증 시스템

1. 백엔드 구현

@token_required라는 데코레이터를 만들고, 이를 /secret-data 라우트에 적용하겠습니다.

from flask import Flask, request, jsonify, render_template
import jwt
import datetime
from functools import wraps

app = Flask(__name__)
app.config['SECRET_KEY'] = 'my_secret_key'  # 실무에선 환경변수로 관리 필수!

# ---------------------------------------------------------
# [핵심] 데코레이터: JWT 검문소
# ---------------------------------------------------------
def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None

        # 프론트엔드 헤더에서 'Authorization: Bearer <토큰>' 확인
        if 'Authorization' in request.headers:
            auth_header = request.headers['Authorization']
            if " " in auth_header:
                token = auth_header.split(" ")[1] # 'Bearer' 제거

        if not token:
            return jsonify({'message': '토큰이 없습니다! 로그인 해주세요.'}), 401

        try:
            # 토큰 해독 및 검증
            jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
        except:
            return jsonify({'message': '토큰이 유효하지 않거나 만료되었습니다.'}), 401

        return f(*args, **kwargs) # 통과! 원본 함수 실행
    return decorated

# ---------------------------------------------------------
# 라우트 (API)
# ---------------------------------------------------------
@app.route('/')
def home():
    return render_template('index.html')

@app.route('/login', methods=['POST'])
def login():
    data = request.json
    # 테스트용 계정 (admin / 1234)
    if data.get('username') == 'admin' and data.get('password') == '1234':
        token = jwt.encode({
            'user': 'admin',
            'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
        }, app.config['SECRET_KEY'], algorithm="HS256")
        
        return jsonify({'token': token})

    return jsonify({'message': '로그인 실패!'}), 401

# ★ 여기에 우리가 만든 데코레이터가 붙습니다!
@app.route('/secret-data')
@token_required
def get_secret_data():
    return jsonify({
        'message': '성공! 당신은 VIP 회원입니다.',
        'data': [100, 200, 300, 400]
    })

if __name__ == '__main__':
    app.run(debug=True, port=5000)

2. 프론트엔드 구현 (templates/index.html)

자바스크립트가 하는 일은 딱 3가지입니다.

  1. 로그인 성공 시 받은 토큰을 브라우저(localStorage)에 저장.
  2. 보안 데이터 요청 시 헤더(Authorization)에 토큰을 실어 보냄
  3. 로그아웃 시 토큰 삭제
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>JWT 로그인 실습</title>
    <style>
        body { font-family: 'Arial', sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f2f5; margin: 0; }
        .container { background: white; padding: 2rem; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 300px; text-align: center; }
        input { width: 90%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }
        button { width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1rem; }
        button:hover { background-color: #0056b3; }
        .hidden { display: none; }
        #result-box { margin-top: 20px; padding: 10px; background: #e9ecef; border-radius: 5px; font-size: 0.9rem; color: #333; min-height: 50px; }
    </style>
</head>
<body>
    <div class="container">
        <h2>🔐 보안 시스템</h2>
        <div id="login-section">
            <input type="text" id="username" placeholder="아이디 (admin)">
            <input type="password" id="password" placeholder="비밀번호 (1234)">
            <button onclick="login()">로그인</button>
        </div>
        <div id="dashboard-section" class="hidden">
            <p>환영합니다, 관리자님!</p>
            <button onclick="getSecretData()" style="background-color: #28a745; margin-bottom: 10px;">비밀 데이터 가져오기</button>
            <button onclick="logout()" style="background-color: #dc3545;">로그아웃</button>
        </div>
        <div id="result-box">결과가 여기에 표시됩니다.</div>
    </div>

    <script>
        const API_URL = "http://127.0.0.1:5000";

        async function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;

            const response = await fetch(`${API_URL}/login`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ username, password })
            });
            const data = await response.json();

            if (response.ok) {
                localStorage.setItem('jwt_token', data.token); // 토큰 저장
                alert("로그인 성공!");
                toggleView(true);
            } else { alert(data.message); }
        }

        async function getSecretData() {
            const token = localStorage.getItem('jwt_token');
            const response = await fetch(`${API_URL}/secret-data`, {
                method: 'GET',
                headers: { 'Authorization': `Bearer ${token}` } // 헤더에 토큰 탑승!
            });
            const data = await response.json();
            
            if (response.ok) {
                document.getElementById('result-box').innerText = `서버 응답: ${JSON.stringify(data)}`;
            } else {
                document.getElementById('result-box').innerText = `에러: ${data.message}`;
                if(response.status === 401) logout();
            }
        }

        function logout() {
            localStorage.removeItem('jwt_token'); // 토큰 삭제
            toggleView(false);
            document.getElementById('result-box').innerText = "로그아웃 됨";
        }

        function toggleView(isLoggedIn) {
            document.getElementById('login-section').style.display = isLoggedIn ? 'none' : 'block';
            document.getElementById('dashboard-section').style.display = isLoggedIn ? 'block' : 'none';
        }
    </script>
</body>
</html>

직접 실행해보면 데코레이터가 어떻게 동작하는지 확실히 알 수 있습니다.

  • 실행: app.py 실행 후 http://127.0.0.1:5000 접속.
  • 로그인 전 테스트: 개발자 도구(F12) → Console 확인. localStorage는 비어있습니다.
  • 로그인: 아이디 admin, 비번 1234 입력.
    • 👉 확인: 개발자 도구 → Application → Local Storage에 jwt_token이라는 긴 문자열이 저장됩니다. (출입증 발급 완료!)
  • 데코레이터 작동 확인: [비밀 데이터 가져오기] 버튼 클릭.
    • 👉 작동 원리: 프론트엔드가 헤더에 토큰을 실어 보내면, 백엔드의 @token_required 데코레이터가 이를 낚아채 검사하고 통과시킵니다.
    • 👉 결과: {"data": [100, 200...]} 데이터가 정상적으로 표시됩니다.
  • 로그아웃: localStorage에서 토큰이 삭제되어, 다시 요청해도 데코레이터 선에서 차단됩니다.

Loading script...