DEV Community

junghwan
junghwan

Posted on

JPA의 @ManyToOne 관계와 조인 전략

JPA의 @ManyToOne 관계와 조인 전략

JPA(Java Persistence API)를 사용하면 데이터베이스 작업을 객체 지향적으로 처리할 수 있다. 그러나 엔티티 간의 관계 설정과 쿼리 방식에 따라 애플리케이션의 성능과 데이터 무결성이 크게 달라질 수 있다.

이 글에서는 @ManyToOne 연관 관계를 중심으로, JPA의 조인 전략과 @JoinColumnnullable 속성이 쿼리에 미치는 영향을 살펴보고, 최적의 설정 방법을 제시한다.

1. 엔티티 관계와 @ManyToOne

다음 예제 코드를 통해 @ManyToOne 관계를 설정하는 방법을 살펴보자.

Team 엔티티

@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "members") // 순환 참조 방지
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team") // 양방향 관계
    private List<Member> members = new ArrayList<>();

    // 양방향 관계 편의 메서드
    public void addMember(Member member) {
        member.setTeam(this);
        members.add(member);
    }
}
Enter fullscreen mode Exit fullscreen mode

Member 엔티티

@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "team") // 순환 참조 방지
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String username;

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    @JoinColumn(name = "TEAM_ID", nullable = false) // NULL 허용 여부 설정
    private Team team;
}
Enter fullscreen mode Exit fullscreen mode

Member 엔티티는 Team 엔티티와 @ManyToOne 관계를 가진다. @JoinColumn(name = "TEAM_ID")을 통해 Member 테이블에 외래 키(Foreign Key) 컬럼을 생성한다.

2. fetch 속성: 즉시 로딩 vs. 지연 로딩

@ManyToOne 어노테이션의 fetch 속성은 연관된 엔티티를 언제 가져올지를 결정한다.

fetch 속성 설명 장점 단점
EAGER Member 조회 시 Team도 즉시 조회 한 번의 쿼리로 모두 조회 불필요한 데이터 로딩 가능
LAZY Member 조회 시 Team은 필요할 때 조회 성능 최적화 가능 추가 쿼리 발생 가능 (N+1 문제)

N+1 문제를 방지하려면 JOIN FETCH (JPQL) 또는 @EntityGraph를 활용하는 것이 좋다.

3. nullable 속성: NULL 허용 여부와 조인 전략

@JoinColumnnullable 속성은 외래 키 컬럼의 NULL 허용 여부를 설정하며, JPA가 생성하는 조인 쿼리의 종류에 영향을 준다.

nullable 속성 설명 조인 방식
true (기본값) 외래 키 컬럼이 NULL을 허용 LEFT OUTER JOIN
false 외래 키 컬럼이 NULL을 허용하지 않음 INNER JOIN

NULL 허용 여부에 따른 조인 방식 변화

  • nullable = trueLEFT OUTER JOIN 사용 (연결된 Team이 없어도 Member 조회 가능)
  • nullable = falseINNER JOIN 사용 (연결된 Team이 없는 Member는 조회되지 않음)

INNER JOIN이 LEFT OUTER JOIN보다 성능상 유리한 경우가 많다.

4. 실험: fetch와 nullable 조합에 따른 쿼리 비교

EAGER + nullable = true (LEFT OUTER JOIN)

SELECT m.MEMBER_ID, t.TEAM_ID, t.name, m.username
FROM Member m
LEFT JOIN Team t ON t.TEAM_ID = m.TEAM_ID
WHERE m.MEMBER_ID = ?;
Enter fullscreen mode Exit fullscreen mode

EAGER + nullable = false (INNER JOIN)

SELECT m.MEMBER_ID, m.TEAM_ID, t.TEAM_ID, t.name, m.username
FROM Member m
JOIN Team t ON t.TEAM_ID = m.TEAM_ID
WHERE m.MEMBER_ID = ?;
Enter fullscreen mode Exit fullscreen mode

LAZY (JOIN 없음, 추가 쿼리 발생)

-- Member 조회 (Team 조인 없음)
SELECT m.MEMBER_ID, m.TEAM_ID, m.username
FROM Member m
WHERE m.MEMBER_ID = ?;

-- findMember.getTeam().getName() 호출 시 추가 쿼리 실행
SELECT t.TEAM_ID, t.name
FROM Team t
WHERE t.TEAM_ID = ?;
Enter fullscreen mode Exit fullscreen mode

em.getReference(Team.class, id)를 사용하면 프록시 객체가 반환된다.

5. 최적의 설정 선택 가이드

✅ 데이터 무결성을 유지하려면:

  • nullable = false (모든 Member는 반드시 Team에 속해야 함)
  • INNER JOIN을 통해 성능 향상 가능

✅ 성능 최적화를 원한다면:

  • FetchType.LAZY 사용 (불필요한 데이터 로딩 방지)
  • 필요 시 JOIN FETCH(JPQL) 또는 @EntityGraph 활용

✅ 유연한 데이터 모델이 필요하다면:

  • nullable = true (소속되지 않은 Member도 허용)
  • LEFT OUTER JOIN을 사용하여 데이터 손실 방지

✅ ID만 필요할 경우:

  • em.getReference() 활용 (불필요한 데이터 조회 방지)

결론

JPA의 조인 전략과 NULL 제약 조건을 적절히 활용하면 애플리케이션의 성능과 데이터 무결성을 동시에 확보할 수 있다.

프로젝트에서 요구하는 데이터 모델과 성능 요건을 고려하여 최적의 설정을 선택하는 것이 중요하다.

Top comments (0)