[Spring boot] JPA 연관 관계 정리(단/양방향, 연관 관계 주인, 연관 관계의 종류)
반응형

JPA에서 중요한 것

객체 관계 매핑(ORM)에서 가장 어려운 부분이자 가장 중요한 것을 뽑자면, "객체와 관계형 데이터베이스 테이블 어떻게 매핑할지" 이다. JPA 목적이 객체 지향 프로그래밍과 데이터베이스 사이의 패러다임 불일치를 해결이라는 것과 직접적인 연관이 있기 때문이다.

연관 관계를 매핑할 때 다음 3가지를 잘 고려해야 한다.

방향

  • 단방향 : 회원, 팀 관계에서 회원 -> 팀 또는 팀 -> 회원 한 쪽으로만 참조한다면 단방향
  • 양방향 : 회원 -> 팀, 팀 -> 회원 양쪽에서 서로를 참조하고 있을 때 양방향
    ( 이렇게 서로를 참조하는 양방향 관계는 객체에서만 존재하고, 테이블은 회원 <-> 팀 으로 항상 양방향이다!
    객체에서의 양방향은 즉... 단방향 2개 관계인 것이나 마찬가지)

DB 테이블은 외래 키 하나로 양 쪽 테이블 조인(양방향 관계 : <->)이 가능하지만,

객체에서는 참조용 필드가 있어야 다른 객체를 참조할 수 있기 때문에 사실상 객체에서의 양방향 관계는

<-> 이런 관계가 아니라

단방향 참조를 각각 가진 <- , -> 이러한 관계

인 것이다. 이러한 차이를 객체 지향 프로그래밍과 데이터베이스 사이의 패러다임 불일치를 발생시키고 JPA는 이러한 문제를 해결하기 위해 나왔다. 아직 이해가 잘 안갈 수 있다.. 예시를 통해 살펴보자

 

DataBase 테이블에서의 양방향(<->) 예시와 이해

사실 DB 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다. 외래 키가 두 개의 테이블을 연결해주는 다리와 같은 역할을 한다. 아래 그림 예시에서 보면 TEAM과 MEMBER는 1대 다 관계를 가지고 있고 MEMBER에서 TEAM의 ID를 외래키로 가지고 있다. 

그림만 봤을 때는 외래키를 MEMBER쪽에서 가지고 있는데 어떻게 양쪽에서 조인이 가능한지 이해가 안갈수도 있다.

하지만 SQL문을 보자

// 내가 속한 팀 찾기
SELECT *
FROM TEAM A INNER JOIN MEMBER B
ON A.ID = B.TEMA_ID

// 팀에 속한 멤버 찾기
SELECT *
FROM MEMBER A INNER JOIN TEAM B
ON A.TEAM_ID = B.ID

내가 소속된 팀 이름을 알고 싶다면 TEAM 테이블의 TEAM_ID로 MEMBER테이블에 조인하면 된다. 또한 팀에 소속된 멤버들을 알고싶으면 MEMBER의 TEAM_ID로 TEAM 테이블에 조인하면 된다.

--> 데이터 베이스 테이블은 외래 키 하나로 양방향으로 조회 가능하다.

 

객체에서의 단방향, 양방향(<- , ->) 연관 관계 예시와 이해

Member -> Team 단방향 연관 관계

class Member{
        Team tema1;
}
class Team {}

위 예시는 참조를 통한 연관 관계이다. 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다. Member 클래스에서 Team 클래스의 참조용 필드를 가지고 있기에 참조가능하지만 Team에서는 Member의 참조용 필드가 없기 때문에 Member를 참조할 수 없다. --> 즉 단방향

 

Member <- -> Team 양방향 연관 관계

class Member{
        Team tema1;
}

class Team {
        Member member1;
}

즉, Member -> Team 단항향 참조 하나, Team -> Member 단방향 참조 하나가 서로가 서로를 참조하고 있기 때문에 양방향이 되는 것이다..

테이블에서의 양방향은 동네 골목 처럼 방향 상관 없이 지나다닐 수 있는 도로 1개가 있는 것과 같고, 객체에서의 양방향은 한 방향으로만 가야하는 중앙선이 있는 도로 2개가 같이 있어 양방향이 된것이라고 생각하면 된다.. 비유가 적절한지 모르겠다.

 

연관 관계의 주인

객체를 양방향 연관 관계로 만들면 연관 관계의 주인을 정해야 한다.

 

왜 연관 관계의 주인을 지정해야 할까?

두 객체 (Member, Team)가 있고 양방향 연관 관계를 갖는다고 생각해보자.

여기서 Member의 Team을 다른 Team으로 옮기려고 할 때 방법은 2가지가 있다.

1. Member 객체에서 코드로 치면 setTeam(...)과 같은 메소드를 사용해서 수정

2. Team 객체에서 getMembers( )와 같은 메소드를 사용해서 Member List를 불러와 수정

두 객체 입장에서는 두 방법 다 맞는 방법이다.

하지만 이렇게 객체에서 양방향 연관 관계의 관리 방법이 2개일 때 테이블과의 매핑을 담당하는 JPA 입장에서는 혼란이 생긴다. 외래 키를 어디서 관리해야할지 혼란이 생기는 것이다. 아래 예시를 보면,

객체 연관관계

  • 회원 → 팀 연관관계 1개(단방향)
  • 팀 → 회원 연관관계 1개(단방향)
  • --> 참조가 2개, 외래키를 어디에,,?

테이블 연관관계

  • 회원 ←> 팀 연관관계 1개(양방향)
  • --> 회원 테이블에 외래 키 하나

객체 연관 관계를 단방향으로 매핑했을 때는 참조를 하나만 사용하기 때문에 그냥 이 참조로 외래키를 관리하면 된다.

