7 분 소요

☀️공지 사항☀️ 해당 게시글은 SK 쉴더스 루키즈, 생성형AI 활용 사이버보안 전문인력 양성과정 을 수업을 듣고 정리한 글입니다. 자세한 교육 정보는 SK 쉴더스 홈페이지 를 확인해주세요.

학습 내용 요약 (25/10/31)

1. 예외 처리 (Exception Handling)와 보안

예외는 시스템의 비정상적인 종료를 막고 정상적인 흐름으로 시스템을 종료시키기 위한 방법이며, 이는 가용성(Availability) 측면에서 중요하다.

1.1. 예외 처리 기본 구조

블록 설명
try 예외 발생 가능성이 있는 코드를 포함한다.
except try 블록에서 발생된 예외를 처리하는 영역이다.
else 예외가 발생하지 않았을 때만 수행하는 영역이다.
finally 예외 발생 여부와 상관없이 항상 수행하는 영역이다.

예시 코드 (IndexError 발생):

listData = [1, 2, 3]

try:
    for index in range(len(listData) + 1):
        print(listData[index])
except Exception as e:
    print(f'{e} 예외 발생 함') # list index out of range 예외 발생 함
else:
    print('예외가 발생하지 않았을 때 수행하는 블럭')
finally:
    print('예외 발생 여부와 상관없이 수행하는 블럭')
# 출력: 정상 종료

1.2. 보안적 관점: 입력값 검증과 raise

사용자 입력은 항상 신뢰할 수 없으므로, 유효성 검증(Input Validation)을 통해 부적절한 데이터를 차단하고 명시적인 예외를 발생시켜야 한다.

예시 코드 (사용자 입력 유효성 검증 및 raise):

def getUserInfo():
    ''' 사용자의 정보를 입력받고, 형식 오류나 유효성을 처리하는 기능'''
    try:
        nameData = input('이름을 입력하세요 : ').strip()
        if not nameData.isalpha():
            # 이름에 문자만 포함하지 않으면 명시적으로 ValueError를 발생시킨다.
            raise ValueError('이름은 문자만 포함해야 합니다.')
        
        ageData = input('나이를 입력하세요 : ').strip()
        if not ageData.isdigit():
            raise ValueError('나이는 숫자만 입력하세요.')
        ageData = int(ageData)
        if ageData < 0 or ageData > 120:
            raise ValueError('1 ~ 120 사이의 숫자만 입력하세요.')
            
    except Exception as validationError:
        # 상세한 오류 정보는 로깅하고, 사용자에게는 일반화된 메시지를 제공한다.
        print(f'입력 오류 {validationError}')
        
    finally:
        print('입력 정보를 정상적으로 처리하였습니다.')

1.3. 보안 로깅 (Logging)

서버 환경에서는 print()보다 로그(Log) 기록을 사용하여 정보 노출 취약점을 방지해야 한다.

예시 코드 (로깅을 포함한 예외 처리):

import logging

# 로깅 설정: WARNING 레벨 이상 메시지를 파일에 추가 기록
logging.basicConfig(level=logging.WARNING, 
                    format="%(asctime)s - %(levelname)s - %(message)s", 
                   filename='shieldus.log', 
                   filemode='a',
                   force = True)

def processList(dataList : list) -> list :
    if not isinstance(dataList, list):
        raise TypeError('매개변수 타입은 리스트 형태로 전달해주세요')
        
    resultList = []   
    for idx, data in enumerate(dataList):
        if not isinstance(data, (int,float)):
            # 숫자 타입이 아닐 경우 WARNING 레벨로 로깅
            logging.warning(f'{idx} 에 숫자가 아닌 값이 포함되었습니다. {idx+1}번째 요소 "{data}"의 타입이 {type(data).__name__}입니다.')
            continue
        try:
            resultList.append(data**2)
        except Exception as e:
            # 예상치 못한 예외는 ERROR 레벨로 로깅
            logging.error(f'예상치 못한 예외 발생 : {e}')
    
    return resultList

tmpList = [10, 20, 30, 40, 'seop', 50, 60]
result = processList(tmpList)
# 출력: 2025-10-31 14:23:29,574 - WARNING - 4 에 숫자가 아닌 값이 포함되었습니다. 5번째 요소 "seop"의 타입이 str입니다.
print(result) # [100, 400, 900, 1600, 2500, 3600]

2. 파일 입출력 (File I/O)

파일 입출력은 자원 관리와 예외 처리가 필수적이다.

2.1. with open()을 사용한 파일 읽기

with open() 구문은 파일 객체를 자동으로 close() 처리하여 자원 누수를 방지하므로 권장되는 방식이다.

예시 코드 (자동 자원 해제):

filePath = './data/greeting.txt'.strip()

# with open() 사용: 파일 객체(file)가 블록을 벗어나면 자동 close
with open(filePath, mode='r', encoding='utf-8') as file:
    listLines = file.readlines()
    print(listLines) # ['1번째줄\n', '2\n', '3\n', '4\n', '5']
    
    for text in listLines:
        print(text.strip('\n')) # 줄바꿈 문자 제거하고 출력

