JPA 다양한 연관관계 Mapping

강정우·2024년 4월 7일
0

JPA

목록 보기
11/12

1. 연관관계 매핑시 고려사항 3가지

1) 다중성

  • 일대일: @OneToOne <=> 일대일
    다대일(가장 많이 사용): @ManyToOne <=> 일대다
    일대다: @OneToMany <=> 다대일
    다대다: @ManyToMany <=> 다대다

여기서 다중성은 DB를 기준으로 다중성을 체크해주면 된다.

참고로 현업에서는 다대다 매핑은 사용하면 안 된다.
why? =>

2) 단방향, 양방향

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

  • 객체
    • 참조용 필드가 있는 쪽으로만 참조 가능
    • 한쪽만 참조하면 단방향
    • 양쪽이 서로 참조하면 양방향 (사실은 단방향이 2개가 서로를 바라보고 있는 개념이다.)

3) 연관관계의 주인

  • 테이블
    외래 키 하나로 두 테이블이 연관관계를 맺음

  • 객체
    • 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데
    • 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키를 관리할 곳을 지정해야함

연관관계의 주인

외래 키를 관리하는 참조가 바로 연관관계의 주인이 됨.
• 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능
혹시 이해가 잘 안 된다면 앞선 포스팅의 예제부분에서 훨신 쉽게 이해할 수 있다.

참고로 이 포스팅에서 나오는 연관관계의 앞 부분을 연관관계의 주인이다고 가정하고 작성하는 것이다.

2. 다양한 연관관계 매핑

1. [N:1]

테이블 관계에서는 당연히 항상 "다" 방향에 외래키가 있는게 맞다.

  • 특징
    가장 많이 사용되는 연관관계이다.
    앞서 언급했듯 외래 키가 있는 쪽이 연관관계의 주인이 된다.
    양쪽을 서로 참조하도록 개발
@Data
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;
    private int age;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
@Data
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

Member 에서는 Team 을 참조해야하기 때문에 "다" 인 부분에 연관관계를 설정해준 모습이고 반대로 Team에서는 굳이 Member를 참조할 필요가 없기 때문에 그냥 본인 테이블의 컬럼만 선언하면 된다.

그리고 만약 양방향을 원한다면 그냥 Team에도 아래 코드처럼 참조하는 코드를 추가하면 되는데 앞서 언급했듯 이때 Table 의 구조는 변하지 않는다.
Table은 원래 양방향이기 때문이다.

@Data
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
}

2. [1:N]

이 모델은 권장하지 않는 모델이다.

일대 다는 "다" 가 아닌 "일" 쪽에서 외래키도 관리하고 뭔가를 해보겠다는 것이다.

그럼 복잡도가 매우 높아지는데 Team의 "List members"가 변경되면Member 테이블의 Team 이 변경되어야한다.

@Data
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;
    private int age;
}
@Data
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    /*@OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();*/

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}

이런식으로 entity 를 설정하고 아래 코드로 query 를 실행시켜보면

public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member = new Member();
            member.setName("member1");

            em.persist(member);

            Team team = new Team();
            team.setName("teamA");
            team.getMembers().add(member);
            
            em.persist(team);
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

결론적으로 불필요한 update query 가 한 번 더 나가게 되는 것이다.

그리고 더 결정적인 이유는 비즈니스 로직상 내가 Team 을 바꿨는데 갑자기 Member 테이블의 쿼리가 나가기 때문에 굉장히 모호하고 추적이 어렵다.
따라서 일대다 로 설계가 나와버린 경우에는 application 레벨에서 조금 손해(굳이 Member->Team 을 바라보는 field 를 추가하는 행위)를 보더라도 양방향으로 만들어두는 것이 더 좋다. 그리고 사용을 다대일 처럼 사용하면 되는 것이다.

  • 특징
    • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
    • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
    • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
    • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)

  • 단점
    • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
    • 연관관계 관리를 위해 추가로 UPDATE SQL 실행

일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

@Data
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    /*@OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();*/

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}
@Data
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;
    private int age;

    // @Column(name = "TEAM_ID")
    // private Long teamId;

    @ManyToOne
    @JoinColumn(name= "TEAM_ID", insertable = false, updatable = false)
    private Team team;
}
  • 일대다 양방향 정리
    • 이런 매핑은 공식적으로 존재X
    • @JoinColumn(insertable=false, updatable=false)
    만약 insertable, updatable 속성을 넣지 않는다면 연관관계 주인이 2개가 되어서 runtimeException 도 안 가고 그냥 순서도 꼬이고 ㄹㅇ 망하는 거다.
    • 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법

3. [1:1]

• 일대일 관계는 그 반대도 일대일
• 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
• 주 테이블에 외래 키
• 대상 테이블에 외래 키
• 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가
• 다대일(@ManyToOne) 단방향 매핑과 유사

public class Locker {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker")
    private Member member;
}
@Data
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

	...

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

그냥 마치 다대일 관계 한 것 처럼 해주면 된다.

주 테이블에 외래 키 양방향

• 다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
• 반대편은 mappedBy 적용

대상 테이블에 외래 키 단방향

