[JAVA] JPA 연관관계(4)

About JPA Relation

Posted by JongMin-Lee on February 10, 2020

관련된 글

  1. [JAVA] JPA(Java Persistence API)란?(1)
  2. [JAVA] JPA 기본설정 및 객체생성(2)
  3. [JAVA] JPA 어노테이션 및 트랜잭션(3)
  4. [JAVA] JPA 연관관계(4)
  5. [JAVA] JPA 영속성 컨텍스트(5)

이번에는 연관관계에 대해 보겠습니다.
연관관계에는 단방향양방향이 있습니다. 차례대로 살펴 보겠습니다.

1. 단방향 매핑

지금 현재 Member객체와 Club객체가 있습니다.

보기 편하기 위해서 다른 컬럼들은 제외했습니다.

Club에 여러 Member가 가입할 수 있다고 해보겠습니다. 그럼 1:N의 관계가 성립합니다.

다음과 같은 관계가 성립됩니다. 그럼 Code로 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entity
@Getter @Setter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long member_id;

    private String name;
    private int age;

    @Enumerated(value = EnumType.STRING)
    private RoleType roleType;

    @Embedded
    private Friend friends;

    @ManyToOne
    @JoinColumn(name = "club_id")
    private Club club;

    public Member(){}

    Member(String name, int age, Friend friend){
        this.name = name;
        this.age = age;
        this.friends = friend;
    }

}

현재 구분을 편하게 하기 위해서 MemberClub의 id앞에 각자의 클래스 이름을 추가해 놨습니다.
두 테이블을 연결하기 위해 @ManyToOne 어노테이션을 사용했습니다. @JoinColumn으로 ‘club_id’를 지정해서 Club 객체의 id와 조인한다고 설정이 되었습니다. 그러면 테스트를 진행해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);

        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jongmin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();

        // 트랜잭션 시작
        transaction.begin();
        try{
            Club club = new Club("baskball");
            entityManager.persist(club);

            Friend friend = new Friend("park", "01022221111");
            Member member = new Member("jongmin", 30, friend);
            member.setClub(club);
            entityManager.persist(member);

            Member findMember = entityManager.find(Member.class, member.getMember_id());
            Club findClub = findMember.getClub();
            System.out.println("Club Name : " + findClub.getClubName());

            transaction.commit(); // 커밋
        }catch (Exception e){
            transaction.rollback();
        }finally {
            entityManager.close();
        }
        entityManagerFactory.close();
    }

Club을 먼저 만들어서 Member객체에 넣은 후 각각 저장을 실행합니다. 그 후에 find()를 통해서 Member객체를 불러온 후 Member객체 안의 Club를 확인해 보면 저장했던 Club가 나오는 것을 확인할 수 있습니다. Club을 조회하는 어떠한 것도 하지 않았는데 어떻게 조회가 될 수 있었을 까요?
@ManyToOne으로 매핑되어 있었기 때문에 Club를 조회해서 가져온 것입니다. 그 결과 우리는 Member객체만 가져온다면 저장할 때 설정한 Club를 알아서 가져오는 편리한 경험을 하게 됩니다.

Member의 ‘CLUB_ID’에는 Club의 ‘id’가 저장된 것을 확인할 수 있습니다. 저희가 @JoinColumn에 ‘club_id’를 지정했기 때문입니다.
그렇다면 항상 Member를 조회하면 Club를 같이 조회해서 가져오는 것일까요? 그렇지 않습니다. @ManyToOne 어노테이션에는 속성이 존재합니다.

  • FetchType.LAZY : ClubMember를 통해 사용될 때 조회됩니다.
  • FetchType.EAGER : 무조건 같이 가지고 옵니다.(default)

보통은 FetchType.LAZY의 사용을 권장합니다. 그러나 둘이 연관되어 사용하는 일이 많다면 FetchType.EAGER를 사용해도 괜찮다고 생각합니다.

2. 양방향 매핑

양방향 매핑에서는 Member가 여러 Club에 가입할 수 있고, Club또한 여러명의 Member를 가질 수 있다고 가정하겠습니다.
JPA에서의 양방향 매핑은 데이터베이스와는 다른 형태입니다. 데이터베이스의 관계와 다른점을 눈치 채셨나요??
데이터베이스에서 M:N이어도 연결된은 선은 하나입니다. 그러나 위의 사진을 보면 연결되는 선이 2개임을 볼 수 있습니다. 그 이유는 JPA에서의 양방향은 단방향+단방향이기 때문입니다.
좀 더 이해하기 쉽게 하기 위해서 데이터베이스 이론을 공부하다 보면 M:N을 해결하는 방법으로 1:N 과 N:1의 조합으로 바꾸는 걸 생각해 보실수 있습니다.