2.2. JSON 데이터 처리


파이썬 dict 객체를 파일에 저장하거나 읽으려면 json 모듈을 사용하여 직렬화/역직렬화해야 한다.

예시 코드 (JSON dump 및 load):

import json

dataDict = { 'id':'xxxx', 'pwd':'xxxx' }

# 쓰기 (dump): dict를 json 형식으로 변환하여 파일에 저장
filePathWrite = './data/msg.json'.strip()
with open(filePathWrite, mode='w', encoding='utf-8') as file:
    json.dump(dataDict, file)
    
# 읽기 (load): json 파일을 읽어 dict 객체로 복원
filePathRead = './data/msg.json'.strip()
with open(filePathRead, mode='r', encoding='utf-8') as file:
    loadedData = json.load(file)
    print(loadedData, '-', type(loadedData)) # {'id': 'xxxx', 'pwd': 'xxxx'} - <class 'dict'>
    print(loadedData['id']) # xxxx

3. (심화) 보안 강화 실습: 로그 데코레이터

데코레이터를 사용하여 함수 실행에 대한 보안 감사 로그를 기록하고, 민감 정보인 사용자 이름을 마스킹 처리하여 정보 노출 위험을 줄이는 기법이다.

# Quiz
# 관리자가 사용자 정보를 확인하고자 한다.
# 관리자가 사용자의 정보를 검색한 시간, 사용자의 이름, 실행시간 정보를 userAccess.log 파일로 저장하고 싶다면? 
# 단, 사용자의 이름은 앞에 두 글자만 출력하고 나머지는 마스킹(*) 처리. 예) su********
# logging format 형태를 파일(userAccess.log)로 저장
# decorator 이용해서 보안적인 측면 강화해보기
# 예외 관련해서 필요한 부분이 있다면 추가해보기

예시 코드

import logging
from time import time, sleep
from functools import wraps # 원본 함수 메타데이터 유지를 위해 필요
import os # 파일 존재 여부 확인을 위해 os 모듈 추가

# 로그 파일 경로 변수 (중복 사용을 위해 정의)
logFilePath = './data/userAccess.log'.strip()

# 로깅 기본 설정 (userAccess.log 파일에 INFO 레벨 기록)
logging.basicConfig(level=logging.INFO, 
                    format="[%(asctime)s] - %(levelname)s - %(message)s",
                    filename=logFilePath, 
                    filemode='a', 
                    force=True)

def secureLog(func) :
    @wraps(func)
    def wrapper(user, *args, **kwargs) :
        startTime = time()
				# 이름 마스킹: 앞 두 글자만 남기고 나머지는 '*' 처리
        nameData = user.get("name", "unknown")
        maskedName = nameData[:2] + '*' * (len(nameData) - 2)
        try:
            result = func(user, *args, **kwargs)
            return result
        except Exception as e:
            # logging.error() 사용: basicConfig 설정에 따라 userAccess.log에 자동 기록
            logging.error(f"함수 '{func.__name__}' 실행 중 오류 발생: {e}. 사용자: {maskedName}")
            raise
        finally:
            endTime = time()
            duration = endTime - startTime
            
            # logging.info() 사용: basicConfig 설정에 따라 userAccess.log에 자동 기록
            logging.info(f"함수 '{func.__name__}' 실행 완료. 사용자: {maskedName}, 실행시간: {duration:.4f}초")
    return wrapper
        
        
# 로그 파일 내용을 읽어 출력하는 함수
def printLogContent(filePath : str) -> None :
    """ 지정된 로그 파일의 전체 내용을 읽어 콘솔에 출력한다. """
    print(f"\n{'='*20} {os.path.basename(filePath)} 파일 내용 {'='*20}")
    
    # 파일이 존재하지 않는 경우 FileNotFoundError를 처리하는 예외 처리 구문
    if not os.path.exists(filePath):
        print(f"오류: {filePath} 경로에 파일이 존재하지 않는다.")
        return

    try:
        # with open()을 사용하여 파일을 읽기 모드로 열고, 읽은 내용을 출력한다.
        with open(filePath, mode='r', encoding='utf-8') as file:
            fileContent = file.read() # 파일 전체 내용을 한 번에 읽는다.
            print(fileContent)
    except Exception as e:
        print(f"로그 파일을 읽는 중 예외가 발생하였다: {e}")
    finally:
        print("="*60)
        
        
@secureLog        
def getProfile(user : dict) -> dict :
    print(f'{user['name']} 프로필을 관리자가 검색합니다.')
    sleep(1) # 실행 시간 측정을 위해 1초 대기
    return {"profile": "user data"}


# caller
userData = {'name' : 'superadmin', 'authenticated' : True}
result = getProfile(userData)

# 로그 파일 출력 함수 호출
printLogContent(logFilePath)

# userAccess.log 파일 출력 예시
==================== userAccess.log 파일 내용 ====================
[2025-11-01 23:11:44,117] - INFO - 함수 'getProfile' 실행 완료. 사용자: su********, 실행시간: 1.0012초
============================================================

