JPA를 사용하면 객체와 관계형 데이터베이스 사이의 패러다임 불일치를 해결할 수 있을까?

Glen·2023년 6월 15일
0

배운것

목록 보기
17/37

서론

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을 가지고 있고, LineSection은 1:N 관계이다.

Linesections 필드는 객체의 책임 분리를 위해 @Embeddable로 정의하여 Sections 클래스로 만들었다.

관계형 데이터베이스 구조에서는 SectionLine의 FK를 가지므로, Section을 연관 관계의 주인으로 설정하여 List<Section>mappedBy 속성을 정의해 주었다.

보통 다대일 매핑을 사용하고, 일대다 관계로 양방향 매핑은 자주 사용되지 않지만, Section의 생명 주기를 노선이 관리하고 있기 때문에 cascadeorphanRemoval을 설정하기 위해 일대다 매핑을 하였다.

여기서 문제점은 SectionLine을 필드로 가지고 있는 것이다.

객체 지향 관점에서 본다면 SectionLine 필드를 가지고 있을 필요가 없다.

하지만 관계형 데이터베이스의 구조 때문에 SectionLine을 가지고 있어야 한다.

객체와 관계형 데이터베이스의 패러다임 불일치를 해소하기 위해 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는 Sectionsections 컬렉션에 들어가야 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이 최종적으로 영속되는 과정은 다음과 같다.

  1. Section을 영속한다.
  2. 이때, SectionLine에 대한 FK가 null인 상태로 저장된다.
  3. 일대다 단방향 연관관계를 설정하여 FK를 Sections가 관리하고 있으므로, Section의 FK를 설정하려면 sections 컬렉션에 Section이 추가되어야 한다.
  4. 트랜잭션이 끝나는 시점에 UPDATE 쿼리가 추가로 날아간다.

패러다임의 불일치를 해결하기 위해 위와 같은 성능상 문제가 발생한다.

더 중요한 N+1 문제도 있다. 하지만 여기서 다루지는 않겠다.

따라서 성능 상 이점을 챙기려면 객체 지향적인 설계를 포기하고 SectionLine 필드를 만들고 @ManyToOne 관계를 설정해 주어야 한다.

하지만 객체 지향적인 설계를 위해선 SectionLine 필드를 두는 것은 올바르지 않다.

결국 이러한 트레이드 오프 사이에서 결정을 내려야 한다.

서로 만족시킬 만한 결과를 찾는다면 Section 엔티티에서 Line 필드를 @ManyToOne 연관 관계를 맺고, Section 엔티티에서 Line의 사용을 하지 않고, 노출도 하지 않으면 된다.

class Section {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    private Line line;
    ...
    // public Line getLine() {
       // return line;  
    // }
}

여러 방법이 더 있겠지만, 성능과 편의를 챙기는 것이 목적이면 이 정도의 객체 지향적인 설계는 모른 척하는 것이 바람직할 것 같다.

결론

JPA를 사용한다고 해서 객체와 관계형 데이터베이스 사이의 패러다임 불일치를 완벽하게 해결할 수 없다.

이러한 패러다임의 불일치를 완벽하게 해결하려면 성능이라는 대가를 치러야 한다.

따라서 이런 트레이드 오프 사이에서 결정을 내려야 하고, 최대한 둘의 장점을 누릴 수 있는 선택을 해야 한다.

profile
꾸준히 성장하고 싶은 사람

0개의 댓글