Step 55. ★ 프로젝트: 로그 분석기 — 기록 더미에서 발자국을 골라내라
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 4시간
전제: Step 41~54 완료. 정규식(Step 48), 딕셔너리(Step 42), 파일 입출력(Step 45), 예외처리(Step 46), 명령줄 인자(Step 51)를 압니다. Level 1의 종합 프로젝트입니다.
- 준비물: 파이썬이 설치된 PC, 텍스트 에디터, 터미널. 새로 설치하는 것은 없습니다 — 전부 표준 라이브러리입니다.
- 주의: 오늘 실습은 100% 안전합니다. 분석할 로그는 우리가 직접 만드는 가짜 로그입니다. 진짜 서버 로그는 함부로 가져오지 않습니다 — 남의 시스템 기록에는 개인정보가 있을 수 있습니다.
보안 분석가의 하루는 로그와 함께 시작됩니다. 서버는 누가, 언제, 어디서, 무엇을 했는지를 줄줄이 기록해 두는데, 그 기록 더미 어딘가에 공격의 발자국이 있습니다. 문제는 양입니다 — 하루 수만 줄을 사람이 눈으로 다 볼 수는 없습니다. 그래서 분석가의 첫 번째 도구는 "로그에서 이상한 것을 자동으로 골라 주는 프로그램"입니다.
오늘 우리가 그것을 만듭니다. 지금까지 배운 것들 — 정규식, 딕셔너리, 파일 입출력, 예외처리, 명령줄 인자 — 이 전부 이 프로젝트 하나에 모입니다. 로그인 실패가 한 주소에서 반복되면 "이 주소는 무차별 대입(브루트포스)을 하고 있을지도 모른다"고 의심하는 것이 오늘의 탐지 규칙입니다. 단순하지만, 실제 보안 시스템들도 이 단순한 생각에서 출발했습니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 로그(log)가 무엇이고 왜 보안 분석의 기본 재료인지 설명한다
- 정규식으로 로그 한 줄에서 IP와 결과를 파싱한다
- Counter로 IP별·계정별 횟수를 집계한다
- 임계값을 넘는 대상을 [의심]으로 자동 표시하는 분석기를 완성한다
- 분석 결과를 숫자와 의미가 함께 적힌 리포트 파일로 저장한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3.12. 표준 라이브러리만 사용 (re, collections, sys) |
| 오늘의 문법 | re.search()와 그룹, Counter와 most_common(), sys.argv 인자 처리, 안전한 파싱(if not m: continue) |
| 필요한 개념 | 로그, 브루트포스의 무늬, 파싱(parsing), 임계값(threshold)과 거짓 경보 |
| 오늘의 산출물 | log_analyzer.py + report.txt — 내 첫 보안 분석 도구와 그 리포트 |
2-1. 로그 — 시스템의 일기
로그(log)는 프로그램이나 서버가 남기는 시간 순 기록입니다. 웹 서버는 방문을, 로그인 시스템은 성공과 실패를 기록합니다. 형식은 제각각이지만 보통 "시간, 누가(IP), 무엇을, 결과"가 들어 있습니다. 사고가 났을 때 "그날 무슨 일이 있었나"를 되짚는 유일한 재료가 로그입니다.
2-2. 브루트포스의 발자국
무차별 대입(brute-force) 공격은 로그에 독특한 무늬를 남깁니다. 같은 IP에서, 짧은 시간에, 로그인 실패가 수십 수백 번. 정상 사용자는 비밀번호를 백 번 틀리지 않습니다. 그래서 "IP별 실패 횟수를 세서, 기준을 넘으면 의심"이라는 규칙이 성립합니다.
2-3. 파싱과 Counter — 줄에서 정보 뽑아 세기
로그 한 줄에서 필요한 조각(IP, 결과)을 뽑는 일을 파싱(parsing)이라고 합니다. Step 48의 정규식이 이 일의 도구입니다. 뽑은 조각을 세는 일에는 표준 라이브러리 collections의 Counter가 전문가입니다 — Counter에 넣기만 하면 항목별 개수를 세어 주고, most_common()으로 많은 순서대로 뽑아 줍니다.
2-4. 임계값 — 그물의 눈 크기
"몇 번부터 의심할 것인가"라는 기준을 임계값(threshold)이라고 합니다. 임계값을 낮추면 정상 사용자까지 걸리고(거짓 경보), 높이면 진짜 공격을 놓칩니다. 임계값은 진실이 아니라 그물의 눈 크기입니다 — 이 균형이 탐지 시스템의 영원한 과제입니다.
3. 따라 하기
3-1. 샘플 로그 만들기
진짜 서버 로그는 함부로 가져올 수 없으니, 공부용으로 우리가 만듭니다. make_log.py:
import random
users = ["admin", "guest", "lee", "park"]
lines = []
for i in range(30):
ip = "192.168.0." + str(random.randint(2, 20))
user = random.choice(users)
result = "SUCCESS" if random.random() < 0.8 else "FAIL"
lines.append(f"2026-09-08 10:{i:02d}:00 {ip} login user={user} result={result}")
# 일부러 공격처럼 보이는 기록을 심는다
for i in range(12):
lines.append(f"2026-09-08 11:{i:02d}:00 10.0.0.99 login user=admin result=FAIL")
# 형식이 깨진 줄도 하나 섞는다 (실제 로그에는 늘 있다)
lines.append("system restart")
random.shuffle(lines)
with open("auth.log", "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print("auth.log 생성 완료:", len(lines), "줄")
auth.log 생성 완료: 43 줄
(2026-09-09 실측. 실행할 때마다 섞이는 순서와 숫자는 달라집니다.)
출력 읽는 법: 정상 기록 서른 줄 사이에 10.0.0.99의 실패 열두 번을 심고, 깨진 줄 하나를 추가했습니다. f문자열의 {i:02d}는 "두 자리로 맞춰라"라는 서식입니다.
왜 하는가: 만드는 과정에서 로그의 형식이 손에 익습니다. 분석할 데이터의 생김새를 아는 것이 파싱의 절반입니다.
3-2. 한 줄 파싱하기
parse_test.py:
import re
line = "2026-09-08 10:05:00 192.168.0.7 login user=lee result=FAIL"
m = re.search(r"(\d+\.\d+\.\d+\.\d+).*result=(\S+)", line)
print("IP:", m.group(1))
print("결과:", m.group(2))
IP: 192.168.0.7
결과: FAIL
(2026-09-09 실측.)
출력 읽는 법: \d+\.\d+\.\d+\.\d+는 "숫자.숫자.숫자.숫자" 즉 IP 모양입니다. 괄호로 감싼 부분이 그룹이 되어 group(1), group(2)로 꺼냅니다. .*는 "사이에 뭐가 있든", result=(\S+)는 result= 뒤의 공백 아닌 덩어리를 잡습니다. 패턴 앞의 r은 날것 문자열 표식입니다(Step 48).
예측해 보기: 같은 패턴을 SUCCESS 줄에 적용하면
group(2)가 뭐라고 나올까요? (2026-09-09 실측 정답:SUCCESS가 잡힙니다 — 패턴은 결과의 내용이 아니라 위치만 봅니다.)
3-3. 전체 파일을 세기 — Counter
count_test.py:
import re
from collections import Counter
fails = Counter()
with open("auth.log", encoding="utf-8") as f:
for line in f:
m = re.search(r"(\d+\.\d+\.\d+\.\d+).*result=(\S+)", line)
if m and m.group(2) == "FAIL":
fails[m.group(1)] += 1
for ip, count in fails.most_common():
print(ip, "실패", count, "번")
10.0.0.99 실패 12 번
192.168.0.19 실패 1 번
192.168.0.20 실패 1 번
192.168.0.17 실패 1 번
...
(2026-09-09 실측. 아래쪽 IP와 순서는 로그를 만들 때마다 달라집니다 — 맨 위는 달라지지 않습니다.)
출력 읽는 법: 실패한 줄의 IP만 Counter에 담았더니, 심어 둔 10.0.0.99가 단번에 맨 위로 올라왔습니다. most_common()이 많은 순서로 정렬해 줍니다. 마흔세 줄 속의 이상 징후가 한눈에 보입니다.
왜 하는가: "집계하면 이상이 드러난다"가 로그 분석의 심장입니다.
3-4. 기준선 두기 — 의심 판정
count_test.py에 이어서:
기준 = 5
for ip, count in fails.most_common():
if count >= 기준:
print("[의심]", ip, "-", count, "번 실패: 브루트포스 가능성")
[의심] 10.0.0.99 - 12 번 실패: 브루트포스 가능성
(2026-09-09 실측.)
출력 읽는 법: 기준(임계값)을 넘는 것만 걸러 냅니다. 기준을 3으로 낮춰 실행해 보세요 — 실측 로그에서는 이 로그에서조차 정상 실패가 1번뿐이라 결과가 같았지만, 우연이 겹치는 로그라면 정상 사용자가 [의심]에 뜨는 거짓 경보를 보게 됩니다. 기준을 바꿔 가며 결과가 어떻게 달라지는지 보는 것, 그것이 임계값 감각을 기르는 유일한 방법입니다.
3-5. 깨진 줄 건너뛰기 — 안전벨트
우리 로그에는 system restart라는 형식이 다른 줄이 섞여 있습니다. 안전벨트 없는 코드로 그 줄을 만나면 어떻게 되는지 실측해 봅시다:
import re
m = re.search(r"(\d+\.\d+\.\d+\.\d+).*result=(\S+)", "system restart")
print(m.group(1))
AttributeError: 'NoneType' object has no attribute 'group'
(2026-09-09 실측.)
읽는 법: search는 못 찾으면 None을 돌려줍니다. None에게 group을 부르면 죽습니다. 그래서 로그 처리의 표준 안전벨트:
m = re.search(r"(\d+\.\d+\.\d+\.\d+).*result=(\S+)", line)
if not m:
continue # 형식이 안 맞는 줄은 건너뛰기
ip, result = m.group(1), m.group(2)
"모든 줄이 형식대로 오리라"는 가정은 로그 분석에서 언제나 깨집니다. 깨진 줄은 세기만 하고 조용히 건너뛰는 것 — 그것이 3-6 완성 코드의 skipped 변수입니다.
3-6. 사용자 이름도 세기 — 공격이 노린 계정
IP만큼 중요한 것이 "어떤 계정을 노렸는가"입니다. 패턴만 살짝 바꿉니다. user_test.py:
import re
from collections import Counter
users = Counter()
with open("auth.log", encoding="utf-8") as f:
for line in f:
m = re.search(r"user=(\S+)", line)
if m:
users[m.group(1)] += 1
print(users.most_common())
[('admin', 20), ('guest', 11), ('lee', 7), ('park', 4)]
(2026-09-09 실측. 숫자는 로그를 만들 때마다 다릅니다 — 단 admin이 가장 많은 것은 심어 둔 공격 때문이라 고정입니다.)
출력 읽는 법: 패턴을 user=(\S+)로만 바꿨는데 계정별 시도 횟수가 나옵니다. admin이 압도적으로 많은 이유는 우리가 심은 공격이 admin을 노렸기 때문입니다. "관리자 계정을 노린 시도가 많다"는 그 자체로 보고서에 적을 가치가 있는 발견입니다. 실패한 줄만 골라 계정을 세면(result 조건 추가) 그림이 더 선명해집니다 — 실측에서는 [('admin', 13), ...]으로 admin 집중이 확인됐습니다.
3-7. 완성 — 로그 분석기
지금까지의 조각을 모아 log_analyzer.py를 완성합니다:
import sys
import re
from collections import Counter
if len(sys.argv) < 2:
print("사용법: python log_analyzer.py <로그파일> [기준]")
sys.exit(1)
filename = sys.argv[1]
threshold = int(sys.argv[2]) if len(sys.argv) > 2 else 5
success = Counter()
fails = Counter()
total = 0
skipped = 0
try:
f = open(filename, encoding="utf-8")
except FileNotFoundError:
print("파일을 찾을 수 없습니다:", filename)
sys.exit(1)
with f:
for line in f:
total += 1
m = re.search(r"(\d+\.\d+\.\d+\.\d+).*result=(\S+)", line)
if not m:
skipped += 1
continue
ip, result = m.group(1), m.group(2)
if result == "FAIL":
fails[ip] += 1
elif result == "SUCCESS":
success[ip] += 1
lines = []
lines.append("=== 로그 분석 리포트 ===")
lines.append(f"대상 파일: {filename}")
lines.append(f"전체 줄 수: {total} / 파싱 성공: {total - skipped} / 건너뜀: {skipped}")
lines.append(f"탐지 기준: 실패 {threshold}번 이상")
lines.append("")
lines.append("--- IP별 현황 ---")
all_ips = sorted(set(success) | set(fails), key=lambda ip: fails[ip], reverse=True)
for ip in all_ips:
mark = " [의심]" if fails[ip] >= threshold else ""
lines.append(f"{ip}: 성공 {success[ip]}, 실패 {fails[ip]}{mark}")
lines.append("")
suspects = [ip for ip in all_ips if fails[ip] >= threshold]
if suspects:
lines.append(f"요약: {len(suspects)}개 주소가 기준을 넘었습니다.")
for ip in suspects:
lines.append(f"- {ip}: 단시간 반복 실패로 보아 무차별 대입 시도로 의심됨")
else:
lines.append("요약: 기준을 넘는 주소가 없습니다.")
report = "\n".join(lines)
print(report)
with open("report.txt", "w", encoding="utf-8") as f:
f.write(report)
실행합니다:
python log_analyzer.py auth.log
=== 로그 분석 리포트 ===
대상 파일: auth.log
전체 줄 수: 43 / 파싱 성공: 42 / 건너뜀: 1
탐지 기준: 실패 5번 이상
--- IP별 현황 ---
10.0.0.99: 성공 0, 실패 12 [의심]
192.168.0.3: 성공 2, 실패 1
...
192.168.0.2: 성공 1, 실패 0
요약: 1개 주소가 기준을 넘었습니다.
- 10.0.0.99: 단시간 반복 실패로 보아 무차별 대입 시도로 의심됨
(2026-09-09 실측. report.txt에도 같은 내용이 저장됐습니다.)
코드 읽는 법: 전부 아는 조각들입니다 — sys.argv 인자 처리(Step 51), 정규식 파싱(3-2), 안전벨트(3-5), Counter 집계(3-3), 파일 저장(Step 45), 예외처리(Step 46). 새로운 것은 조립뿐입니다. set(success) | set(fails)는 두 Counter의 주소 전체를 합집합으로 모으는 것이고, key=lambda ip: fails[ip]로 "실패 많은 순" 정렬했습니다. 마지막의 if suspects: 부분이 숫자에 의미를 덧붙이는 자리입니다 — "12번 실패"가 숫자라면 "무차별 대입 시도로 의심됨"이 해석입니다.
검증 포인트: 인자 없이 실행하면 사용법: python log_analyzer.py <로그파일> [기준]이 나오고, 없는 파일을 주면 파일을 찾을 수 없습니다:가 나오며 조용히 끝납니다 (둘 다 2026-09-09 실측 확인). 두 번째 인자로 기준을 바꿀 수 있습니다: python log_analyzer.py auth.log 3.
4. 미션과 연습문제
미션 — 분석기 단련시키기
완성된 분석기를 더 단단하게 만들어 봅니다:
- make_log.py를 수정해 규모를 키웁니다: 정상 기록 100줄, 심은 공격 30번으로
- 키운 로그로
python log_analyzer.py auth.log를 실행해 탐지가 흔들리지 않는지 확인합니다 - 기준을 3, 5, 10으로 바꿔 가며 실행하고 [의심] 목록이 어떻게 달라지는지 기록합니다
- 공격 줄의 계정을 admin이 아니라 guest로 바꿔 로그를 다시 만들고, 계정 집계(user_test.py)가 그 변화를 잡아내는지 확인합니다
- report.txt를 열어 숫자와 의미(요약 줄)가 함께 있는지 점검하고, 소감 한 줄을 손으로 덧붙입니다
연습문제
문제 1. 브루트포스 공격이 로그에 남기는 무늬 세 가지(어느 IP, 얼마나, 어떤 결과)를 말해 보세요.
문제 2. if not m: continue가 없으면 어떤 일이 일어나며, 그때 나는 오류의 이름은 무엇인가요?
문제 3. 임계값을 너무 낮추면 어떤 문제가, 너무 높이면 어떤 문제가 생기나요? 각각을 보안 용어(거짓 경보 관점)로 말해 보세요.
문제 4. 좋은 분석 리포트는 "숫자"와 "의미"를 함께 적는다고 했습니다. 10.0.0.99: 성공 0, 실패 12라는 줄에 덧붙일 "의미" 문장을 직접 써 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
1~2번: make_log.py에서 range(30)을 range(100)으로, 공격 줄의 range(12)를 range(30)으로 바꾸고 로그를 다시 만듭니다. 분석기를 다시 돌리면 전체 줄 수가 131로 늘어나도 맨 위는 여전히 10.0.0.99 — 규모가 커져도 집계 기반 탐지는 흔들리지 않습니다.
3번의 기록 예 (2026-09-09 실측 로그 기준):
기준 3: [의심] 10.0.0.99 (12번)
기준 5: [의심] 10.0.0.99 (12번)
기준 10: [의심] 10.0.0.99 (12번)
이 로그에서는 정상 실패가 1번뿐이라 셋이 같지만, 우연한 실패가 많은 로그에서는 기준 3에서 거짓 경보가 생깁니다. "기준을 바꾸며 관찰했다"는 기록 자체가 미션의 핵심입니다.
4번: 공격 줄을 user=guest로 바꾸면 user_test.py의 1위가 guest로 바뀝니다. 질문을 바꾸면(집계 기준을 바꾸면) 데이터가 새로 답한다는 것을 확인하는 단계입니다.
5번: report.txt에 "10.0.0.99: 성공 0, 실패 12"(숫자)와 "무차별 대입 시도로 의심됨"(의미)이 함께 있어야 합니다.
연습문제 해답
문제 1 해답. 같은 IP에서, 짧은 시간에, 로그인 실패가 반복되는 무늬입니다. 정상 사용자는 비밀번호를 수십 번 틀리지 않기 때문에 이 조합이 성립하면 무차별 대입을 의심합니다.
문제 2 해답. 정규식이 매치되지 않는 줄에서 m.group(...)을 부르게 되고, m이 None이므로 AttributeError: 'NoneType' object has no attribute 'group'가 나며 프로그램이 죽습니다 (2026-09-09 실측). 안전벨트 한 줄이 분석 중단을 막습니다.
문제 3 해답. 너무 낮추면 정상 사용자까지 [의심]에 걸리는 거짓 경보(false positive)가 늘어 분석가가 경보를 무시하게 됩니다. 너무 높이면 진짜 공격을 놓치는 누락(false negative)이 생깁니다. 임계값 조정은 이 둘 사이의 균형 잡기이며, 정답은 환경마다 다릅니다.
문제 4 해답. 예: "성공 없이 실패만 12번 반복되어, 정상 사용이 아닌 무차별 대입 시도로 의심됨. 해당 주소의 차단과 추가 감시를 권고." — 숫자(12번, 성공 0)가 근거이고 문장이 판단입니다. 분석의 완성은 코드가 아니라 문장입니다.
완료 기준 체크리스트
- [ ] 로그가 무엇이고 브루트포스가 어떤 무늬를 남기는지 설명할 수 있다
- [ ] 정규식으로 로그 한 줄에서 IP와 결과를 파싱할 수 있다
- [ ] Counter와 most_common()으로 집계와 정렬을 할 수 있다
- [ ]
if not m: continue안전벨트가 왜 필요한지 설명할 수 있다 - [ ] 임계값과 거짓 경보의 관계를 설명할 수 있다
- [ ] log_analyzer.py가 인자 처리·집계·[의심] 표시·파일 저장을 모두 한다
- [ ] 미션: 규모를 키운 로그와 기준 변화 실험까지 마쳤다
6. 흔한 실수와 해결
벽 1. AttributeError: ‘NoneType’ object has no attribute ‘group’
증상 (2026-09-09 실측):
AttributeError: 'NoneType' object has no attribute 'group'
원인: 정규식이 매치되지 않는 줄(형식이 다른 줄)에서 group을 불렀습니다. search는 못 찾으면 None을 줍니다.
해결: 3-5절의 안전벨트 if not m: continue. 로그 분석에서 "모든 줄이 형식대로 온다"는 가정은 언제나 깨집니다.
벽 2. 정규식이 아예 아무것도 못 찾는다
증상: Counter가 텅 비어 있습니다.
원인: 패턴과 실제 로그 형식이 어긋납니다. result= 앞뒤 공백, 날짜 형식의 차이 등.
해결: 한 줄만 골라 3-2절의 parse_test.py처럼 단독으로 시험하세요. 정규식은 "한 줄에서 먼저 맞추고, 파일로 넓히는" 순서가 정석입니다.
벽 3. 백슬래시(\d)가 이상하게 먹힌다
증상: 패턴을 썼는데 \d가 숫자로 인식되지 않습니다.
원인: 파이썬 문자열에서 백슬래시는 특수 취급됩니다.
해결: Step 48에서 배운 대로 패턴 앞에 r을 붙인 날것 문자열(r"...")을 쓰세요. 오늘 모든 예시가 그렇게 되어 있습니다.
벽 4. 파일을 못 찾는다
증상: FileNotFoundError로 죽거나, 완성 코드라면 파일을 찾을 수 없습니다: 메시지.
원인: 파일 이름 오타이거나, 스크립트를 실행한 폴더와 로그 파일이 있는 폴더가 다릅니다. 상대 경로는 "실행한 폴더" 기준입니다 (Step 51 복습).
해결: os.listdir()나 탐색기로 실제 파일 이름과 위치를 확인하세요. 인자로 경로를 줄 때 폴더가 다르면 python log_analyzer.py logs\auth.log처럼 경로째 줍니다.
벽 5. 기준을 숭상한다
증상: 기준 5에 4번 실패한 공격은 놓치고, 5번을 "공격 확정"처럼 믿습니다.
원인: 임계값의 본질을 잊은 것입니다. 기준은 진실이 아니라 그물의 눈 크기입니다.
해결: 미션 3번처럼 기준을 바꿔 가며 결과가 어떻게 달라지는지 직접 보세요. "그물을 촘촘히 하면 잡히는 것도 노이즈도 는다"를 몸으로 아는 것이 분석가의 감각입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 로그 | 시간 순 기록 — 보안 분석의 기본 재료 |
| 브루트포스의 무늬 | 같은 IP + 짧은 시간 + 반복 실패 |
| 파싱(parsing) | 로그 한 줄에서 정보 조각을 뽑는 일 |
| Counter | 항목별 개수를 세는 전문 도구 |
| 임계값 | 의심의 기준선 — 그물의 눈 크기 |
| 거짓 경보 | 정상인데 의심받는 것 — 기준이 너무 낮을 때 |
오늘의 문법
| 문법 | 하는 일 |
|---|---|
re.search(r"패턴", 줄) |
첫 매치 찾기 — 못 찾으면 None |
m.group(1), m.group(2) |
괄호 그룹 꺼내기 |
Counter() / c[x] += 1 |
항목별 세기 |
c.most_common() |
많은 순서로 정렬 |
if not m: continue |
깨진 줄 건너뛰는 안전벨트 |
sys.argv[1] |
분석 대상 파일을 인자로 받기 |
명령어보다 중요한 감각
로그 파일을 받았을 때 분석가의 질문은 정해져 있습니다. (가) 얼마나 많은가 — 줄 수. (나) 누가 많이 오는가 — IP별 집계. (다) 무엇을 노리는가 — 계정별 집계. (라) 이상한 시간은 없는가. (마) 성공한 것 중 이상한 것은 없는가 — 실패 백 번 뒤의 성공 한 번이 가장 무섭습니다. 오늘 우리는 (가)~(다)를 코드로 했습니다. 나머지도 도구는 같습니다 — 시간대별 집계도 결국 시간 조각을 뽑아 Counter에 넣는 일입니다.
두 가지를 더 기억해 두세요. 첫째, 오늘 만든 것은 실제 보안 시스템(SIEM이라 부르는 대규모 로그 분석 체계)의 원형입니다 — 수집, 파싱, 집계, 기준 넘으면 경보. 규모만 다를 뿐 구조는 같습니다. 이 원형을 만들어 본 사람은 공격자의 입장도 보입니다: "내가 공격한다면 로그에 어떤 무늬를 남길까." 둘째, 로그는 거짓말을 하지 않지만 다 말하지도 않습니다. 기록이 꺼져 있던 시간은 침묵입니다. "로그가 조용하다 = 아무 일 없었다"가 아니라는 것, 이 하나는 지금 새겨 두세요.
전부 체크되면 Step 55 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.