Python 제어 흐름문 (Control Flow Statements)

try-except-finally와 반복 제어

Python의 제어 흐름문은 프로그램의 실행 흐름을 제어하는 핵심 구문이다. 예외 처리를 위한 try-except-finally 구문은 에러 발생 시에도 프로그램이 안전하게 동작하도록 보장하며, break와 continue는 반복문의 실행 흐름을 세밀하게 제어한다. 이러한 제어문들을 적절히 활용하면 견고하고 유연한 프로그램을 작성할 수 있다.

Engineering
Python
Control Flow
저자

Kwangmin Kim

공개

2023년 07월 01일

1 제어 흐름문 개요

Python의 제어 흐름문(Control Flow Statements)은 프로그램의 실행 순서를 제어하는 구문이다. 크게 예외 처리, 반복 제어, 조건문으로 나뉜다.

2 제어 흐름문 분류

2.1 예외 처리 (Exception Handling)

  • try - 예외 발생 가능 코드 실행
  • except - 예외 처리
  • finally - 항상 실행되는 정리 코드
  • else - 예외 없을 때 실행
  • raise - 예외 발생

2.2 반복 제어 (Loop Control)

  • break - 반복문 종료
  • continue - 현재 반복 건너뛰기
  • pass - 아무것도 하지 않음

2.3 조건문 (Conditional Statements)

  • if - 조건 검사
  • elif - 추가 조건
  • else - 기본 경우

2.4 반복문 (Iteration Statements)

  • for - 시퀀스 순회
  • while - 조건 기반 반복

2.5 함수 제어

  • return - 함수 종료 및 값 반환
  • yield - 제너레이터 값 생성

3 예외 처리: try-except-finally

3.1 예외 처리의 필요성

프로그램 실행 중 예상치 못한 에러가 발생하면 프로그램이 즉시 중단된다. 예외 처리 구문을 사용하면 에러 발생 시에도 프로그램을 안전하게 제어할 수 있다.

3.1.1 예외 처리 없는 경우

x = 10 / 0  # ZeroDivisionError 발생 → 프로그램 중단
print("이 코드는 실행되지 않음")

3.1.2 예외 처리 있는 경우

try:
    x = 10 / 0
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")
    x = 0

print("프로그램이 계속 실행됨")

3.2 try-except-finally 구조

try:
    # 예외가 발생할 수 있는 코드
    result = risky_operation()
except SpecificError as e:
    # 예외가 발생하면 실행 (선택적)
    print(f"오류 발생: {e}")
finally:
    # 예외 발생 여부와 무관하게 항상 실행 (선택적)
    cleanup()

3.3 각 블록의 역할

3.3.1 try 블록

예외가 발생할 수 있는 코드를 “시도”한다. 예외 발생 시 즉시 중단되고 해당 예외를 처리하는 except 블록으로 이동한다.

try:
    file = open('data.txt', 'r')
    data = file.read()
    value = int(data)  # 예외 발생 가능
except ValueError:
    print("숫자로 변환할 수 없음")

3.3.2 except 블록

특정 예외가 발생하면 실행된다. 예외를 잡아서 처리하여 프로그램 중단을 방지한다.

try:
    age = int(input("나이 입력: "))
except ValueError:
    print("올바른 숫자를 입력하세요")
    age = 0

3.3.2.1 여러 예외 처리

try:
    result = 10 / int(input("숫자 입력: "))
except ValueError:
    print("숫자가 아닙니다")
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")
except Exception as e:
    print(f"예상치 못한 오류: {e}")

3.3.2.2 예외 객체 활용

try:
    with open('missing.txt', 'r') as f:
        data = f.read()
except FileNotFoundError as e:
    print(f"파일을 찾을 수 없음: {e.filename}")
except IOError as e:
    print(f"입출력 오류: {e}")

3.3.3 finally 블록

예외 발생 여부와 무관하게 항상 실행된다. 주로 리소스 정리(파일 닫기, 연결 종료 등)에 사용된다.

f = None
try:
    f = open('file.txt', 'r')
    data = f.read()
    process(data)  # 예외 발생 가능
except FileNotFoundError:
    print("파일이 없습니다")
except Exception as e:
    print(f"처리 중 오류: {e}")
finally:
    if f:
        f.close()  # 예외 발생 여부와 무관하게 파일 닫기
    print("정리 작업 완료")

3.3.4 else 블록 (선택적)

