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

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

by @GodWin 2024. 10. 11.

-

-
안녕하세요? 오늘은 JPA의 다양한 연관관계 매핑에 대해서 알아보도록 하겠습니다.


※ JPA의 연관관계에 대해서는 이전 포스팅을 참조해주시길 바라겠습니다.

https://logger-debug.tistory.com/entry/JPA-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EB%AA%A8%EB%8D%B8%EB%A7%81-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-1%ED%83%84

 

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

- -안녕하세요? 오늘은 JPA의 가장 중요 관점인, 객체 지향 모델링에 대해서, 알아보도록 하겠습니다.-기존의 DB 모델링과 비교를 했을때, JPA는 객체 중심으로 이루어져있는데,객체를 테이블에 맞

logger-debug.tistory.com

 


-
관계 매핑 시 고려사항은 아래와 같습니다.

[다중성]
[단방향/양방향]
[연관관계의 주인]


그럼, 각각의 연관관계 대해서 자세히 알아보도록 하겠습니다.


* [다중성]


다중성에는, 아래와 같은 연관관계가 존재합니다.

다대일 [N:1] - @ManyToOne
일대다 [1:N] - @OneToMany
일대일 [1:1] - @OneToOne
다대다 [N:N] - @ManyToMany

 

-
다대일 [N:1]
@ManyToOne

: 실무에서 가장 많이 쓰임
: 외래 키 관리 엔티티가 주인으로
타 엔티티를 참조 사용
: 다대일의 반대는 일대다




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

@Entity
public class EntityA {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  @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_1");

em.persist(entityB);

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

em.persist(entityA);

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

EntityB findEntityB = findEntityA.getEntityB();

tx.commit();



+
다대일 양방향 연관관계시에는, 외래 키가 있는 쪽이 연관관계의 주인이 됩니다.
양쪽을 서로 참조하도록 개발이 이루어져야 합니다.


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

@Entity
public class EntityA {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  @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;
  
  @ManyToOne(mappedBy = "entityB") // 연관관계의 주인의 EntityB 변수명
  private List<EntityA> entityA = new ArrayList<>();
  
  ...
  
}


샘플코드)

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

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

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

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

entityB.getEntityAs().add(entityA);

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

tx.commit();




※ 양방향은, 필요시에 추가를 하면 되지만, 객체 시점에서의 설계는, 단방향 매핑 연관관계에서 끝나야 올바른 설계입니다.


-
일대다 [1:N]
@OneToMany

: 다대일의 반대
    > 1이 연관관계의 주인
: 테이블 DB설계상, 일대다 관계는 항상 다(N) 쪽에 외래 키가 존재
: 객체와 테이블의 차이 때문에, 반대편 테이블의 외래 키를 관리하는 특이한 구조
: @JoinCloumn 사용이 필수
     > 그렇지 않다면, 테이블 사이 중간 조인 매핑 테이블이 자동으로 생성 (default : join table 방식)




다대일 단방향 - 주 테이블 엔티티 생성)

@Entity
public class EntityB {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYB_ID")
  private Long id;
  
  @Column(name = "ENTITYB_NAME")
  private String name;
  
  @OneToMany
  @JoinColumn(name = "ENTITYB_ID")
  private List<EntityA> entityAs = new ArrayList<>();
  
  ...
  
}


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

@Entity
public class EntityA {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  ...
  
}


샘플코드)

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().add(entityA);
// └> EntityA 테이블의 변경(Update)이 이루어진다.

em.persist(entityB);

tx.commit();



+
일대다 단방향 매핑의 단점

: 엔티티가 관리하는 외래 키가 다른 테이블에 존재
: 연관관계 관리를 위해 추가로 Update SQL가 실행된다.
     > 성능의 저하 (entityA insert -> entityB insert -> entityA update)



※ 일대다 단방향 매핑보다는, 다대일 양방향 매핑을 사용 추천


일대다 양방향 - 주 테이블 엔티티 생성)

@Entity
public class EntityB {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYB_ID")
  private Long id;
  
  @Column(name = "ENTITYB_NAME")
  private String name;
  
  @OntToMany
  @JoinColumn(name = "ENTITYB_ID")
  private List<EntityA> entityAs = new ArrayList<>();
  
  ...
  
}


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

@Entity
public class EntityA {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  @ManyToOne
  @JoinColumn(name = "ENTITYB_ID", insertable=false, updateable=false)
  private EntityB entityB;
  
  ...
  
}



공식적으로 이런 매핑은 존재하지 않는다.
읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
해당 방법은 스펙상에서 존재하지 않음

※ 다대일 양방향 매핑을 사용 추천


-
일대일 [1:1]
@OneToOne

: 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
: 외래 키에 DB 유니크 제약조건 추가
: 다대일 양방향 연관관계와 유사 (외래키가 있는곳이 주인)