요청하신 대로 **로그 파일 회전(Log File Rotation)**에 대해 보안 및 운영 관점에서 자세히 설명해 드리겠습니다.


4. 개인 추가 학습: 로그 파일 회전 (Log File Rotation)

로그 파일 회전은 시스템이나 애플리케이션의 로그 파일을 주기적으로 교체하고 관리하는 프로세스이다. 로그 파일이 무한정 커지는 것을 방지하고, 특정 크기나 시간이 지난 오래된 로그 파일을 자동으로 압축, 보관 또는 삭제한다.

4.1. 로그 파일 회전의 필요성 (보안 및 운영 관점)

관점 문제점 회전의 이점
운영 (가용성) 로그 파일이 디스크 공간을 모두 차지하면 **시스템 다운(Disk Full)**으로 이어져 가용성을 해친다. 디스크 공간 확보를 통해 **시스템 안정성(가용성)**을 유지한다.
보안 (무결성/접근성) 파일이 너무 크면 로그 분석 도구가 처리하기 어렵고, 로그 위변조의 위험이 커진다. 로그 파일을 작게 분할하여 **분석 및 감사(Auditing)**를 용이하게 하고 로그 무결성을 높인다.
성능 하나의 대형 파일에 로그를 기록하면 파일 잠금(Locking) 문제나 I/O 부하가 커져 애플리케이션의 성능 저하를 유발할 수 있다. 작은 파일에 분산 기록하여 I/O 성능을 최적화하고 애플리케이션의 부하를 줄인다.

4.2 파이썬 logging.handlers를 사용한 구현

파이썬의 표준 logging 모듈은 logging.handlers를 통해 로그 회전 기능을 지원한다. 주로 사용되는 핸들러는 두 가지이다.

핸들러 회전 기준 특징
RotatingFileHandler 파일 크기 로그 파일이 설정된 최대 크기에 도달하면 새 파일로 교체.
TimedRotatingFileHandler 시간 매일, 매주, 매월 등 설정된 시간 주기에 따라 새 파일로 교체.

4.3 RotatingFileHandler 예시

가장 일반적인 크기 기반 회전을 설정하는 방법이다.

import logging
from logging.handlers import RotatingFileHandler
import os

# 1. 로거 인스턴스 생성
logger = logging.getLogger('SecurityLogger')
logger.setLevel(logging.INFO)

# 2. 핸들러 설정 (RotatingFileHandler 사용)
logFilePath = './data/secure_access.log'
# maxBytes=10MB, backupCount=5:
# 파일 크기가 10MB에 도달하면 회전하고, 최근 5개 파일만 보관한다.
handler = RotatingFileHandler(
    logFilePath, 
    maxBytes=10 * 1024 * 1024,  # 10MB (바이트 단위)
    backupCount=5, 
    encoding='utf-8'
)

# 3. 포맷터 설정 (기존 format 그대로 사용)
formatter = logging.Formatter("[%(asctime)s] - %(levelname)s - %(message)s")
handler.setFormatter(formatter)

# 4. 로거에 핸들러 추가
logger.addHandler(handler)

# 5. 로깅 테스트 (기존의 logging.info 대신 logger.info 사용)
logger.info("새 로그 회전 시스템으로 테스트 접근 시도.") 

4.4 동작 원리

위의 설정에서 로그 파일이 10MB에 도달하면 다음과 같이 파일 이름이 변경된다.

  1. secure_access.log → secure_access.log.1 (기존 파일 이름에 인덱스가 붙음)
  2. 새로운 secure_access.log 파일이 생성되어 로깅이 계속된다.
  3. 로그 파일이 다시 10MB에 도달하면, 기존의 .4 파일은 삭제되고, .3은 .4로, .2는 .3로, .1은 .2로 변경된 후, 현재 파일이 .1로 변경된다. (즉, backupCount=5 이므로 .1부터 .5까지 총 5개의 백업 파일이 유지된다.)

공부 후기

오늘은 파이썬의 예외 처리 방법과 파일 입출력, 로깅을 배우고 배운 것을 응용하는 문제도 풀었다.. 퀴즈가 너무 어려워서 처음엔 혼자해보다가 Gemini의 도움도 받고 강사님이 설명해주시는 내용을 최대한 이해해보려고 하고 코드를 완성했다. 강사님이 제시한 답안에서 보안적으로 한 단계 더 나아갈 수 있을까 고민해서 추가적으로 코드도 작성했는데 잘 쓴 것 같다..!

지금도 데코레이터가 이해가 잘 안가고 로깅 쓰는 법도 어렵다ㅠㅠ. 강의 내용과 별개로 로그 파일 회전이라는 기법도 있어서 추가적으로 공부하여 작성했다. 아직 초보 단계이지만 계속 공부하고 기록하면서 실력이 늘었으면 좋겠다..ㅠ

그리고 정보처리기사 실기 시험이 일주일 남았다. 이번에 꼭 붙을 수 있도록 기사 시험 공부도 열심히 병행해야겠다. 화이팅!

태그: ,

카테고리:

업데이트:

댓글 남기기