try 블록에서 예외가 발생하지 않았을 때만 실행된다.

try:
    result = 10 / 2
except ZeroDivisionError:
    print("0으로 나눌 수 없음")
else:
    print(f"결과: {result}")  # 예외 없을 때만 실행
finally:
    print("작업 종료")

3.4 실행 흐름

f = open('file.txt', 'r')
try:
    data = f.read()
    result = process(data)  # 예외 발생 가능
except ProcessingError as e:
    print(f"처리 실패: {e}")
finally:
    f.close()  # 예외 발생해도 반드시 실행

실행 순서: 1. 파일 열기 (open) 2. try 블록 실행 3. 예외 발생 시 → except 블록 실행 4. 예외 여부 무관 → finally 블록 항상 실행

3.5 실용 예제

3.5.1 파일 처리

def read_config(filename):
    config = None
    try:
        with open(filename, 'r') as f:
            config = json.load(f)
    except FileNotFoundError:
        print(f"설정 파일 {filename}을 찾을 수 없습니다")
        config = default_config()
    except json.JSONDecodeError:
        print("설정 파일 형식이 잘못되었습니다")
        config = default_config()
    finally:
        print("설정 로드 시도 완료")
    
    return config

3.5.2 네트워크 요청

import requests

try:
    response = requests.get('https://api.example.com/data', timeout=5)
    response.raise_for_status()
    data = response.json()
except requests.Timeout:
    print("요청 시간 초과")
except requests.ConnectionError:
    print("연결 실패")
except requests.HTTPError as e:
    print(f"HTTP 오류: {e.response.status_code}")
except Exception as e:
    print(f"예상치 못한 오류: {e}")
else:
    print("데이터 수신 성공")
finally:
    print("요청 완료")

3.5.3 데이터베이스 트랜잭션

import sqlite3

conn = sqlite3.connect('database.db')
try:
    cursor = conn.cursor()
    cursor.execute('INSERT INTO users VALUES (?, ?)', (1, 'John'))
    cursor.execute('INSERT INTO users VALUES (?, ?)', (2, 'Jane'))
    conn.commit()
except sqlite3.IntegrityError:
    print("중복된 데이터")
    conn.rollback()
except Exception as e:
    print(f"오류 발생: {e}")
    conn.rollback()
finally:
    conn.close()

4 반복 제어: break와 continue

4.1 break: 반복문 종료

break반복문을 즉시 종료하고 빠져나온다.

for i in range(10):
    if i == 5:
        break  # 5에서 반복문 완전 종료
    print(i)

# 출력: 0, 1, 2, 3, 4

4.1.1 실용 예제

# 특정 값 찾기
numbers = [1, 3, 5, 7, 9, 2, 4]
target = 7

for num in numbers:
    if num == target:
        print(f"{target}을 찾았습니다")
        break
else:
    print(f"{target}을 찾지 못했습니다")
# 조건 만족 시 조기 종료
while True:
    user_input = input("종료하려면 'q' 입력: ")
    if user_input == 'q':
        break
    print(f"입력값: {user_input}")

4.2 continue: 현재 반복 건너뛰기

continue현재 반복만 건너뛰고 다음 반복으로 넘어간다.

for i in range(5):
    if i == 2:
        continue  # 2일 때만 건너뛰고 계속
    print(i)

# 출력: 0, 1, 3, 4 (2는 건너뜀)

4.2.1 실용 예제

# 짝수만 처리
for num in range(10):
    if num % 2 == 0:
        continue  # 짝수는 건너뛰기
    print(f"홀수: {num}")
# 유효한 데이터만 처리
for user in users:
    if not user.is_active:
        continue  # 비활성 사용자는 건너뛰기
    if user.age < 18:
        continue  # 미성년자는 건너뛰기
    
    process_user(user)  # 유효한 사용자만 처리
# 에러 데이터 무시
for line in file:
    try:
        data = parse(line)
    except ValueError:
        continue  # 파싱 실패 시 다음 줄로
    
    results.append(data)

4.3 break vs continue 비교

# break 예제
for i in range(5):
    if i == 2:
        break
    print(i)
# 출력: 0, 1 (2에서 완전 종료)

# continue 예제
for i in range(5):
    if i == 2:
        continue
    print(i)
# 출력: 0, 1, 3, 4 (2만 건너뜀)

4.4 중첩 반복문에서의 사용

