1. Entities
- entity는 persistence domain object이다.
- entity는 ralational database의 table을 나타낸다.
- 각 entity Instance는 table의 row에 해당한다.
- entity는 helper class를 사용할 수 있다.
엔티티를 설계할 때는 가능한 한 데이터와 핵심 규칙만 담아 깔끔하게 유지하고, 복잡한 비즈니스 로직이나 변환, 검증 등의 부수적인 로직은 도메인 서비스나 헬퍼 클래스로 분리하는 것이 좋다.
1. Requirements for Entity Classes
- Class에 반드시
@Entity
annotation이 있어야 한다. - public 또는 protected 접근자의 인자가 없는 기본 Constructor가 있어야 한다.
- Class,method, 영속성 변수에 final을 선언할 수 없다.
- 만약 Entity가 분리되어 원격으로 전달된다면 Serializalbe Interface를 implement해야 한다.
- 엔티티가 다른 (엔티티/비엔티티)클래스를 상속할 수 있고, 그 반대도 가능하다.
- 영속성 변수는 public으로 선언할 수 없다.
- 영속성 변수에 접근은 게터 세터나 비지니스 메서드를 사용해야만 한다.
JPA가 프록시나 리플렉션을 통해 값을 읽고 쓸 때 final이면 변경이 불가능하기 때문에 final을
쓸 수 없다.
2. 영속성 변수 in Entity Classes
타입은 다음과 같아야 한다.
- primitive types
- String
- Other serializable types, including:
- Wrappers of Java primitive types
java.math.BigInteger
java.math.BigDecimal
java.util.Date
java.util.Calendar
java.sql.Date
java.sql.Time
java.sql.TimeStamp
- User-defined serializable types
byte[]
Byte[]
char
Character[]
- Enumerated types
- Other entities and/or collections of entities
- Embeddable classes
영속성 변수는 영속 필드와 영속 프로퍼티 두가지가 있다.
field에 직접 @Id,@Column을 붙이면 Persistent Field이고,
getter method에 붙이면 Persistent Properties이다.
단, 한 Entity 내에서는 한 가지 방식으로 일관되게 사용해야만 한다.
추가적으로 클래스에 @Access(AccessType.FIELD)와 같이 어노테이션을 붙여 명시적으로 표시할 수 있다.
2.1 Persistent Field
Persistence runtime은 getter, setter 없이 리플렉션으로 Persistent Field에 직접 접근한다.
Entity 내의 @Transient를 제외한 모든 Field는 DB에 저장된다.
2.2 Persistent Properties
Persistent Properties는 getter setter를 통해 접근된다.
이때, setProperty, getProperty, Boolean이라면 isProperty 네이밍을 따라야 한다.
(the method conventions of JavaBeans components)
반드시 게터 메서드에 붙여야 한다.
@Transient이 붙어 있는 변수의 getter에는 @Id, @Column을 붙여주면 안된다.
(Transient는 영속성을 안쓴다는 의미로 붙인거니까..)
2.2.+ 서로 비교
@Entity
@Access(AccessType.FIELD)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_name")
private String name;
// 필드 접근 방식은 직접 필드에 어노테이션을 붙임
}
@Entity
@Access(AccessType.PROPERTY)
public class Product {
private Long id;
private String name;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@Column(name = "product_name")
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
필드 접근
- 성능이 대부분 더 좋음
- Persistence runtime이 reflection으로 접근한다.
- CRUD 작업만 수행할 때, 불필요한 추가 로직 없음. 빠르게 값에 접근.
프로퍼티 접근
- 공통적으로 적용되는 추가적인 검증, 변환, 로깅 등의 로직을 쉽게 넣을 수 있음
- Persistence runtime이 getter/setter를 호출하여 접근한다.
예: getter에서 값을 가공하거나, setter에서 입력 검증을 수행해야 할 때 유리.
변수가 사용되는 모든 경우에 예외없이 필요한 로직이 있는 경우 프로퍼티 접근이 유리할 것 같다.
프로퍼티 접근이 아니라면, 새로 비지니스 로직이 추가 될 때마다 공통으로 적용할 로직을 적용해야 할 것이다.
2.3 Using Collections in Entity Fields and Properties
엔티티의 속성이 단일 값이 아니라 여러 값을 저장할 수 있는 컬렉션인 경우,
해당 컬렉션은 Java의 표준 컬렉션 인터페이스(예: Collection, Set, List, Map)를 사용하면 된다.
SQL에서는 이것이 여러 테이블이 서로 조인된 형태로 보여지게 된다.
컬렉션의 Value가 값일 수도 있지만, 엔티티일 수도 있다.
- 컬렉션의 Value가 단일 값
- ElementCollection,@Collection Table을 사용
@ElementCollection는 기본 타입이나 임베디드 타입의 컬렉션을 매핑할 때 사용하는 어노테이션이고, 이 컬렉션 데이터를 저장할 테이블의 세부 사항(테이블 이름, 조인 컬럼 등)을 지정하려면 @CollectionTable 어노테이션을 함께 사용함.
@CollectionTable은 엔티티의 컬렉션 타입 속성이 기본 타입이나 임베디드(Embeddable) 타입일 때, 그 값을 저장할 별도의 테이블을 지정하는 역할을 함.
@ElementCollection를 사용하면 JPA는 컬렉션 테이블의 기본 키로 해당 엔티티의 외래 키와 컬렉션 값 자체를 복합 기본 키로 사용
- 컬렉션의 Value가 entity
- 관계 매핑 어노테이션(@OneToMany, @ManyToOne 등)을 사용
2.3.+ JPA와 SQL 비교(set)
JPA 코드
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 값 컬렉션: 별명(nicknames)을 별도의 테이블에 저장
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(
name = "PERSON_NICKNAMES,// 별도의 컬렉션 테이블 이름
joinColumns = @JoinColumn(name = "person_id")
// Person 테이블의 기본키를 참조하는 외래 키
)
@Column(name = "nickname")// 컬렉션 내의 각 값이 저장될 컬럼 이름
private Set<String> nicknames = new HashSet<>();
protected Person() { }
public Person(String name) {
this.name = name;
}
// Getter/Setter 생략
}
SQL DDL
Hibernate가 위 엔티티를 기반으로 DDL을 자동 생성하는 테이블
- Person 테이블
CREATE TABLE Person (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
- PERSON_NICKNAMES 테이블 (컬렉션 테이블)
CREATE TABLE PERSON_NICKNAMES (
person_id BIGINT NOT NULL,
nickname VARCHAR(255),
PRIMARY KEY (person_id, nickname),
FOREIGN KEY (person_id) REFERENCES Person(id)
);
PERSON_NICKNAMES
테이블은 Person 엔티티의nicknames
컬렉션을 저장하기 위해 존재한다.- 각 별명 값은 Person의 기본키(
person_id
)와 함께 복합 기본키로 관리된다.
2.3.+ JPA와 SQL 비교(map : value가 단일값)
JPA 코드
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
@ElementCollection
@CollectionTable(name = "EMPLOYEE_DEPARTMENT", joinColumns = @JoinColumn(name = "employee_id"))
@MapKeyColumn(name = "dept_number") // Map의 키를 저장하는 컬럼 이름
@Column(name = "dept_name") // Map의 값(부서명)을 저장하는 컬럼 이름
private Map<Integer, String> departments = new HashMap<>();
// 기본 생성자, getter/setter 생략
}
- CollectionTable은
EMPLOYEE_DEPARTMENT
라는 테이블을 생성하여, 이 테이블이 Employee 엔티티와 연관된 컬렉션 값을 저장한다는 것을 의미한다. joinColumns
는 이 컬렉션 테이블이 Employee 엔티티의 기본 키(id
)와 어떻게 연결되는지를 정의한다.- 여기선
employee_id
가 외래 키 컬럼으로 Employee 테이블의id
를 참조한다. - MapKeyColumn은 Map의 Key와 매핑되는 컬럼명
- Column은 Map의 Value와 매핑되는 컬럼명
SQL DDL
CREATE TABLE Employee (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE EMPLOYEE_DEPARTMENT (
employee_id BIGINT NOT NULL,
dept_number INTEGER NOT NULL,
dept_name VARCHAR(255),
PRIMARY KEY (employee_id, dept_number),
FOREIGN KEY (employee_id) REFERENCES Employee(id)
);
2.3.+ JPA와 SQL 비교(map : value가 엔티티 && 관계테이블 없음)
JPA 코드
import jakarta.persistence.*;
import java.util.HashMap;
import java.util.Map;
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 단방향 OneToMany 관계를 Map으로 매핑:
// - Department 엔티티가 자신의 Employee들을 Map으로 관리함.
// - Map의 키는 employeeCode (예: 사원 번호)로 사용
// - @JoinColumn: Employee 테이블에 'department_id' 컬럼을 추가하여, 해당 사원이 어느 부서에 속하는지 저장
// - @MapKeyColumn: Employee 테이블에 'employee_code' 컬럼을 추가하여, Map의 키 값으로 사용
@OneToMany
@JoinColumn(name = "department_id")
@MapKeyColumn(name = "employee_code")
private Map<String, Employee> employees = new HashMap<>();
// 기본 생성자, getter, setter 생략
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Employee 엔티티에는 Department에 대한 참조가 없음 (단방향)
// 기본 생성자, getter, setter 생략
}
SQL DDL 코드
CREATE TABLE Department (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE Employee (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
department_id BIGINT, -- 부서(Department)와의 외래 키
employee_code INTEGER, -- Map의 키값을 저장 (예: 사원번호)
CONSTRAINT FK_employee_department FOREIGN KEY (department_id) REFERENCES Department(id)
);
2.3.+ JPA와 SQL 비교(map : value가 엔티티 && 관계테이블 있음)
JPA코드
import jakarta.persistence.*;
import java.util.HashMap;
import java.util.Map;
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 단방향 OneToMany를 조인 테이블을 사용하여 Map으로 매핑
@OneToMany
@JoinTable(
name = "DEPT_EMP_MAP",
joinColumns = @JoinColumn(name = "department_id"),
inverseJoinColumns = @JoinColumn(name = "employee_id")
)
@MapKeyColumn(name = "employee_code")
private Map<String, Employee> employees = new HashMap<>();
// 기본 생성자, getter, setter 생략
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 단방향이므로 Department 참조는 없음
// 기본 생성자, getter, setter 생략
}
SQL 코드
CREATE TABLE Department (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE Employee (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE DEPT_EMP_MAP (
department_id BIGINT NOT NULL,
employee_id BIGINT NOT NULL,
employee_code INTEGER,
PRIMARY KEY (department_id, employee_id),
FOREIGN KEY (department_id) REFERENCES Department(id),
FOREIGN KEY (employee_id) REFERENCES Employee(id)
);
2.3.+ JPA와 SQL 비교(List 순서O,X 중복 O, X)
List 순서 O, 중복 X
OrderColumn은 순서를 지정하는 컬럼을 추가로 넣어준다.
JPA 코드
@Entity
public class Blog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// List는 순서를 유지합니다. @OrderColumn을 사용하여 순서 정보를 별도 컬럼에 저장할 수 있습니다.
@ElementCollection
@CollectionTable(name = "BLOG_TAGS", joinColumns = @JoinColumn(name = "blog_id"))
@Column(name = "tag")
@OrderColumn(name = "tag_order")
private List<String> tags = new ArrayList<>();
// 기본 생성자 및 getter/setter 생략
}
SQL코드
CREATE TABLE Blog (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255)
);
CREATE TABLE BLOG_TAGS (
blog_id BIGINT NOT NULL,
tag_order INTEGER, -- List 내의 순서를 저장하는 컬럼
tag VARCHAR(255),
PRIMARY KEY (blog_id, tag_order),
FOREIGN KEY (blog_id) REFERENCES Blog(id)
);
List 순서 X, 중복 O
surrogate id를 넣으려면 @CollectionId를 사용하면 된다.
JPA 코드
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Collection 인터페이스 사용 - 여기서는 중복을 허용할 수 있으며, 순서 보장이 필요 없다면 List 대신 Collection을 사용할 수 있습니다.
@ElementCollection
@CollectionTable(name = "PRODUCT_IMAGES", joinColumns = @JoinColumn(name = "product_id"))
@Column(name = "image_url")
private Collection<String> imageUrls = new ArrayList<>();
// 기본 생성자 및 getter/setter 생략
}
SQL코드
CREATE TABLE Product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE PRODUCT_IMAGES (
product_id BIGINT NOT NULL,
image_url VARCHAR(255),
PRIMARY KEY (product_id, image_url), -- 중복된 image_url은 허용하지 않을 수도 있고, 허용하려면 별도의 PK를 설정해야 함.
FOREIGN KEY (product_id) REFERENCES Product(id)
);
출처
https://docs.oracle.com/javaee/7/tutorial/persistence-intro.htm
https://docs.oracle.com/javaee/7/tutorial/persistence-intro001.htm#BNBQA
'백엔드' 카테고리의 다른 글
[Spring JPA] 2. JPA 시작 (김영한 JPA 교재) (0) | 2025.03.07 |
---|---|
[Spring JPA] 1. JPA 이야기 (김영한 JPA 교재) (0) | 2025.03.05 |
MySQL 쿼리 최적화: 테이블 스캔, 인덱스 스캔, 옵티마이저, 인덱스 힌트, EXPLAIN ANALYZE (0) | 2025.02.25 |
Included Columns(SQL-server) (0) | 2025.02.21 |
인덱스 조각화, 실행계획, 인덱스 scripts (0) | 2025.02.21 |