F-lab Java 1주차 / Phase 4 / Unit 4.1 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Phase 1, 2 (OOP, 클래스/객체)
다음 Unit: 4.2 — Pass by Value (자바의 진실)이 Unit의 의미: Phase 4 의 시작 — 추상의 세계에서 물리의 세계로.
면접에서 거의 100% 출제 되는 영역. 4-5주차 동시성/GC, 11-12주차 JPA 영속성 컨텍스트의 토대.
큰 도서관을 상상해보세요. 도서관에는 여러 공간 이 있습니다:
1층 — 카운터 (PC Register):
2층 — 작업 책상 (Stack):
3층 — 장서 보관실 (Heap):
지하 — 카탈로그실 (Method Area):
→ JVM 메모리도 비슷한 구조. 각 공간이 다른 목적.
당신이 회사에서 일한다고 상상해보세요:
공유 캐비닛 (Heap):
개인 책상 (Stack):
회사 매뉴얼 (Method Area):
지금 하는 작업 표시 (PC Register):
→ 자바 프로그램이 실행될 때 JVM 안에서 정확히 이런 공간들이 만들어집니다.
"JVM은 자바 프로그램 실행을 위해 메모리를 5가지 영역으로 나누어 관리한다."
5가지 영역의 핵심 차이:
| 영역 | 공유 여부 | 보관 대상 |
|---|---|---|
| Method Area | 모든 스레드 공유 | 클래스 정보, static |
| Heap | 모든 스레드 공유 | 모든 객체 |
| Stack | 스레드별 | 메서드 호출, 지역변수 |
| PC Register | 스레드별 | 현재 명령 위치 |
| Native Method Stack | 스레드별 | 네이티브 메서드 |
프로그램은 결국 메모리에서 실행 됩니다. 메모리 관리가 잘못되면:
C에서는 메모리를 개발자가 직접 관리:
// C 코드
int* arr = malloc(sizeof(int) * 100); // 메모리 할당
// ... 사용 ...
free(arr); // 명시적으로 해제 ❗
// 해제 안 하면 → 메모리 누수
// 두 번 해제하면 → 충돌
문제:
Java(1995) 의 핵심 약속:
"메모리 관리는 JVM이 알아서"
자바 개발자는:
new 로 객체 생성만이를 위해 JVM은 명확한 메모리 영역 구분 이 필요했습니다.
JVM Specification (1995~) 에서 정의:
5가지 영역:
1. Method Area (모든 스레드 공유)
2. Heap (모든 스레드 공유)
3. Stack (스레드별)
4. PC Register (스레드별)
5. Native Method Stack (스레드별)
→ 이게 면접 단골 "JVM 메모리 구조".
영역을 나누는 이유 ⭐ :
"JVM 메모리 구조를 모르면 자바를 진짜 이해 못한 것이다."
Pass by Value (Unit 4.2), 동시성 (4주차), GC (5주차), JPA 영속성 컨텍스트 (12주차) 등 — 모든 자바의 깊은 주제가 이 메모리 구조 위에 있다.
면접에서도 거의 100% 묻는 영역. 답할 수 있느냐 없느냐가 시니어 차별화 지점.
깊이 이해 없이 자바를 쓰면 다양한 문제에서 막힙니다.
public class FareService {
private List<Fare> fares;
public int countFares() {
return fares.size(); // 💥 NullPointerException
}
}
메모리 구조 모르면:
메모리 구조 알면:
fares 는 참조 변수 (Stack 또는 Heap의 객체 안)public void process(List<Integer> list) {
list.add(100); // 호출자의 list에도 영향?
}
List<Integer> myList = new ArrayList<>();
process(myList);
System.out.println(myList); // [100] ⚠️ 변경됨!
메모리 구조 모르면:
메모리 구조 알면:
myList 는 Stack의 참조 변수process(myList) → 참조의 값 이 복사되어 전달list 와 외부의 myList 가 같은 Heap 객체 가리킴→ Pass by Value의 진실 (Unit 4.2 미리보기).
List<byte[]> leaks = new ArrayList<>();
while (true) {
leaks.add(new byte<[1024 * 1024]); // 1MB씩 추가
}
// 💥 OutOfMemoryError: Java heap space
메모리 구조 모르면:
메모리 구조 알면:
leaks 가 GC 대상 안 됨 (참조 살아있음)-Xmx 옵션으로 Heap 증가 가능→ 5주차 GC 학습의 토대.
public void recursion() {
recursion(); // 무한 재귀
}
recursion(); // 💥 StackOverflowError
메모리 구조 모르면:
메모리 구조 알면:
public class Counter {
private int count = 0;
public void increment() {
count++; // 멀티스레드 환경에서 문제
}
}
메모리 구조 모르면:
메모리 구조 알면:
count 는 Heap 의 객체에 속한 인스턴스 변수→ 4주차 동시성 학습의 토대.
"JVM 메모리 구조를 설명해주세요"
대답 못함:
잘 답함:
지금 학습 안 하면:
→ 이 Unit이 향후 모든 자바 학습의 토대.
┌─────────────────────────────────────────────┐
│ JVM Runtime │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Method Area │ │ Heap │ │
│ │ (Class Info) │ │ (Objects) │ │
│ │ │ │ │ │
│ │ - 클래스 메타 │ │ - 모든 객체 │ │
│ │ - static 변수 │ │ - 인스턴스 변수│ │
│ │ - Constant Pool│ │ - 배열 │ │
│ └─────────────────┘ └─────────────────┘ │
│ (모든 스레드 공유) │
│ │
│ ┌──────────────┬──────────────┬─────────┐ │
│ │ Thread 1 │ Thread 2 │ ... │ │
│ │ │ │ │ │
│ │ - PC Register│ - PC Register│ │ │
│ │ - Stack │ - Stack │ │ │
│ │ - Native │ - Native │ │ │
│ │ Method Stk │ Method Stk │ │ │
│ └──────────────┴──────────────┴─────────┘ │
│ (스레드별 분리) │
└─────────────────────────────────────────────┘
역할: 클래스 정보 저장
저장 내용:
특징:
예시:
public class Customer {
public static int count = 0; // Method Area에
private String name; // 객체 마다 (Heap에)
public void hello() { ... } // 메서드 코드 → Method Area
}
역할: 모든 객체 저장
저장 내용:
new 로 만든 모든 객체특징:
-Xmx, -Xms 옵션으로 크기 설정Heap 의 내부 구조 (HotSpot JVM):
[Heap]
├── Young Generation
│ ├── Eden
│ ├── Survivor 0
│ └── Survivor 1
└── Old Generation
→ 5주차 GC에서 자세히.
예시:
Customer c = new Customer();
// ↑ Heap에 Customer 인스턴스 생성
// c는 Stack에 있는 참조 변수
역할: 메서드 호출 정보, 지역변수
저장 내용:
특징:
Stack Frame 구조:
[Stack Frame]
- Local Variable Array (지역변수)
- Operand Stack (계산용)
- Frame Data (리턴 주소 등)
예시:
public void method1() { // Stack Frame 1
int x = 10; // Frame 1의 지역변수
method2(); // 새 Frame 2 추가
int y = 20; // 다시 Frame 1
}
public void method2() { // Stack Frame 2
int a = 100; // Frame 2의 지역변수
// method2 종료 → Frame 2 제거
}
역할: 현재 실행 중인 명령의 주소
특징:
예시:
public void method() {
int x = 1; // PC: line 1
int y = 2; // PC: line 2
int z = x + y; // PC: line 3
}
→ 스레드 전환 시 어디까지 했는지 기억하는 용도.
역할: 네이티브 메서드 (C/C++) 호출 정보
저장 내용:
특징:
예시:
public class System {
public static native long currentTimeMillis();
// ↑ 네이티브 메서드
}
System.currentTimeMillis();
// → Java Stack에서 호출
// → Native Method Stack으로 전환
// → C 코드 실행
→ 일반 개발자가 직접 다룰 일 적음.
| 영역 | 공유 여부 | 저장 | 크기 | 생성 시점 |
|---|---|---|---|---|
| Method Area | 모든 스레드 | 클래스 정보, static | 중 | JVM 시작 |
| Heap | 모든 스레드 | 객체, 배열 | 가장 큼 | JVM 시작 |
| Stack | 스레드별 | 메서드 프레임 | 작음 | 스레드 시작 |
| PC Register | 스레드별 | 현재 명령 위치 | 매우 작음 | 스레드 시작 |
| Native Method Stack | 스레드별 | 네이티브 호출 | 작음 | 스레드 시작 |
public class Customer {
private static int totalCount = 0; // ① static
private String name; // ② 인스턴스 변수
public void hello() {
int localVar = 10; // ③ 지역변수
Customer other = new Customer(); // ④ 지역 참조 변수
}
}
저장 위치:
totalCount (static) → Method Areaname (인스턴스) → Heap (객체 안)localVar (지역, 기본형) → Stackother (지역 참조) → Stack (참조), Heap (객체)→ 면접에서 자주 묻는 질문.
public class Hello {
public static void main(String[] args) {
Customer c = new Customer();
}
}
JVM 시작 시:
1. ClassLoader가 .class 파일 읽음
↓
2. 클래스 정보를 Method Area에 저장
- Customer 클래스 메타데이터
- 메서드 바이트코드
- static 변수 영역 할당
↓
3. main 메서드 실행 시작
↓
4. main 메서드용 Stack Frame 생성
- args 매개변수
- c 지역변수 자리 (아직 비어있음)
↓
5. new Customer() 실행
- Heap에 Customer 인스턴스 생성
- 인스턴스 변수 초기화
↓
6. c에 인스턴스 참조 저장 (Stack의 c 변수에)
결과 메모리 상태:
[Method Area]
Customer 클래스 정보
[Heap]
Customer 인스턴스 (인스턴스 변수들)
↑
│ 참조
[Stack — main 스레드]
main Frame
- args
- c (참조) ─────┘
public class Calculator {
public int add(int a, int b) {
int sum = a + b;
return sum;
}
}
Calculator calc = new Calculator();
int result = calc.add(3, 5);
Step 1 — 호출 직전:
[Heap]
Calculator 인스턴스
↑
[Stack — main]
main Frame
- calc (참조)
- result (아직 미할당)
Step 2 — calc.add(3, 5) 호출:
[Stack — main]
main Frame
- calc
- result
┌──────────────┐
│ add Frame │ ← 새 Frame 추가
│ - this (참조)│ ← 자동으로 Calculator 인스턴스 참조
│ - a = 3 │ ← 매개변수
│ - b = 5 │
│ - sum = ? │ ← 지역변수
└──────────────┘
Step 3 — sum = a + b 실행:
add Frame
- this
- a = 3
- b = 5
- sum = 8 ← 계산 결과
Step 4 — return:
[Stack — main]
main Frame
- calc
- result = 8 ← 반환값 받음
add Frame은 제거됨
→ Stack Frame이 메서드 호출의 핵심.
public class Shared {
public int counter = 0; // 공유 데이터
}
public class MyThread extends Thread {
private Shared shared;
private int localValue; // 스레드별 데이터
public void run() {
shared.counter++;
localValue = 100;
}
}
Shared shared = new Shared();
MyThread t1 = new MyThread();
t1.shared = shared;
t1.start();
MyThread t2 = new MyThread();
t2.shared = shared;
t2.start();
메모리 상태:
[Method Area]
Shared 클래스 정보, MyThread 클래스 정보
[Heap] (모든 스레드 공유)
Shared 인스턴스 (counter)
↑ ↑
│ │
[Stack t1] [Stack t2] (스레드별 분리)
- shared - shared
참조 참조
- localValue - localValue
(각자 독립) (각자 독립)
핵심:
shared.counter 는 Heap 에 1개 → 두 스레드가 동시 변경 시 race conditionlocalValue 는 각 스레드의 Stack 에 따로 → 안전→ 4주차 동시성 학습의 토대.
Java 7 까지:
[JVM Heap]
├── Young Generation
├── Old Generation
└── PermGen (영구 영역) ← Method Area의 일부
- 클래스 정보
- static 변수
- 상수 풀
문제:
OutOfMemoryError: PermGen space 자주 발생Java 8+:
[JVM Heap]
├── Young Generation
└── Old Generation
[Native Memory]
└── Metaspace ← Method Area
- 동적 크기 조정 (OS 메모리 한도까지)
- 더 안정적
효과:
OutOfMemoryError: PermGen space 사라짐OutOfMemoryError: Metaspace 가능 (드문 일)→ 면접 빈출 질문: "Java 8 이후 메모리 변화?"
문자열은 특별 취급:
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
s1 == s2; // true (같은 String Pool 참조)
s1 == s3; // false (s3는 Heap에 새 객체)
s1.equals(s3); // true (값 비교)
메모리 상태:
[Method Area / Heap의 특별 영역]
String Constant Pool
"hello" ← 1개만
↑ ↑
│ │
[Heap]
새 String 객체 (s3가 가리킴)
↑
│
[Stack]
s1 ───┘
s2 ───┘
s3 ───┘ (새 객체 가리킴)
→ 6주차 String 학습 으로 연결.
public class FareSystem {
// Method Area에 저장
private static int totalFareCount = 0;
private static final String VERSION = "1.0";
// 인스턴스 변수 — 객체와 함께 Heap에
private Long id;
private int amount;
private FareStatus status;
// 메서드 코드 — Method Area에
public void changeAmount(int newAmount) {
// 매개변수 newAmount, 지역변수 → Stack
int oldAmount = this.amount; // 지역변수
if (newAmount < 0) {
throw new IllegalArgumentException();
}
this.amount = newAmount; // Heap의 인스턴스 변수 변경
totalFareCount++; // Method Area의 static 변경
}
}
메모리 추적:
totalFareCount → Method AreaVERSION → Method Area + String Poolid, amount, status → Heap (각 인스턴스마다)changeAmount 메서드 코드 → Method AreanewAmount, oldAmount → Stack (호출 시)public class FareDemo {
public static void main(String[] args) {
// Step 1: Customer 인스턴스 생성
Customer alice = new Customer("Alice");
// alice → Stack
// Customer 인스턴스 → Heap
// Step 2: Fare 인스턴스 생성
Fare fare = new Fare(50000, alice);
// fare → Stack
// Fare 인스턴스 → Heap (customer 필드는 alice 참조)
// Step 3: 같은 alice 참조
Fare fare2 = new Fare(80000, alice);
// alice 객체는 Heap에 1개, 두 Fare가 공유
// Step 4: alice 변경
alice.setName("Alice Smith");
// → Heap의 Customer 객체 변경
// → fare.customer.getName() == "Alice Smith"
// → fare2.customer.getName() == "Alice Smith"
}
}
메모리 그림:
[Heap]
Customer ("Alice Smith") ← 1개
↑ ↑
│ │
Fare(50000) Fare(80000)
↑ ↑
│ │
[Stack — main]
alice ─────────────┐
fare ──────────────┤
fare2 ─────────────┘
public class StackDemo {
public static int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 재귀
}
public static void main(String[] args) {
System.out.println(factorial(10)); // OK
System.out.println(factorial(100000)); // 💥 StackOverflowError
}
}
왜 StackOverflow?:
해결:
// 반복문으로
public static int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
→ Stack을 거의 안 씀.
public class HeapDemo {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
list.add(new byte[1024 * 1024]); // 1MB씩 추가
// 💥 OutOfMemoryError: Java heap space
}
}
}
해결:
# JVM 옵션으로 Heap 크기 증가
java -Xmx4g HeapDemo # 4GB로 증가
근본 해결: 불필요한 참조 제거 (5주차 GC).
public class CounterTest {
private int count = 0; // Heap의 인스턴스 변수
public void increment() {
count++; // 멀티스레드에 위험
}
public static void main(String[] args) throws InterruptedException {
CounterTest counter = new CounterTest();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count); // 20000? — 보통 적게 나옴
}
}
왜 적게?:
count 는 Heap 의 객체에 속함 (공유)count++ 는 read → modify → write 3 단계해결:
private AtomicInteger count = new AtomicInteger(0);
// 또는 synchronized
→ 4주차 동시성에서 본격.
Q: "JVM 메모리 구조?"
A: "Stack과 Heap이요"
→ 5가지 영역 중 2개만. 면접에서 깊이 부족 노출.
올바른 답: Method Area, Heap, Stack, PC Register, Native Method Stack — 5가지 + 각각의 역할.
Q: "static 변수는 어디에?"
A: "Heap이요" ❌
정답: Method Area (Java 8+ Metaspace).
이유:
Customer.totalCount; // 인스턴스 없이 접근
// → Method Area에 있어야 가능
Q: "객체는 어디에 저장?"
A: "Heap" ❌ (틀린 건 아니지만 부정확)
보다 정확한 답:
Q: "Java 8 이후 PermGen은?"
A: "PermGen이 더 커졌어요" ❌
정답: PermGen이 사라지고 Metaspace로 교체.
public void recursion(int depth) {
// 1만 번 재귀
if (depth < 10000) recursion(depth + 1);
}
recursion(0); // 💥 StackOverflowError 가능
→ Stack은 수백 KB ~ 수 MB. Heap (수 GB) 와 매우 다름.
Q: "지역변수가 GC 대상?"
A: "네" ❌
정답: 지역변수는 메서드 종료 시 자동 제거 (Stack Frame 제거). GC 대상 아님.
public void method() {
int x = 10; // Stack 에
String s = "hello"; // s는 Stack의 참조, "hello"는 별도
}
// 메서드 종료 → x, s 자동 제거
GC 대상은 Heap 의 객체 만.
흔한 오해:
java -Xmx4g # Heap만 설정?
정답: 다양한 영역 설정 가능.
JVM 옵션 ⭐ :
-Xms<size> # Heap 초기 크기
-Xmx<size> # Heap 최대 크기
-Xss<size> # Stack 크기 (스레드별)
-XX:MetaspaceSize=<size> # Metaspace 초기
-XX:MaxMetaspaceSize=<size> # Metaspace 최대
-XX:+HeapDumpOnOutOfMemoryError # OOM 시 덤프
[Unit 4.1: JVM 런타임 데이터 영역] ← 지금 여기
↓
[Unit 4.2: Pass by Value (자바의 진실)]
메모리 구조 위에서 값/참조 전달 이해
Phase 4의 의미:
이전 학습 다시 보기:
| Phase 2 학습 | 메모리 관점 |
|---|---|
| Unit 2.1 (메서드) | Stack Frame |
| Unit 2.3 (상속) | Heap의 객체에 부모+자식 필드 함께 |
| Unit 2.4 (다형성) | VMT가 Method Area에 |
| Unit 2.6 (Inner) | Outer 인스턴스 참조가 Heap에 |
→ 이전 학습이 메모리 구조 위에서 이해됨.
Phase 5 (GC):
Phase 6 (String, 컬렉션):
Phase 7 (I/O):
4주차 (동시성):
5주차 (Spring):
11-12주차 (JPA):
17주차 (MSA):
→ 모든 자바 학습의 토대.
[Java 1.0 ~ 1.4]
- 단순 메모리 모델
- Stop-the-world GC
[Java 5]
- JVM Memory Model 정립
- volatile, synchronized 의미 명확화
[Java 7]
- G1 GC 도입 (Heap 영역화)
[Java 8] ⭐
- PermGen → Metaspace
- 람다, Stream
[Java 11]
- ZGC (대용량 Heap)
[Java 21+]
- Generational ZGC
- Project Loom (가상 스레드)
→ 메모리 모델은 끝없이 진화.
| 질문 | 이 Unit에서의 답 |
|---|---|
| "JVM 메모리 구조?" | 5가지 영역 + 각 역할 |
| "static 변수는 어디?" | Method Area |
| "Stack과 Heap 차이?" | 스레드별/공유, 작음/큼, 자동 정리/GC |
| "Java 8 메모리 변화?" | PermGen → Metaspace |
| "OutOfMemoryError 종류?" | Heap, Metaspace, Stack |
| "Stack Overflow vs OOM?" | Stack vs Heap |
1️⃣ JVM 메모리는 5가지 영역 — Method Area, Heap, Stack, PC Register, Native Method Stack.
공유 영역 (Method Area, Heap) 과 스레드별 영역 (Stack, PC Register, Native Method Stack) 으로 나뉜다. Method Area 는 클래스 정보와 static, Heap 은 모든 객체, Stack 은 메서드 호출과 지역변수. 이 구분이 동시성/GC/성능 최적화의 토대.
2️⃣ 변수는 종류에 따라 저장 위치가 다르다.
static 변수 → Method Area, 인스턴스 변수 → Heap (객체 안), 지역변수 (기본형) → Stack, 지역 참조 변수 → Stack (참조), Heap (객체). 면접에서 자주 묻는 영역. Pass by Value (Unit 4.2) 의 핵심 토대.
3️⃣ Java 8 이후 PermGen이 Metaspace 로 교체됐다.
Java 7 까지의 PermGen 은 Heap 안에 고정 크기로 있어
OutOfMemoryError: PermGen space가 잦았다. Java 8+ 의 Metaspace 는 Native Memory 에서 동적 크기, 더 안정적. JVM 옵션-Xms/-Xmx(Heap),-Xss(Stack),-XX:MaxMetaspaceSize(Metaspace) 로 각 영역 통제 가능.
Q1: Member m = new Member() 에서 m은 어디에, 객체 본체는 어디에 저장되는가?
한 줄 답: m 은 Stack (또는 Heap, 위치에 따라), 객체 본체는 Heap.
상세 설명:
위 코드를 분해:
Member m = new Member();
// ↑ ↑
// 변수 객체
m 의 저장 위치 — 컨텍스트에 따라:
// Case 1: 메서드 안의 지역변수
public void method() {
Member m = new Member();
// m은 Stack의 method Frame 안 ← Stack
}
// Case 2: 클래스의 인스턴스 변수
public class Service {
private Member m; // m은 Service 인스턴스 안 ← Heap
}
// Case 3: static 변수
public class Service {
private static Member m; // m은 Method Area
}
객체 본체 (new Member() 로 생성된 것):
new 키워드 = "Heap에 객체 생성"메모리 그림 (Case 1):
[Stack — 메서드 Frame]
m (참조) ─────┐
│
[Heap] │
Member 인스턴스 ◄─┘
- 인스턴스 변수들
m == 객체 본체 가 아님:
m 은 참조 변수 (객체의 주소를 담음)Q2: static 변수와 인스턴스 변수의 저장 위치 차이는?
한 줄 답: static 변수 → Method Area, 인스턴스 변수 → Heap (객체 안).
상세 설명:
public class Customer {
private static int totalCount = 0; // static
private String name; // 인스턴스
public Customer(String name) {
this.name = name;
totalCount++;
}
}
Customer c1 = new Customer("Alice");
Customer c2 = new Customer("Bob");
메모리 상태:
[Method Area]
Customer 클래스 정보
- totalCount: 2 ← 1개만 존재 ⭐
[Heap]
c1 인스턴스 c2 인스턴스
- name: "Alice" - name: "Bob"
(totalCount는 여기 없음)
↑ ↑
│ │
[Stack]
c1 ──┘ │
c2 ───────────────────┘
핵심 차이 ⭐ :
| static 변수 | 인스턴스 변수 | |
|---|---|---|
| 개수 | 클래스당 1개 | 인스턴스마다 1개 |
| 저장 위치 | Method Area | Heap (객체 안) |
| 수명 | 클래스 로드 ~ JVM 종료 | 객체 생성 ~ GC |
| 접근 | 클래스명.변수 | 인스턴스.변수 |
| GC | 일반적으로 X | 객체 GC 시 함께 |
왜 이렇게 다른가?:
실용적 함의:
public class Counter {
private static int total = 0; // 모든 인스턴스가 공유
private int myCount = 0; // 각 인스턴스만의 값
public void count() {
total++;
myCount++;
}
}
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.count(); c1.count(); // total = 2, c1.myCount = 2
c2.count(); // total = 3, c2.myCount = 1
결과:
total 은 공유 → 3myCount 는 각자 → c1: 2, c2: 1→ static 의 본질이 메모리 위치에서 드러남.
멀티스레드 시 위험 ⚠️ :