+
주 테이블과 대상 테이블에서의 외래 키 선택 관리 대한 차이점


++주 테이블에 외래 키

: 주 객체가 대상 객체의 참조를 가지는 것처럼, 주 테이블에 외래 키를 두고, 대상테이블을 찾음
: JPA 매핑 편리
: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
: 값이 없으면 외래 키에 Null 허용



++대상 테이블에 외래 키

: 대상 테이블에 외래 키가 존재
: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때, 테이블 구조 유지
: 프록시 기능의 한계로 지연 로딩으로 설정해도, 항상 즉시 로딩 됨



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

@Entity
public class EntityA {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  @OneToOne
  @JoinColumn(name = "ENTITYC_ID")
  private EntityC entityC;
 
  ...
 
}


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

@Entity
public class EntityC {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYC_ID")
  private Long id;
  
  @Column(name = "ENTITYC_NAME")
  private String name;
  
  ...
  
}


샘플 코드)

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);

EntityC entityC = new EntityC();
entityC.setName("entityC_1");

em.persist(entityC);

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

EntityB findEntityC = findEntityA.getEntityC();

tx.commit();



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

@Entity
public class EntityA {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  @OneToOne
  @JoinColumn(name = "ENTITYC_ID")
  private EntityC entityC;
 
  ...
 
}


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

@Entity
public class EntityC {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYC_ID")
  private Long id;
  
  @Column(name = "ENTITYC_NAME")
  private String name;
  
  @OneToOne(mappedBy = "entityC")
  private EntityA entityA;
  
  ...
  
}


일대일 양방향 주테이블 외래키 관리 샘플 코드)

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);

EntityC entityC = new EntityC();
entityC.setName("entityC_1");

em.persist(entityC);

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

EntityC findEntityC = findEntityA.getEntityC().getEntityAs();

tx.commit();




※ 일대일 단방향 - 대상 테이블 외래키 관리는 JPA에서 지원 불가


일대일 양방향 - 대상 테이블 외래키 관리 주 테이블 엔티티 생성

@Entity
public class EntityA {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYA_ID")
  private Long id;
  
  @Column(name = "ENTITYA_NAME")
  private String name;
  
  @OneToOne
  @JoinColumn(name = "ENTITYC_ID")
  private EntityC entityC;
  
  ...
  
}


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

@Entity
public class EntityC {
  
  @Id @GeneratedValue
  @Column(name = "ENTITYC_ID")
  private Long id;
  
  @Column(name = "ENTITYC_NAME")
  private String name;
  
  @OneToOne(mappedBy = "entityC")
  private List<EntityA> entityAs = new ArrayList<>();
  
  ...
  
}


일대일 양방향 대상 테이블 외래키 관리 샘플 코드)

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

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

EntityC entityC = new EntityC();
entityC.setName("entityC_1");

em.persist(entityC);

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

em.persist(entityA);

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

EntityC findEntityC = findEntityA.getEntityC().getEntityAs();

tx.commit();

 

-
다대다 [N:N]
@ManyToMany

: 실무에서 사용 불가
: 연결 테이블이 단순히 연결만 하고 끝나지 않음
ex) 주문시간, 수량 같은 데이터가 들어올 수 있음



+
DB 구조상, 관계형 DB는 정규화된 테이블 2개로 다대다 관계 표현이 불가능
> 연결 테이블을 추가해서, 일대다, 다대일 관계로 풀어내야함


+
객체는 컬렉션을 사용해서, 객체 2개로 다대다 관계 가능

다대다 연관관계의 한계를 극복하기 위해서는, @ManyToMany -> @OntToMany, @MantToOne 형식으로, 연결 테이블용 엔티티를 추가해서, 연결 테이블을 엔티티로 승격시킨다.


하지만, 다시한번 말하지만, 해당 연관관계는 사용이 불가능하다.


* [단방향/양방향]


테이블

: 외래 키 하나로 양쪽 조인 가능
    > 사실 방향이라는 개념이 없음



객체

: 참조용 필드가 있는 쪽으로만 참조 가능
: 한쪽만 참조하면 단방향
: 양쪽이 서로 참조하면 양방향
> 사실상 양방향은 단방향이 두개




* [연관관계의 주인]

: 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
: 객체 양방향 관계는 A -> B, B -> A처럼 참조가 두 곳
    > 둘 중 한곳에서 외래 키를 관리할 곳을 지정해야 함
    > 외래치를 관리하는 참조가 주인이 된다.
※ 주인의 반대편은 외래 키에 영향을 주지 않고, 단순 조회만 가능





-
오늘은 JPA의 다양한 연관관계 매핑에 대해서 알아보았습니다.
그럼 오늘도 즐거운하루 되시길 바라겠습니다.