# Part 2: 파이썬 핵심 문법 마스터하기

**⬅️ 이전 시간: [Part 1: AI 개발 환경 완벽 구축 가이드](../01_ai_development_environment/part_1_ai_development_environment.md)**
**➡️ 다음 시간: [Part 3: 파이썬 컬렉션, 더 깊게 이해하기](../03_python_collections/part_3_python_collections.md)**

---

## 1. 학습 목표 (Learning Objectives)

이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.

- 변수를 선언하고 숫자, 문자열, 불리언 등 기본 자료형을 활용할 수 있습니다.
- [리스트](../../glossary.md#리스트-list), [튜플](../../glossary.md#튜플-tuple), [딕셔너리](../../glossary.md#딕셔너리-dictionary), 셋의 차이점을 이해하고 상황에 맞게 선택하여 사용할 수 있습니다.
- `if`, `for`, `while` 등 제어문을 사용하여 코드의 실행 흐름을 제어할 수 있습니다.
- 코드의 재사용성을 높이는 [함수](../../glossary.md#함수-function)를 직접 정의하고, `lambda`를 이용한 익명 함수를 작성할 수 있습니다.
- [리스트 컴프리헨션](../../glossary.md#리스트-컴프리헨션-list-comprehension)과 같은 파이썬다운(Pythonic) 코드를 작성할 수 있습니다.

## 2. 핵심 요약 (Key Summary)
이 파트에서는 파이썬 프로그래밍의 가장 기본적인 구성 요소들을 배웁니다. 데이터를 저장하는 변수, 다양한 종류의 데이터를 나타내는 자료형(숫자, 문자열, 불리언)을 시작으로, 여러 데이터를 묶어서 관리하는 리스트, 튜플, 딕셔너리, 셋의 사용법과 차이점을 익힙니다. 또한, `if`, `for`, `while`과 같은 제어문을 사용하여 코드의 흐름을 제어하고, 함수를 정의하여 코드의 재사용성을 높이는 방법을 학습합니다.

- **핵심 키워드**: `변수(Variable)`, `자료형(Data Type)`, `리스트(List)`, `튜플(Tuple)`, `딕셔너리(Dictionary)`, `셋(Set)`, `제어문(Control Flow)`, `들여쓰기(Indentation)`, `함수(Function)`, `람다(Lambda)`

## 3. 왜 파이썬 문법이 중요할까요? (Introduction)

AI 개발의 세계에 오신 것을 환영합니다! 파이썬은 AI와 데이터 과학 분야에서 가장 사랑받는 언어입니다. 문법이 간결하고 사람의 생각과 비슷해서, 프로그래밍을 처음 시작하는 분들도 쉽게 배울 수 있습니다. 이번 주차에는 앞으로 우리가 만들 모든 AI 서비스의 뼈대가 될 파이썬의 가장 핵심적인 문법들을 하나씩 정복해나갑니다.

> [!TIP]
> 이 파트의 모든 코드 예제는 아래 링크의 파이썬 파일에서 직접 실행하고 수정해볼 수 있습니다.
> **실행 가능한 소스 코드: [`part_2_3_python_syntax_collections.py`](../../source_code/part_2_3_python_syntax_collections.py)**

---

## 4. 모든 것의 시작, 변수와 자료형

> **🎯 1일차 목표:** 변수(Variable)를 사용해 데이터를 저장하고, 파이썬의 기본 데이터 종류(자료형)를 이해합니다.

### 4.1. 변수(Variable): 데이터에 이름표 붙이기
<a href="../../glossary.md#변수-variable">변수(Variable)</a>는 숫자, 글자 같은 데이터를 저장하는 '상자'입니다. `age = 30` 이라는 코드는, `30`이라는 데이터를 `age`라는 이름의 상자에 넣는다는 뜻입니다. 이제부터 우리는 `age`라는 이름만 부르면 컴퓨터가 알아서 `30`이라는 값을 찾아줍니다.

```python
my_name = "파이"      # 문자열 (String)
my_age = 20         # 정수 (Integer)
pi = 3.14           # 실수 (Float)
is_student = True   # 불리언 (Boolean)

print(f"안녕하세요, 제 이름은 {my_name}이고 나이는 {my_age}살입니다.")
```

### 4.2. 기본 자료형(Data Types)
파이썬은 데이터의 종류를 구분하며, 이를 <a href="../../glossary.md#자료구조-data-structure">자료형</a>이라고 합니다.
- **정수 (Integer)**: `1`, `100`, `-5` 같이 소수점이 없는 숫자.
- **실수 (Float)**: `3.14`, `-0.5` 같이 소수점이 있는 숫자.
- **문자열 (String)****: `"안녕하세요"`, `'Python'` 같이 따옴표로 감싼 글자들.
- **<a href="../../glossary.md#boolean">불리언 (Boolean)</a>**: `True` 또는 `False`. 조건의 참/거짓을 나타냅니다.

---

## 5. 여러 데이터를 담는 그릇 (1): 리스트와 튜플

> **🎯 2일차 목표:** 순서가 있는 데이터 묶음인 [리스트(List)](../../glossary.md#리스트-list)와 변경이 불가능한 [튜플(Tuple)](../../glossary.md#튜플-tuple)을 다루고, [리스트 컴프리헨션](../../glossary.md#리스트-컴프리헨션-list-comprehension)으로 파이썬다운 코드를 작성합니다.

### 5.1. 리스트(List): 순서가 있고 변경 가능한 만능 주머니
- 여러 값을 순서대로 저장하는 **변경 가능한** <a href="../../glossary.md#자료구조-data-structure">자료구조</a>입니다. (`[]` 사용)

```python
fruits = ["apple", "banana", "cherry"]
fruits[1] = "blueberry"      # 값 변경
fruits.append("strawberry")  # 요소 추가
print(f"최종 리스트: {fruits}")
```

### 5.2. 파이썬다운 코드: 리스트 컴프리헨션 (List Comprehension)
`for` 반복문을 한 줄로 간결하게 표현하여 새로운 리스트를 만드는 방법입니다.

```python
# 0부터 9까지의 숫자 중 짝수만 제곱하여 리스트 만들기
squares = [i**2 for i in range(10) if i % 2 == 0]
print(f"짝수의 제곱 리스트: {squares}") # [0, 4, 16, 36, 64]
```

### 5.3. 튜플(Tuple): 순서가 있지만 변경 불가능한 금고
- 여러 값을 순서대로 저장하는 **변경 불가능한** <a href="../../glossary.md#자료구조-data-structure">자료구조</a>입니다. (`()` 사용)
- 한 번 만들어지면 내용을 바꿀 수 없어 안정성이 중요할 때 (e.g., 함수의 반환 값, 좌표) 사용됩니다.

```python
point = (10, 20)
# point[0] = 15 #! TypeError 발생
```

---

## 6. 여러 데이터를 담는 그릇 (2): 딕셔너리와 셋

> **🎯 3일차 목표:** `Key:Value` 쌍으로 데이터를 저장하는 [딕셔너리(Dictionary)](../../glossary.md#딕셔너리-dictionary)와 중복을 허용하지 않는 셋(Set)을 다룹니다.

### 6.1. 딕셔너리(Dictionary): 의미를 부여한 데이터 관리법
- `Key:Value` 쌍으로 데이터를 저장하며, 순서보다 '의미(Key)'를 통해 값을 찾는 데 특화된 <a href="../../glossary.md#자료구조-data-structure">자료구조</a>입니다. (`{}` 사용)

```python
user = {"name": "앨리스", "age": 30}
user['email'] = "alice@example.com" # 데이터 추가/변경
print(f"이름: {user['name']}, 이메일: {user.get('email')}")
```

### 6.2. 셋(Set): 중복을 제거하고 관계를 파악하는 도구
- **중복을 허용하지 않는** 순서 없는 데이터의 모음입니다. (`{}` 사용)
- 데이터의 중복 제거, 그룹 간의 합집합, 교집합 등을 구할 때 유용한 <a href="../../glossary.md#set">자료구조</a>입니다.

```python
unique_numbers = {1, 2, 3, 2, 1, 4}
print(f"중복이 제거된 집합: {unique_numbers}") # {1, 2, 3, 4}

set_a = {1, 2, 3}
set_b = {3, 4, 5}
print(f"교집합: {set_a & set_b}") # {3}
```

### 6.3. 더 파이썬답게: 딕셔너리와 셋 컴프리헨션
[리스트 컴프리헨션](../../glossary.md#리스트-컴프리헨션-list-comprehension)처럼, 딕셔너리와 셋도 `for` 반복문을 한 줄로 간결하게 표현하여 만들 수 있습니다.

```python
# 딕셔너리 컴프리헨션: 숫자를 키로, 제곱을 값으로
square_dict = {x: x**2 for x in range(5)}
print(f"제곱 딕셔너리: {square_dict}") # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 셋 컴프리헨션: 중복된 리스트에서 유일한 짝수만 추출
numbers_list = [1, 2, 2, 3, 4, 4, 5]
even_set = {x for x in numbers_list if x % 2 == 0}
print(f"고유한 짝수 셋: {even_set}") # {2, 4}
```

---

## 7. 코드의 흐름 제어하기

> **🎯 4일차 목표:** `if`, `for`, `while` 제어문을 사용하여 조건과 반복에 따라 코드의 흐름을 제어합니다.

### 7.1. 파이썬의 상징: 들여쓰기(Indentation)
파이썬은 **[들여쓰기](../../glossary.md#들여쓰기-indentation)(스페이스 4칸)**로 코드의 소속을 구분하는 '문법'입니다. `if`, `for` 등에 속한 코드들은 반드시 들여쓰기를 해야 합니다.

### 7.2. `if-elif-else`: 조건에 따라 움직이기
```python
score = 85
grade = "" # 학점을 저장할 변수 초기화

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
else:
    grade = "F"

print(f"학점: {grade}")
```

> **[TIP] 한 줄로 쓰는 조건부 표현식 (Conditional Expression)**
> 간단한 `if-else` 구문은 `참일때_값 if 조건 else 거짓일때_값` 형태로 더 간결하게 표현할 수 있습니다.
> ```python
> # 80점 이상이면 "Pass", 아니면 "Fail"을 부여하는 경우
> status = "Pass" if score >= 80 else "Fail"
> print(f"결과: {status}") # 출력: Pass
> ```

### 7.3. `for`: 파이썬답게 반복하기 (enumerate, zip)

`for`문은 파이썬에서 가장 많이 사용되는 반복문입니다. 리스트, 튜플, 문자열 등 순회 가능한 객체의 요소를 하나씩 순회하며, `enumerate`나 `zip`과 함께 사용하면 더욱 강력하고 파이썬다운 코드를 작성할 수 있습니다.

#### `enumerate`: 인덱스와 값을 동시에 얻기

`for`문으로 리스트를 순회할 때, 각 요소의 인덱스 번호가 필요할 때가 많습니다. `enumerate`를 사용하면 별도의 변수 없이 인덱스와 값을 함께 얻을 수 있습니다.

```python
fruits = ["apple", "banana", "cherry"]
for idx, fruit in enumerate(fruits):
    print(f"{idx+1}번째 과일: {fruit}")
```

#### `zip`: 여러 리스트를 나란히 묶기

여러 개의 리스트를 마치 하나의 리스트처럼, 각 리스트의 같은 인덱스에 있는 요소들을 짝지어 반복하고 싶을 때 `zip`을 사용합니다.

```python
names = ['앨리스', '밥', '찰리']
ages = [25, 30, 28]

for name, age in zip(names, ages):
    print(f"{name}의 나이는 {age}세입니다.")
```

> [!TIP]
> `zip`은 가장 짧은 리스트의 길이를 기준으로 동작합니다. 리스트들의 길이가 다르면, 짧은 쪽이 끝났을 때 반복도 함께 종료됩니다.

#### `enumerate`와 `zip` 함께 사용하기: 인덱스와 여러 값 동시에 다루기

두 기능을 합치면, 여러 리스트를 순회하면서 동시에 인덱스 번호까지 얻을 수 있습니다. 학생들의 성적을 처리하는 경우를 예로 들어보겠습니다.

```python
names = ['Alice', 'Bob', 'Charlie']
subjects = ['Math', 'English', 'Science']
scores = [95, 88, 76]

for i, (name, subject, score) in enumerate(zip(names, subjects, scores)):
    print(f"{i+1}번 학생 {name}의 {subject} 점수는 {score}점 입니다.")
```
이처럼 `enumerate`와 `zip`을 활용하면 복잡한 반복 로직도 간결하고 읽기 좋은 코드로 표현할 수 있습니다.

### 7.4. `while`: 조건이 만족하는 동안 반복하기
주어진 조건이 `True`인 동안 코드를 계속해서 반복합니다. 조건이 언젠가 `False`가 되도록 만들지 않으면 '무한 루프'에 빠지므로 주의해야 합니다.

```python
count = 3
while count > 0:
    print(f"카운트 다운: {count}")
    count -= 1 # 이 줄이 없으면 무한 루프!
print("발사!")
```

### 7.5. `lambda`의 단짝 친구: `map`과 `filter`
`lambda`는 리스트 같은 순회 가능한 데이터의 모든 요소에 함수를 일괄 적용하는 `map`이나, 특정 조건에 맞는 요소만 걸러내는 `filter`와 함께 사용될 때 매우 강력합니다.

- **`map(함수, 이터러블)`**: 각 요소에 `함수`를 적용한 결과를 반환합니다.
- **`filter(함수, 이터러블)`**: `함수`의 결과가 `True`인 요소만 걸러서 반환합니다.

```python
numbers = [1, 2, 3, 4, 5]

# map과 lambda: 모든 숫자를 2배로 만들기
doubled_numbers = list(map(lambda x: x * 2, numbers))
print(f"2배가 된 숫자들: {doubled_numbers}") # [2, 4, 6, 8, 10]

# filter와 lambda: 홀수만 필터링하기
odd_numbers = list(filter(lambda x: x % 2 != 0, numbers))
print(f"홀수만 필터링: {odd_numbers}") # [1, 3, 5]
```
> [!NOTE]
> `map`과 `filter`의 결과는 그 자체로 리스트가 아닌 '이터레이터'이므로, 결과를 눈으로 확인하려면 `list()`로 변환해야 합니다. 하지만 최근에는 이보다 가독성이 좋은 **리스트 컴프리헨션**이 더 선호되는 추세입니다.
>
> `doubled_numbers = [x * 2 for x in numbers]`
> `odd_numbers = [x for x in numbers if x % 2 != 0]`

### 8.4. 파이썬의 모든 것은 객체: 변수와 함수의 동작 원리 (Call by Object Reference)

파이썬의 동작 방식을 깊이 이해하려면 "모든 것이 객체(Object)다"라는 개념을 알아야 합니다. 숫자, 문자열, 심지어 함수까지도 모두 메모리 상의 '객체'이며, 변수는 그 객체를 가리키는 '이름표'일 뿐입니다. 이 개념은 함수에 인자를 전달하는 방식에도 그대로 적용됩니다.

#### 함수는 1급 객체 (First-class Citizen)

파이썬에서 함수는 특별한 취급을 받지 않습니다. 숫자나 리스트처럼 변수에 할당할 수 있고, 다른 함수의 인자로 전달하거나, 함수가 함수를 반환할 수도 있습니다. 이를 '함수는 1급 객체'라고 부릅니다.

```python
def greeting(name):
    print(f"안녕하세요, {name}님!")

# 함수 객체를 변수에 할당
welcome = greeting
welcome("파이썬") # "안녕하세요, 파이썬님!" 출력

# print 함수 자체도 객체이므로 변수에 할당 가능
say = print
say("이것도 print 함수처럼 동작합니다.")
```
이 특징은 데코레이터처럼 함수의 기능을 동적으로 확장하는 강력한 패턴의 기반이 됩니다.

#### Call by Object Reference: 변수는 이름표일 뿐

많은 언어에서 '값에 의한 호출(Call by Value)'과 '참조에 의한 호출(Call by Reference)'을 구분합니다. 파이썬은 이 둘을 섞어놓은 듯한 **'객체 참조에 의한 호출(Call by Object Reference)'** 방식을 사용합니다.

- 함수에 인자를 전달할 때, 값의 복사본이나 변수의 메모리 주소가 넘어가는 것이 아니라, 변수가 가리키고 있는 **객체의 참조(주소값)가 복사되어** 전달됩니다.

**1. 변경 가능한(Mutable) 객체를 전달할 때 (e.g., 리스트, 딕셔너리)**

함수 안에서 전달받은 객체의 내용을 변경하면, 원본 객체에도 그 변경 사항이 그대로 반영됩니다. 왜냐하면 함수 안과 밖의 변수가 **같은 객체**를 가리키고 있기 때문입니다.

```python
def add_element(my_list):
    my_list.append(4) # 리스트 내용 변경

numbers = [1, 2, 3]
add_element(numbers)
print(f"결과: {numbers}") # 출력: [1, 2, 3, 4]
```
> 함수 안과 밖의 `numbers`와 `my_list`는 정확히 동일한 리스트 객체를 가리키므로, 함수 안에서의 변경이 밖에 영향을 줍니다.

**2. 변경 불가능한(Immutable) 객체를 전달할 때 (e.g., 숫자, 문자열, 튜플)**

변경 불가능한 객체는 그 내용을 바꿀 수 없습니다. 만약 함수 안에서 해당 변수에 새로운 값을 할당하면, 그것은 기존 객체를 바꾸는 것이 아니라 **새로운 객체를 만들어** 그 변수가 가리키게 하는 것입니다. 따라서 원본 변수에는 아무런 영향이 없습니다.

```python
def reassign_value(my_var):
    my_var = 100 # 새로운 정수 객체를 생성하고 my_var가 가리키게 함

num = 10
reassign_value(num)
print(f"결과: {num}") # 출력: 10 (바뀌지 않음!)
```
> `reassign_value` 함수 안에서 `my_var`는 새로운 객체(`100`)를 가리키게 되지만, 함수 밖의 `num`은 여전히 원래 객체(`10`)를 가리키고 있습니다.

이러한 동작 방식을 이해하는 것은 파이썬 코드가 어떻게 실행되고 데이터가 어떻게 변하는지 정확히 예측하는 데 매우 중요합니다.

---

## 9. 심화 학습: Pythonic 코드 탐구 (Decorators & Generators)

숙련된 개발자를 위해, 코드의 효율성과 표현력을 극대화하는 파이썬의 강력한 기능인 데코레이터와 제너레이터를 소개합니다.

### 9.1. 데코레이터(Decorators): 함수를 포장하는 기술

**데코레이터**는 기존 함수의 코드를 수정하지 않고도, 그 함수에 새로운 기능을 덧붙이거나 감싸는(wrapping) '포장지' 같은 역할을 합니다. 로깅, 실행 시간 측정, 접근 제어 등 다양한 [횡단 관심사(Cross-cutting concerns)](../../glossary.md#횡단-관심사-cross-cutting-concern)를 우아하게 처리할 수 있습니다.

```python
import time

def timing_decorator(func):
    """함수의 실행 시간을 측정하는 데코레이터"""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs) # 원래 함수 실행
        end_time = time.time()
        print(f"'{func.__name__}' 함수 실행 시간: {end_time - start_time:.4f}초")
        return result
    return wrapper

@timing_decorator
def slow_function(delay_time):
    """시간이 오래 걸리는 가상 함수"""
    print("느린 함수 시작...")
    time.sleep(delay_time)
    print("느린 함수 종료.")

# 데코레이터가 적용된 함수 호출
slow_function(2)
```
위 예제에서 `@timing_decorator`는 `slow_function`에 실행 시간 측정 기능을 '장식'합니다.

### 9.2. 제너레이터(Generators): 메모리 효율적인 이터레이터

**제너레이터**는 모든 값을 메모리에 올려두고 시작하는 리스트와 달리, 필요할 때마다 값을 '생성(yield)'하여 전달하는 특별한 종류의 [이터레이터](../../glossary.md#이터레이터-iterator)입니다. 이 방식은 대용량 데이터 스트림을 처리할 때 메모리 사용량을 획기적으로 줄여줍니다.

- **`yield` 키워드**: 함수가 제너레이터임을 나타냅니다. 값을 반환하고 함수의 상태를 일시 정지시킵니다. 다음에 호출될 때 정지된 지점부터 다시 시작합니다.
- **제너레이터 표현식(Generator Expression)**: 리스트 컴프리헨션과 유사하지만, `[]` 대신 `()`를 사용합니다. 즉시 리스트를 생성하는 대신 제너레이터 객체를 반환합니다.

```python
# 제너레이터 함수
def count_up_to(max_num):
    count = 1
    while count <= max_num:
        yield count # 값을 생성하고 멈춤
        count += 1

# 제너레이터 사용
counter = count_up_to(5)
for num in counter:
    print(num) # 1, 2, 3, 4, 5 출력

# 제너레이터 표현식
large_squares = (x**2 for x in range(1_000_000))
# sum(large_squares) # 메모리 문제 없이 거대한 시퀀스의 합계를 계산 가능
```

이러한 기능들은 AI/ML 파이프라인에서 대규모 데이터셋을 효율적으로 다루거나, 모델의 행동을 로깅하는 등 실제 개발 현장에서 매우 유용하게 사용됩니다.

---

## 10. 연습 문제 (Exercises)

이번 주에 배운 내용을 바탕으로 다음 함수들을 직접 완성해보세요.

**문제 1: 학점 계산기 함수**
- 학생의 점수(0~100)를 매개변수로 받아 학점을 반환하는 `get_grade` 함수를 `if-elif-else`를 사용하여 만들어보세요.
  - 90점 이상: "A", 80점 이상: "B", 70점 이상: "C", 70점 미만: "F"

**문제 2: 리스트에서 특정 단어 개수 세기**
- 과일 이름 리스트와 특정 과일 이름을 매개변수로 받아, 리스트 안에 그 과일이 몇 번 나타나는지 `for`문을 사용해 세고 개수를 반환하는 `count_word` 함수를 만들어보세요.

**문제 3: 구구단 출력 함수**
- 숫자를 하나 매개변수로 받아 해당 숫자의 구구단을 `while`문을 사용하여 출력하는 `multiplication_table` 함수를 만들어보세요. (이 함수는 값을 반환할 필요가 없습니다.)

모범 답안은 [`part_2_python_core_syntax.py`](../../source_code/02_python_core_syntax/part_2_python_core_syntax.py) 파일에서 확인할 수 있습니다.

---

## 11. 트러블슈팅 (Troubleshooting)

코드를 작성하다 보면 예상치 못한 문제들을 마주하게 됩니다. 괜찮습니다! 모든 개발자가 겪는 과정이며, 문제 해결 능력은 뛰어난 개발자의 핵심 역량입니다.

- **`IndentationError`가 발생했나요?**
  - 파이썬은 들여쓰기로 코드 블록을 구분합니다. `if`, `for`, `def` 아래의 코드가 정확히 스페이스 4칸으로 들여쓰기 되었는지 확인하세요.
- **`SyntaxError`는 무엇인가요?**
  - 문법이 틀렸다는 의미입니다. 괄호 `()`, 따옴표 `""`, 콜론 `:` 등을 빠뜨리지 않았는지 확인해보세요.
- **코드가 예상대로 동작하지 않나요?**
  - `print()` 함수를 사용해 중간중간 변수의 값이 어떻게 변하는지 출력해보면 문제의 원인을 찾는 데 큰 도움이 됩니다.

더 자세한 문제 해결 가이드나 다른 동료들이 겪은 문제에 대한 해결책이 궁금하다면, 아래 문서를 참고해주세요.

- **➡️ [Geumdo-Docs: TROUBLESHOOTING.md](../../../TROUBLESHOOTING.md)**

---

## 되짚어보기 (Recap)

- **변수와 자료형**: 모든 데이터는 변수에 담기고, 각 데이터는 고유한 자료형(정수, 문자열 등)을 가집니다.
- **자료구조**: 리스트(변경 가능, 순서 O), 튜플(변경 불가, 순서 O), 딕셔너리(Key-Value, 순서 X), 셋(중복 불가, 순서 X)의 특징을 이해하고 상황에 맞게 사용하는 것이 중요합니다.
- **제어문**: `if`문으로 조건을, `for`/`while`문으로 반복을 제어하며 코드의 흐름을 만듭니다.
- **함수**: `def`를 사용하여 반복되는 코드를 재사용 가능한 '레시피'로 만들어 코드의 구조를 개선합니다.

## 더 깊이 알아보기 (Further Reading)

- [Official Python 3.10.12 documentation](https://docs.python.org/3/)
- [A Byte of Python (한국어)](https://python.bytebank.org/)
- [점프 투 파이썬](https://wikidocs.net/book/1)

---

## 🚀 캡스톤 미니 프로젝트: 간단한 로그 분석기 만들기

지금까지 배운 Python 핵심 문법을 모두 활용하여, 앞으로 진행할 최종 캡스톤 프로젝트의 기반을 다지는 작은 실습을 진행해봅시다. 이번 미니 프로젝트의 목표는 여러 줄의 로그(log) 텍스트 파일을 읽고, 분석하여, 의미 있는 요약 정보를 출력하는 간단한 프로그램을 만드는 것입니다.

이 과정을 통해 여러분은 파일을 읽고, 문자열을 다루고, 배운 자료구조(리스트, 딕셔너리)와 제어문, 함수를 실제 문제에 적용하는 경험을 하게 됩니다.

### 프로젝트 목표

주어진 형식의 로그 파일을 분석하여 다음과 같은 요약 리포트를 출력하는 Python 스크립트(`log_analyzer.py`)를 완성합니다.

**샘플 로그 파일 (`sample.log`):**
```
2023-10-27 10:00:00 INFO: User 'alice' logged in.
2023-10-27 10:01:30 WARNING: Disk space is running low.
2023-10-27 10:02:15 INFO: Data processing started.
2023-10-27 10:05:00 ERROR: Failed to connect to database.
2023-10-27 10:05:05 INFO: User 'bob' logged in.
2023-10-27 10:06:00 ERROR: File not found: 'data.csv'.
```

**예상 출력 결과:**
```
Log Analysis Report
--------------------
Total Logs: 6
Log Levels Count:
  - INFO: 3
  - WARNING: 1
  - ERROR: 2
```

### 단계별 구현 가이드

아래 단계에 따라 함수를 하나씩 완성해보세요.

**1. 로그 파일 읽기 함수**
- `read_logs(file_path)` 라는 이름의 함수를 만드세요.
- 이 함수는 파일 경로를 입력받아, 파일의 각 줄을 리스트의 요소로 담아 반환해야 합니다.
- (힌트) 파일 처리는 다음과 같은 `with open(...)` 구문을 사용하면 편리합니다.
  ```