[자바 ORM 표준 JPA 프로그래밍] 4주차 스터디

박서영·2026년 3월 30일

5장. 연관관계 매핑 기초

엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 이때 자바의 객체는 참조(주소)를 사용해 관계를 맺고, 테이블은 외래 키를 사용해 관계를 맺는다. 이 둘은 앞에서 나왔듯이 특징이 꽤 다르기에 객체의 참조와 테이블의 외래 키를 매핑하는 것이 ORM(객체 관계 매핑)에서 가장 까다로운 부분이다.

키워드 정리

방향: 단방향양방향이 존재. 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.

다중성: 다대일, 일대다, 일대일, 다대다 다중성이 존재한다.

연관관계의 주인: 객체를 양방향 연관관계로 만들 때에는 연관관계의 주인을 설정해줘야한다.


5.1 단방향 연관관계

  • 회원과 팀
  • 회원은 하나의 팀에만 소속될 수 있음
  • 회원과 팀은 다대일관계
    즉, 하나의 팀에는 여러 회원이 속할 수 있지만, 각 회원은 하나의 팀에 속해야함.

객체와 연관관계

  • 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺음
  • 회원 객체와 팀 객체는 단방향 관계
    멤버 객체에서는 member.getTeam() 과 같이 팀을 알 수 있지만, 반대로 팀에서는 어떤 멤버들이 팀에 들어있는지 확인할 수 없음
  • 객체 그래프 탐색: 객체에서 참조를 이용해 연관관계를 탐색하는 것을 말함.

테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래 키를 통해 팀 테이블과 연관관계를 매음
  • 회원 테이블과 팀 테이블은 양방향 관계
    외래키를 사용해 회원과 팀을 조인할 수도 있고, 그 반대 역시 할 수 있다.
  • 조인: 외래키를 이용해 연관관계를 탐색하는 것을 말함.

객체와 테이블 연관관계의 차이점

객체의 참조를 통한 연관관계는 항상 단방향이다. 즉, 이 연관관계를 양방향으로 만들기 위해서는 양쪽에 필드를 추가해 참조를 보관해야한다. 즉, 양방향 관계가 아니라 서로 다른 단방향 관계 2개라고도 할 수 있다. 이와 다르게 테이블은 외래 키 하나만으로 양방향으로 조인할 수 있다.

객체 관계 매핑

Member에서 회원 엔티티를 매핑하고, Team에서 팀 엔티티를 매핑.

  • 객체 연관관계: 회원 객체의 Member.team 필드 사용
  • 테이블 연관관계: 회원 테이블의 MEMBER_TEAM_ID 외래키 컬럼 사용

여기서 두 관계의 Member.teamMEMBER_TEAM_ID를 매핑하는 것이 중요하다

Member

@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;
    }
}

Team

@Entity
public class Team {
    @Id
    @Column
    private String id;
    
    private String name;
}

@ManyToOne

다대일(N:1) 관계라는 매핑 정보로 연관관계 매핑 시에는 다중성을 나타내는 어노테이션이 필수적이다.

@JoinColumn(name="TEAM_ID")

조인 컬럼은 외래 키를 매핑할 때 사용하며 name 속성에는 매핑할 외래 키 이름을 지정한다. 해당 어노테이션은 생략이 가능하고, 속성은 아래와 같다.

  • name: 매핑할 외래 키 이름
  • referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명
  • unique, nullable, insertable...등

5.2 연관관계 사용

아래는 연관관계를 등록, 수정, 삭제, 조회하는 내용이다.

저장

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);

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

조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지 이다

  • 객체 그래프 탐색
  • 객체 지향 쿼리 사용 (JPQL)

객체 그래프 탐색
member.getTeam()을 사용해 member와 관련된 team 엔티티를 조회할 수 있음.

객체지향 쿼리(JPQL) 사용
JPQL도 조인을 지원하는데 SQL과 문법은 조금 다르다.

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");

for (Member member : resultList) {
	System.out.println("[query] member.username=" 
    	+ member.getUsername());
}

from Member m join m.team t
:회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해 MemberTeam을 조인하였다. 이후 where절을 통해서 조인한 t.name을 검색조건으로 사용해 팀1에만 속한 팀을 검색한 것이다.

:teamName
: :로 시작하는 것은 파라미터를 바인딩받는 문법이다.

수정

Team team2 = new Team("team2", "팀2");
em.persist(team2);

Member member = em.find(Member.class, "member1");
member.setTeam(team2);

수정의 경우에는 다른 메소드가 없기 때문에 엔티티를 조회한 후에 엔티티의 값을 변경해두면 트랜잭션 커밋 시에 플러시가 일어나며 변경 감지 기능이 작동한다.

연관관계 제거

회원1을 팀에 소속하지 않도록 연관관계를 제거

Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); //연관관계 제거

엔티티를 조회한 후 수정하여 연관관계를 제거한다.

연관된 엔티티 삭제

연관된 엔티티를 삭제하기 위해서는 기존의 연관관계를 먼저 제거한 후에 삭제해야한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다.

예를 들면 팀1에 회원1,2가 소속되어있다고 할 때, 팀1을 삭제하기 위해서는 이 둘 사이의 연관관계를 끊어줘야한다.

member1.setTeam(null);
member2.setTeam(null);
em.remove(team);

5.3 양방향 연관관계

위에서는 회원에서 팀으로만 접근하는 다대일 단방향 매핑을 만들었다. 해당 장에서는 반대인 팀에서 회원으로 접근하는 관계를 추가해본다.

