단방향/양방향 연관관계 매핑

0taetae·2025년 1월 10일
post-thumbnail

객체는 참조(주소)를 사용해서 관계를 맺고 테이블을 외래 키를 사용해서 관계를 맺는다. 이때, 객체 연관 관계와 테이블 연관관계를 매핑하는 일을 알아볼 것이다.

다음은 연관관계 매핑을 이해하기 위한 핵심 키워드이다.

  1. 방향
    • 단방향 : 한 쪽만 참조하는 것
    • 양방향 : 서로 참조하는 것
    • 방향은 객체관계에만 존재하고, 테이블 관계는 항상 양방향이다.
  2. 다중성
    • 다대일, 일대다, 일대일, 다대다 다중성
  3. 연관관계의 owner

다음은 연관관계의 종류이다.

1-1 : 참조키 방식 단방향/양방향, 키 공유 방식 단방향/양방향
N-1 : 단 방향
1-N : 콜렉션 단 방향
N-1/1-N : 양방향
M-N : 단방향/양방향

📙단방향 연관관계

💡객체 연관관계와 테이블 연관관계

  1. 객체 연관관계

    • 참조(주소)단방향 연관관계를 형성한다.
    • 항상 단방향이다.
      객체 간에 연관관계를 양방향으로 만들고자 한다면, 연관관계를 하나 더 만들어야 한다. 이렇게 되면, 서로 다른 단방향 관계 2개라고 볼 수 있다.
    • 참조로 연관된 데이터를 조회한다.
  2. 테이블 연관관계

    • 외래 키로 양방향으로 조인할 수 있다.
    • 외래 키로 연관관계를 형성한다.
    • 조인으로 연관 데이터를 조회한다.

💡객체 관계 매핑

  • @ManyToOne
    • 다대일 관계 매핑
    • 연관관계를 매핑할 때 다중성을 나타내는 어노테이션을 사용해야 한다.
  • @JoinColumn
    • 외래 키를 매핑
    • 생략 가능하다.
  • 예제
    @Entity
    public class Member {
       @Id
       @Column(name = "MEMBER_ID")
       private String id;
    
       private String username;
    
       //연관관계 매핑
       @ManyToOne
       @JoinColumn(name="TEAM_ID")
       private Team team;
    
       //연관관계 설정
       public void setTeam(Team team) {
          this.team = team;
       }
    }
    • 객체 연관관계 : 회원 객체의 Member.team 필드 사용
    • 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 칼럼을 사용

💡@JoinColumn

  • 외래 키를 매핑
  • 속성
    • name : 매핑할 외래 키 이름
    • referencedColumnName : 외래 키가 참조하는 대상 테이블의 칼럼명
    • foreignKen(DDL) : 외래 키 제약조건을 직접 지정. 테이블을 생성할 때만 사용.
    • unique, nullable, insertable, updatable, columnDefinition, table

💡@ManyToOne

  • 다대일 관계에서 사용.
  • 속성
    • optional : false로 설정하면 연관된 엔티티가 항상 있어야 한다.(default : true)
    • fetch : 글로벌 페치 전략 설정
      • @ManyToOne=FetchType.EAGER (default)
      • @OneToMany=FetchType.LAZY (default)
    • cascade : 영속성 전이 기능 사용 설정

📙연관관계 사용

💡저장