참고: 위키피디아

저희는 위에서 Member에서 Clob로 가는 매핑은 설정을 했습니다. 그렇다면 이번에는 Club에서 Member로 가는 매핑을 설정해보겠습니다.

데이터베이스와 JPA의 차이점이 여기서 나타납니다.
데이터베이스에서는 한쪽이 연결되면 다른쪽에서도 참조가 가능합니다. 예를들어 Member와 Club이 연결되었다면 서로 JOIN하여 검색이 가능합니다. 그러나 JPA에서는 위의 설정만 가지고 Member에서 Club은 참조가 가능하나 Club에서는 Member를 참조할 수가 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Getter @Setter
public class Club {

    @Id
    @GeneratedValue
    Long club_id;

    String clubName;

    @OneToMany(mappedBy = "club")
    List<Member> memberList = new ArrayList<Member>();

    public Club(){}

    public Club(String clubName){
        this.clubName = clubName;
    }

}

Club Entity에 @OneToMany를 추가하여 Member와 단방향 연결을 하였습니다. 먼저 실습 후 양방향관계에 대해서 더 알아보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);

        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jongmin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();

        // 트랜잭션 시작
        transaction.begin();
        try{
            Club club = new Club("baskball");
            entityManager.persist(club);

            Friend friend = new Friend("park", "01022221111");
            Member member = new Member("jongmin", 30, friend);
            member.setClub(club);
            entityManager.persist(member);

            entityManager.flush();
            entityManager.clear();

            Member findMember = entityManager.find(Member.class, member.getMember_id());
            Club findClub = findMember.getClub();

            List<Member> members = findClub.getMemberList();
            for(Member member1 : members){
                System.out.println("Member Name : " + member1.getName());
            }

            transaction.commit(); // 커밋
        }catch (Exception e){
            transaction.rollback();
        }finally {
            entityManager.close();
        }
        entityManagerFactory.close();

    }

이전의 단방향 코드에서 List<Member> members = findClub.getMemberList();를 통해서 MemberList가 출력되는지 확인 하는 코드가 추가 되었습니다.(flush()와 clear()가 추가 되었는데 이후의 장에서 설명하겠습니다.)
콘솔창을 확인해 보시면 Member Entity에 저장한 ‘jongmin’ 이 출력되는 것을 볼 수 있습니다.
Club 엔터티에 추가 된것을 보면 @OneToManymappedBy 입니다. 양방향 관계에서 이 mappedBy는 정말 중요한 역할을 합니다. JPA에서의 양방향 관계는 주인(Owner) 을 정해줘야 합니다.

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인(Owner) 으로 지정해야 한다.
  • 연관관계의 주인만이 왜래 키를 관리(등록, 수정) 한다.
  • 주인은 ‘mappedBy’ 속성을 사용하지 않는다.
  • 주인이 아니면 ‘mappedBy’ 속성으로 주인(Owner) 를 지정한다.

두 앤터티중 누구를 주인으로 정하는가는 보통 외래키가 있는 엔터티를 주인으로 지정합니다. 그렇지만 단방향으로 설계하는 것이 가장 좋다고 생각합니다. 단방향으로 설계 후 양방향 관계가 필요한 경우에만 사용하는 방식으로 개발하면 좋을 것 같습니다.

주의사항
양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
(순수한 객체 관계를 고려하면 양쪽다 값을 입력해야 한다.)

양방향 매핑의 장점

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료.
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많음.
  • 단방향 매핑을 잘 하고 양방향 매핑은 필요할 때 추가해도 됨(테이블에 영향 X)

이번에는 연관관계에 대해서 알아봤습니다. 마지막으로 Persistence Context(영속성 컨텍스트) 에 대해 알아보겠습니다.

전체 코드는 Github에서 확인해보실수 있습니다.

참고
  • Baeldung (https://www.baeldung.com/hibernate-one-to-many)
  • SKplanet Academy (https://www.youtube.com/channel/UCtV98yyffjUORQRGTuLHomw)