F-LAB JAVA · 7주차 · Phase 2 · ORM 패러다임
🔄 Phase 2 시작 — "왜 ORM 이 필요한가" 의 근본 답
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Order 가 List<OrderItem> 을 RDB 로? 표현은?객체-관계 미스매치 (Object-Relational Impedance Mismatch) 는 객체 (OOP) 와 관계형 DB 가 모델링·상속·연관관계·식별·데이터 타입 5가지 측면에서 본질적으로 다른 패러다임이라 매번 손으로 변환 코드를 쓰는 고통이며, 이 차이를 자동으로 메우는 게 ORM 등장의 이유다.
객체-관계 미스매치 (Impedance Mismatch) 는 객체 (OOP) 와 관계형 DB 가 근본적으로 다른 패러다임 이라 생기는 불일치다.
5가지 측면에서 다르다 — (1) 모델링 (객체는 상태+행동, RDB 는 행과 열), (2) 상속 (객체에 있음, RDB 에 없음), (3) 연관 관계 (객체는 참조order.member, RDB 는 외래 키), (4) 식별 (객체 식별자 vs PK), (5) 데이터 타입 (List·Map 같은 풍부 컬렉션 vs 제한적 SQL 타입).
이 차이를 매번 손으로 메우는 코드 (객체 → SQL, ResultSet → 객체, List → 별도 테이블 등) 가 ORM 등장 전의 고통이며, 자동으로 메워주는 게 ORM 의 핵심이다.
특히 —Order가List<OrderItem>을 갖는 구조는 RDB 에서orders + order_items + FK로 표현하고, 객체 상속은SINGLE_TABLE / JOINED / TABLE_PER_CLASS3가지 전략 중 하나로 표현해야 하며, 이런 변환을 ORM 이 자동화한다.
객체 ↔ RDB = 영어 ↔ 한국어:
영어 (객체):
- 동사/목적어/주어 분명
- 풍부한 시제
- 단/복수 명확
한국어 (RDB):
- 다른 어순
- 다른 표현 방식
- 다른 문법
5가지 차이:
1. 모델링: 문장 구조 (vs 단어)
2. 상속: 관계 표현 방식
3. 연관: 참조 (vs 외래 키)
4. 식별: 이름 (vs 번호)
5. 타입: 풍부 (vs 제한)
번역의 고통:
- 매번 손으로 문장 변환
- 단어 1:1 매핑 X
- 의미 보존 어려움
- 시간 낭비
자동 번역기 (ORM):
- 영어 → 한국어 자동
- 객체 → SQL 자동
- 사용자는 모국어로
- 변환 코드 X
ILIC:
- Shipment (객체) ↔ shipments (테이블)
- Customer.shipments (List) ↔ FK
- JPA 가 자동 매핑
→ 객체-관계 미스매치 = 5가지 본질적 차이, ORM = 자동 번역기.
1. 미스매치 정의
2. 모델링 차이
3. 상속 차이
4. 연관 관계 차이
5. 식별 차이
6. 데이터 타입 차이
7. Order → List<OrderItem> 표현
8. 객체 상속 RDB 표현 3가지
9. ORM 등장 전 고통
객체-관계 미스매치
(Object-Relational Impedance Mismatch):
객체 (OOP) 와 관계형 DB (RDB) 가
본질적으로 다른 패러다임:
- 모델링 방식 다름
- 표현 방식 다름
- 5가지 측면 차이
"Impedance Mismatch":
전기 공학 용어 차용:
- 두 회로의 임피던스 다름
- 전류 전달 비효율
- 비유: 두 시스템의 부조화
5가지 측면:
1. 모델링 (상태+행동 vs 행과 열)
2. 상속 (있음 vs 없음)
3. 연관 관계 (참조 vs FK)
4. 식별 (== vs PK)
5. 데이터 타입 (풍부 vs 제한)
→ ORM 의 도전 과제
객체 vs RDB (ILIC)
ILIC 의 객체 (Java):
class Shipment {
Long id;
String blNo;
Customer customer; // 참조
List<ShipmentItem> items; // 컬렉션
BigDecimal weight;
void calculateFreight() {} // 행동
}
ILIC 의 RDB (MySQL):
shipments
- id, bl_no, customer_id, weight
customers
- id, name
shipment_items
- id, shipment_id, item_name
→ 매핑이 필요:
- 참조 customer ↔ customer_id (FK)
- List items ↔ 별도 테이블 + FK
- 행동 X (RDB)
객체-관계 미스매치의 정의는?
답:
1. 정의:
용어:
5가지:
결과:
객체 (OOP):
상태 + 행동:
- 필드 (상태)
- 메서드 (행동)
- 캡슐화 (private + 메서드)
예:
class Account {
private BigDecimal balance; // 상태
void deposit(BigDecimal amount) { // 행동
this.balance = balance.add(amount);
}
}
RDB:
행과 열:
- 행 (Record): 한 단위 데이터
- 열 (Column): 속성
- 행동 X (데이터만)
예:
accounts 테이블:
id | balance
───┼────────
1 | 1000.00
2 | 5000.00
| 측면 | 객체 | RDB |
|---|---|---|
| 단위 | 객체 (Instance) | 행 (Row) |
| 속성 | 필드 | 컬럼 |
| 행동 | 메서드 | 없음 (SP 별개) |
| 캡슐화 | 있음 | 없음 |
| 상태 변경 | 메서드 통해 | UPDATE 직접 |
매핑:
필드 ↔ 컬럼:
- 객체의 필드 → DB 의 컬럼
- 자동 가능 (이름 같으면)
- 타입 변환 필요
메서드:
- DB 엔 없음
- 객체에서만 동작
- DB 는 그저 데이터 저장소
// 객체 (ILIC)
public class Shipment {
private Long id;
private String blNo;
private BigDecimal weight;
private String status;
// 행동
public void markAsShipped() {
if (this.status != "BOOKED") {
throw new IllegalStateException();
}
this.status = "SHIPPED";
}
public BigDecimal calculateFreight(BigDecimal rate) {
return this.weight.multiply(rate);
}
}
-- RDB (ILIC)
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50),
weight DECIMAL(10,2),
status VARCHAR(20)
);
-- 행동 X (단지 데이터)
-- 메서드는 객체에서만
-- DB 는 데이터 저장소
모델링 차이 (상태+행동 vs 행과 열) 는?
답:
1. 객체:
RDB:
차이:
매핑:
객체의 상속:
- 부모 클래스 + 자식 클래스
- 자식은 부모 속성 + 메서드 상속
- 다형성 (Polymorphism)
예:
class Vehicle { }
class Car extends Vehicle { }
class Truck extends Vehicle { }
RDB 의 상속:
- 직접 표현 X
- 모든 테이블은 평면 (flat)
- 우회 표현 필요
→ 객체 상속을 RDB 로 매핑 어려움
매핑 전략 3가지:
1. SINGLE_TABLE:
- 한 테이블 + 구분 컬럼
2. JOINED:
- 부모/자식 테이블 분리
3. TABLE_PER_CLASS:
- 자식마다 별도 테이블
→ 상세는 섹션 8
실무 시나리오:
배송 종류:
abstract class Shipment { }
class SeaShipment extends Shipment { }
class AirShipment extends Shipment { }
class LandShipment extends Shipment { }
RDB 표현?
- 한 테이블 (구분 컬럼)?
- 여러 테이블?
- 자식마다 따로?
→ 전략 선택 필요
// 객체 상속 (ILIC, 가상 예시)
public abstract class Shipment {
protected Long id;
protected String blNo;
protected BigDecimal weight;
}
public class SeaShipment extends Shipment {
private String vesselName;
private String containerNo;
}
public class AirShipment extends Shipment {
private String airwayBillNo;
private String flightNo;
}
public class LandShipment extends Shipment {
private String truckPlate;
}
// RDB 표현?
// → 3가지 전략 (SINGLE_TABLE / JOINED / TABLE_PER_CLASS)
// → 섹션 8 에서 상세
상속 차이 (있음 vs 없음) 는?
답:
1. 객체:
RDB:
매핑:
시나리오:
객체의 연관:
참조 (Reference):
- 한 객체가 다른 객체 가리킴
- .필드 접근
예:
class Order {
Member member; // 참조
}
Order order = ...;
Member m = order.member; // 직접 접근
RDB 의 연관:
외래 키 (Foreign Key):
- 다른 테이블의 PK 저장
- JOIN 으로 접근
예:
orders 테이블:
| id | member_id |
-- 접근
SELECT m.*
FROM orders o
JOIN members m ON o.member_id = m.id;
단방향 vs 양방향:
객체:
- 단방향: order → member
- 양방향: order ↔ member (서로 참조)
- 양쪽 필드 필요
RDB:
- 양방향이 본질 (FK 로 양쪽 알 수 있음)
- 단방향 개념 약함
- JOIN 으로 어느 쪽이든
매핑:
객체 참조 → FK 컬럼:
Member member; // 객체에서
→
member_id BIGINT; // DB 에서
+ FOREIGN KEY (member_id) REFERENCES members(id)
ORM 이 자동:
- @ManyToOne
- @OneToMany
- @OneToOne
// 객체 (ILIC)
public class Shipment {
Long id;
String blNo;
Customer customer; // 참조 (객체)
}
// RDB
// CREATE TABLE shipments (
// id BIGINT,
// bl_no VARCHAR(50),
// customer_id BIGINT, // FK
// FOREIGN KEY (customer_id) REFERENCES customers(id)
// );
// 접근 방식:
// 객체:
shipment.getCustomer().getName(); // 자연스러움
// RDB:
SELECT c.name
FROM shipments s
JOIN customers c ON s.customer_id = c.id
WHERE s.id = ?;
// JOIN 명시적
class Customer { String getName() { return null; } }
class Shipment { Customer getCustomer() { return null; } }
연관 관계 차이 (참조 vs FK) 는?
답:
1. 객체:
RDB:
방향:
매핑:
객체 식별:
- 객체 동일성 (==): 메모리 주소
- 객체 동등성 (equals): 내용 비교
- 자동 (JVM)
예:
Member m1 = new Member();
Member m2 = new Member();
m1 == m2 // false (다른 주소)
m1.equals(m2) // 구현 따라
RDB 식별:
- Primary Key (PK)
- 유일한 식별자
- 명시적 지정
예:
CREATE TABLE members (
id BIGINT PRIMARY KEY,
name VARCHAR(100)
);
차이:
객체:
- 메모리 주소 (==)
- JVM 의 객체 식별
- 같은 내용도 다른 객체
RDB:
- PK 값
- 명시적 (id 컬럼)
- 같은 PK = 같은 데이터
매핑 — @Id:
객체의 PK 필드 명시:
@Id
private Long id;
객체 ↔ DB 행 매핑:
- id 가 같으면 같은 데이터
- 영속성 컨텍스트 (다음 주차)
영속성 컨텍스트 (간단):
JPA 의 1차 캐시:
- 같은 트랜잭션 안에서
- 같은 id 의 엔티티는 같은 객체
- 메모리 주소 동일 (==)
→ 객체 식별 + DB 식별 일치
// 식별 (ILIC)
// 객체 식별
Shipment s1 = new Shipment();
Shipment s2 = new Shipment();
s1 == s2; // false (다른 객체)
s1.equals(s2); // false (내용 다름)
// DB 식별 (PK)
INSERT INTO shipments (id, bl_no) VALUES (1, 'BL001');
INSERT INTO shipments (id, bl_no) VALUES (2, 'BL002');
-- id 가 다르면 다른 데이터
// JPA 매핑
@Entity
public class Shipment {
@Id
private Long id; // DB PK 와 매핑
private String blNo;
}
// JPA 영속성 컨텍스트
Shipment s1 = em.find(Shipment.class, 1L);
Shipment s2 = em.find(Shipment.class, 1L);
s1 == s2; // true (같은 1차 캐시)
class Shipment {}
식별 차이 (== vs PK) 는?
답:
1. 객체:
RDB:
매핑:
영속성:
객체의 타입:
기본 타입 + 컬렉션 + 사용자 정의:
- int, long, String, BigDecimal
- List, Set, Map
- 사용자 정의 클래스
- 중첩 객체
매우 풍부
RDB 의 타입:
기본 SQL 타입:
- INTEGER, BIGINT
- VARCHAR, TEXT
- DECIMAL, FLOAT
- DATE, DATETIME, TIMESTAMP
- BOOLEAN
- 컬렉션 X (List/Map 직접 저장 불가)
- 사용자 정의 타입 제한적
컬렉션 매핑 문제:
객체:
List<Order> orders; // 자연스러움
RDB:
- 컬렉션 컬럼 X
- 별도 테이블 필요
- FK 로 연결
→ ORM 이 자동 매핑
사용자 정의 타입:
객체:
class Address {
String city;
String street;
}
class Member {
Address address; // 사용자 정의
}
RDB:
- Address 타입 X
- 분해 필요 (city, street 컬럼)
- 또는 별도 테이블
// 풍부한 타입 (ILIC)
public class Shipment {
Long id;
String blNo;
BigDecimal weight;
LocalDateTime createdAt;
// 컬렉션
List<ShipmentItem> items;
Set<String> tags;
// 중첩 객체
Address loadingAddress;
Address dischargeAddress;
// Enum
ShipmentStatus status;
}
class Address {
String country;
String city;
String street;
}
enum ShipmentStatus { BOOKED, CONFIRMED, SHIPPED }
-- RDB 매핑 (분해)
CREATE TABLE shipments (
id BIGINT,
bl_no VARCHAR(50),
weight DECIMAL(10,2),
created_at DATETIME,
-- Address 분해
loading_country VARCHAR(50),
loading_city VARCHAR(100),
loading_street VARCHAR(200),
discharge_country VARCHAR(50),
discharge_city VARCHAR(100),
discharge_street VARCHAR(200),
-- Enum → VARCHAR
status VARCHAR(20)
);
-- List items → 별도 테이블
CREATE TABLE shipment_items (
id BIGINT,
shipment_id BIGINT,
item_name VARCHAR(200),
FOREIGN KEY (shipment_id) REFERENCES shipments(id)
);
-- Set tags → 별도 테이블
CREATE TABLE shipment_tags (
shipment_id BIGINT,
tag VARCHAR(50),
FOREIGN KEY (shipment_id) REFERENCES shipments(id)
);
데이터 타입 차이 (풍부 vs 제한) 는?
답:
1. 객체:
RDB:
컬렉션:
사용자 정의:
// 객체 구조
public class Order {
Long id;
Member member;
LocalDateTime orderDate;
List<OrderItem> items; // ★ 컬렉션
}
public class OrderItem {
Long id;
Product product;
int quantity;
}
-- 1. orders 테이블
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
member_id BIGINT,
order_date DATETIME,
FOREIGN KEY (member_id) REFERENCES members(id)
);
-- 2. order_items 테이블 (별도!)
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT, -- FK
product_id BIGINT,
quantity INT,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
1:N 관계:
Order 1 : N OrderItem
객체에선:
order.items // List
RDB 에선:
order_items 의 order_id 가 FK
→ ORM 이 매핑
// JPA 매핑 (Phase 3 에서)
@Entity
@Table(name = "orders")
public class Order {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order")
private List<OrderItem> items; // 자동 매핑
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "order_id")
private Order order; // FK 반대편
}
// 사용
Order order = repository.findById(1L);
List<OrderItem> items = order.getItems(); // 객체 그래프 자동
SQL Mapper 시 고통:
1. orders 조회
2. order_items 별도 조회 (order_id 로)
3. 자바에서 합치기 (Order.items 채우기)
4. N+1 위험
매번 이 코드:
- 손으로 매핑
- 누락 위험
- 시간 ↓
→ ORM 등장 동기
// ILIC 의 1:N (Shipment → ShipmentItem)
public class Shipment {
Long id;
String blNo;
Customer customer;
List<ShipmentItem> items; // 1:N
}
public class ShipmentItem {
Long id;
String itemName;
int quantity;
}
-- RDB
CREATE TABLE shipments (id, bl_no, customer_id);
CREATE TABLE shipment_items (id, shipment_id, item_name, quantity);
// JPA 매핑 (다음 Phase)
@Entity
class Shipment {
@Id Long id;
@OneToMany(mappedBy = "shipment")
List<ShipmentItem> items;
}
@Entity
class ShipmentItem {
@Id Long id;
@ManyToOne
@JoinColumn(name = "shipment_id")
Shipment shipment;
}
// 사용
Shipment s = repo.findById(1L);
List<ShipmentItem> items = s.getItems(); // 자동
Order 가 List 갖는 구조 RDB 로?
답:
1. 객체:
RDB:
관계:
JPA:
SINGLE_TABLE (단일 테이블):
한 테이블에 모든 자식:
- 구분 컬럼 (dtype)
- 공통 + 자식별 필드 모두
장점:
- JOIN X (빠름)
- 단순
단점:
- 컬럼 NULL 多
- 한 테이블 크기 ↑
-- SINGLE_TABLE
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
dtype VARCHAR(20), -- 'SEA' / 'AIR' / 'LAND'
bl_no VARCHAR(50), -- 공통
weight DECIMAL(10,2), -- 공통
-- SeaShipment
vessel_name VARCHAR(100),
container_no VARCHAR(50),
-- AirShipment
airway_bill_no VARCHAR(50),
flight_no VARCHAR(20),
-- LandShipment
truck_plate VARCHAR(20)
);
-- 모든 데이터 한 테이블
-- 자식 안 쓰는 컬럼은 NULL
JOINED (조인):
부모 + 자식 테이블 분리:
- 부모: 공통
- 자식: 자식별 필드
- JOIN 으로 합침
장점:
- 정규화
- NULL X
- 명확
단점:
- JOIN 필요 (느림)
- 복잡
-- JOINED
-- 부모 (공통)
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50),
weight DECIMAL(10,2)
);
-- 자식 (자식별)
CREATE TABLE sea_shipments (
id BIGINT PRIMARY KEY,
vessel_name VARCHAR(100),
container_no VARCHAR(50),
FOREIGN KEY (id) REFERENCES shipments(id)
);
CREATE TABLE air_shipments (
id BIGINT PRIMARY KEY,
airway_bill_no VARCHAR(50),
flight_no VARCHAR(20),
FOREIGN KEY (id) REFERENCES shipments(id)
);
CREATE TABLE land_shipments (
id BIGINT PRIMARY KEY,
truck_plate VARCHAR(20),
FOREIGN KEY (id) REFERENCES shipments(id)
);
-- 조회
SELECT s.*, ss.vessel_name
FROM shipments s
JOIN sea_shipments ss ON s.id = ss.id;
TABLE_PER_CLASS (구체 클래스별 테이블):
자식마다 별도 테이블:
- 공통 필드 중복 (각 자식 테이블에)
- 부모 테이블 X
장점:
- JOIN X
- 자식별 분리
단점:
- 공통 필드 중복
- 부모 조회 시 UNION
-- TABLE_PER_CLASS
-- 자식별 (공통 + 자기)
CREATE TABLE sea_shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50), -- 공통 (중복)
weight DECIMAL(10,2), -- 공통 (중복)
vessel_name VARCHAR(100),
container_no VARCHAR(50)
);
CREATE TABLE air_shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50), -- 공통 (중복)
weight DECIMAL(10,2), -- 공통 (중복)
airway_bill_no VARCHAR(50),
flight_no VARCHAR(20)
);
CREATE TABLE land_shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50), -- 공통 (중복)
weight DECIMAL(10,2), -- 공통 (중복)
truck_plate VARCHAR(20)
);
-- 부모 조회 (UNION)
SELECT id, bl_no, weight FROM sea_shipments
UNION ALL
SELECT id, bl_no, weight FROM air_shipments
UNION ALL
SELECT id, bl_no, weight FROM land_shipments;
| 전략 | 테이블 수 | JOIN | 정규화 | 빠르기 |
|---|---|---|---|---|
| SINGLE_TABLE | 1 | X | ↓ | 빠름 |
| JOINED | N+1 | O | ↑ | 느림 |
| TABLE_PER_CLASS | N | X (UNION) | ↓ | 보통 |
선택 기준:
SINGLE_TABLE:
- 자식 클래스 적음
- 공통 필드 많음
- 성능 우선
JOINED:
- 정규화 중시
- NULL 싫음
- 객체 모델 충실
TABLE_PER_CLASS:
- 자식 자주 분리 조회
- 거의 사용 X
→ JPA 의 @Inheritance 로 선택
// ILIC 상속 매핑 가정 (실제는 X)
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "dtype")
public abstract class Shipment {
@Id Long id;
String blNo;
BigDecimal weight;
}
@Entity
@DiscriminatorValue("SEA")
public class SeaShipment extends Shipment {
String vesselName;
}
// → 부모 shipments + 자식 sea_shipments 테이블
// → JOIN 으로 조회
// → JPA 자동
// ILIC 실무는:
// - shipments + 구분 컬럼 (transport_mode)
// - 자식 클래스 X (상속 안 씀)
// - 또는 컴포지션 사용
class JoinedShipment {}
객체 상속을 RDB 로 표현하는 3가지는?
답:
1. SINGLE_TABLE:
JOINED:
TABLE_PER_CLASS:
선택:
손으로 메우는 코드:
매번 작성:
1. 객체 → SQL (INSERT/UPDATE)
2. ResultSet → 객체 매핑
3. List → 별도 테이블 + 합치기
4. 객체 그래프 (1:N) 처리
5. 상속 매핑 (SINGLE/JOINED/...)
6. 캐시 (영속성 컨텍스트)
7. 변경 감지 (Dirty Checking)
8. 트랜잭션 동기화
→ 5가지 미스매치 × N 메서드
시간 낭비:
보일러플레이트:
- 비즈니스 로직 < 매핑 코드
- 6주차 Unit 7.1 과 비슷
- 매번 똑같은 패턴
유지보수:
- 컬럼 추가 시 여러 곳 수정
- 누락 위험
- 디버깅 어려움
실수 위험:
- 컬럼명 오타
- 타입 불일치
- 1:N 매핑 누락
- 캐시 일관성
→ 버그의 온상
객체지향 깨짐:
객체 ↔ DB 변환 코드:
- 객체에 DB 코드 섞임
- SoC 깨짐 (5주차)
- 도메인 로직 흐려짐
→ 잘못된 OOP
ORM 의 약속:
자동 매핑:
- 5가지 미스매치 모두
- 어노테이션 기반
- 객체로만 작업
개발자:
- 비즈니스 로직 집중
- 객체로 사고
- SQL 신경 ↓
→ 다음 Unit (2.2) 에서 정의
ORM 없으면 (ILIC 가정)
ILIC 의 102 테이블:
- 매 테이블마다 매핑 코드
- 객체 → SQL
- ResultSet → 객체
- 1:N (Shipment ↔ ShipmentItem)
- 5+ 테이블 JOIN 매핑
코드 비율:
- 비즈니스 로직 30%
- 매핑 코드 70%
- 매우 부담
ORM (JPA) 사용 시:
- 매핑 코드 자동
- 비즈니스 로직 집중
- 102 테이블도 관리 가능
- ILIC 의 현재
이 차이를 메우는 코드 = ORM 등장 전 고통의 의미는?
답:
1. 손으로:
고통:
객체지향 깨짐:
ORM 의 약속:
1. 5가지 미스매치
2. 표현의 어려움
Order 의 List<OrderItem> → orders + order_items + FK3. ORM 등장의 이유
이번 Unit에서 미스매치를 봤다면, 다음은 ORM 의 정의 (Phase 2 마지막).
🔄 Phase 2 — ORM 패러다임
✅ Unit 2.1 객체-관계 미스매치 ← 여기
⏭ Unit 2.2 ORM 의 정의와 효과 — Phase 2 완주
🗂️ Part A — 데이터 모델링과 ORM
✅ Phase 1 (5) ← 완주
🔄 Phase 2 (1/2)
총: 6/24 Unit
🔄 Phase 2 시작 — 객체-관계 미스매치