백엔드/SPRING

[JPA 프로그래밍] 4. 연관관계 매핑 기초

-minari- 2025. 2. 3. 21:58

연관관계가 필요한 이유


객체 지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다 
                                                                                                                           - 조영호 (객체지향의 사실과 오해)

 

객체를 테이블에 맞추어 모델링

 

아래와 같은 시나리오를 생각해보자
1. 회원과 팀이 있다

2. 회원은 하나의 팀에만 소속될 수 있다

3. 회원은 하나의 팀에만 소속될 수 있다

 

객체를 테이블에 맞추어 모델링해보자. 다대일 관계에서는 다쪽인 테이블에 외래키가 존재한다. 

 @Entity
  public class Member { 
  
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
    
    @Column(name = "TEAM_ID")
    private Long teamId; 
    … 
  } 
  
  
  @Entity
  public class Team {
  
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name; 
    … 
  }

 

이런식으로 코드를 작성하게 되면 외래키 식별자를 직접 다루게 된다. 

 // 팀 저장
 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());

 

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

  • 테이블 : 외래키로 조인을 사용해 연관된 테이블을 찾음
  • 객체 : 참조를 사용해 연관된 객체를 찾음

 

단방향 연관관계


객체 지향 모델링

객체의 참조와 테이블의 외래 키를 매핑하면 아래와 같이 표현할 수 있다.

@Entity
public class Member { 

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
    
    private int age;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}

 

Member 객체의 Team team과 MEMBER 테이블의 TEAM_ID를 단방향으로 매핑했다. 즉, Member에서 Team으로 조회는 가능하지만, 그 반대인 Team에서 Member로는 불가능하다.

//팀 저장
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);
member.setTeam(teamB);

 

@JoinColumn

@JoinColumn은 외래 키를 매핑할 때 사용한다.

속성 기능 기본값
name 매핑할 외래 키 이름 필드명_참조하는 테이블의 기본 키 칼럼
ex) 필드명(team), 테이블의 컬럼명(TEAM_ID)          => team_TEAM_ID
referenceColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다.
테이블을 생성할 때만 사용한다.
 
unique
nullable
insertable
updatable
columnDefinition
table
@Column 속성과 동일  

 

@ManyToOne

@ManyToOne은 다대일 관계에서 사용된다.

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다 true
fetch 글로벌 페치 전략을 설정한다 - @ManyToOne=FetchType.EAGER
- @OneToMany=FetchType.LAZY
cascade 영속성 전이 기능을 사용한다  
targetEntity 연관된 엔티티의 타입 정보를 설정한다.
이 기능은 거의 사용하지 않는다
컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다
 

 

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


단방향 연관관계에서는 회원에서 팀으로만 접근이 가능했다. 이번에는 반대로 팀에서 회원으로도 접근을 해보자.

 

 

  • 테이블의 연관관계는 외래키 하나로 양방향 연관관계가 맺어진다
  • 객체의 연관관계는 서로 참조할 수 있도록 필드를 각각 추가해야 양방향 연관관계가 맺어진다

팀에서 회원을 조회할 수 있도록 팀 엔티티에 컬렉션인 List members를 추가했다. 

  @Entity
  public class Team {
  
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<Member>();
    … 
  }

 

연관관계의 주인

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

  • 객체 연관관계 = 2개
    - 회원 -> 팀 연관관계 1개 (단방향)
    - 팀 -> 회원 연관관계 1개 (단방향)
  • 테이블 연관관계 = 1개
    - 회원 <-> 팀의 연관관계 1개 (양방향) 

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다. 즉, 엔티티를 양방향으로 설정하면 객체의 참조는 둘인데, 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.

 

연관관계의 주인

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

  • 주인은  mappedBy 속성을 사용하지 않는다
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

그렇다면 Member.team과 Team.members 둘 중 어떤 것을 연관관계의 주인으로 정해야 할까?

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다. 회원 테이블에 있는 TEAM_ID 외래키를 관리할 관리자를 선택해야 한다. Member 엔티티의 team을 주인으로 선택하면 자기 테이블에 있는 외래키를 관리하면 된다. 하지만 Team 엔티티에 있는 members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래키를 관리해야 한다. 따라서 연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 한다.

 

 

Member 테이블에 외래키가 있기 때문에 Member.team이 연관관계의 주인이 된다. 주인이 아닌 Team.members는 mappedBy 속성을 사용해 주인이 아님을 설정한다.

  • mappedBy 속성은 양방향 매핑일 떄 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.

 

주의점

양방향 매핑 시 가장 많이 하는 실수는 연관관계의 주인에 값을 입력하지 않는 것이다. 

연관관계의 주인이 아닌 쪽에 값을 넣으면 연관관계의 주인에 값이 들어가지 않는다.

 

양방향 연관관계를 설정할 때,

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자 > 연관관계 편의 메서드 생성
  • 양방향 매핑 시 무한 루프 조심

 

정리


  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • 단방향 매핑은 잘 하고 양방향 매핑은 필요할 때 추가해도 됨
  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 함