05강. 연관관계 매핑 기초


1. 연관관계가 필요한 이유


객체를 테이블에 맞추어 모델링했을 때의 문제점

: 객체 간 연관관계 없음


  • 참조하지 않고 외래 키를 그대로 사용

      // Member.java
      @Entity
      public class Member {
        
      	@Id @GeneratedValue
      	private Long id;
        
      	@Column(name = "USERNAME")
      	private String name;
        
      	@Column(name = "TEAM_ID")
      	private Long teamId;
      	
      }
        
      // Team.java
      @Entity
      public class Team {
        
      	@Id @GeneratedValue
      	private Long id;
      	private String name;
       
      }
    
      // 생략
      // 문제점 : 참조가 아니고 외래키 값을 그대로 가지고 있음
        
      // 저장
      Team team = new Team();
      team.setName("TeamA");
      em.persist(team); // 영속 상태가 되려면 ID 값 생김
        
      Member member = new Member();
      member.setName("member1");
      // member1을 TeamA에 소속시키자
      member.setTeamId(team.getId()); // ID값 가져오기 : 영속 상태 될 때 생긴 ID 값을 가져옴
      em.persist(member); // 객체면 setTeam이어야 할 것 같은데 ..
        
      tx.commit();
      // 생략
    

    실행 결과

      // 생략 : MEMBER 테이블, TEAM 테이블 CREATE 쿼리
        
      Hibernate: 
          call next value for hibernate_sequence
      Hibernate: 
          call next value for hibernate_sequence
      Hibernate: 
          /* insert hellojpa.Team
              */ insert 
              into
                  Team
                  (name, TEAM_ID) 
              values
                  (?, ?)
      Hibernate: 
          /* insert hellojpa.Member
              */ insert 
              into
                  Member
                  (USERNAME, TEAM_ID, MEMBER_ID) 
              values
                  (?, ?, ?)
    

    DB

    MEMBER

    MEMBER_ID USERNAME TEAM_ID
    2 member1 1

    TEAM

    TEAM_ID NAME
    1 TeamA


💡 ID가 TEAM_ID는 1, MEMBER_ID는 2인 이유?

  • 두 객체가 같은 Sequence 이용
  • 따로 ID 관리하려면 따로 만들고 매핑
  • 외래 키 식별자를 직접 다룸

      //팀 저장
      Team team = new Team();
      team.setName("TeamA");
      em.persist(team);
        
      //회원 저장
      Member member = new Member();
      member.setName("member1");
      member.setTeamId(team.getId());
      em.persist(member);
    
  • 식별자로 다시 조회 : 객체 지향적인 방법이 아님

      Member findMember = em.find(Member.class, member.getId());
      Team findTeam = em.find(Team.class, team.getId());
      // 연관관계가 없기 때문에 계속 다시 쿼리를 보내야 함
    


-> 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다

  • 테이블과 객체의 차이 : 외래키(테이블) vs. 참조(객체)
    • 테이블 : 외래 키로 조인 사용해서 연관된 테이블 찾음
    • 객체 : 참조 사용해서 연관된 객체를 찾음



2. 단방향 연관관계


