List
Search
1. 컬렉션 매핑
1.1. 런타임 래퍼 클래스
•
단방향 OneToMany 컬렉션 매핑
◦
Movie가 Screening의 컬렉션을 포함한다고 가정
◦
1:N 관계 / 연관관계가 Movie → Screening인 상황
•
런타임 래퍼 클래스
◦
Hibernate는 해당 컬렉션을 단순한 Java 컬렉션(List, Set 등)이 아니라, "특수한 감시용 래퍼 객체"로 바꿔치기(프록시)해서 관리
▪
예) PersistentBag
◦
지연로딩(Lazy Loading), 변경 감지(Dirty Checking), 관계 관리 자동화
•
초기화(선할당)와 Hibernate 래퍼의 관계
◦
선할당 시, Hibernate가 엔티티를 영속화할 때 내부적으로 PersistentBag 같은 객체로 "덮어쓰기"해서 바꿔치기함 (프록시 패턴과 유사).
▪
컬렉션에 대한 코드 호출이 정상 동작하며, 변경 감지도 잘 됨
◦
초기화하지 않고 null로 남겨둘 경우,
▪
영속화 시점에 Hibernate가 알아서 래퍼를 할당함
▪
그러나, 엔티티가 영속화되기 전(persist() 전)에 컬렉션을 직접 사용하면 NPE가 발생할 수 있음
@Entity
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Money fee;
@OneToMany(cascade = CascadeType.PERSIST)
private Collection<Screening> screenings = new ArrayList();
public void addScreening(Screening screening) {
this.screenings.add(screening);
}
}
Java
복사
@Entity
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int sequence;
private LocalDateTime screeningTime;
}
Java
복사
1.2. 컬렉션 타입
•
종류
◦
(Bag) 컬렉션
▪
중복 허용 / 순서 유지 하지 않음 / 가장 효율적인 성능 특징
◦
셋(Set) 컬렉션
▪
중복 허용하지 않음 / 순서 유지하지 않음
◦
리스트 (List) + @OrderColumn 컬렉션
▪
중복 허용 / 순서 유지
◦
맵(Map) 컬렉션
▪
중복 허용하지 않음 / 순서 유지하지 않음 / 키와 값 구성
// Bag 컬렉션
private Collection<Screening> screenings = new ArrayList<>();
// Set 컬렉션
private Set<Screening> screenings = new HashSet<>();
// List + OrderColumn 컬렉션
@OrderColunm(name=”CONDITION_ORDER”)
private List<Screening> screenings = new ArrayList<>();
// Map 컬렉션
private Map<Screening> screenings = new HashMap<>();
TypeScript
복사
1.3. 컬렉션의 런타임 동작 방식
컬렉션은 매핑 방식보다 런타임의 동작 방식이 더 중요
•
컬렉션 요소 삭제 시 주의점
◦
컬렉션에서 관계를 끊은 것 remove() . 요소 자체가 삭제되는 것이 아님
◦
FK, 참조하는 것만 null로 바꿈
◦
컬렉션은 두 객체간의 관계를 끊는거지 데이터를 삭제하는 것이 아님.
•
요소 자체를 삭제하고 싶다면?
◦
@OneToMany(orphanRemoval = true)
◦
컬렉션에서 요소가 제거될 때 테이블 로우도 함께 삭제
1.4. 단방향 컬렉션 매핑 → 많은 쿼리 실행
•
컬렉션은 FK를 안갖고 있음. FK는 자식쪽에 있음 (연관관계의 주인)
•
단방향 컬렉션은 INSERT 후 UPDATE 가 나올 수 밖에 없음
•
단방향 컬렉션 매핑은 엔티티 추가할 때 많은 쿼리가 실행될 수 있음
•
쿼리 횟수를 줄이고 싶다면 양방향 컬렉션 매핑 사용
◦
mappedBy로 양방향 참조가 되었다면 FK를 알고 있기 때문에 UPDATE 쿼리 불필요
◦
컬렉션 매핑에서 IINSERST 쿼리 횟수로 줄이려면 양방향 연관관계 매핑으로 설정
•
양방향 관계와 캡슐화
◦
양방향의 관계라도 캡슐화의 경계로 묶었다면 괜찮음.
◦
캡슐화의 경계? 외부에 보여주고 싶지않음. 캡슐화 경계라면 같이 코드를 수정하겠다는 의미
1.5. 리스트 + @OrderColumn 컬렉션
•
특징
◦
중복 허용 + 순서 유지
•
결론
◦
안쓰는게 좋음 / 일관성이 없음.
◦
중간에 특정 데이터를 제거하는 경우 → 순서가 다 바뀌어야함
▪
Order 컬럼과 FK 컬럼 모두 Null 처리 후 업데이트
Movie loadedMovie = em.find(Movie.class, movie.getId());
loadedMovie.getScreenings().remove(1);
em.flush();
Java
복사
update screening
set movie_id=null,play_order=null
where movie_id=1 and id=3
update screening
set movie_id=null,play_order=null
where movie_id=1 and id=2
update screening
set movie_id=1,play_order=1
where id=3
SQL
복사
•
리스트 양방향 매핑
◦
리스트 인덱스 관리를 위해 Movie에서 외래키를 관리하므로 mappedBy 대신 @JoinColumn 지정
◦
외래키를 Movie에 맡기기 때문에 updatable, Insertable을 null로 설정
@Entity
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Integer runningTime;
private Money fee;
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(name = "MOVIE_ID")
@OrderColumn(name = "PLAYING_ORDER")
private List<Screening> screenings = new ArrayList<>();
public void addScreening(Screening screening) {
this.screenings.add(screening);
}
}
SQL
복사
@Entity
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int sequence;
private LocalDateTime screeningTime;
@ManyToOne
@JoinColumn(name = "MOVIE_ID", updatable = false, insertable = false)
private Movie movie;
}
SQL
복사
1.6. 값 객체 컬렉션 매핑
•
@ElementCollection
◦
OneToMany 이므로 기본값은 지연 로딩
•
@CollectionTable
◦
PK + 값 객체의 모든 속성을 가지고 JPA가 테이블을 만들어줌
◦
값 객체 컬렉션을 저장할 테이블 이름과 FK 컬럼 지정
◦
Embeddable을 담을 테이블이라는 것을 알려주는 것
@Entity
@NoArgsConstructor
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Money fee;
@ElementCollection
@CollectionTable(name = "PRICES",
joinColumns = @JoinColumn(name = "MOVIE_ID"))
private Set<Money> prices = new HashSet<>();
public void addPrice(Money price) {
this.prices.add(price);
}
}
Java
복사
@Embeddable
public class Money {
private BigDecimal fee;
public void plus(Money other) {
this.amount = this.amount.plus(other.fee);
}
}
Java
복사
erDiagram MOVIE { Long id PK String title Money fee } PRICES { Long movie_id FK, PK BigDecimal fee PK } MOVIE ||--o{ PRICES : "1:N"
Mermaid
복사
•
값 객체 컬렉션 데이터베이스 매핑
◦
값 객체 컬렉션의 PK
▪
값 객체 속성(fee) + 값 객체를 포함하는 테이블 ID(movie_id)
•
CasCasde 옵션이 따로 없음
◦
이유? 라이프사이클이 무조건 종속적이기때문에 필요없는것
◦
CascadeType.ALL, orphanRemoval = true 와 동일
•
값 객체는 식별성이 없음
◦
1000개에서 1개 추가 → 999개 삭제하고 1개 추가함
◦
값객체 컬렉션 관리 → 컬렉션 갯수가 적어야함
◦
혹은 만든 순간 이뮤터블해야함
•
값 객체 컬렉션 전체 삭제
◦
전체 삭제
1.7. 값 객체 컬렉션과 참조 객체 컬렉션 차이
구분 | 컬렉션 요소 추가 쿼리 동작 | 컬렉션 삭제 쿼리 동작 |
PRICES(값타입 컬렉션) | 전체 요소 삭제 후 전체 요소를 다시 저장
delete from prices where movie_id=1
insert into prices (movie_id, fee) values (1, 10000)
insert into prices (movie_id, fee) values (1, 2000) | 하나의 쿼리로 전체 요소 삭제
delete from prices where movie_id=1 |
SCREENING(엔티티 컬렉션) | 추가된 요소만 저장
insert into screening (screening_time, sequence, id) values ('2024-12-09 13:00', 3, default) | 요소만큼 삭제 쿼리 실행
delete from screening where id=1
delete from screening where id=2
delete from screening where id=3 |
•
컬렉션과 관련된 두가지 문제점
◦
즉시 로딩
▪
데카르트 곱 문제
◦
지연 로딩
▪
N + 1 문제
2. 상속 매핑
•
Java interface는 @entity로 매핑 불가능
•
인터페이스를 추상클래스로 변경 후 매핑
classDiagram class DiscountPolicy { +calculateDiscount(screening) } class AmountDiscountPolicy class PercentDiscountPolicy class DiscountCondition { <<interface>> +isSatisfiedBy(screening) } class SequenceCondition class PeriodCondition DiscountPolicy <|-- AmountDiscountPolicy DiscountPolicy <|-- PercentDiscountPolicy DiscountPolicy "1" --> "1..*" DiscountCondition DiscountCondition <|.. SequenceCondition DiscountCondition <|.. PeriodCondition
Mermaid
복사
2.1. 상속의 특징
2.1.1. 상속의 용도
•
다형적인 연관관계 / Polymorphic Association
◦
구체 클래스가 아닌 추상클래스에만 의존
▪
DiscountPolicy는 DiscountCondition에만 의존
2.1.2. JPA는 적절한 구현 클래스의 인스턴스 조회
•
예) 할인 조건 → SequenceCondition, Period Condition
2.1.3. 의존성 주입
•
구체적인 타입을 내부에서 직접 생성하지 않고 외부에서 생성하여 주입
2.2. 단일 테이블 전략
•
성능이 중요하고 상속 계층 리팩터링이 자주 발생할 경우 사용
•
장점
◦
빠른 조회 성능, 단순한 조회 쿼리
•
단점
◦
nullable 제약 조건 강제 불가능
◦
테이블의 크기가 커지면 성능이 느려질 수도 있음
classDiagram class DiscountCondition { +isSatisfiedBy(screening) } class SequenceCondition { sequence } class PeriodCondition { dayOfWeek startTime endTime } DiscountCondition <|-- SequenceCondition DiscountCondition <|-- PeriodCondition class DISCOUNT_CONDITION { ID CONDITION_TYPE SEQUENCE DAY_OF_WEEK START_TIME END_TIME }
Mermaid
복사
2.2.1. 클래스 매핑
•
JPA 단일 테이블 상속 전략(SINGLE_TABLE)
•
하나의 테이블(discount_condition)에 모든 서브 클래스의 데이터를 함께 저장하는 방식
◦
모든 컬럼이 하나의 테이블(discount_condition)에 포함되며, 서브 클래스별 컬럼은 사용하지 않을 경우 NULL로 저장
•
condition_type 컬럼으로 각 객체의 타입을 구분
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="condition_type")
public abstract class DiscountCondition {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
}
Java
복사
@Entity
@DiscriminatorValue("PERIOD")
public class PeriodCondition extends DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime, endTime;
}
@Entity
@DiscriminatorValue("SEQUENCE")
public class SequenceCondition extends DiscountCondition {
private int sequence;
}
Java
복사
2.2.2. 다형적 쿼리
•
모든 필드를 한 테이블에서 조회
select c from DiscountCondition c
SQL
복사
select
dc1_0.id,
dc1_0.condition_type,
dc1_0.day_of_week,
dc1_0.end_time,
dc1_0.start_time,
dc1_0.sequence
from
discount_condition dc1_0
SQL
복사
2.2.3. 서브클래스 별 쿼리
•
SequenceCondition만 조회
select c from SequenceCondition c
SQL
복사
jpql
select
sc1_0.id,
sc1_0.sequence
from
discount_condition sc1_0
where
sc1_0.condition_type='SEQUENCE'
SQL
복사
•
PeriodCondition만 조회
select c from PeriodCondition c
SQL
복사
jpql
select
pc1_0.id,
pc1_0.day_of_week,
pc1_0.end_time,
pc1_0.start_time
from
discount_condition pc1_0
where
pc1_0.condition_type='PERIOD'
SQL
복사
2.3. 조인 전략
•
데이터 무결성이 중요하고 상속 계층이 안정적일때 사용
•
장점
◦
not nullable 제약조건 강제 가능
◦
테이블 정규화
•
단점
◦
상속 계층이 복잡할 경우 조인으로 인한 성능 저하 발생
◦
조회 쿼리 복잡
classDiagram class DiscountCondition { +isSatisfiedBy(screening) } class SequenceCondition { sequence } class PeriodCondition { dayOfWeek startTime endTime } DiscountCondition <|-- SequenceCondition DiscountCondition <|-- PeriodCondition
Mermaid
복사
classDiagram class DISCOUNT_CONDITION { ID } class SEQUENCE_CONDITION { SEQUENCE_ID (FK, PK) SEQUENCE } class PERIOD_CONDITION { PERIOD_ID (FK, PK) DAY_OF_WEEK START_TIME END_TIME } DISCOUNT_CONDITION <|-- SEQUENCE_CONDITION : SEQUENCE_ID = ID DISCOUNT_CONDITION <|-- PERIOD_CONDITION : PERIOD_ID = ID
Mermaid
복사
2.3.1. 다형적 쿼리
•
discount_condition(부모) 테이블을 기준으로, 하위 테이블(period_condition, sequence_condition)을 LEFT JOIN
•
어떤 하위 타입인지 구분(case문 등)
•
모든 타입의 객체를 한 번에 조회 가능
select c from DiscountCondition c
SQL
복사
select
dc1_0.id,
case
when dc1_1.period_id is not null then 1
when dc1_2.sequence_id is not null then 2
end,
dc1_1.day_of_week,
dc1_1.end_time,
dc1_1.start_time,
dc1_2.sequence
from
discount_condition dc1_0
left join period_condition dc1_1 on dc1_0.id = dc1_1.period_id
left join sequence_condition dc1_2 on dc1_0.id = dc1_2.sequence_id
SQL
복사
2.3.2. 서브클래스별 쿼리
•
SequenceCondition만 조회
◦
INNER JOIN
◦
시퀀스 타입에 해당하는 조건만 조회
select c from SequenceCondition c
Plain Text
복사
select
sc1_0.sequence_id,
sc1_0.sequence
from
sequence_condition sc1_0
join discount_condition sc1_1 on sc1_0.sequence_id = sc1_1.id
SQL
복사
•
PeriodCondition만 조회
◦
INNER JOIN
◦
기간 타입에 해당하는 조건만 조회
select c from PeriodCondition c
SQL
복사
select
pc1_0.period_id,
pc1_0.day_of_week,
pc1_0.end_time,
pc1_0.start_time
from
period_condition pc1_0
join discount_condition pc1_1 on pc1_0.period_id = pc1_1.id
SQL
복사
2.4. 구현 클래스 마다 테이블 전략
•
테이블끼리 관계를 설정하지 않고 별도로 만듬
•
장점
◦
서브 타입을 구분해서 처리할 때 효과적
◦
not null 제약 조건 사용 가능
•
단점
◦
여러 자식 테이블을 함께 조회할때 성능 저하
◦
자식 테이블을 통합하여 쿼리하기 어려움
classDiagram class DiscountCondition { +isSatisfiedBy(screening) } class SequenceCondition { sequence } class PeriodCondition { dayOfWeek startTime endTime } DiscountCondition <|-- SequenceCondition DiscountCondition <|-- PeriodCondition
Mermaid
복사
classDiagram class SEQUENCE_CONDITION { ID SEQUENCE } class PERIOD_CONDITION { ID DAY_OF_WEEK START_TIME END_TIME }
Mermaid
복사
2.4.1. 클래스 매핑
•
상속 전략
◦
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
◦
IDENTITY 전략은 사용 불가. PK를 공유하지 못함
•
각 서브클래스마다 별도의 테이블 생성
(예: sequence_condition, period_condition)
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class DiscountCondition {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "movie_sequence")
private Long id;
}
@Entity
public class PeriodCondition extends DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime, endTime;
}
@Entity
public class SequenceCondition extends DiscountCondition {
private int sequence;
}
Java
복사
2.4.2. 다형적 쿼리
•
서브클래스별로 생성된 테이블을 UNION ALL로 합쳐 조회
•
공통되지 않은 컬럼은 NULL로 채움
•
서브타입 구분을 위해 clazz(타입값) 컬럼 추가
select c from DiscountCondition c
SQL
복사
select
dc1_0.id,
dc1_0.clazz_,
dc1_0.day_of_week,
dc1_0.end_time,
dc1_0.start_time,
dc1_0.sequence
from (
select
id,
1 as clazz_,
day_of_week,
end_time,
start_time,
null as sequence
from
period_condition
union all
select
id,
2 as clazz_,
null as day_of_week,
null as end_time,
null as start_time,
sequence
from
sequence_condition
) dc1_0
SQL
복사
2.4.3. 서브클래스 별 쿼리
•
서브클래스 테이블에 직접 쿼리
2.5. 상속과 코드 재사용
2.5.1. 취약한 기반 클래스 문제
•
부모 클래스를 추가해야한다면?
◦
다중 상속을 지원하지 않는 언어에서는 부모 클래스 추가 불가능
•
부모클래스의 구현이 바뀌면 자식 클래스의 구현이 깨짐
◦
예) 할인이 적용 안될 때 Movie에서 정가 대신 기본 금액을 반환하려고 한다면??
@Entity
public class Movie {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Money fee;
private Money baseFee;
private Money discountAmount;
private Set<DiscountCondition> conditions = new HashSet<>();
public Money calculateFee(Screening screening) {
for (DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return fee.minus(discountAmount);
}
}
return fee.isLessThan(baseFee) ? fee : baseFee;
}
public Money getFee() {
return fee;
}
}
Java
복사
@Entity
public class PercentDiscountMovie extends Movie {
private double discountPercent;
public PercentDiscountMovie(String title, Money fee, double discountPercent) {
super(title, fee, Money.ZERO);
this.discountPercent = discountPercent;
}
@Override
public Money calculateFee(Screening screening) {
// 모든 경우에 할인이 적용되는 경우로 판단
if (super.calculateFee(screening).equals(getFee())) {
return getFee().minus(getFee().times(discountPercent));
}
return getFee();
}
}
Java
복사
2.5.2. 상속 보다는 합성 사용
•
재사용하고싶다면? 합성을 써라
◦
상속은 다형성을 위해서만 쓸것
•
재사용을 위해서는 합성을 쓸 것
classDiagram class Movie { id discountAmount } class PercentDiscountMovie { id discountPercent } Movie <|-- PercentDiscountMovie
Mermaid
복사
classDiagram class Movie { id } class DiscountPolicy { id } class AmountDiscountPolicy { discountAmount } class PercentDiscountPolicy { discountPercent } DiscountPolicy <|-- AmountDiscountPolicy DiscountPolicy <|-- PercentDiscountPolicy Movie "1" -- "0..1" DiscountPolicy
Mermaid
복사
2.5.3. 올바른 상속
•
다형적인 연관관계
◦
구체적인 타입인 isSatisfiedBy 메서드 실행
◦
DiscountCondition 는 인터페이스일뿐 실제 객체는 구체 클래스임
◦
자바의 다형성에 따라 실제 객체의 타입에 맞는 isSatisfiedBy 가 자동으로 실행
◦
즉, 부모 타입(DiscountCondition)을 참조하더라도 자식 타입(SequenceCondition , PeriodCondition ) 의 실제 구현이 동작한다는 것을 의미
•
올바른 상속 계층
◦
특정 클래스 안에서 메서드를 구현하는 메서드는 단 한 하나만 존재
◦
부모 클래스의 구체 메서드를 오버라이딩하면 동작이 이상해질 수 있음
◦
클라이언트의 동작이 이상해질 수 있음. 리스코프 치환의 원칙 위반
◦
상속 계층이 있을 때, 구체메서드는 하나만 있어야함
public abstract class DiscountPolicy {
private Set<DiscountCondition> conditions = new HashSet<>();
// 특정 클래스 안에서 메서드를 구현하는 메서드는 단 한 하나만 존재
public Money calculateDiscount(Screening screening) {
for (DiscountCondition each : conditions) {
// 구체적인 타입인 isSatisfiedBy 메서드 실행
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening screening);
}
Java
복사
public abstract class DiscountCondition {
public abstract boolean isSatisfiedBy(Screening screening);
}
public class SequenceCondition implements DiscountCondition {
private int sequence;
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
public class PeriodCondition implements DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime, endTime;
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek)
&& startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0
&& endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
Java
복사