본문 바로가기
프로그래밍/Back-end

JPA 객체 지향 모델링 연관관계 매핑 (1탄)

by @GodWin 2024. 10. 11.

-

 

-
안녕하세요? 오늘은 JPA의 가장 중요 관점인, 객체 지향 모델링에 대해서, 알아보도록 하겠습니다.

 


-
기존의 DB 모델링과 비교를 했을때, JPA는 객체 중심으로 이루어져있는데,
객체를 테이블에 맞추어, 데이터 중심으로 모델링을 하게되면, 협력 관계를 만들 수 없습니다.

테이블은 외래 키로 조인을 사용해서, 연관된 테이블을 찾게 되고, 객체는 참조를 사용해서 연관된 객체를 찾게 되는데요,
이렇게 테이블과 객체 사이에는 이런 큰 간격이 존재합니다.

그렇기에, JPA에서는 연관관계 설정을 해서 사용하게 됩니다.


연관관계 설정에서는, 단방향 연관관계양방향 연관관계가 존재합니다.


-
단방향 연관관계


샘플코드로 한번 알아보도록 하겠습니다.


단방향 주 테이블 외래 키 관리 엔티티 생성)

@Entity
public class EntityA {
  
  @ID @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  // 관계 선언 : 1-N 관계 중, 해당 엔티티가 N
  @ManyToOne
  @JoinColumn(name = "ENTITYB_ID") // 조인할 테이블의 조인 컬럼
  private EntityB entityB;
  
  ...
  
}


단방향 대상 테이블 엔티티 생성)

@Entity
public class EntityB {
  
  @ID @GeneratedValue
  @Column(name = "ENTITYB_ID")
  private Long id;
  
  @Column(name = "ENTITYB_NAME")
  private String name;
  
  ...
  
}


샘플 코드)

EntityManagerFactory emf = Persistence.createEntityManagerFactory("유닛명");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

EntityB entityB = new EntityB();
entityB.setName("EntityB");
em.persist(entityB);

EntityA entityA = new EntityA();
entityA.setName("entityA");
entityA.setEntityB(entityB);
em.persist(entityA);

// -- TEST CODE [START]
// 1차캐시 영속성 데이터 말고, 실 DB를 보고 싶다면
em.flush();
em.clear();
// -- TEST CODE [END]

EntityA findEntityA = em.find(EntityA.class, entityA.getId());

EntityB findEntityB = findEntityA.getEntityB();

// 조회한 EntityA의 EntityB를 다른 EntityB으로 변경
EntityB newEntityB = em.find(EntityB.class, primaryKey: 100L);
findEntityA.setEntityB(newEntityB);

tx.commit();



이렇게, 객체 지향 모델링을 사용해서, 객체로써의 사용을 하게 됩니다..

하지만 해당 내용은, EntityA에서 EntityB으로의 조인이 가능하지만,
반대로 EntityB에서 EntityA 정보를 얻어올 수가 없습니다.

그렇기에, 양방향 연관관계를 사용해서, 서로 참조로 매핑이 가능합니다.


-
양방향 연관관계 


위의 내용처럼, EntityA와 EntityB가 서로 조인을 해서, 서로가 서로의 데이터를 가지고 오기위해서는, 양방향 연관관계를 설정해야합니다.

샘플코드를 사용해서 알아보도록 하겠습니다.


양방향 주 테이블 외래 키 관리 엔티티 생성)

@Entity
public class EntityA {
  
  @ID @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  // 관계 선언 : 1-N 관계 중, 해당 엔티티가 N
  @ManyToOne
  @JoinColumn(name = "ENTITYB_ID")	// 조인할 테이블의 조인 컬럼
  private EntityB entityB;
  
  ...
  
}


양방향 대상 테이블 엔티티 생성)

@Entity
public class EntityB {
  
  @ID @GeneratedValue
  @Column(name = "ENTITYB_ID")
  private Long id;
  
