JPA 연관관계 매핑 기초 공부의 기록

timothy jeong·2021년 7월 20일
0

Study

목록 보기
7/11

엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 그런데 객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 가진다.(패러다임 불일치) 객체 관계 매핑(ORM)에서 가장 어려운 부분이 바로 객체 연관 관계와 테이블 연관관계를 매핑하는 일이다.

연관관계 매핑을 위해 알아야 하는 기초 개념은 다음과 같다.

■ 방향(Direction): [단방향, 양방향]이 있다. 객체는 하나의 객체가 다른 객체를 참조하는 단방향만 존재한다. 테이블은 외래키를 이용해 양방향 참조가 가능하다.

■ 다중성(Multiplicity): [다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)] 다중성이 있다. 예를 들어 회원과 팀이 관계가 있을 때 여러 회원은 한 팀에 속하므로 회 원과 팀은 다대일 관계다. 반대로 한 팀에 여러 회원이 소속될 수 있으므로 팀과 회원은 일대다 관계다.

■ 연관관계의 주인(owner): 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다

단방향 연관관계

JPA를 이용해서 단방향, 다대일 관계를 매핑해보자.
Member 엔티티는 Team 엔티티를 참조한다.(단방향)
이때 Team은 다수의 Member 를 가질 수 있다. (다대일)

@GeneratedValue(strategy = GenerationType.IDENTITY) 전략을 사용하려면 매핑되는 기본키 값이 Long 형태여야 한다.

@Entity
@Getter @Setter
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    
    //연관관계 매핑
    @ManyToOne // 다대일
    @JoinColumn(name="TEAM_ID") // join에 사용되는 pk
    private Team team;
    
}

@Entity
@Getter @Setter
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

■ @ManyToOne
이름 그대로 다대일(N:1) 관계라는 매핑 정보다. Member와 Team은 다 대일 관계다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.

@ManyToOne 이 있으면 당연히 @OneToMany도 있다.

■ @OneToMany

@OneToMany
private List<Member> members; //제네릭으로 타입 정보를 알 수 있다. 

@OneToMany(targetEntity=Member.class)
private List members; //제네릭이 없으면 타입 정보를 알 수 없다.

■ @JoinColumn(name="TEAM_ID")
조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다. 이 어노테이션은 생략 할 수 있다.

연관관계 사용하기

연관관계 매핑이된 엔티티를 crud 하면서 살펴보자.
아래의 모든 경우에서 동일한 main문이 사용되며, logic함수의 내부 코드만 변경될 것이다.

public class Main {
    static EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("jpabook");

