[ Spring Boot ] JPA 연관 관계 매핑

2025. 8. 26. 00:40·Spring

 

1. 학습 목표


최근 복잡한 테이블 구조를 갖고 있는 사이드 프로젝트를 진행하고 있다.

하지만 JPA 관련 지식이 부족하다보니 Entity 간 관계를 다루는데 어려움을 겪고 있다.

 

따라서 이번에는 JPA의 연관 관계 매핑에 대해서 학습하여 현재 부족한 부분에 대해서 채워볼 예정이다.

 

 

2. 단방향 연관관계 (단방향 매핑)


단방향 연관관계는 참조용 필드를 통해 한 방향으로만 객체 참조가 이루어지는 연관 관계이다.

따라서 엔티티 간 한 방향만 사용하여 참조가 이루어지는 경우 해당 매핑 방식을 사용하면 된다.

2-1. 단방향 다대일 관계


단방향 다대일 관계는 Entity 간 부모 / 자식 관계를 나타내기 위해서 사용하는 일반적인 연관 관계이다.

Entity에서 자식 Entity에 @ManyToOne 어노테이션을 갖는 필드를 선언하여 단방향 다대일 관계를 나타낼 수 있다.

이 때, Hibernate는 자동으로 @ManyToOne 필드에 대해서 FK(Foreign Key) 칼럼을 설정한다

단방향 다대일 관계에서 관계의 주인은 @ManyToOne이 있는 Entity이다.

@Entity
public class User{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long Id;
}

@Entity
public class Post{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long postId
    
    @ManyToOne
    @JoinColumn(name="user_id")
    private Long userId
}

다대일 관계는 주체가 Many에 있기에 여러개의 Entity가 한 개의 다른 Entity와 연관관계를 갖는다.

따라서 위와 같이 연관 객체를 참조용 필드로 갖는다.

 

2-2. 단방향 일대다 관계


단방향 일대다 관계는 부모 엔티티를 하나 이상의 자식 엔티티와 연결시키는 연관 관계이다.

Entity에서 부모 Entity에 @OneToMany 선언하되, 자식 Entity에는 @ManyToOne 필드를 선언하지 않는 방식으로 단방향 일대다 관계를 형성할 수 있다.

단방향 일대다 관계에서 관계의 주인은 @OneToMany가 있는 부모 Entity이다.

[ 단방향 일대다 관계의 링크 테이블 ]

기본적으로 단방향 일대다 관계는 자식 테이블이 @ManyToOne 필드를 갖지 않는다.

따라서 Hibernate는 자식 테이블에 FK 칼럼을 생성해주지 않고, 대신 링크 테이블을 생성하여 연관 관계를 관리한다.

링크 테이블은 관계의 주인에 의해서만 수정 및 생성이 발생한다.

@Entity
public class User{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long userId;
    
    @OneToMany
    private List<Post> posts;
}

@Entity
public class Post{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long postId
}

따라서 위와 같은 Entity 구조에서는 다음과 같이 세 개의 테이블이 생성된다.

  • user ( 사용자 테이블 )
  • post ( 게시판 테이블 )
  • user_post ( 링크 테이블 )

기본적으로 단방향 일대다 관계는 현업에서 사용을 금지하는 경향이 있다.

 

2-3. 단방향 일대일 관계


단방향 일대일 관계는 두 엔티티 간 일대일 매핑이 필요할 때 사용하는 연관 관계이다.

기본적으로 단방향 다대일 관계와 비슷하게 두 엔티티 중 하나의 엔티티에 @OneToOne 필드를 추가하여 생성한다.

이 때, Hibernate는 자동으로 @OneToOne 필드에 대해 FK 칼럼을 생성해준다.

단방향 일대일 관계에서 관계의 주인은 @OneToOne이 있는 Entity이다.

@Entity
public class User{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long userId;
    
    @OneToOne
    @JoinColumn(name="locker_id")
    private Locker locker;
}

@Entity
public class Locker{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long lockerId
}

따라서 위 코드에서는 User가 관계의 주인이 된다.

 

2-4. 단방향 다대다 관계


단방향 다대다 관계는 [ 학생 - 수업 관계 ] 와 같이 두 엔티티 간 n : n의 연관관계가 필요할 때 사용한다.

