1. 엔티티 매핑(Entity Mapping)과 PK 생성 전략
앞 장에서는 영속성 컨텍스트를 통해 JPA가 엔티티를 어떻게 관리하는지 살펴봤다. 이번 장에서는 "자바의 객체를 관계형 데이터베이스(RDB) 테이블과 어떻게 연결할 것인가?"라는 구체적인 매핑 방법을 다룬다. JPA의 엔티티 매핑은 크게 두 가지로 분류된다.
- 객체와 테이블 매핑 (`@Entity`, `@Table`)
- 필드와 컬럼 매핑 (`@Column`, `@Enumerated`, `@Lob`, `@Transient` 등)
1.1 객체와 테이블 매핑
1) @Entity
JPA가 관리할 객체임을 선언하는 어노테이션이다. 이 어노테이션이 붙은 클래스만 데이터베이스 테이블과 매핑될 수 있다.
- 기본 생성자 필수: JPA는 기술적으로 리플렉션(Reflection)을 사용해 객체를 동적으로 생성하므로, 파라미터가 없는 public 또는 protected 기본 생성자가 반드시 있어야 한다.
- 클래스 제약: `final` 클래스, `enum`, `interface`에는 `@Entity`를 사용할 수 없다.
프록시 객체 생성 및 상속 구조 활용에 제약이 생기기 때문이다.
2) @Table
엔티티 클래스가 매핑될 데이터베이스 테이블 이름을 명시적으로 지정한다.
생략하면 클래스 이름을 테이블 이름으로 그대로 사용한다.
@Entity
@Table(name = "TB_MEMBER") // DB의 TB_MEMBER 테이블과 매핑
public class Member { ... }
1.2 필드와 컬럼 매핑 어노테이션
1) @Column (컬럼 매핑)
객체의 필드를 테이블 컬럼에 매핑한다. 속성을 통해 다양한 DDL 제약조건을 부여할 수 있다.
- `name`: 매핑할 테이블의 컬럼명을 지정한다.
- `nullable`: false로 설정 시 DDL 생성 시 `NOT NULL` 제약조건이 붙는다.
- `length`: 문자열 데이터의 길이를 제한한다. (기본값 255)
@Column(name = "member_name", nullable = false, length = 50)
private String name; // DB에는 member_name VARCHAR(50) NOT NULL로 생성됨
2) @Enumerated (Enum 타입 매핑) ⭐ 매우 중요
자바의 enum 타입을 테이블에 저장할 때 사용한다.
public enum MemberStatus { ACTIVE, INACTIVE }
@Enumerated(EnumType.STRING)
private MemberStatus status;
🚨 실무 절대 규칙: `EnumType.ORDINAL` 사용 금지
기본값인 ORDINAL은 Enum의 순서(`0, 1, 2...`)를 DB에 숫자로 저장한다. 만약 나중에 기존 Enum 사이에 새로운 상태가 추가되면, 기존에 저장된 데이터의 숫자 의미가 모두 뒤바뀌어 데이터가 오염된다. 따라서 무조건 `EnumType.STRING`만 사용해야 한다.
3) 날짜 타입 매핑 (LocalDate, LocalDateTime)
과거에는 `java.util.Date` 타입을 매핑하기 위해 `@Temporal` 어노테이션이 필수였으나, Java 8 이후부터는 어노테이션 없이 자바 표준 날짜 타입을 그대로 선언하면 JPA가 알아서 최신 날짜 타입(TIMESTAMP 등)으로 자동 매핑한다.
private LocalDate birthday; // 연월일 (DATE)
private LocalDateTime createdAt; // 연월일시분초 (TIMESTAMP)
4) @Lob (대용량 데이터 매핑)
데이터베이스의 대용량 데이터 타입인 CLOB, BLOB과 매핑된다.
필드 타입이 문자면 CLOB(게시글 본문 등)으로, 나머지는 BLOB(이미지, 파일 등)으로 매핑된다.
5) @Transient (매핑 제외)
특정 필드를 데이터베이스 테이블 컬럼과 매핑하지 않고, 메모리상에서만 임시 계산용 등으로 활용하고 싶을 때 사용한다.
DB에 저장되지도 않고 조회되지도 않는다.
3. 기본 키(PK) 매핑 전략
모든 엔티티는 반드시 식별자(`@Id`)를 가져야 한다.
실무에서는 주로 데이터베이스가 기본 키를 자동으로 생성해 주는 대리키 전략을 사용한다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
주요 생성 전략 비교
| 전략 구분 | 설명 | 주로 사용하는 DB | 특징 및 실무 유의사항 |
| `IDENTITY` | 기본 키 생성을 DB에 위임 | MySQL (AUTO_INCREMENT) |
em.persist() 시점에 즉시 INSERT SQL이 실행됨 (이유 아래 설명) |
| `SEQUENCE` | DB 시퀀스 오브젝트 사용 | Oracle, PostgreSQL | 시퀀스에서 식별자를 먼저 조회한 후 영속성 컨텍스트에 담음 |
| `TABLE` | 키 생성 전용 테이블 활용 | 모든 DB | 테이블을 계속 조회/업데이트하므로 성능 저하 발생, 실무 사용 지양 |
| `AUTO` | DB 방언에 따라 자동 선택 | - | 선택된 DB 종류에 맞춰 위 3가지 중 하나를 자동 선택함 |
엔티티 매핑 핵심 포인트
Q. IDENTITY 전략을 사용할 때 em.persist() 시점에 INSERT SQL이 즉시 나가는 이유는 무엇인가?
- A. 영속성 컨텍스트는 엔티티를 관리하기 위해 데이터의 식별자(PK)를 Key 값으로 사용해야 한다. 하지만 IDENTITY 전략은 데이터가 실제 DB에 INSERT 되어야만 DB가 PK 값을 생성해 준다. 따라서 예외적으로 `em.persist()`를 호출하는 즉시 DB에 INSERT 쿼리를 날려 PK 값을 받아온 뒤, 영속성 컨텍스트의 1차 캐시에 등록하는 방식을 취한다. (쓰기 지연이 제한됨)
Q. 실무에서 어떤 타입을 PK로 가져가는 것이 좋은가?
- A. 자연키(주민등록번호, 이메일, 전화번호 등)를 PK로 쓰는 것은 절대 지양해야 한다. 비즈니스 환경이 변하면 이메일이나 주민번호 형식도 변경될 수 있으며, 이는 연관된 테이블 전체의 외래 키 구조를 흔들기 때문이다. 실무에서는 비즈니스와 무관한 Long 타입 + 대체키(대리키) + IDENTITY 또는 SEQUENCE 전략을 조합하여 PK를 구성하는 것을 강력히 권장한다.
객체와 테이블은 `@Entity`와 `@Table`로 매핑하고, Enum은 무조건 `EnumType.STRING`을 써야 하며, 실무 PK는 비즈니스와 무관한 대리키(`Long id`) 구조를 사용하는 것이 안전하다.
2. 연관관계 매핑(Association Mapping)과 연관관계의 주인
지금까지는 단일 엔티티 매핑만 다뤘다. 하지만 실제 비즈니스 도메인에서는 회원과 팀, 주문과 회원처럼 엔티티 간의 연관관계가 반드시 존재한다. JPA 핵심 중의 핵심이자, 실무에서 가장 많은 개발자가 혼란스러워하는 "연관관계의 주인(Owner)"과 mappedBy 개념을 완벽하게 정리한다.
2.1 객체와 테이블의 패러다임 차이
객체와 관계형 데이터베이스는 관계를 표현하는 근본적인 방식부터 다르다.
- 객체: 참조(Reference)를 사용한다. 한 방향으로만 흐르기 때문에 양방향을 만들려면 서로 참조를 품는 쌍방 단방향 관계(`Member.team`, `Team.members`)가 필요하다.
- 테이블: 외래 키(FK) 하나를 사용한다. 외래 키 하나만 있으면 JOIN을 통해 양방향으로 자유롭게 조회가 가능하다.
JPA의 연관관계 매핑은 바로 "객체의 참조"와 "DB의 외래 키"를 연결하는 작업이다.
2.2 다대일(@ManyToOne) 단방향 연관관계
가장 기본적이면서 설계의 뼈대가 되는 구조다.
회원은 하나의 팀에 속할 수 있고, 팀은 여러 회원을 가질 수 있다. (회원 -> 팀 단방향)
엔티티 설계
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne // 여러 회원은 하나의 팀에 속한다 (N:1)
@JoinColumn(name = "team_id") // DB의 어떤 외래 키 컬럼과 매핑할지 지정
private Team team;
}
- `@ManyToOne`: 다대일(N:1) 관계를 나타내는 매핑 어노테이션이다.
- `@JoinColumn`: 외래 키를 가지는 컬럼을 지정한다. 생략해도 되지만 실무에서는 컬럼명 명시를 위해 필수적으로 사용한다.
2.3 다대일(@ManyToOne) 양방향 연관관계와 mappedBy
실무에서는 특정 팀에 속한 회원 목록을 함께 조회해야 하는 경우가 많다.
이때 팀(Team) 엔티티에도 회원 목록을 추가하여 양방향 구조를 만든다.
Team 엔티티 수정
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // 일대다(1:N) 매핑, 반대편 매핑 필드명을 지정
private List<Member> members = new ArrayList<>();
}
💡 mappedBy의 본질
- `mappedBy = "team"`은 "나는 Member 엔티티의 team 필드에 의해 매핑(관리)되는 거울일 뿐이야"라는 뜻이다.
- 즉, `Team.members`는 조회용(읽기 전용)일 뿐, 여기에 회원을 넣고 뺀다고 해서 DB의 외래 키 값이 바뀌지 않는다.
1) 연관관계의 주인(Owner)을 정해야 하는 이유
데이터베이스는 MEMBER 테이블에 있는 TEAM_ID 외래 키 하나로 두 테이블의 관계를 관리한다. 하지만 객체 세상에는 관계를 관리하는 포인트가 두 곳(`Member.team`, `Team.members`)이다. 만약 두 군데의 값이 서로 다르게 수정된다면 JPA는 둘 중 어느 객체를 믿고 DB의 외래 키를 업데이트해야 할까? 이 문제를 해결하기 위해 JPA는 두 객체 중 하나를 외래 키를 관리하는 '연관관계의 주인'으로 임명한다.
2) 주인 선정의 대원칙
"외래 키(FK)가 있는 곳을 주인으로 정한다."
- 주인 (`Member.team`): 외래 키 값을 등록, 수정, 삭제할 수 있다.
- 주인이 아님 (`Team.members`): `mappedBy`로 지정된다. 오직 읽기(조회)만 가능하다.
2.4 실무에서 빈번하게 발생하는 실수 3가지
1) 주인이 아닌 곳에만 값을 입력하는 실수 (DB 반영 누락)
Member member = new Member();
member.setUsername("kim");
em.persist(member);
Team team = new Team();
team.setName("Backend");
team.getMembers().add(member); // ❌ 주인이 아닌 곳에만 세팅
em.persist(team);
💡 결과: DB를 조회해 보면 MEMBER 테이블의 TEAM_ID 컬럼에 NULL이 들어간다.
실제 권한이 있는 `member.setTeam(team)`을 호출하지 않았기 때문이다.
2) 양방향 동기화 누락 (객체 상태 불일치)
반대로 주인에게만 값을 세팅하고 주인이 아닌 곳을 비워두면, 순수한 자바 객체 상태에서 문제가 발생한다.
영속성 컨텍스트의 1차 캐시에서 `team.getMembers()`를 조회했을 때 새로 추가된 회원이 조회되지 않는 버그가 생긴다.
3) 해결책: 연관관계 편의 메서드 작성
이를 방지하기 위해 한쪽에서 관계를 맺으면 반대쪽도 자동으로 세팅되도록 하나의 메서드로 묶어야 한다.
// Member 엔티티 내부
public void changeTeam(Team team) {
this.team = team;
// 무한 루프 방지를 위해 방어 코드 작성
if (!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
연관관계 핵심 포인트
Q. 연관관계의 주인은 무엇이며, 어떻게 선정해야 하나?
- A. 양방향 연관관계에서 데이터베이스의 외래 키(FK)를 제어할 수 있는 권한을 가진 객체를 말한다. 무조건 실제 외래 키가 위치한 테이블과 매핑되는 엔티티를 주인으로 선정해야 한다. (일대다 관계에서는 항상 '다' 쪽이 주인이 된다.)
Q. mappedBy 속성의 역할은 무엇인가?
- A. 양방향 매핑 시 해당 필드가 연관관계의 주인이 아님을 선언하는 속성이다. 속성의 값으로는 반대편 엔티티에 선언된 주인의 필드명을 지정하며, mappedBy가 선언된 필드는 데이터베이스 수정 권한 없이 읽기 전용으로만 작동한다.
연관관계 매핑의 기본은 단방향이며, 외래 키가 있는 곳이 연관관계의 주인이 된다. 양방향 관계는 오직 조회 편의성을 위해 mappedBy를 사용하여 추가하는 보조 수단일 뿐이다.
3. 다양한 연관관계 매핑과 실무적인 선택 기준
이전 장에서는 연관관계 매핑의 기본 개념과 연관관계의 주인이라는 핵심 규칙을 학습했다. 이번 장에서는 실제로 구현할 수 있는 모든 연관관계 구조(다대일, 일대다, 일대일, 다대다)를 살펴보고, 왜 실무에서는 특정 매핑을 철저히 금지하는지 그 이유를 명확히 이해해 본다.
3.1 다대일 (N:1) ─ 실무 사용 빈도: ★★★★★
실무 프로젝트에서 가장 많이 사용하며, 모든 연관관계 중 가장 기본이 되는 구조다.
구조적 특징
- 데이터베이스 테이블 구조상 외래 키(FK)는 항상 '다(N)' 쪽에 위치한다.
- 따라서 객체 설계 시 '다(N)' 쪽 엔티티에 `@ManyToOne`, `@JoinColumn`을 함께 선언하여 연관관계의 주인으로 삼는다.
- 가장 직관적이며 외래 키 관리와 객체의 흐름이 일치한다.
3.1 일대다 (1:N) 단방향 ─ 실무 사용 빈도: ☆☆☆☆☆ (권장 안 함)
팀(1)에서 회원(N)을 참조하는 리스트 컬렉션을 직접 관리하는 구조다.
엔티티 설계
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany
@JoinColumn(name = "team_id") // MEMBER 테이블의 team_id 외래 키를 매핑
private List<Member> members = new ArrayList<>();
}
❌ 실무에서 권장하지 않는 이유
- 외래 키의 관리 주체 불일치: 데이터베이스 테이블 구조상 외래 키는 여전히 MEMBER 테이블에 존재한다.
하지만 연관관계의 주인은 Team 엔티티다. - 불필요한 UPDATE 쿼리 발생: `team.getMembers().add(member)`코드를 실행하면, MEMBER 테이블에 데이터를 넣은 뒤 다시 MEMBER 테이블의 외래 키를 수정하는 UPDATE SQL이 추가로 실행된다.
- 유지보수의 어려움: 개발자 입장에서는 Team을 수정했는데 왜 MEMBER 테이블에 쿼리가 나가는지 직관적으로 이해하기 힘들어, 대규모 시스템에서 성능 추적이 어려워진다.
💡 실무 규칙: 일대다 단방향 매핑이 필요해 보이는 상황이라면, 차라리 반대편에 다대일 단방향을 잡거나 다대일 양방향 매핑으로 전환하는 것이 정석이다.
3.3 일대일 (1:1) ─ 실무 사용 빈도: ★★☆☆☆
회원 1명이 사물함 1개만 가질 수 있는 경우처럼 주 테이블이나 대상 테이블 중 어느 곳에나 외래 키를 둘 수 있는 구조다.
외래 키 위치 선택 기준
- 주 테이블에 외래 키 배치 (권장): 자주 조회되는 엔티티(예: Member)에 외래 키를 두는 방식이다. 객체지향 개발자가 선호하며, JPA가 이를 다루기 훨씬 매끄럽다.
- 대상 테이블에 외래 키 배치: 사물함(Locker) 테이블에 외래 키를 두는 방식이다. 전통적인 DBA가 선호하며, 관계가 1:1에서 1:N으로 변경될 때 테이블 구조를 유지할 수 있다는 장점이 있다.
3.4 다대다 (N:M)와 @ManyToMany ─ 실무 사용 빈도: ❌ (사용 금지)
관계형 데이터베이스는 테이블 2개만으로 다대다 관계를 표현할 수 없다. 반드시 중간에 두 테이블의 PK를 외래 키로 가지는 연결 테이블을 두어야 한다. JPA는 `@ManyToMany`와 `@JoinTable`을 쓰면 이 중간 연결 테이블을 코딩 없이 자동으로 생성해 주기 때문에 처음에는 매우 편리해 보인다.
❌ 하지만 실무에서는 절대로 사용하지 않는다
실제 프로덕션 서비스 환경에서는 연결 테이블이 단순히 두 테이블의 ID 값만 매핑하고 끝나지 않는다. 대부분 주문일시, 주문 수량, 결제 금액, 상태 등 비즈니스 관점에서 매우 중요한 추가 컬럼들이 연결 테이블에 채워지게 된다. 그러나 `@ManyToMany`를 사용하면 자동으로 매핑되는 중간 테이블에 개발자가 원하는 추가 컬럼을 넣을 방법이 없다. 또한, 중간 테이블이 숨겨져 있어 예측하지 못한 복잡한 SQL 쿼리가 나가는 부작용이 크다.
1) 실무 표준: 다대다를 일대다 + 다대일로 풀어내기
`@ManyToMany`를 과감히 버리고, 중간 연결 테이블을 하나의 독립된 엔티티로 직접 설계한다.
- [ Member (회원) ] 1 ── N [ OrderItem (주문상품) ] N ── 1 [ Product (상품) ]
연결 엔티티 패턴 구현 예시
@Entity
public class OrderItem { // 중간 테이블을 엔티티로 승격
@Id @GeneratedValue
private Long id; // 비즈니스와 무관한 대리키 PK를 권장
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
// 추가할 수 있는 다양한 필드들
private int count; // 주문 수량
private int price; // 주문 금액
private LocalDateTime orderDate;
}
이렇게 연결 엔티티 구조로 변경하면 중간 테이블이 완전한 제어권 안으로 들어오므로 실무의 복잡한 요구사항을 완벽히 수용할 수 있게 된다.
연관관계 핵심 포인트 정리
Q. 일대다(1:N) 단방향 매핑의 단점과 실무적 대안은 무엇인가?
- A. 일대다 단방향은 엔티티가 관리하는 외래 키가 정작 자기 자신이 아닌 반대편 테이블에 존재한다. 이로 인해 값 변경 시 추가적인 UPDATE SQL이 실행되는 성능 손실과 직관성이 떨어지는 단점이 있다. 실무에서는 이를 다대일(N:1) 단방향 또는 양방향 매핑으로 전환하여 해결한다.
Q. JPA에서 @ManyToMany 어노테이션을 실무에서 지양하는 정확한 이유는?
- A. @ManyToMany를 사용하면 중간 매핑 테이블이 자동으로 생성되지만, 이 테이블에 주문 수량이나 생성일 같은 추가 컬럼을 명시할 수 없기 때문이다. 따라서 실무에서는 중간 테이블 역할을 할 연결 엔티티를 직접 정의한 뒤, 이를 중심으로 일대다(1:N)와 다대일(N:1) 관계로 풀어서 설계한다.
연관관계 설계는 무조건 다대일(@ManyToOne) 단방향을 기본 뼈대로 시작하고, 조회 편의가 필요할 때만 양방향을 열어주며, 다대다(@ManyToMany)는 절대 쓰지 않고 연결 엔티티로 풀어서 다루는 것이 실무 표준이다.
4. 상속 관계 매핑과 공통 속성 처리 (@MappedSuperclass)
객체지향 프로그래밍에서는 상속 구조를 활용해 도메인을 모델링하는 것이 매우 자연스럽다. 하지만 관계형 데이터베이스(RDB)에는 자바의 상속과 똑같은 개념이 존재하지 않는다. 대신 객체의 상속 구조와 유사한 '슈퍼타입-서브타입 관계'라는 논리적 모델링 기법이 존재한다. JPA는 이러한 객체의 상속 구조와 DB의 슈퍼타입-서브타입 관계를 매핑하기 위한 3가지 전략을 제공하며, 이와는 완전히 다르게 공통 필드만 재사용하는 `@MappedSuperclass` 기능도 함께 지원한다.
1.1 DB 상속 관계 매핑 전략 3가지
JPA에서 상속 관계를 매핑할 때는 부모 클래스에 `@Inheritance` 어노테이션을 선언하고, 전략(strategy)을 지정해야 한다.
1) 조인 전략 (`JOINED`) ─ 실무 사용 빈도: ★★★☆☆
부모와 자식 객체를 각각 테이블로 만들고,
자식 테이블이 부모 테이블의 PK를 외래 키(FK)로 잡아서 JOIN하는 가장 정규화된 방식이다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "dtype") // 자식 구분 컬럼 생성
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("BOOK") // DTYPE에 들어갈 값 지정
public class Book extends Item {
private String author;
}
- 동작: 저장 시 부모, 자식 테이블에 INSERT가 각각 2번 발생하며, 조회 시 JOIN 쿼리가 수행된다.
- 장점: 테이블이 정규화되어 데이터가 깔끔하게 관리되며, 외래 키 무결성 제약조건을 온전히 활용할 수 있다.
- 단점: 조회할 때마다 JOIN을 해야 하므로 쿼리가 복잡해지고 성능 부담이 생길 수 있다.
2) 단일 테이블 전략 (`SINGLE_TABLE`) ─ 실무 사용 빈도: ★★★★☆
자식 객체들이 가진 모든 컬럼을 부모 테이블 하나에 다 몰아넣고, 어떤 자식 데이터인지 구분 컬럼(DTYPE)으로 식별하는 방식이다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype") // 단일 테이블에서는 필수적으로 생성됨
public abstract class Item { ... }
- 동작: 하나의 테이블에 다 저장되므로 INSERT도 1번, 조회 시에도 JOIN 없이 1번의 쿼리로 다 끝난다.
- 장점: JOIN이 없으므로 조회 성능이 3가지 전략 중 가장 빠르고 단순하다.
- 단점: 자식 엔티티가 선언한 컬럼들은 전부 NULL을 허용(`nullable = true`)해야 한다. 자식이 많아질수록 테이블이 과도하게 비대해져 오히려 성능이 떨어질 수 있다.
3) 구현 클래스마다 테이블 전략 (`TABLE_PER_CLASS`) ─ 실무 사용 빈도: ❌ (사용 금지)
부모 테이블 없이 자식 테이블마다 중복된 공통 컬럼(id, name, price)을 각각 독립적으로 다 가지고 있는 방식이다.
- ❌ 실무 배제 이유: 부모 타입으로 여러 자식 데이터를 한 번에 조회할 때(예: `em.find(Item.class, id)`), 모든 자식 테이블을 `UNION ALL`로 묶어서 조회하는 재앙 같은 SQL이 발생한다. 실무에서는 절대 쓰지 마라.
매핑 전략 핵심 비교 분석
| 전략 구분 | 조회 성능 | 저장 성능 | 데이터 정규화 | 실무 추천도 |
| JOINED | 보통 (JOIN 필요) | 느림 (2번 분할) | 좋음 (정규화) | 비즈니스가 복잡할 때 권장 |
| SINGLE_TABLE | 매우 빠름 | 빠름 (1번) | 나쁨 (대량 NULL) | 대부분의 상황에서 우선 고려 |
| TABLE_PER_CLASS | 최악 (UNION 발생) | 보통 | 최악 (중복 발생) | 철저히 배제 |
4) 실무 필수: @MappedSuperclass (공통 속성 상속)
실무에서 상속 매핑 구조를 마주하면 `@Inheritance`보다 `@MappedSuperclass`를 훨씬 더 많이 사용하게 된다. 이 기능은 위의 3가지 전략처럼 테이블을 상속 구조로 만드는 것이 아니라, 단순히 엔티티들이 공통으로 사용하는 필드(컬럼) 정보만 재사용하고 싶을 때 사용한다. 실무에서 거의 필수적인 등록일, 수정일, 등록자, 수정자 같은 공통 감사(Audit) 정보를 처리할 때 완벽한 해결책이 된다.
BaseEntity 설계 예시
@MappedSuperclass // 테이블 매핑 없이 속성만 넘겨주는 선언
@EntityListeners(AuditingEntityListener.class) // Spring Data JPA 오디팅 연동
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt; // 생성일
@LastModifiedDate
private LocalDateTime updatedAt; // 수정일
}
자식 엔티티에서의 상속 적용
@Entity
public class Member extends BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
- 결과: 데이터베이스 테이블에는 BASE_ENTITY라는 테이블이 생성되지 않는다. 대신 MEMBER 테이블 내부에 id, name과 함께 부모의 created_at, updated_at 컬럼이 자동으로 포함되어 생성된다.
상속 전략 핵심 포인트
Q. @Inheritance 상속 전략과 @MappedSuperclass 컬럼 상속의 가장 결정적인 차이는 무엇인가?
- A. 부모 클래스 명의로 실제 데이터베이스 테이블이 생성되느냐의 차이다. @Inheritance는 객체의 상속 관계를 데이터베이스 구조로 표현하기 위해 부모 테이블을 실제로 생성하거나 제어한다. 반면 @MappedSuperclass는 자식 엔티티에게 매핑 정보(컬럼)만 상속해 줄 뿐, 부모 명의의 테이블은 생성되지 않으며 부모 타입으로 다형성 조회(em.find(BaseEntity.class, id))도 불가능하다.
Q. 상속 관계 매핑 시 JOINED와 SINGLE_TABLE 전략의 실무 선택 기준은?
- A. 기본적으로 비즈니스 도메인이 단순하고 데이터의 양이 많지 않으며, 확장 가능성이 낮다면 조회 성능이 극대화된 SINGLE_TABLE 전략을 우선 채택한다. 반면, 자식 엔티티 종류가 매우 많고 각각의 자식 엔티티마다 들고 있는 특화 컬럼이 수십 개 이상으로 비대하다면, 테이블 정규화와 무결성이 보장되는 JOINED 전략으로 선회하는 것이 장기적인 아키텍처 관점에서 유리하다.
객체 상속을 테이블 구조로 표현할 때는 성능의 SINGLE_TABLE이나 정규화의 JOINED 중 선택하며, 단순히 공통 컬럼(생성일/수정일 등)만 공유할 목적이라면 무조건 @MappedSuperclass를 사용하는 것이 실무 표준이다.
'🍃SpringBoot' 카테고리의 다른 글
| Spring IoC & DI 완전 정복: Chapter 2: "객체는 왜 스스로 필요한 객체를 만들면 안 될까?" (1) | 2026.07.22 |
|---|---|
| Spring IoC & DI 완전 정복: Chapter 1: "객체지향은 왜 등장했을까?" (0) | 2026.07.22 |
| JPA [1/4] : JPA 시작과 영속성 컨텍스트 (0) | 2026.06.23 |
| JUnit [2/2]: Service, Repository, Controller, 동시성 테스트 (0) | 2026.06.22 |
| JUnit [1/2]: 테스트 개념과 JUnit, Mockito 사용법 (0) | 2026.06.22 |