  @Column(name = "ENTITYB_NAME")
  private String name;
  
  // 관계 선언 : N-1 관계 중, 해당 엔티티가 1
  // 연관관계의 주인이 아니기 때문에 mappedBy 속성 부여
  @OneToMany(mappedBy = "entityB")
  private List<EntityA> entityAs = new ArrayList<>();

  ...
  
}


샘플코드)

EntityManagerFactory emf = Persistence.createEntityManagerFactory("유닛명");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

EntityB entityB = new EntityB();
entityB.setName("EntityB");
em.persist(entityB);

EntityA entityA = new EntityA();
entityA.setName("entityA");
entityA.setEntityB(EntityB);
em.persist(entityA);

// 순수 객체 상태를 고려해서, 항상 양쪽에  값을 셋팅
entityB.getEntityAs().add(entityA);
// 해당 방법이 싫다면, 연관관계 편의 메소드를 생성해서 사용하는 방법 존재
/*
// EntityB 엔티티 getter/setter 부분에 추가
public void setEntityB(EntityB entityB) {
  this.entityB = entityB;
  entityB.getEntityAs().add(this);
}
*/

// -- TEST CODE [START]
// 1차캐시 영속성 데이터 말고, 실 DB를 보고 싶다면
em.flush();
em.clear();
// -- TEST CODE [END]

EntityA findEntityA = em.find(EntityA.class, entityA.getId());
List<EntityA> entityAs = findEntityA.getEntityB().getEntityAs();

tx.commit();



여기서 중요한 점은, 객체의 양방향 관계는 사실 양방향 관계가 아니라, 서로 다른 단방향 관계 2개임을 숙지하셔야합니다.

이처럼 양방향 연관관계를 사용하기 위해서는, 규칙이 존재합니다.

-

양방향 매핑 규칙


: 객체의 두 관계 중, 하나를 연관관계의 주인으로 지정 필수
: 연관관계의 주인만이 외래 키를 관리 (등록/수정)
: 주인이 아닌쪽은 읽기만 가능
: 주인은 mappedBy 속성의 사용 불가
: 주인이 아니면, mappedBy 속성으로 주인 지정 필수

그렇다면, 주인은 어떻게 지정을 해줘야할까요?

비지니스 로직을 기준으로 연관관계의 주인을 선택하는 것이 아니라, 외래 키가 존재하는 테이블을 주인으로 지정해 줍니다.

샘플에서는, EntityA.entityB이 연관관계의 주인이 됩니다. 그러므로 EntityB의 entityAs에는 데이터를 넣어도, DB에서는 등록/수정이 이뤄지지 않습니다.


샘플소스)

EntityManagerFactory emf = Persistence.createEntityManagerFactory("유닛명");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

EntityA entityA = new EntityA();
entityA.setName("entityA_1");
em.persist(entityA);

EntityB entityB = new EntityB();
entityB.setName("EntityB_1");
entityB.getEntityAs(entityA);
em.persist(entityB);

tx.commit();


DB 결과)

ENTITYA_ID ENTITYA_NAME ENTITYB_ID
1 entityA_1 null



해당 소스에서는, 주인은 EntityA객체이기때문에, EntityB 객체의 entityAs 에서는 1차캐시에 값을 셋팅하더라도, DB에서는 등록/수정이 이루어지지 않음을 확인할 수 있습니다.



또한, 양방향 연관관계에서 주의하실 점은, 양방향 매핑 시, 무한 루프를 조심해야합니다.

toString() / lombok / JSON 생성 라이브러리 등등..
-> controller에서는 Entity를 반환하지 않도록,
DTO로 변환해서 반환 추천


※ 사실상 설계시점에서, 단방향 매핑만으로도 이미 연관관계 매핑이 완료가 되어야 한다.


오늘은 JPA의 가장 중요 관점인, 객체 지향 모델링에 대해서, 알아보았습니다.
그럼 오늘도 즐거운 하루 되시길 바라겠습니다.