기본적으로 두 엔티티 중 하나의 엔티티에 @ManyToMany 필드를 추가하여 생성한다.

Hibernate는 자동으로 두 엔티티 간 링크 테이블을 생성해준다.

 

단방향 다대다 관계에서 관계의 주인은 @ManyToMany가 있는 Entity이다.

또한, 링크 테이블은 관계의 주인에 의해서만 수정 및 생성이 발생한다.

@Entity
public class Student {
    @Id @GeneratedValue
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List<Course> courses = new ArrayList<>();
}

@Entity
public class Course {
    @Id @GeneratedValue
    private Long id;
}

위 코드에서는 총 3개의 테이블이 생성된다.

  • student ( 학생 테이블 )
  • course ( 수업 테이블 )
  • student_course ( 링크 테이블 )

기본적으로 다대다 관계는 현업에서 사용을 금지하는 경향이 있다.

중간 테이블을 자동으로 생성하여 관리하기에 예기치 않은 쿼리가 발생할 수 있고, 링크 테이블에 다른 정보를 넣기 어렵기 때문이다.

따라서 주로 직접 링크 테이블을 생성하여 관리하는 방식을 사용한다.

 

3. 양방향 연관관계 매핑


3-1. 앙뱡향 연관관계 생성 규칙


기본적으로 양방향 연관관계를 생성할 때는 아래 규칙들을 따라야 한다

  1. mappedBy 속성을 활용하여 연관관계의 주인을 설정한다.
    • 비주인 Entity가 mappedBy 속성을 갖는다.
  2. 연관관계의 주인이 DB 외래키를 관리한다.
    • 주인 Entity는 연관 관계를 생성 / 수정 / 조회 / 삭제 등 관리가 가능하다.
    • 비주인 Entity는 연관 관계에 대해 조회만 가능하다.
  3. 양쪽 참조를 일관되게 처리한다.
    • 참조가 변경된 경우, 두 Entity의 참조를 동일하게 맞춰야 한다.
    • 주로 헬퍼 메서드를 생성하여 처리한다.

따라서 기본적으로 위 내용을 숙지하고 양방향 연관관계 규칙을 사용하는것이 중요하다.

 

3-2. 양방향 일대다 / 다대일 관계


기본적으로 양방향 다대일 / 일대다 관계는 관계의 주인을 어디에 설정하는지에 따라서 결정된다.

주로 양방향 다대일 관계를 많이 사용하고, 연관관계의 주인을 @ManyToOne이 있는 Entity로 설정한다

 

@Entity
public class User{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long Id;
    
    @OneToMany(mappedBy="user")
    private Set<Post> posts;
    
    public Set<Post> getPosts(){
        return posts;
    }
}

@Entity
public class Post{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long postId
    
    @ManyToOne
    @JoinColumn(name="user_id")
    private User user;
    
    public void changeUser(User user){
        this.user.getPosts().remove(this);
        this.user = user;
        user.getPosts().add(this);
    }
}

위 코드는 전형적인 양방향 다대일 관계의 예시이다.

Post가 연관관계의 주인이므로 User Entity에 mappedBy 속성을 추가하여 비주인으로 설정했다.

양방향 관계에서는 두 엔티티 간 참조를 일관성 있게 관리해야하기에 changeUser를 통해서 이를 가능하게 설정했다.

 

3-3. 양방향 일대일 관계


양방향 일대일 관계는 다른 양방향 매핑과 마찬가지로 mappedBy가 있는 Entity가 비주인, 없는 Entity가 관계의 주인이 된다.

@Entity
public class User{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long userId;
    
    @OneToOne
    @JoinColumn(name="locker_id")
    private Locker locker;
}

@Entity
public class Locker{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long lockerId
    
    @OneToOne(mappedBy="locker")
    private User user;
}

위 코드에서는 @OneToOne이 총 2개가 나타나있다.

하지만 실제 일대일 관계는 두 엔티티 중 관계의 주인만 외래키를 갖기에 실제로는 User만 외래키를 갖는 구조이다.

 

 

3-4. 양방향 다대다 관계


