JPA - 연관관계 매핑 1(외래키 설정)

 


 

1. 연관관계 매핑

연관관계 매핑은 테이블 간의 참조 관계를 JPA의 어노테이션을 통해 자바 코드에서 표현하는 기능입니다. 이를 통해 객체 지향적으로 연관 관계를 쉽게 관리할 수 있습니다.

 

1.1. @OneToOne

@OneToOne은 두 엔티티 간의 1:1 관계를 매핑할 때 사용하는 어노테이션입니다. 한 엔티티가 다른 엔티티와 고유하게 연결될 때 사용됩니다. 예를 들어, 회원과 회원 상세 정보를 각각의 엔티티로 분리하고 1:1 관계를 설정할 수 있습니다.

 

이때, 외래 키를 명시적으로 설정하려면 @JoinColumn을 함께 사용합니다.

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

    private String name;

    @OneToOne
    @JoinColumn(name = "locker_id") // 외래 키 설정
    private Locker locker;
}

 

1.2. @ManyToOne

@ManyToOne은 다수의 엔티티가 하나의 엔티티와 연관될 때 사용하는 어노테이션입니다. 예를 들어, 여러 주문(Order)이 하나의 사용자(User)와 연관될 수 있는 경우에 사용됩니다.

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

    private String orderName;

    @ManyToOne
    @JoinColumn(name = "user_id") // 외래 키 설정
    private User user;
}

 

1.3. @OneToMany

@OneToMany는 한 엔티티가 여러 엔티티와 연관될 때 사용하는 어노테이션입니다. 예를 들어, 한 사용자가 여러 개의 주문을 가지고 있는 경우에 사용됩니다. ManyToOne의 반대 케이스입니다. 이때 참조하는 객체가 여러개 이므로 주로 컬렉션(List, Set 등)으로 관리합니다.

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

    private String name;

    @OneToMany
    @JoinColumn(name = "user_id") // 외래 키 설정
    private List<Order> orders = new ArrayList<>();
}

 

1.4. @ManyToMany

@ManyToMany는 두 엔티티가 다대다 관계를 가질 때 사용하는 어노테이션입니다. 예를 들어, 학생(Students)과 강의(Courses) 간의 관계는 한 학생이 여러 강의를 들을 수 있고, 하나의 강의에 여러 학생이 등록될 수 있으므로 다대다 관계로 표현됩니다.

  • 중간 테이블(연결 테이블)을 통해 관계를 매핑합니다.
  • 기본적으로 JPA가 중간 테이블을 자동으로 생성하고 관리합니다.
  • 추가 컬럼이 필요한 경우에는 중간 테이블을 엔티티로 따로 정의하는 것이 좋습니다.
@Entity
public class Student {
    @Id @GeneratedValue
    private Long id;

    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
    private Long id;

    private String title;

    @ManyToMany(mappedBy = "courses") // 반대쪽 매핑 설정
    private List<Student> students = new ArrayList<>();
}

 

 

2. 양방향 매핑

 

2.1. mappedBy

 

데이터베이스에서는 테이블 간의 참조 관계를 설정하면, 어느 테이블을 기준으로 조회하든 상대방의 데이터를 확인할 수 있습니다. 그러나 객체에서는 특정 객체가 다른 객체를 참조하지 않으면 관계를 알 수 없습니다.

 

 JPA에서는 이를 해결하기 위해 양방향 매핑을 지원합니다. 양방향 매핑을 사용하면 한 객체에서 다른 객체를 참조할 뿐만 아니라, 반대로 참조되는 객체에서도 관계를 확인할 수 있습니다.

 

 양방향 매핑을 사용할 때, 두 객체 중 하나는 연관 관계의 "주인"이 되고, 다른 하나는 이를 읽기 전용으로 매핑해야 합니다. 일반적으로, 양방향 매핑에서는 외래 키를 관리하는 쪽이 연관 관계의 "주인"이 됩니다.

 

다음의 관계에서 예시를 들겠습니다.

 

유저는 여러개의 주문을 가질수 있습니다. 따라서 User:Order의 관계는 n:1관계입니다.

여기서 Order에 외래키(user_id)가 있으므로 객체 관계에서 Order가 연관관계의 주인이 됩니다.


Order가 연관관계의 주인이 되고 User는 역방향 참조(mappedBy)를 지정합니다.

@Entity
public class Order {
    @Id @GeneratedValue
    private Long orderId;

    @ManyToOne
    @JoinColumn(name = "user_id") // 실제 외래 키 관리
    private User user;
}