# break는 가장 안쪽 반복문만 종료
for i in range(3):
    for j in range(3):
        if j == 1:
            break  # 안쪽 for만 종료
        print(f"i={i}, j={j}")
    print("안쪽 반복 종료")
# 바깥 반복문까지 종료하려면 플래그 사용
found = False
for i in range(3):
    for j in range(3):
        if i == 1 and j == 1:
            found = True
            break
    if found:
        break

5 기타 제어문

5.1 pass: 빈 블록

아무것도 하지 않는 자리 채우기용 구문이다. 문법적으로 블록이 필요하지만 아직 구현하지 않았을 때 사용한다.

def future_feature():
    pass  # TODO: 나중에 구현

for i in range(10):
    if i % 2 == 0:
        pass  # 짝수는 나중에 처리
    else:
        print(i)

5.2 return: 함수 종료

함수 실행을 종료하고 값을 반환한다.

def find_first_even(numbers):
    for num in numbers:
        if num % 2 == 0:
            return num  # 첫 짝수 찾으면 즉시 반환
    return None  # 못 찾으면 None

5.3 raise: 예외 발생

명시적으로 예외를 발생시킨다.

def divide(a, b):
    if b == 0:
        raise ValueError("0으로 나눌 수 없습니다")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(e)

6 예시

6.1 구체적인 예외 처리

# ❌ 너무 광범위
try:
    data = process()
except:
    pass

# ✅ 구체적 예외 처리
try:
    data = process()
except ValueError as e:
    logger.error(f"값 오류: {e}")
except IOError as e:
    logger.error(f"IO 오류: {e}")

6.2 finally보다 with 선호

# ❌ 수동 관리
f = open('file.txt')
try:
    data = f.read()
finally:
    f.close()

# ✅ 자동 관리
with open('file.txt') as f:
    data = f.read()

6.3 조기 반환으로 중첩 줄이기

# ❌ 깊은 중첩
def process(data):
    if data is not None:
        if len(data) > 0:
            if data[0] == 'valid':
                return do_work(data)
    return None

# ✅ 조기 반환
def process(data):
    if data is None:
        return None
    if len(data) == 0:
        return None
    if data[0] != 'valid':
        return None
    
    return do_work(data)

6.4 Tip: Try statement 사용 시 주의사항

try:
    if history_file.exists():
        with open(history_file) as f:
            history = json.load(f)
    if response_times_file.exists():
        with open(response_times_file) as f:
            response_times = json.load(f)
    if messages_file.exists():
        with open(messages_file) as f:
            messages = json.load(f)
    if metrics_file.exists():
        with open(metrics_file) as f:
            metrics = json.load(f)
except:
    pass
  • 첫번째 문제 except 블록이 pass로 되어 있어 예외의 조건이 너무 추상적이고 광범위하다. 어떤 예외가 발생했는지 알 수 없고, 모든 예외를 무시하기 때문에 디버깅이 어려워진다.
  • 두번째 문제는 history.json에서 오류가 나면 나머지 파일(response_times, messages, metrics)도 로드 안 됨. 각 파일마다 try-except 블록을 분리해서 처리하는 것이 좋다.
  • 코드 중복
  • 해결책: 헬퍼 함수를 만들어 코드 중복을 줄이고, 구체적인 예외를 처리하도록 개선한다.
def _load_json_safe(file_path: Path):
    """JSON 파일을 안전하게 로드"""
    if not file_path.exists():
        return []
    
    try:
        with open(file_path, encoding='utf-8') as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError) as e:
        print(f"⚠️ {file_path.name} 로드 실패: {e}")
        return []

def load_persistent_data():
    history = _load_json_safe(LOG_DATA_DIR / "history.json")
    response_times = _load_json_safe(LOG_DATA_DIR / "response_times.json")
    messages = _load_json_safe(LOG_DATA_DIR / "messages.json")
    metrics = _load_json_safe(LOG_DATA_DIR / "metrics.json")
    return history, response_times, messages, metrics

7 요약

구문 용도 선택성
try 예외 발생 가능 코드 필수
except 예외 처리 선택
finally 항상 실행 (정리) 선택
else 예외 없을 때 실행 선택
break 반복문 종료 -
continue 현재 반복 건너뛰기 -
pass 아무것도 안 함 -

제어 흐름문은 Python 프로그래밍의 핵심이며, 적절히 사용하면 견고하고 읽기 쉬운 코드를 작성할 수 있다.

Subscribe

Enjoy this blog? Get notified of new posts by email: