본문 바로가기
백엔드/스프링

[Spring] JPA 연관관계 및 참조 복습

by AI읽어주는남자 2025. 11. 6.
반응형

JPA 연관관계 및 참조 복습

이 문서는 제공된 수업 자료 및 실습 코드를 바탕으로 JPA의 핵심 개념인 객체 참조, 연관관계 설정(단방향, 양방향), 그리고 주요 어노테이션(@ManyToOne, @OneToMany) 및 속성(cascade, fetch)에 대해 정리합니다.


1. 자바 객체 참조: JPA 연관관계의 기초

JPA의 연관관계는 데이터베이스의 테이블 관계를 자바 객체 세상에서 표현하는 것입니다. 이를 이해하기 위해 먼저 순수 자바 객체 간의 참조 관계를 살펴보겠습니다.

example2/day03/_자바참조 패키지의 예제는 CategoryBoard 객체를 통해 이를 설명합니다.

  • Board.java: 게시물 객체는 자신이 속한 카테고리 객체 하나를 참조합니다.

    // Board.java
    public class Board {
        private int bno;
        private String btitle;
        private String bcontent;
        private Category category; // FK **단방향** 참조
    }
  • Category.java: 카테고리 객체는 자신에게 속한 여러 게시물 객체를 List로 참조할 수 있습니다.

    // Category.java
    public class Category {
        private int cno;
        private String cname;
        @ToString.Exclude // 순환참조 방지
        List<Board> boardList = new ArrayList<>(); // board 여러개를 참조하여 **양방향**
    }
  • Example.java

    • 단방향 참조: 게시물 객체에서 setCategory()를 통해 카테고리 객체를 참조하면, board.getCategory()를 통해 언제든지 자신과 연결된 카테고리 정보를 가져올 수 있습니다.

      // 게시물 객체가 카테고리 객체를 참조
      Board board1 = new Board();
      board1.setCategory(category1); 
      
      // board1을 통해 category1의 이름 조회 가능
      System.out.println(board1.getCategory().getCname()); 
    • 양방향 참조: 반대로 카테고리 객체에서도 자신에게 속한 게시물들을 관리하려면, 카테고리의 boardList에 게시물 객체를 추가해야 합니다.

      // 카테고리 객체의 리스트에 게시물 객체를 추가
      category1.getBoardList().add(board1);
      
      // category1을 통해 자신에게 속한 게시물 목록 조회 가능
      System.out.println(category1.getBoardList());

      ⚠️ 순환 참조 문제: 양방향 참조 시, 각 객체의 toString() 메소드가 서로를 계속 호출하여 StackOverflowError가 발생할 수 있습니다. 이를 방지하기 위해 한쪽의 toString()에서 해당 필드를 제외해야 합니다. (@ToString.Exclude)


2. JPA 연관관계 매핑

JPA에서는 자바 객체 참조를 데이터베이스의 외래 키(FK) 관계로 매핑합니다. 이를 연관관계 매핑이라고 합니다.

2.1. 단방향 연관관계: @ManyToOne

BoardEntityCategoryEntity를 참조하는 것처럼, 다(N) 쪽에서 일(1) 쪽을 참조하는 관계입니다.

  • BoardEntity.java: N:1 관계에서 N에 해당하는 엔티티

    @Entity
    public class BoardEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int bno;
        // ...
    
        // N:1 (다수가 하나에게)
        @ManyToOne 
        @JoinColumn(name = "cno") // DB에 생성될 FK 필드명
        private CategoryEntity categoryEntity;
    }
    • @ManyToOne: 다대일 관계임을 명시합니다. BoardEntity가 '다(Many)', CategoryEntity가 '일(One)'입니다.
    • @JoinColumn(name = "cno"): eboard 테이블에 cno라는 이름의 외래 키 컬럼을 생성하여 ecategory 테이블의 PK와 연결합니다.

2.2. 양방향 연관관계: @OneToMany

단방향 관계에 더해, 일(1) 쪽에서 다(N) 쪽을 참조할 수 있도록 설정하는 것입니다.

  • CategoryEntity.java: 1:N 관계에서 1에 해당하는 엔티티

    @Entity
    public class CategoryEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int cno;
        // ...
    
        // 1:N (하나가 다수에게)
        @OneToMany(mappedBy = "categoryEntity") // '연관관계의 주인'이 아님을 명시
        @ToString.Exclude // 순환참조 방지
        private List<BoardEntity> boardEntityList = new ArrayList<>();
    }
    • @OneToMany(mappedBy = "categoryEntity"): 일대다 관계임을 명시합니다.
    • mappedBy = "categoryEntity": 매우 중요합니다. 이것은 이 관계가 BoardEntity에 있는 categoryEntity 필드에 의해 매핑되었음을 나타냅니다. 즉, CategoryEntity연관관계의 주인이 아니며, 외래 키 관리를 하지 않습니다. 실제 DB 테이블에는 아무런 변화가 없으며, 오직 객체 참조를 위한 필드입니다. 연관관계의 주인은 항상 외래 키를 가지는 쪽(@ManyToOne이 있는 쪽)입니다.

3. 다대다(N:M) 관계 해결: 연결 엔티티 사용

실습3Course, Student, Enroll 예제는 다대다(N:M) 관계를 어떻게 모델링하는지 보여줍니다. 한 학생은 여러 과정을 수강할 수 있고, 한 과정에는 여러 학생이 등록할 수 있습니다.

JPA에서 @ManyToMany를 직접 사용할 수도 있지만, 보통은 중간에 연결 엔티티(Associative Entity)를 두어 두 개의 1:N 관계로 풀어내는 것을 권장합니다. 실습3에서는 EnrollEntity가 이 역할을 합니다.

  • CourseEntity (1)EnrollEntity (N)StudentEntity (1)

  • EnrollEntity.java (연결 엔티티)

    @Entity
    public class EnrollEntity extends BaseTime {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int enrollId;
    
        // 과정(Course)과 N:1 단방향 연결
        @ManyToOne
        @JoinColumn(name = "courseId")
        private CourseEntity courseEntity;
    
        // 학생(Student)과 N:1 단방향 연결
        @ManyToOne
        @JoinColumn(name = "studentId")
        private StudentEntity studentEntity;
    }
    • EnrollEntityCourseEntityStudentEntity에 대한 외래 키를 각각 가집니다.
  • CourseEntity.java / StudentEntity.java

    // CourseEntity.java
    @OneToMany(mappedBy = "courseEntity")
    private List<EnrollEntity> enrollEntities = new ArrayList<>();
    
    // StudentEntity.java
    @OneToMany(mappedBy = "studentEntity")
    private List<EnrollEntity> enrollEntities = new ArrayList<>();
    • CourseEntityStudentEntity는 각각 EnrollEntity와 양방향 관계를 맺어, 자신과 관련된 수강 기록들을 참조할 수 있습니다.

4. 연관관계 주요 속성

4.1. 영속성 전이 (Cascade)

부모 엔티티의 영속성 상태 변화(저장, 수정, 삭제 등)를 자식 엔티티에게도 전파하는 옵션입니다.

  • BoardEntity.java의 예시
    @ManyToOne(cascade = CascadeType.ALL)
    private CategoryEntity categoryEntity;
  • CascadeType 종류
    • ALL: 모든 영속성 상태 변화를 전파합니다. (저장, 수정, 삭제 등)
    • PERSIST: 부모 엔티티를 저장(persist)할 때 자식 엔티티도 함께 저장됩니다.
    • MERGE: 부모 엔티티를 수정(merge)할 때 자식 엔티티도 함께 수정됩니다.
    • REMOVE: 부모 엔티티를 삭제(remove)할 때 자식 엔티티도 함께 삭제됩니다.
    • REFRESH: 부모 엔티티를 새로고침할 때 자식도 새로고침됩니다.
    • DETACH: 부모 엔티티가 영속성 컨텍스트에서 분리되면 자식도 분리됩니다.

4.2. 로딩 전략 (Fetch)

연관된 엔티티를 언제 데이터베이스에서 조회할지 결정하는 전략입니다.

  • BoardEntity.java의 예시
    @ManyToOne(fetch = FetchType.LAZY)
    private CategoryEntity categoryEntity;
  • FetchType 종류
    • EAGER (즉시 로딩): 부모 엔티티를 조회할 때 연관된 자식 엔티티도 즉시 함께 조회합니다. @ManyToOne의 기본값입니다. 불필요한 조회를 유발하여 성능 저하의 원인이 될 수 있습니다.
    • LAZY (지연 로딩): 부모 엔티티를 조회할 때는 연관된 자식 엔티티를 조회하지 않고, 실제 그 자식 엔티티를 사용하는 시점(board.getCategory())에 조회합니다. @OneToMany의 기본값이며, 성능 최적화를 위해 권장됩니다.

5. 공통 필드 상속: @MappedSuperclass

실습3BaseTime 클래스는 여러 엔티티에서 공통으로 사용되는 필드(생성일, 수정일)를 상속하기 위한 클래스입니다.

  • BaseTime.java

    @Getter
    @MappedSuperclass // 엔티티 상속 전용 클래스
    @EntityListeners(AuditingEntityListener.class) // JPA Auditing 기능 활성화
    public class BaseTime {
        @CreatedDate // 생성 시간 자동 기록
        private LocalDateTime createdDate;
    
        @LastModifiedDate // 수정 시간 자동 기록
        private LocalDateTime updatedDate;
    }
    • @MappedSuperclass: 이 클래스는 테이블로 매핑되지 않고, 자식 엔티티에게 필드만 상속해주는 역할을 합니다.
    • @EntityListeners(AuditingEntityListener.class): 엔티티의 변화를 감지하여 createdDate, updatedDate를 자동으로 관리해줍니다. (메인 클래스에 @EnableJpaAuditing 필요)
    • 각 엔티티에서는 extends BaseTime으로 상속받아 사용합니다.
반응형