회원과 팀이 다대일 관계인데 반해, 팀과 회원은 일대다 관계이다. 여러 연관관계를 맺을 수 있기에 컬렉션을 사용해야한다.
팀1에 여러 명의 회원들이 속해있을 수 있기에, 이 여러 명의 회원들을 관리하기 위해서 리스트/컬렉션을 사용해야한다.

데이터베이스 입장에서는 원래부터 외래 키 하나로 양방향 조회가 가능하기에 추가할 부분이 없다.

양방향 연관관계 매핑

@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>();
}

@OneToMany:

팀과 회원은 일대다 관계이기에, 팀 엔티티에는 컬렉션인 List<Member> members를 추가하고 어노테이션 역시 위의 @OneToMany를 사용한다.

여기서 mappedBy 속성은 양방향 매핑일 경우 사용하는 것으로 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 위에서는 반대의 매핑이 Member.team이므로 team을 준다.

일대다 컬렉션 조회

팀에서 회원 컬렉션(리스트)로 객체 그래프 탐색을 사용해 조회한 회원들을 출력하게된다

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); // 팀->회원 객체 그래프 탐색

for (Member member : members) {
	System.out.println("member.username = "+ member.getUsername());
}

5.4 연관관계의 주인

mappedBy를 보면 회원 엔티티를 매핑할 때에는 사용하지 않은 반면, 팀 엔티티의 매핑에서만 사용하였다. 이 필요성을 해당 장에서 설명한다.

앞서 설명된 객체와 테이블의 연관관계 차이 때문에 JPA에서는 두 객체 연관관계 중 하나를 정해 테이블의 외래키를 관리하기 위한 연관관계의 주인을 정하게된다.

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

양방향 연관관계 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야한다.

  • 연관관계의 주인만 데이터베이스 연관관계와 매핑.
  • 외래키를 관리(등록, 수정, 삭제).
  • 주인이 아닌 쪽은 읽기만 가능하다.

이 주인을 정하기 위해 mappedBy 속성을 사용하게된다.

  • 주인은 mappedBy 속성을 사용하지 않는다
  • 주인이 아니라면, mappedBy를 통해 연관관계 주인을 저장해야함

즉, 연관관계의 주인을 정하는 것 = 외래 키 관리자 설정과도 같다. 앞서서는 Member가 키 관리자이자 연관관계의 주인이다.

연관관계의 주인 = 외래키가 있는 곳

위의 코드에서는 회원 테이블이 외래키를 갖고 있기에 Member.team이 이 연관관계의 주인이 된다.

  • 연관관계 주인이 아닌 Team.members에는 mappedBy = "team" 속성을 사용해 주인이 아님을 설정
  • mappedBy의 값은 연관관계 주인인 엔티티의 필드
  • 연관관계의 주인이 아닌 팀은 외래키를 읽기만 가능하고 변경하지 못함
@OneToMany (mappedBy = "team")
private List<Member> members = new ArrayList<>();

5.5 양방향 연관관계 저장

양방향 연관관계에서 연관관계의 주인이 외래 키를 관리하기에 주인이 아닌 방향은 값을 설정하지 않아도, 데이터베이스에 외래 키 값이 정상 입력된다.

team1.getMembers().add(member1); //무시

즉, 위와 같은 코드가 필요하다고 생각되어도 결국 무시되고, 이 값은 외래 키 값에 영향을 주지 못한다.


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

연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것. 데이터베이스에 외래 키 값이 정상적으로 저장되지 않기에 주의해야한다.

이 경우 외래키에 널값이 입력된다.

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

어차피 연관관계의 주인이 아닌쪽에는 값을 저장해도 데이터베이스에 반영되지 않기에 저장할 필요가 없다. 하지만, 객체 관점에서는 양쪽 방향에 모두 값을 입력하는 것이 가장 안전하다.

...
member1.setTeam(team1); //연관관계: member1 -> team1
team1.getMembers().add(member1); // 연관관계: team1 -> member1

member2.setTeam(team1); //연관관계: member2 -> team1
team1.getMembers().add(member2); //연관관계: team1 -> member2

이렇게 양쪽 모두에 관계를 설정해준 경우에만 후에 필요에 의해 팀에 있는 인원 수를 출력한다거나, 팀에 있는 팀원 목록을 출력할 때 객체로 접근할 수 있다.

전체

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.team의 값이 사용된다.

연관관계 편의 메소드

양방향 연관관계에서는 결국 명시적으로 양쪽에 값을 써주고 관리하는 불편함이 있고, 실수의 여지가 많다.

member.setTeam(team);
team.getMembers().add(member);

양방향 관계에서 두 코드를 하나처럼 사용하는 것이 안전하기에 Member 클래스의 메소드를 수정해 코드를 리팩토링할 수 있다.

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

메소드 하나로 양쪽에 모두 값을 설정하도록 하면 실수나 빼먹을 가능성이 줄어든다.

연관관계 편의 메소드 작성 시 주의사항: 연관관계 제거

앞선 setTeam() 메소드에서는 팀을 변경해주어도 이전 팀의 멤버에서 변경한 팀원을 조회할 수 있다는 문제가 존재한다.

member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); //여전히 멤버1 조회 가능

즉, 팀을 변경할 때 관계를 제거하지 않았다. 연관관계를 변경할 때는 기존 연관관계를 삭제하는 코드를 추가해줘야한다. 따라서 앞의 setTeam()을 아래와 같이 수정해줘야한다.

public void setTeam(Team team) {
	//이전의 연관관계 삭제 (팀이 있었다면 해당 팀의 컬렉션에서 멤버 삭제
	if (this.team != null) {
    	this.team.getMembers().remove(this);
    }
    
    this.team = team;
    team.getMembers().add(this);
}
profile
이불 밖은 위험해.

0개의 댓글