• 단방향 관계는 JPA 지원X, 방법 없음.
• 양방향 관계는 지원 (대상 테이블에 외래 키 양방향)

대상 테이블에 외래 키 양방향

• 사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같음
사실 굉장히 어색한 것 임 그냥 Member 에서 관리하던걸 Locker 로 넘기고 그것에 맞춰 객체 연관관계를 다시 설정해준 것일 뿐임.

trade off

그래서 "주 테이블에 외래 키 양방향" 과 "대상 테이블에 외래 키 양방향" 뭘 사용해야 할까?
사실 트레이드 오프라고 표현할 수 있을지 모르겠지만 개발자와 DBA 의 관점 차이가 여기서 갈린다. 실제로 배포중인 DB를 갈아엎은 건 굉장히 위험한 일이라 DB를 기준으로 설계하는 것이 맞겠지만 만약 위와같은 상황에서 비즈니스가 "멤버가 여러개의 락커를 사용가능" 혹은 "하나의 락커에 여러명의 사용자가 사용가능" 이런식으로 어떻게 바뀌나에 따라 이 외래키를 Member 에서 관리하나 Locker 에서 관리하냐 갈릴 수 있다. 그래서 그걸 잘 예측하여 설계를하면 좋은데 사실 개발자 기준에서는 Member 에 해당 키를 갖고있는게 훨씬 유리하다. 멤버만 조회하면 어떤 locker를 사용하는지, locker 가입자 인지 아닌지 등을 판별할 수 있기 때문이다.

특징

  • 주 테이블에 외래 키 (개발자 관점)
    • 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
    • 객체지향 개발자 선호
    • JPA 매핑 편리
    • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
    • 단점: 값이 없으면 외래 키에 null 허용

  • 대상 테이블에 외래 키 (DBA 관점)
    • 대상 테이블에 외래 키가 존재
    • 전통적인 데이터베이스 개발자 선호
    • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
    • 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨 => Member 테이블만으로는 Locker 사용 여부등 조회가 불가능 => Locker 를 조회하는 query 가 반드시 날아감 => proxy 만들 필요가 없음 => 무조건 즉시 로딩이 됨.

4. [N:M]

  • 테이블
    • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
    • 연결 테이블(join table)을 추가해서 일대다, 다대일 관계로 풀어내야함
  • 객체
    • 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능

    이에 따라 ORM 에서는 테이블 같은 형식(중간에 연결 테이블을 놓는 형태)으로 동작함

• @ManyToMany 사용
• @JoinTable로 연결 테이블 지정
• 다대다 매핑: 단방향, 양방향 가능

@Entity
@Data
public class Product {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

	// 양방향을 원한다면 아래 코드 추가
    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
}
@Data
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

	...

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
}

이렇게 하면 바로위 사진 처럼 동작하게 된다.

한계

• 편리해 보이지만 실무에서 사용X
• 현장은 연결 테이블이 단순히 연결만 하고 끝나지 않는다. 분명 주문시간, 수량 같은 데이터가 들어올 수 있는데 이 정보 데이터를 더 추가할 수 없다는 단점이 존재한다.

극복 방안(?)

• 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
• @ManyToMany -> @OneToMany, @ManyToOne 사실 그냥 @ManyToMany 를 안 쓰는 것임 ㅋㅋ

ID(PK) 는 어지간 하면 비즈니스와 관련이 없는 랜덤값으로 넣어주는 것이 좋다.
그래야 비즈니스 변경이 잦은데 그런 영향으로부터 자유로울 수 있다.
그니까 그냥 @GenerateValue 를 애용하자

3. 어노테이션 정리

1. @JoinColumn

• 외래 키를 매핑할 때 사용

속성설명기본 값
name매핑할 외래 키 이름"필드명"+"_"+"참조하는 테이블의 기본 키 컬럼 명"
referencedColumnName외래 키가 참조하는 대상 테이블의 컬럼명참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL)외래 키 제약조건을 직접 지정할 수 있다.
이 속성은 테이블을 생성할 때만 사용한다.
-
unique
nullable insertable
updatable
columnDefinition
table
@Column의 속성과 같다.-

2. @ManyToOne - 주요 속성

• 다대일 관계 매핑

속성설명기본 값
optionalfalse로 설정하면 연관된 엔티티가 항상 있어야한다.TRUE
fetch글로벌 페치 전략을 설정한다.-@ManyToOne=FetchType.EAGER
-@OneToMany=FetchType.LAZY
cascade영속성 전이 기능을 사용한다.-
targetEntity연관된 엔티티의 타입 정보를 설정한다.
이 기능은 거의 사용하지 않는다.
컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.
-

3. @OneToMany - 주요 속성

• 일대다 관계 매핑

속성설명기본 값
mappedBy연관관계의 주인 필드를 선택한다.-
fetch글로벌 페치 전략을 설정한다.-@ManyToOne=FetchType.EAGER
-@OneToMany=FetchType.LAZY
cascade영속성 전이 기능을 사용한다.-
targetEntity연관된 엔티티의 타입 정보를 설정한다.
이 기능은 거의 사용하지 않는다.
컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.
-
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글