@Entity
public class User {
    @Id @GeneratedValue
    private Long userId;

    @OneToMany(mappedBy = "user") // 반대편에서 사용하는 객체의 변수이름으로 지정
    private List<Order> orders = new ArrayList<>();
}

 

2.2. 양방향 매핑 주의사항

 

- 관계를 설정할 때, 양쪽 필드가 서로 참조하도록 동기화해야 합니다.

// user 객체
User user = new User();
user.setName("abc");

// order 객체
Order order = new Order();

// 양쪽 필드 동기화
order.setUser(user); // order -> user
user.getOrders().add(order); // user -> order

 

그 이유는 영속성 컨텍스트의 캐시 기능 때문에 다음과 같은 상황에서 버그가 납니다.

// user 객체
User user = new User();
user.setName("abc");
em.persit(user)

// order 객체
Order order = new Order();
order.setUser(user); // order - user관계 설정
em.persist(order);


Order findOrder = em.find(Order.class, order.getId()); //order객체를 먼저 조회하고

User findUser = findOrder.getUser(); // order객체의 user를 조회하고

List<Order> orders = findUser.getOrders(); // 해당 user가 주문한 모든 주문내역 불러오기(빈칸)

 

 

findUser는 영속성 컨텍스트안에 있는 User를 불러오기 때문에 빈칸인 orders가 반환 됩니다. 따라서 양쪽에서 서로 설정을 해야합니다.

 

 

- 무한 루프 문제

@Override
public String toString() {
    return "User{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", orders=" + orders +
            '}';
}

@Override
public String toString() {
    return "Order{" +
            "id=" + id +
            ", orderName='" + orderName + '\'' +
            ", user=" + user +
            '}';
}

다음과 같이 toString에서 서로가 서로를 참조하는 상황에서 무한루프에 빠져서 StackOverFlowError가 납니다. 따라서 ToString을 작성할 때 참조 필드에 대해 객체 전체가 아닌 식별자나 요약 정보만 출력하도록 설계합니다.

 

2.3. 선호하는 ManyToMany 양방향 방식

 @ManyToMany 관계에서는 중간 테이블을 자동으로 생성하지만, 해당 테이블에 추가적인 속성(예: 등록 날짜, 상태 등)을 필요한 경우, 자동 생성된 중간 테이블만으로는 충분하지 않습니다. 이럴 때, 중간 테이블을 엔티티로 따로 정의하여 필요한 속성을 추가하고 관리하는 것이 더 유리합니다.

 

따라서 위에서 언급한 @ManyToMany 예시를 @OneToMany, @ManyToOne으로 표현하면 다음과 같습니다.

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

    private String name;

    @OneToMany(mappedBy = "student") // 연관 관계 주인은 StudentCourse
    private List<StudentCourse> studentCourses = new ArrayList<>();
}

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

    private String title;

    @OneToMany(mappedBy = "course") // 연관 관계 주인은 StudentCourse
    private List<StudentCourse> studentCourses = new ArrayList<>();
}

@Entity
public class StudentCourse {  // 중간 테이블을 엔티티로 정의
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "student_id")
    private Student student;

    @ManyToOne
    @JoinColumn(name = "course_id")
    private Course course;

    private String enrollmentDate;  // 추가 속성 예: 등록 날짜
}

 

 

3. 공통 속성

 

 

위와 같이 테이블마다 공통 속성이 존재하는 경우가 있습니다. 도메인마다 하나 씩 기술할 수 있지만 JPA에서는 이러한 공통속성을 "@MappedSuperClass"를 사용하여 지원합니다. "@MappedSuperclass"는 공통 속성을 별도의 클래스에서 정의하고, 이를 상속받은 엔티티 클래스들이 해당 속성을 공유할 수 있도록 합니다.

 

@MappedSuperclass
public abstract class BaseEntity {
    private LocalDateTime createDate;
    private LocalDateTime modifiedDate;
}

@Entity
public class Movie extends BaseEntity{
	//코드
}

@Entity
public class Product extends BaseEntity{
	//코드
}

@Entity
public class Student extends BaseEntity{
	//코드
}

 

이때, @MappedSuperclass로 정의된 클래스는 테이블과 매핑되는 엔터티가 아니기 때문에 추상 클래스를 사용하는 것이 권장됩니다. 이렇게 추상 클래스를 사용하면, 해당 클래스를 직접 인스턴스화하거나 테이블에 매핑되는 엔티티로 사용하지 않도록 방지할 수 있습니다.