앙뱡향 다대다 관계는 마찬가지로 mappedBy가 있는 Entity가 비주인, 없는 Entity가 관계의 주인 Entity이다.

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long studentId;

    private String name;

    @ManyToMany
    @JoinTable(
        name = "student_course", // 조인 테이블
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List<Course> courses = new ArrayList<>();
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long courseId;

    private String title;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students = new ArrayList<>();
}

마찬가지로 양방향 다대다 관계도 실무에서 잘 사용되지 않는다.

만약 필요하다면 직접 링크 테이블을 생성하여 사용하는 방식이 필요하다.

 

4. 연관관계 비교


4-1. 일대다 vs 다대일


일반적으로 양방향 매핑에서 @OneToMany는 자주 사용된다.

하지만, @OneToMany에서 One이 관계의 주인이 되는 연관 관계 매핑은 거의 사용되지 않는다.

[ 문제 1 : 단방향 일대다 매핑의 문제 ]

기본적으로 단방향 일대다 매핑은 링크 테이블을 생성한다.

하지만 다대다 관계에서와 동일하게 링크 테이블에서 예기치 않은 쿼리가 발생할 수 있기에 사용을 지양한다.

[ 문제 2 : 일대다 매핑에서 더 많은 쿼리가 발생하는 문제 ]

기본적으로 관계를 수정하는 것은 관계의 주인에 의해서 이루어진다.

이 때 일대다 관계에서는 연관 관계 설정 시 더 많은 쿼리가 발생할 수 있다.

  • 일대다 관계 ( @OneToMany )
// one to many
User user = new User(); // query1 : insert user
Post post = new Post(); // query2 : insert post
user.getPosts().add(post); // query3 : update post
  • 다대일 관계 ( @ManyToOne )
// many to one
User user = new User(); // query1 : insert user
Post post = new Post(user); // query2 : insert post

[ 결론 ]

현업에서는 일대다 관계보다는 다대일 관계를 사용해야 한다.

@OneToMany는 양방향 매핑에서 참조 객체에 대한 read-only를 위해서만 사용하는 것이 좋다

 

4-2. 양방향 vs 단방향


개발을 할 때 모든 기능에는 trade-off가 존재한다.

이는 양방향과 단방향 매핑에서도 마찬가지로 단방향을 양방향 매핑으로 확장했을 때 trade-off가 존재한다.

[ 장점 ]

  • 연관 관계의 두 Entity 간 양방향 객체 참조가 간단해짐
  • 양방향 참조를 위한 새로운 메서드를 생성 및 관리 부담이 사라짐

[ 단점 ]

  • 객체 참조 불일치 문제 발생 가능
  • 양방향 편의 메서드 ( 참조 일치 등 ) 관리 부담이 증가함
  • 영속성 컨텍스트 메모리 사용량 증가

[ 결론 ]

프로젝트에서 불필요한 기능은 미래에 대비할 수 있지만, 오히려 관리 부담만 늘어날 수 있다는 단점이 있다.

특히 양방향 참조의 경우, 불필요할 때 사용한다면 오히려 참조 불일치라는 심각한 문제로 이어질 수 있다.

따라서 단방향 매핑을 사용하되, 양방향 매핑이 필수적인 상황에서만 양방향 매핑으로 확장하여 사용하는 것이 좋다.

 

5. Entity 연관관계 변경


5-1. 실제 영속 객체 기반 방식


첫 번째 방식을 JPA 기반 방식이다.

이는 DB에서 실제 Entity를 조회하여 연관 객체의 참조를 바꾸는 방식이다.

[ EntityManager 기반 ]

entityManager의 find 메서드는 데이터베이스에서 실제 Entity를 조회한다.

이를 통해 참조하고 있는 영속 객체를 다른 영속 객체로 변경하는 연관관계 변경이 가능하다.

// 가정 : 단방향 연관관계 매핑

@Service
public PostService{

    @PersistentContext
    private EntityManager em;
    
    @Transactional
    public void changeUser(Long postId, Long userId){
        Post post = em.find(Post.class, postId);
        User user = em.find(User.class, userId);
        post.user = user;
        em.flush();
        em.clear();
    }
}

[ JpaRepository 기반 ]

다음으로 JpaRepository 기반으로 영속 객체를 DB에서 조회하여 연관관계 변경이 가능하다.

// 가정 : 단방향 연관관계 매핑

