JPA와 같은 ORM 기술을 사용하면 데이터베이스에 의존적인 개발을 해소할 수 있다.
하지만 ORM 기술을 사용한다고 객체와 관계형 데이터베이스 간의 패러다임 불일치 문제를 해결할 수 있을까?
지하철 노선도 미션을 JPA로 다시 구현해보는 중 다음과 같은 문제가 발생했다.
@Entity
class Line {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private Sections sections;
private String name;
private String color;
...
}
@Embeddable
class Sections {
@OneToMany(mappedBy = "line", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Section> sections;
...
}
@Entity
class Section {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.EAGER)
private Station upBoundStation;
@OneToOne(fetch = FetchType.EAGER)
private Station downBoundStation;
@ManyToOne(fetch = FetchType.LAZY)
private Line line;
private int distance;
...
}
Line
이 여러 개의 Section
을 가지고 있고, Line
과 Section
은 1:N 관계이다.
Line
의 sections
필드는 객체의 책임 분리를 위해 @Embeddable
로 정의하여 Sections
클래스로 만들었다.
관계형 데이터베이스 구조에서는 Section
이 Line
의 FK를 가지므로, Section
을 연관 관계의 주인으로 설정하여 List<Section>
에 mappedBy
속성을 정의해 주었다.
보통 다대일 매핑을 사용하고, 일대다 관계로 양방향 매핑은 자주 사용되지 않지만, Section
의 생명 주기를 노선이 관리하고 있기 때문에 cascade
와 orphanRemoval
을 설정하기 위해 일대다 매핑을 하였다.
여기서 문제점은 Section
이 Line
을 필드로 가지고 있는 것이다.
객체 지향 관점에서 본다면 Section
이 Line
필드를 가지고 있을 필요가 없다.
하지만 관계형 데이터베이스의 구조 때문에 Section
은 Line
을 가지고 있어야 한다.
객체와 관계형 데이터베이스의 패러다임 불일치를 해소하기 위해 ORM인 JPA를 사용한 것인데, 해당 문제를 해결할 수 없었다.
물론 다음과 같이, Section
에서 Line
에 대한 참조를 없애고 Sections
에서 일대다 단방향 매핑을 사용하면 패러다임의 불일치 문제를 해결할 수 있다.
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Sections {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "line_id")
private List<Section> sections;
...
}
@Entity
class Section {
...
// @ManyToOne(fetch = FetchType.LAZY)
// private Line line;
...
}
하지만 이 경우 또 다른 문제점이 발생한다.
바로 Section
을 저장할 때, INSERT 쿼리가 발생하고 UPDATE 쿼리가 발생하기 때문이다.
Section
이 데이터베이스에 정상적으로 저장되려면 Line
의 PK가 필요하다.
하지만 sections
필드에 @OneToMany
단방향 의존관계를 설정했으므로, Section
의 FK는 Section
이 sections
컬렉션에 들어가야 FK가 매핑된다.
cascade
와는 전혀 상관이 없다.cascade
설정은 엔티티의 생명 주기 관리를 부모 엔티티에서 해주기 위한 편의 기능일 뿐이다.
cascade
를 사용하지 않을 때, 저장되는 과정은 다음과 같다.
public void createSection(Long lineId, SectionCreateRequest request) {
Section section = createSection(request);
sectionRepository.save(section); // 영속화, section은 line의 PK를 모른다.
log.info("section 영속했음");
Line line = findLineById(lineId);
line.addSection(section);
log.info("sections에 section 추가했음");
}
이때 발생되는 로그를 보자.
insert
into
section
(id, distance, down_bound_station_id, up_bound_station_id)
values
(default, 3, 2, 1)
section 영속했음
select
line0_.id as id1_0_0_,
line0_.color as color2_0_0_,
line0_.name as name3_0_0_
from
line line0_
where
line0_.id=1
select
sections0_.line_id as line_id5_1_0_,
sections0_.id as id1_1_0_,
sections0_.id as id1_1_1_,
sections0_.distance as distance2_1_1_,
sections0_.down_bound_station_id as down_bou3_1_1_,
sections0_.up_bound_station_id as up_bound4_1_1_,
station1_.id as id1_2_2_,
station1_.name as name2_2_2_,
station2_.id as id1_2_3_,
station2_.name as name2_2_3_
from
section sections0_
left outer join
station station1_
on sections0_.down_bound_station_id=station1_.id
left outer join
station station2_
on sections0_.up_bound_station_id=station2_.id
where
sections0_.line_id=1
sections에 section 추가했음
update
section
set
line_id=1
where
id=1
로그를 보면 Section
이 최종적으로 영속되는 과정은 다음과 같다.
Section
을 영속한다.Section
의 Line
에 대한 FK가 null인 상태로 저장된다.Sections
가 관리하고 있으므로, Section
의 FK를 설정하려면 sections
컬렉션에 Section
이 추가되어야 한다.패러다임의 불일치를 해결하기 위해 위와 같은 성능상 문제가 발생한다.
더 중요한 N+1 문제도 있다. 하지만 여기서 다루지는 않겠다.
따라서 성능 상 이점을 챙기려면 객체 지향적인 설계를 포기하고 Section
에 Line
필드를 만들고 @ManyToOne
관계를 설정해 주어야 한다.
하지만 객체 지향적인 설계를 위해선 Section
에 Line
필드를 두는 것은 올바르지 않다.
결국 이러한 트레이드 오프 사이에서 결정을 내려야 한다.
서로 만족시킬 만한 결과를 찾는다면 Section
엔티티에서 Line
필드를 @ManyToOne
연관 관계를 맺고, Section
엔티티에서 Line
의 사용을 하지 않고, 노출도 하지 않으면 된다.
class Section {
...
@ManyToOne(fetch = FetchType.LAZY)
private Line line;
...
// public Line getLine() {
// return line;
// }
}
여러 방법이 더 있겠지만, 성능과 편의를 챙기는 것이 목적이면 이 정도의 객체 지향적인 설계는 모른 척하는 것이 바람직할 것 같다.
JPA를 사용한다고 해서 객체와 관계형 데이터베이스 사이의 패러다임 불일치를 완벽하게 해결할 수 없다.
이러한 패러다임의 불일치를 완벽하게 해결하려면 성능이라는 대가를 치러야 한다.
따라서 이런 트레이드 오프 사이에서 결정을 내려야 하고, 최대한 둘의 장점을 누릴 수 있는 선택을 해야 한다.