객체 지향 모델링


  • 객체 연관관계 사용 : Member에서 TeamId가 아니라 Team을 가져오도록 모델링

  • 객체의 참조와 테이블의 외래 키를 매핑
    • @ManyToOne, @OneToMany

        @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 // Member가 Many, Team이 One (Member : Team = N : 1)
            @JoinColumn(name = "TEAM_ID") // Join 해야 하는 Column이 뭔지
        	private Team team;
              
        	// getter, setter..
      

      JpaMain.java

        // 생략
              
        // 저장
        Team team = new Team();
        team.setName("TeamA");
        em.persist(team);
              
        Member member = new Member();
        member.setName("member1");
        // member1을 TeamA에 소속시키자
        member.setTeam(team); // team에서 PK값 꺼내서 알아서 FK로 사용
        em.persist(member); // 객체면 setTeam이어야 할 것 같은데 ..
              
        Member findMember = em.find(Member.class, member.getId());
              
        Team findTeam = findMember.getTeam();
        System.out.println("findTeam = " + findTeam.getName()); // findTeam = TeamA
              
        tx.commit();
        // 생략
      
  • ORM 매핑

  • 연관관계 저장

      //팀 저장
      Team team = new Team();
      team.setName("TeamA");
      em.persist(team);
        
      //회원 저장
      Member member = new Member();
      member.setName("member1");
      member.setTeam(team); //단방향 연관관계 설정, 참조 저장
      em.persist(member);
    
  • 참조로 연관관계 조회 - 객체 그래프 탐색

      //조회
      Member findMember = em.find(Member.class, member.getId());
        
      //참조를 사용해서 연관관계 조회
      Team findTeam = findMember.getTeam();
    
  • 연관관계 수정

      // 새로운 팀B
      Team teamB = new Team();
      teamB.setName("TeamB");
      em.persist(teamB);
        
      // 회원1에 새로운 팀B 설정
      member.setTeam(teamB);
    
    • 이와 같이 수정하면 DB에 연관관계가 업데이트 됨



3. 양방향 연관관계와 연관관계의 주인


양방향 매핑


  • 지금까지 작성한 코드에서 Member -> Team은 가능하지만 Team -> Member는 불가능

    -> 양방향 매핑을 하면 가능해짐

    대상 방법
    테이블 외래키로 Join
    객체 Team -> Member 조회할 수 있도록 Team에 members라는 List 넣어줌
  • 엔티티에 필요한 컬렉션을 추가

    • Member 엔티티는 단방향과 동일, Team 엔티티는 컬렉션 추가

    -> 역방향 조회 가능

      @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;
    
      @Entity
      public class Team {
        
      	@Id @GeneratedValue
      	private Long id;
      	private String name;
        
      	@OneToMany(mappedBy = "team")
      	List<Member> members = new ArrayList<Member>();
      	
       }
    
      //조회
      Team findTeam = em.find(Team.class, team.getId());
      int memberSize = findTeam.getMembers().size(); //역방향 조회
    



연관관계의 주인과 mappedBy


객체와 테이블이 관계를 맺는 차이


대상 설명
객체 - 연관관계 2개 : 회원 -> 팀 연관관계 1개(단방향) / 팀 -> 회원 연관관계 1개(단방향)
-> 양쪽에 참조가 필요함
테이블 - 연관관계 1개 : 회원 <-> 팀의 연관관계 1개(양방향)
-> Foreign Key 하나만 있으면 양쪽 모두 조회 가능


(1) 객체의 양방향 관계


  • 사실 양방향 관계가 아닌 서로 다른 단방향 관계 2개
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 함
    • A -> B (a.getB()) / B -> A (b.getA())
class A {
	B b;
}

class B {
	A a;
}


(2) 테이블의 양방향 관계


  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐 (양쪽으로 Join)
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID



연관관계의 주인(Owner)


  • 참조 두 가지 중 하나로 외래 키를 관리해야 함 -> 연관관계의 주인!

    ex. Team team 또는 List members



양방향 매핑 규칙


  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • ⭐️ 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용X, 주인이 아니면 mappedBy 속성으로 주인 지정



연관관계의 주인 정하기


  • 외래 키가 있는 있는 곳
    • 1:N 관계에서 N쪽(@ManyToOne)

ex. 여기서는 Member.team이 연관관계의 주인