@Service
public PostService{

    @Autowired
    private PostRepository postRepository;
    
    @Autowired
    private UserRepository userRepository;
    
    @Transactional
    public void changeUser(Long postId, Long userId){
        User user = userRepository.findById(userId).orElse(null);
        Post post = postRepository.findById(postId).orElse(null);
        post.user = user;
    }
}

[ 단점 ]

위 두 가지 방법은 매우 강력한 방식이다.

하지만, n회의 연관관계 변경이 발생하면 user에 대한 n번의 조회와 post에 대한 n번의 수정이 필요하므로 비효율이 발생한다.

따라서 직접 JPQL 쿼리를 작성하거나, 아래와 같은 간접적인 방식을 고려할 수 있다.

5-2. 프록시 객체 기반 방식


[ EntityManager의 getReference 사용 ]

다음은 EntityManager의 getReference 메서드를 활용한 JPA 프록시 객체 기반 방식이다.

프록시 객체는 데이터베이스 조회를 미루는 가짜 엔티티 객체를 조회하는 방식이다.

따라서 프록시 객체를 생성할 때가 아닌, 프록시 객체 내부 필드에 접근할 때 select 쿼리가 발생한다.

 

기본적으로 프록시 객체도 영속 상태이므로 해당 객체를 활용하여 연관관계를 변경할 수 있다.

// 가정 : 단방향 연관관계 매핑

@Service
public PostService{

    @PersistentContext
    private EntityManager em;
    
    @Transactional
    public void changeUser(Long postId, Long userId){
        User user = em.getReference(User.class, userId);
        Post post = em.find(Post.class, postId);
        post.user = user;
        em.flush();
        em.clear();
    }
}

결과적으로 n회의 연관관계 변경에 대해서 user 조회는 없고, post에 대한 n회의 수정 쿼리만 발생하는 강력한 방식이다.

[ 단점 ]

다만 프록시 객체 기반 방식도 n회의 연관관계 변경에서 n회의 수정 쿼리에 대한 DB 접근이 발생한다.

이는 jdbc batch update나 hibernate batch 옵션을 사용해서 DB 접근 횟수를 줄이는 방안을 사용하면 좋다.

 

6. 마무리


이번에는 JPA의 기본적인 관계 매핑에 대해서 알아보았다.

 

이전에는 뜻을 모르고 사용하던 코드들에 대해서 학습하여 각각의 장단점을 알 수 있었고 언제 사용해야하는지 알게 되었다.

 

더 나아가 보다 자원 효율적인 서버 구축을 위한 방안에 대해서 고려할 수 있었다.

 

다음에는 JPA의 영속성 컨텍스트의 심화 내용과 JpaRepository에 대해서 학습해볼 예정이다.

 

'Spring' 카테고리의 다른 글

[ Spring Boot ] 스트리밍 서비스에서 Buffer Pool로 ByteBuffer 관리  (6) 2025.07.29
[ Spring Boot ] 웹소켓 통신에서 메모리 누수 제어  (3) 2025.07.21
[ Spring ] Spring의 디자인 패턴과 아키텍처  (1) 2025.07.14
[Spring Boot] Spring Boot 설정 파일 분리 : submodule  (0) 2025.03.31
'Spring' 카테고리의 다른 글
  • [ Spring Boot ] 스트리밍 서비스에서 Buffer Pool로 ByteBuffer 관리
  • [ Spring Boot ] 웹소켓 통신에서 메모리 누수 제어
  • [ Spring ] Spring의 디자인 패턴과 아키텍처
  • [Spring Boot] Spring Boot 설정 파일 분리 : submodule
코드래곤
코드래곤
코드래곤 님의 블로그 입니다.
  • 코드래곤
    코드래곤 님의 블로그
    코드래곤
  • 전체
    오늘
    어제
    • 분류 전체보기 (61)
      • 알고리즘 (3)
        • 그리디 (1)
        • 그래프 (2)
      • 시스템 설계 (6)
      • CS 및 기본 개념 (17)
      • Docker (5)
      • Spring (23)
        • 백준 서비스 구현하기 (1)
        • 기초 개념 (14)
        • MSA (2)
        • JPA (1)
      • Dart (3)
      • Flutter (1)
      • Kubernetes (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
코드래곤
[ Spring Boot ] JPA 연관 관계 매핑
상단으로

티스토리툴바