public void testSave() {
   // 팀1 저장
   Team team1 = new Team("team1", "팀1");
   em.persist(team1)
   
   // 회원1 저장
   Member member1 = new Memeber("member1", "회원1");
   member1.setTeam(team1);  // 연관관계 설정 member1 -> team1
   em.persist(member1);
   
   // 회원2 저장
   Member member2 = new Member("member2", "회원2");
   member2.setTeam(team1); // 연관관계 설정 member2 -> team1
   em.persist(member2);
  • 회원 엔티티는 팀 엔티티를 참조하고 저장했다.
  • JPA는 참조한 팀의 식별자를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.

💡조회

  1. 객체 그래프 탐색(객체 연관관계를 사용한 조회)

    • member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.
    • 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라 한다.
  2. 객체지향 쿼리 사용

    • 회원을 대상으로 조회하는데 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색 조건으로 사용해야 한다.
      -> SQL은 연관된 테이블을 조인해서 검색조건을 사용하면 된다. JPQL도 조인을 지원한다.

💡수정

  • 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다.
  • 변경사항을 데이터베이스에 자동으로 반영한다.
  • 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.
private static void updateRelation(EntityManager em) {
   // 새로운 팀2
   Team team2 = new Team("team2", "팀2");
   em.persist(team2);
   
   // 회원1에 새로운 팀2 설정
   Member member = em.find(Member.class, "member1");
   member.setTeam(team2);

💡연관관계 제거

private static void deleteRelation(EntityManager em) {
   Member member1 = em.find(Member.class, "member1");
   member1.setTeam(null); // 연관관계 제거 
  • 연관관계를 null로 설정

💡연관된 엔티티 삭제

  • 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
    • 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.
member1.setTeam(null); // 회원1 연관관계 제거
member2.setTeam(null); // 회원2 연관관계 제거
em.remove(team);  // 팀 삭제 

📙양방향 연관관계

  • 객체 연관관계
    • 회원에서 팀은 다대일 관계, 팀에서 회원은 일대다 관계이다.
    • 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.
  • 테이블 연관관계
    • 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다.

💡양방향 연관관계 매핑

  • 매핑한 회원 엔티티
    @Entity
    public class Member {
       @Id
       @Column(name = "MEMBER_ID")
       private String id;
       
       private String username;
       
       @ManyToOne
       @JoinColumn(name="TEAM_ID")
       private Team team;
       
       // 연관관계 설정
       public void setTeam(Team team) {
          this.team = team;
       }
  • 매핑한 팀 엔티티
    @Entity
    public class Team {
       @Id
       @Column(name = "TEAM_ID")
       private String id;
       
       private String name;
       
       @OneToMany(mappedBy = "team")  // 일대다 관계 매핑 
       private List<Member> members = new ArrayList<Member>();
    • mappedBy 속성은 양방향 매핑일 때 사용한다. 이때, 반대쪽 매핑의 필드 이름을 값으로 주면 된다.

💡일대다 컬렉션 조회

  • 팀에서 회원 컬렉션으로 객체 그래프 탐색을 사용해서 조회한 회원들을 출력한다.
    public void biDirection() {
       Team team = em.find(Team.clas, "team1");
       List<Member> members = team.getMembers();
       
       for(Member member : members) {
          System.out.println("member.username = " + member.getUsername());
       }
    }

📙연관관계의 주인

💡mappedBy 속성의 필요성

앞서 언급하였듯이,
객체에는 양방향 연관관계라는 것이 없으며, 두 개의 서로 다른 단방향 연관관계를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 한 것이다.
반면 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
이렇게 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데, 외래 키는 하나이다. 이때 발생하는 차이로 인해 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

✔️mappedBy 속성은 양방향 연관관계에서 외래키를 관리할 객체, 즉 연관관계의 주인을 명시적으로 지정하는 역할을 한다.

💡연관관계의 주인

  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 그리고 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아닌 쪽은 읽기만 할 수 있다. 그리고 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

이렇게 연관관계의 주인을 정하는 것은 외래 키 관리자를 선택하는 것이다.
그러므로 연관관계의 주인은 테이블을 외래 키가 있는 곳으로 정해야 한다.

✔️@ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyTOOne에는 mapppedBy 속성이 없다.

📙양방향 연관관계 저장

다음은 팀1을 저장하고, 회원1과 회원 2에 연관관계의 주인을 통해서 회원과 팀의 연관관계를 설정하고 저장하는 코드이다.

public void test{

   // 팀1 저장 
   Team team1 = new Team("team1", "팀1");
   em.persist(team1);
   
   // 회원 1 저장 
   Member member1 = new Member("member1", "회원1");
   member1.setTeam(team1); // 연관관계 설정 
   em.persist(member1);
   
   // 회원 2 저장 
   Member member2 = new Member("member2", "회원2");
   member2.setTeam(team1); // 연관관계 설정 
   em.persist(member2);
}

✔️이때, 연관관계의 주인은 외래키인 Member.team 필드이다. 엔티티 매니저는 이곳에 입력된 값을 사용해서 외래 키를 관리한다.

📙양방향 연관관계의 주의점

연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 경우 어떠한 문제점이 발생할까❓

  • 외래키 값이 null로 저장된다.
    • 연관관계의 주인만이 외래 키를 관리할 수 있기 때문에, 주인이 아닌 쪽에 값을 입력해도 데이터베이스에 반영되지 않는다.
  • 데이터 불일치가 발생한다.
    • 객체 상에서는 연관관계가 설정된 것처럼 보이지만, 실제 데이터베이스에는 연관관계가 반영되지 않아 객체와 데이터베이스 간의 불일치가 발생한다.

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

ORM은 객체와 데이터베이스 모두 고려해야 한다.
양쪽 모두 관계를 설정하지 않으면 올바른 결과가 아닐 수 있다.
양쪽에 연관관계를 설정하면, 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력된다.
따라서 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주어야 한다.

다음은 데이터베이스와 객체를 고려하여 양쪽 다 관계를 맺은 코드이다.

public void test{
   
   // 팀1 저장
   Team team1 = new Team("team1", "팀1");
   em.persist(team1);
   
   Member member1 = new Member("member1", "회원1");
   
   // 양방향 연관관계 설정
   member1.setTeam(team1); // member1 -> team1
   team1.getMembers().add(member1); // team1 -> member1 
   em.persist(member1);
   
   Member member2 = new Member("member2", "회원2");
   
   // 양방향 연관관계 설정
   member2.setTeam(team1); // member2 -> team1
   team1.getMembers().add(member2);  // team1 -> member2
   em.persist(member2);
}

💡연관관계 편의 메소드

다음은 setTeam() 메소드로 양방향 관계를 모두 설정하는 코드이다.

public class Member{

   private Team team;
   
   public void setTeam(Team team){
      this.team = team;
      team.getMembers().add(this);
   }
}

public void test{
   
   // 팀1 저장
   Team team1 = new Team("team1", "팀1");
   em.persist(team1);
   
   Member member1 = new Member("member1", "회원1");
   member1.setTeam(team1); // 양방향 설정 
   em.persist(member1);
   
   Member member2 = new Member("member2", "회원2");
   member2.setTeam(team1); // 양방향 설정 
   em.persist(member2);
}

이렇게 리팩토링하면 효율적으로 관리할 수 있다.
이와 같이 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.
✔️다만 주의해야 할 점이 있다❗
연관관계를 변경할 때는 기존의 연관관계를 삭제해야 한다.

0개의 댓글