🚨 주의 : 양방향 매핑할 때 가장 많이 하는 실수

  • 연관관계의 주인에 값을 입력하지 않음

     Team team = new Team();
     team.setName("TeamA");
       
     em.persist(team);
     Member member = new Member();
     member.setName("member1");
       
     //역방향(주인이 아닌 방향)만 연관관계 설정
     team.getMembers().add(member);
       
     em.persist(member);
    
    • 양방향 매핑시 연관관계의 주인에 값을 입력해야 함 (순수한 객체 관계를 고려하면 항상 양 쪽 다 값을 입력해야 한다.)

        Team team = new Team();
        team.setName("TeamA");
        em.persist(team);
             
        Member member = new Member();
        member.setName("member1");
             
        team.getMembers().add(member);
             
        //연관관계의 주인에 값 설정
        member.setTeam(team); //**
             
        em.persist(member);
      
    • 값을 입력하지 않은 경우의 문제점

      JpaMain.java

        // 생략
        Member member = new Member();
        member.setName("member1");
        em.persist(member);
             
        Team team = new Team();
        team.setName("TeamA");
        team.getMembers().add(member);
        em.persist(team);
             
        em.flush(); // 1차 캐시가 아닌 DB에서 값을 가져오도록
        em.clear();
             
        tx.commit();
        // 생략
      

      실행 결과

        Hibernate: 
            /* insert hellojpa.Member
                */ insert 
                into
                    Member
                    (USERNAME, TEAM_ID, MEMBER_ID) 
                values
                    (?, ?, ?)
        Hibernate: 
            /* insert hellojpa.Team
                */ insert 
                into
                    Team
                    (name, TEAM_ID) 
                values
                    (?, ?)
      

      DB : TEAM_ID가 null

      • 연관관계의 주인이 Team team이기 때문에, team의 members는 읽기만 가능(mappedBy)

        -> setTeam 해줘야 함

      JpaMain.java

        // 생략
        Team team = new Team();
        team.setName("TeamA");
        em.persist(team);
             
        Member member = new Member();
        member.setName("member1");
        member.setTeam(team);
        em.persist(member);
             
        team.getMembers().add(member);
             
        em.flush();
        em.clear();
             
        tx.commit();
        // 생략
      

      실행 결과

        Hibernate: 
            /* insert hellojpa.Team
                */ insert 
                into
                    Team
                    (name, TEAM_ID) 
                values
                    (?, ?)
        Hibernate: 
            /* insert hellojpa.Member
                */ insert 
                into
                    Member
                    (USERNAME, TEAM_ID, MEMBER_ID) 
                values
                    (?, ?, ?)
      

      DB : TEAM_ID가 입력되어있음

  • 양방향 매핑할 때는 가급적 양쪽에 다 값을 넣어주자 (실수 방지)

    JpaMain.java

     // 생략
       
     Team team = new Team();
     team.setName("TeamA");
     em.persist(team);
       
     Member member = new Member();
     member.setName("member1");
     member.setTeam(team);
     em.persist(member);
       
     //team.getMembers().add(member); // List에 값 세팅 안함
       
     em.flush();
     em.clear();
       
     Team findTeam = em.find(Team.class, team.getId());
     List<Member> members = findTeam.getMembers();
     for (Member m : members) {
            System.out.println("m = " + m.getName());
     }
       
     tx.commit();
     // 생략
    

    실행 결과 : FK를 이용하여 연관된 값을 가져옴

     Hibernate: 
         /* insert hellojpa.Team
             */ insert 
             into
                 Team
                 (name, TEAM_ID) 
             values
                 (?, ?)
     Hibernate: 
         /* insert hellojpa.Member
             */ insert 
             into
                 Member
                 (USERNAME, TEAM_ID, MEMBER_ID) 
             values
                 (?, ?, ?)
     Hibernate: 
         select
             team0_.TEAM_ID as TEAM_ID1_1_0_,
             team0_.name as name2_1_0_ 
         from
             Team team0_ 
         where
             team0_.TEAM_ID=?
     Hibernate: 
         select
             members0_.TEAM_ID as TEAM_ID3_0_0_,
             members0_.MEMBER_ID as MEMBER_I1_0_0_,
             members0_.MEMBER_ID as MEMBER_I1_0_1_,
             members0_.USERNAME as USERNAME2_0_1_,
             members0_.TEAM_ID as TEAM_ID3_0_1_ 
         from
             Member members0_ 
         where
             members0_.TEAM_ID=?
     m = member1
    
    • 그런데 List값 세팅하지 않으면 두 가지 문제가 발생
      1. flush, clear 하지 않는 경우 : 1차 캐시에만 있는 값은 가져오지 못함

        JpaMain.java

         // 생략
                   
         Team team = new Team();
         team.setName("TeamA");
         em.persist(team);
                   
         Member member = new Member();
         member.setName("member1");
         member.setTeam(team);
         em.persist(member);
                   
         //team.getMembers().add(member);
                   
         //em.flush();
         //em.clear();
                   
         Team findTeam = em.find(Team.class, team.getId()); // 1차 캐시에 있음
         List<Member> members = findTeam.getMembers();
                   
         System.out.println("=================");
         for (Member m : members) {
             System.out.println("m = " + m.getName());
         }
         System.out.println("=================");
                   
         tx.commit();
         // 생략
        

        실행결과

         =================
         =================
         Hibernate: 
             /* insert hellojpa.Team
                 */ insert 
                 into
                     Team
                     (name, TEAM_ID) 
                 values
                     (?, ?)
         Hibernate: 
             /* insert hellojpa.Member
                 */ insert 
                 into
                     Member
                     (USERNAME, TEAM_ID, MEMBER_ID) 
                 values
                     (?, ?, ?)
        
  1. tc 작성할 때 : Jpa 사용하지 않고 Java로만 테스트



