Spring/Spring Data JPA

[자바 ORM 표준 JPA 프로그래밍] 5. 연관관계 매핑 기초

noahkim_ 2023. 12. 27. 19:44

김영한 님의 "자바 ORM 표준 JPA 프로그래밍" 책을 정리한 포스팅 입니다.

 

1. 연관 관계 매핑

구분 설명 방향성 비고
객체 연관 관계 참조를 통해 연관관계 표현
(참조에 의해 객체 그래프 탐색)
단방향
A → B (B는 A를 모름)
객체 그래프 탐색 제한
테이블 연관 관계 외래키를 통해 연관관계 표현
(조인을 통해 연관 테이블 조회)
양방향
A ↔ B (서로 참조)
무한 루프 주의

 

어노테이션

항목 설명 기본값
@JoinColumn 외래키 설정 (조인 시 사용) -
- name 외래키 컬럼 이름 -
- referencedColumnName 외래키가 참조하는 컬럼명 (기본값: 참조하는 엔티티의 기본 키) -
- foreignKey 외래키 제약조건 (테이블 생성 시에만 적용됨) -
@ManyToOne 다대일 관계 설정 -
- optional 관련 엔티티가 존재하는지 여부 (기본: true) TRUE
- fetch 페치 전략 설정 FetchType.EAGER
- cascade 영속성 전이 기능 (연관된 엔티티도 영속성 전이가 가능) CascadeType.ALL
- targetEntity 관련 엔티티 타입 지정 -
@OneToMany 일대다 관계 설정  
- mappedBy 양방향 매핑에서 연관관계 주인 설정 (반대편 매핑 객체의 필드 이름 지정)  

 

예시) @JoinColumn

더보기
@Entity
public class User {
    @ManyToOne
    @JoinColumn(name = "team_id", referencedColumnName = "team_code") 
    private Team team;
}

// name: 외래키 컬럼 이름을 "team_id"로 설정
// referencedColumnName: 외래키가 슈퍼타입 테이블의 "team_code"를 참조
@Entity
public class User {
    @ManyToOne
    @JoinColumn(name = "team_id", foreignKey = @ForeignKey(name = "FK_USER_TEAM"))
    private Team team;
}

// foreignKey: 외래키 제약조건을 명시적으로 이름을 지정

 

예시) @ManyToOne

더보기
@Entity
public class User {
    @ManyToOne(optional = false) // Team이 반드시 존재해야 한다.
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
public class User {
    @ManyToOne(fetch = FetchType.LAZY) // team 정보를 지연 로딩
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
public class User {
    @ManyToOne(fetch = FetchType.EAGER) // team 정보를 즉시 로딩
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
public class User {
    @ManyToOne(cascade = CascadeType.PERSIST) // User 저장 시 연관된 Team도 저장
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
public class User {
    @ManyToOne(targetEntity = Team.class) // 연관된 엔티티 타입을 명시적으로 지정
    @JoinColumn(name = "team_id")
    private Team team;
}

 

예제) @OneToMany

더보기
@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; 
}

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

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

 

2. 양방향 연관관계

연관관계 주인

  • 테이블 기준으로, 외래키 하나로 두 테이블의 연관관계 관리
  • 객체 기준으로, 양방향 연관관계 존재 X
    • 테이블과 마찬가지로 객체에서도 양방향 조회가 가능하도록 함 
    • 단방향 연관관계 2개를 묶어 구현 (두 개의 참조 발생)
    • 외래키를 관리할 주인 설정 필요 (연관관계 주인)
구분 설명
주인 (Owning Side)
외래키 지정 및 관리 (설정, 변경)
주인이 아닌 쪽
읽기만 가능

 

예제

더보기
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "team_id") // 🔸 연관관계의 주인 (외래키)
    private Team team;

    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this); // 편의 메서드 (비주인도 동기화)
    }
}
@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team") // 🔹 연관관계의 비주인 (mappedBy 설정)
    private List<Member> members = new ArrayList<>();
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("noah");

// 연관관계 주인에만 값 설정해야 외래키 제대로 들어감
member.setTeam(team);  // 🔥 여기 설정이 핵심
em.persist(member);

 

주의점

문제 유형 원인 해결 방안
연관관계 누락 시 DB에 null 저장 연관관계의 주인 객체에 값을 설정하지 않음 주인 객체에 반드시 연관관계 설정
연관관계 변경 시 이전 객체에 정보가 남음 연관관계 변경 시, 기존 연관 객체에서 제거 안함 기존 객체에서 remove(this) 수행
무한 루프 (순환 참조) toString(), equals(), hashCode(), JSON 직렬화 시 서로 참조
1. toString()에서 연관 필드 제외
2. @JsonManagedReference / @JsonBackReference 사용

 

예시) 연관관계 누락 시 DB에 null 저장

더보기

문제

Member member = new Member(); // 주인
member.setName("Member1");
em.persist(member);

Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);

 

해결방안 1 - 주인 객체에 반드시 연관관계 설정

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

 

해결방안 2 - 연관관계 편의 메서드

public class Member {
    // ...

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

 

 

예시) 연관관계 변경 시 이전 객체에 정보가 남음

더보기

문제

member1.setTeam(team1);
member1.setTeam(team2);

Member findMember = team1.getMembers().get(0); // 여전히 member1이 조회된다.

 

해결 방안 - 기존 객체의 연관관계 삭제하기

public void setTeam(Team team) {
    if (this.team != null) {
    	this.team.getMembers().remove(this);
    }

    this.team = team;
    team.getMembers().add(this);
}

 

예시) 무한 루프 (순환 참조)

더보기

해결 방안 - toString() 오버라이딩

public class Team {
    private String name;
    private List<Member> members = new ArrayList<>();

    @Override
    public String toString() {
        return "Team{name='" + name + "'}"; // members 리스트 제외
    }
}

public class Member {
    private String name;
    private Team team;

    @Override
    public String toString() {
        return "Member{name='" + name + "'}"; // team 필드 제외
    }
}


해결 방안 2 - JSON Serializing

public class Team {
    private String name;
    
    @JsonManagedReference
    private List<Member> members = new ArrayList<>();
}

public class Member {
    private String name;
    
    @JsonBackReference
    private Team team;
}
  • @JsonManagedReference: 직렬화 시, 포함되는 필드 지정
  • @JsonBackReference: 직렬화 시, 제외되는 필드 지정

 

3. 연관관계 연산

작업 설명 구체적인 방법
저장 참조 객체와 함께 DB에 저장
1. 슈퍼타입: 참조 객체 저장
2. 서브타입: 객체 저장
조회 연관관계를 사용하여 조회 1. 객체 그래프 탐색 ()
2. JPQL 사용 
수정 커밋할 때 변경 감지 기능이 동작
1. 엔티티가 변경되면 변경 감지
2. flush() 호출 시, 변경된 데이터를 DB에 반영
제거 연관관계를 사용하여 삭제
연관된 엔티티를 null로 설정하여 연관 관계 해제 후, 저장하면 삭제됨

 

예제) 조회 

더보기
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
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();

 

예제) 제거

더보기
private void deleteRelation(EntityManager em) {
    Member member = em.find(Member.class, "member1");
    member.setTeam(null);
}