하지만 양방향 관계로 매핑해버리면 참조가 2곳이 된다. 여기서 JPA는 혼란이 온다.. 외래키를 어디서..? 즉, 참조는 둘인데, 외래키는 하나여서 둘 사이에 차이가 발생한다. (패러다임 불일치..) 어디서 외래키를 관리해야 할까?

이런 차이가 발생하기 때문에 JPA에서는 두 객체 연관 관계 중 하나를 정해 테이블의 외래키를 관리해야 한다. 그래서 외래키를 관리하는 쪽을 연관 관계의 주인이라고 하는 것이다.

연관관계 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 주인이 아닌 쪽은 읽기만 할 수 있다.

-> 즉 연관 관계의 주인은 외래 키가 있는 쪽으로 설정. 위 예시에서는 Member 테이블에서 Team_id를 외래키로 가지고 있기 때문에 Member 엔티티를 주인으로 설정해야 한다!

 

연관 관계의 주인만 제어하면 될까?

데이터베이스에 외래 키가 있는 테이블을 수정하려면 연관 관계의 주인만 변경하는 것이 맞는가? 맞긴하다.

맞긴 하지만, 그것은 데이터베이스만 생각했을 때고, 객체를 생각해보면 사실 둘 다 변경해주는 것이 좋다. (연관 관계의 주인이 아닌 곳에서도 변경!)

왜냐하면 두 참조를 사용하는 순수한 두 객체는 데이터 동기화를 해줘야하기 때문이다. 참조가 두 개이니.. 데이터 변경 시에는 양쪽을 변경해주는 것이 좋다.

 

다중성 - 연관 관계의 종류

데이터베이스를 기준으로 다중성을 결정한다.

  • 다대일(N:1)
  • 일대다(1:N)
  • 일대일(1:1)
  • 다대다(N:N)

예시로 하나씩 살펴보자

다대일(N:1)

위 예시로 이어서.. Member와 Team 관계로 예시를 들어보자.

요구사항이 다음과 같을 때 N:1 관계를 가짐

  • 하나의 Team에는 여러 Member가 있을 수 있다.
  • Member하나는 하나의 Team을 가진다.

Member가 여러 개의 팀에 속할 수 없다는 조건이 있을 경우 TeamN : Member1로 다대일 관계를 가진다.

단방향 코드로 간략하게 표현해보면

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "MEMBER_NAME", nullable = false)
    private String NAME;

    @ManyToOne // 멤버 여러명 : 팀 1
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    //... getter, setter
}

@Entity
public class Board {
    @Id @GeneratedValue
    private Long id;

    @Colum(name = "TEAM_NAME", nullable = false)
    private String name;
    //... getter, setter
}

멤버에서 외래키로 팀을 참조하여 멤버에서 외래 키를 관리하는 형태이다.

 

일대다(1:N)

사실 연관 관계는 대칭성을 가진다.

  • 일대다 ↔ 다대일
  • 일대일 ↔ 일대일
  • 다대다 ↔ 다대다

 

따라서 위에서 보여준 N:1의 예제와 같지만 1:N의 차이는 참조를 1쪽에서하는 것, 즉 외래키 관리는 1쪽에서 한다.

-> 보통 N:1 즉, N 쪽에서 외래키를 관리하는 것이 보통이며 실무에서는 1:N 단방향 연관 관계는 잘 안쓴다고 한다.

그래도 일단 예시니..한번 코드로 보면

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "MEMBER_NAME", nullable = false)
    private String NAME;
    //... getter, setter
}

@Entity
public class Board {
    @Id @GeneratedValue
    private Long id;

    @Column(name = "TEAM_NAME", nullable = false)
    private String name;

    @OneToMany
    @JoinColumn(name="MEMBER_ID")
    private List<MEMBER> members = new ArrayList<>();
    //... getter, setter
}

--> 이러한 관계의 경우 만약 Team 엔티티 안에 있는 Member의 정보를 수정한다면..?

Team 엔티티는 Team 테이블에서 직접 지정할 수 있으나, Member 테이블에 FK가 없기 때문에 조인 + 업데이트 쿼리를 날려 수정해야 한다..

일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑해버리는게 추후에 유지보수에 훨씬 수월하기 때문에 이 방식을 채택하는 것을 추천한다고 한다..

 

일대일(1:1)

1대 1은 말그대로 하나랑 하나가 매칭되는 것이어서 사실 어느쪽에 외래키를 설정하든 문제는 없다.. 그냥 특별할게 없는 관계이다. 예시 생략..

 

다대다(N:N)

실무에서 보통 사용 금지라고 한다.

  • 중간 테이블이 숨겨져 있어 개발자도 모르는 복잡한 조인 쿼리가 발생할 경우가 생길 수 있다.
  • 다대다로 자동 생성된 중간 테이블은 두 객체의 테이블 외래키만 저장되기 때문에 문제가 될 확률이 높다.. JPA를 실행해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가 있는 경우가 많기 때문에 다대다의 경우
    일대다, 다대일로 풀어서(-> 중간 테이블을 엔티티로 만드는 것) 만드는 것이 추후 유지보수에도 좋다.

 

자세한 사용 예시를 제외한 개념만 정리해보았다.. 개념으로 정리했을 때는 헷갈리긴 해도 이해를 할 수 있었지만.. 이것을 실제 코드에서 복잡한 DB관계에 알맞게 매핑하려고 하면 생각보다 JPA사용은 어렵다는 것을 느낄 수 있다.. 프로젝트에 정확하게 적용하기 위해서는 좀 더 명확한 이해와 경험이 필요할 것 같다.

반응형