양방향 연관관계 주의 사항


  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정
  • 연관관계 편의 메소드 생성

    ex. Team 또는 Member에 연관관계 메서드를 생성

    Member.java : Member에 생성한 경우

      @Entity
      public class Member {
        
          @Id @GeneratedValue
          @Column(name = "MEMBER_ID")
          private Long id;
        
          @Column(name = "USERNAME")
          private String name;
        
          @ManyToOne // Member가 Many, Team이 One (Member : Team = N : 1)
          @JoinColumn(name = "TEAM_ID") // Join 해야 하는 Column이 뭔지
          private Team team;
        
          // getter, setter
          public Long getId() {
              return id;
          }
        
          public void setId(Long id) {
              this.id = id;
          }
        
          public String getName() {
              return name;
          }
        
          public void setName(String name) {
              this.name = name;
          }
        
          public Team getTeam() {
              return team;
          }
        
          public void changeTeam(Team team) { // setter를 수정하기보다는 새로 만들기
              this.team = team;
              team.getMembers().add(this);
          }
        
      }
    

    Team.java : Team에 만드는 경우

      public void addMember(Member member) {
      	member.setTeam(this);
      	members.add(member);
      }
    
  • 무한 루프 발생 주의 : toString(), lombok, JSON 생성 라이브러리
    • toString()

      Member.java의 toString()

        @Override
            public String toString() {
                return "Member{" +
                        "id=" + id +
                        ", name='" + name + '\'' +
                        ", team=" + team + // team.toString()을 호출
                        '}';
            }
      

      Team.java의 toString()

        @Override
            public String toString() {
                return "Team{" +
                        "id=" + id +
                        ", name='" + name + '\'' +
                        ", members=" + members + // 각 member의 toString() 모두 호출
                        '}';
            }
      
    • lombok 라이브러리에서 toString 생성시 주의 -> 가능하면 lombok에서 toString 생성 사용하지 않기
    • JSON 생성 라이브러리
      • 컨트롤러에서 엔티티를 직접 response로 보내면 엔티티를 json으로 바꿀 때 계속 또 파고 들어가서 장애 발생
      • 컨트롤러에서 엔티티 반환하지 말고, 가능하면 DTO로 변환하여 반환하기

🚨 컨트롤러에 엔티티 반환하는 경우의 문제점

  1. 무한 루프 발생
  2. 엔티티가 변경될 가능성


4. 실전 예제


🚨 주의 사항

  1. 최대한 단방향으로 작성! 조회의 편의를 위해 필요한 경우 양방향으로
  2. 모든 것을 양방향 매핑하지 않고, 관심사를 적당히 끊어내는 것이 중요 ex. Member에 Orders List 만들기보다는 Order에서 쿼리로 가져오지
  3. mappedBy의 주인은 FK쪽
  4. List는 ArrayList로 초기화(관례)


Leave a comment