    public static void main(String[] args) {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try {
            tx.begin();
            persistLogic(em);
            findLogic(em);
            updateLogic(em);
            findLogic(em);
            deleteLogic(em);
            findLogic(em);
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

저장

 static void persistLogic(EntityManager em) throws Exception{

        //팀1 저장
        Team team1 = new Team();
        team1.setName("팀1");
        em.persist(team1);

        //회원1 저장
        Member member1 = new Member();
        member1.setUsername("회원1");
        member1.setTeam(team1); //연관관계 설정 member1 -> team1

        //회원2 저장
        Member member2 = new Member();
        member2.setUsername("회원2");
        member2.setTeam(team1); //연관관계 설정 member2 -> team1

        Member member3 = new Member();
        member3.setUsername("회원3");
        member3.setTeam(team1); //연관관계 설정 member2 -> team1


        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
    }

member1.setTeam(team1);
em.persist(member1);

회원 엔티티는 팀 엔티티를 참조하고 저장했다. JPA는 참조한 팀의 식별자 (Team.id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.

조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지다.
■ 객체 그래프 탐색(객체 연관관계를 사용한 조회)
■ 객체지향 쿼리 사용JPQL

static void findLogic(EntityManager em) {
        //객체 그래프 탐색
        Member member = em.find(Member.class, 1L);
        Team team = member.getTeam(); //객체 그래프 탐색
        System.out.println("팀 이름 = " + team.getName());
        
         //JPQL 사용
        String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";

        List<Member> resultList = em.createQuery(jpql, Member.class)
                .setParameter("teamName", "팀1")
                .getResultList();
        for (Member x : resultList) {
            System.out.println("[query] member.username=" +x.getUsername());
        }
    }

객체 그래프 탐색 방법을 더 볼건 없을것 같고, JPQL 문법을조금 살펴보자.

select m 
from Member m 
join m.team t 
where t.name=:teamName

JPQL은 테이블이 아니라 객체를 기준으로 하는 방법이다.

  • select 문은 반환할 엔티티를
  • From 은 검색할 엔티티를 명시한다.
  • join에서는 join에 상용될 FK를 명시한다.
    sql 문으로 치면 JOIN Team T ON MEMBER.TEAM_ID = T.TEAM_ID 이지 않을까?
  • WHERE 절의 teamName과 같이 :로 시작하는 것은 파라미터를 바인딩받는 문법이다. 이 바인딩된 파라미터는 setParameter("teamName", "팀1")를 통해서 파라미터 값이 확정되었다.

수정

딱히 언급할게 없어보인다.

  static void updateLogic(EntityManager em) {

        Team team2 = new Team();
        team2.setName("팀2");
        em.persist(team2); // 영속 상태인 엔티티만 관계를 주입할 수 있다.

        Member member2 = em.find(Member.class, 2L);
        member2.setTeam(team2);
    }

연관관계 삭제

Member 엔티티와 연관된 Team 엔티티를 없애는 방법은 null로 세팅해주는 것이다.

   static void deleteLogic(EntityManager em) {

        Member member1 = em.find(Member.class, 1L);
        member1.setTeam(null);

        // team1을 없애고 싶더라도 바로 없애면 안되고, 
        // 연관된 member 엔티티와의 관계를 모두 없애야 한다.
        String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";

        List<Member> resultList = em.createQuery(jpql, Member.class)
                .setParameter("teamName", "팀1")
                .getResultList();
        for (Member x : resultList) {
            x.setTeam(null);
        }
        em.remove(em.find(Team.class, 1l));
    }

연관된 엔티티 삭제

만약에 Team 엔티티를 없애고 싶다면, 연관된 모든 Member 엔티티에서 관계를 없애야 한다. 그렇지 않으면 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.

양방향 연관 관계

Team에서도 연관된 Member를 조회할 수 있도록 연관관계를 추가하자.
Team과 Member의 관계는 일 대 다의 관계이므로 OneToMany 에노테이션을 매핑되는 엔티티의 리스트에 명시한다.

/*---Team 엔티티 매핑---*/
@Entity
@Getter @Setter
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

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

/*---main문---*/
public class Main {
    static EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("jpabook");

    public static void main(String[] args) {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try {
            tx.begin();
            persistLogic(em);
            biDirection(em);
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
    
    static void persistLogic(EntityManager em) throws Exception{

        //팀1 저장
        Team team1 = new Team();
        team1.setName("팀1");
        em.persist(team1);

        //회원1 저장
        Member member1 = new Member();
        member1.setUsername("회원1");
        member1.setTeam(team1); //연관관계 설정 member1 -> team1

        //회원2 저장
        Member member2 = new Member();
        member2.setUsername("회원2");
        member2.setTeam(team1); //연관관계 설정 member2 -> team1

        Member member3 = new Member();
        member3.setUsername("회원3");
        member3.setTeam(team1); //연관관계 설정 member2 -> team1

        team1.getMembers().add(member1);
        team1.getMembers().add(member2);
        team1.getMembers().add(member3);
        
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
    }
    
    static public void biDirection(EntityManager em) {
        Team team = em.find(Team.class, 1l);
        List<Member> members = team.getMembers(); //(팀 -> 회원)
        //객체 그래프 탐색
        for (Member member : members) {
            System.out.println("member.username = " +  member.getUsername());
        }
    }

연관관계의 주인

@OneToMany 에노테이션에서 mappedBy 라는 속성이 나왔다. 이는 연관관계의 주인을 설정하는 것인데, 이게 무엇이고 왜 필요한지 알아보자.

이전에도 말했지만, 객체는 단방향 연관관계만 가능하기 때문에 단방향 연관관계를 두번 만듦으로써 양방향처럼 보이는 연관관계를 만든다. 하지만 DB에서는 FK를 활용한 양방향 연관관계를 구현하기 때문에 여기서 불일치가 찾아온다.

즉 ,엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따 라서 둘 사이에 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까? 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

양방향 매핑의 규칙: 연관관계의 주인

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다. 즉, 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다. 위의 예시에서는 MEMBER 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 한다.

만약 Member 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 된다. 하지만 Team 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. 왜냐하면 이 경우 Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리해야할 외래 키는 MEMBER 테이블에 있기 때문이다.

아래와 같이 Team 엔티티에 쓰인 부분은 Member 엔티티의 team 변수로 매핑된다는 것을 의미하고, 그러므로 연관관계의 주인은 Member 엔티티가 된다는 것을 의미한다.

    @OneToMany(mappedBy = "team")
    private List<Member> members;

N:1 이든, 1:1 이든 양방향 연관관계는 무조건 연관관계 주인의 설정이 필요하다. N:1의 경우 연관관계의 주인은 항상 N인 쪽이므로, @ManyToOne 에노테이션은 mappedBy 속성이 없다.

양방향 연관관계 저장

우리는 이전부터 Member 엔티티에서 Team 연관관계를 지정했다. 만약
Team 엔티티의 List에 Member를 추가하면 어떻게 될까?
연관관계의 주인은 Member 엔티티이기 때문에 Team 엔티티에서 설정한 연관관계는 무시된다.

    // 이런 연관관계 설정은 무시된다.
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);
    
    // 오로지 연관관계의 주인만 연관관계를 설정할 수 있다. 
    member1.setTeam(team1);
    member2.setTeam(team1);

양방향 연관관계의 주의점

순수한 객체까지 고려한 양방향 연관관계

연관관계의 주인에 연관관계를 주입하는 것이 유효하다고 했다. 하지만 자바 코드의 관점에서 연관관계 주인쪽에만 연관관계를 저장하는 것은 바람직하지 않다. 양쪽 모두 입력하지 않으면 JPA를 사용하지 않는 순수 객체 상태에서는 심각한 문제를 직면할 수 있다.

위의 예시코드 처럼 team에도 연관된 member를 넣어주자.

연관관계 편의 메소드

이렇게 연관된 모든 객체에 getter, setter를 호출하여 연관된 엔티티를 주입해주다보면 실수를 하기 마련이다.
메소드 하나로 축약한다면 훨씬 실수가 줄어들 것이다. lombok에서 지원하는 기본 setter외에 직접 setter를 작성해주자.
이때 순수 자바코드로는 여러 Team의 members 리스트에 동일한 member가 들어갈 수 있으므로, member가 기존에 team이 설정되어 있으면 기존 team에서 member를 삭제하는 코드를 작성해야한다.

@Entity
@Getter @Setter
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;

    //연관관계 매핑
    @ManyToOne // 다대일
    @JoinColumn(name="TEAM_ID") // join에 사용되는 pk
    private Team team;

    public void setTeam(Team team) {

	// 기존 팀이 있었으면 기존 팀에서 member 삭제
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }
        this.team = team;
        team.getMembers().add(this);
    }
}
profile
개발자

0개의 댓글