SPRING

JAVA/SPRING JPA 연관관계 매핑 (@OneToOne, @OneToMany, @ManyToMany)

6uiw 2025. 2. 26. 14:45

 

🔸Intro🔸

프로젝트를 진행할 때, 코드 구현에 앞서 ERD를 설계하고 테이블 간의 관계 매핑을 하다보면, 굉장히 복잡해지는 경우가 많습니다.

이를 참고하여 코드를 구현할 때, JPA를 이용하여 엔티티를 설계하려면 JPA의 연관관계 매핑 개념과 어노테이션들의 의미를 잘 알아두어야 합니다. 

따라서 오늘은 JPA(Java Persistence API)의 연관관계 매핑에 대한 기본적인 개념과 사용법을 알아보겠습니다.

 

 

 

자동목차




📌연관관계 매핑 종류

  • @OneToOne 관계
  • @OneToMany 관계
  • @ManyToOne 관계
  • @ManyToMany관계

 

 

 

 

 

 



📌 @OneToOne 관계

한 엔티티가 다른 엔티티와 1대1로 연결되는 경우

 

 

 

🔎예시

예) 한 사람이 하나의 주민등록증을 가질 수 있다(1명의 Person → 1개의 IDCard).

 

이를 엔티티로 나타내면 다음과 같다. 

@Entity
public class Person { //'사람'을 나타내는 엔티티 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long personId; //기본 키
    private String name;

    @OneToOne
    @JoinColumn(name = "idCardId") //외래 키 이름 지정해주기, IDCard 테이블의 idCardId를 참조한다는 의미(참조하는 테이블의 기본키와 같게 정해준다.) 
    private IDCard idCard; //관례적으로 참조하는 엔티티 이름과 비슷하게 짓는다. 
}




@Entity
public class IDCard { //'주민등록증'을 나타내는 엔티티 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idCardId; // 기본 키
    private String number;
    
    /**
    * 양뱡항 참조시 
    * @OneToOne(mappedBy = "idCard") //주인 엔티티(연관관계 주인)에서 반대쪽 엔티티를 참조하는 필드 이름
    * private Person person;
    */
}
🔹참고) 외래 키 위치: 1:1 관계에서는 두 테이블 중 어느 쪽에 두어도 상관없다.

 

 

 

이를 DB 테이블로 나타내보면 다음과 같다. 

 

<Person>

personId (PK) name  idCardId (FK)
1 "홍길동" 1

 

<IDCard>

idCardId (PK) number
1 "12345678"

 

 

이제 코드에서 의미하는 어노테이션들을 자세히 알아보자.  

 

 

 

 

 

 

 

🔎@JoinColumn

  • 엔티티 간 관계를 매핑할 때 데이터베이스 테이블의 외래 키(Foreign Key)를 정의하거나 설정하는 데 사용되는 어노테이션
  • 연관관계의 주인 엔티티에 사용

 

 

🔹@JoinColumn의 주요 속성들  

속성 설명
name(필수 속성) 🔹 테이블에서 외래 키 컬럼의 이름 지정
🔸예: @JoinColumn(name = "idCard")는 외래 키 컬럼 이름을 idCard으로 설정
referencedColumnName 🔹 외래 키가 참조하는 대상 테이블의 기본 키 컬럼 이름을 지정
🔹 외래 키 이름(name)과 참조하는 기본 키 이름이 다를 때 사용
🔸 예: @JoinColumn(name = "idCard") 라고 했는데, 참조하는 엔티티의 기본키 이름이 “id”이면 @JoinColumn(name = "idCard", referencedColumnName =”id”) 라고 설정해주어야 한다.
nullable 🔹  외래 키 컬럼이 NULL 값을 허용할지 여부를 지정 (기본값은 true)
🔹  nullable = false로 설정하면 외래 키가 반드시 값이 있어야 한다.(즉, NOT NULL 제약)
unique 🔹  외래 키 컬럼에 UNIQUE 제약을 추가할지 여부를 지정(기본값은 false)
🔹  1:1 관계에서는 unique = true로 설정해 1대1 관계를 보장
insertable, updatable 🔹 외래 키 컬럼에 INSERT나 UPDATE 작업을 허용할지 여부를 지정 (기본값은 true)
🔹 특정 상황(예: 읽기 전용 외래 키)에서 false로 설정할 수 있다.

 

 

 

 

 

🔎 MappedBy

  • 연관관계의 주인을 지정하는 역할을 하며, 반대쪽 엔티티에서 사용된다. (주인:@JoinColumn, 반대쪽: mappedBy)
  • JPA가 데이터베이스 외래 키를 어느 엔티티가 관리할지, 즉 연관관계의 주인을 지정하는 데 사용 mappedBy가 있는 쪽은 외래키를 직접 관리하지 않고 주인 엔티티의 설정을 따라간다.
  • mappedBy=”주인 엔티티에서 반대쪽 엔티티를 참조하는 필드 이름”
🔹주인 엔티티 : @JoinColumn사용, 외래키 관리
🔹 반대 엔티티 : @OneToOne(mappedBy=””) (양방향 관계일때 사용)

 

관계 설정을 잘못해줬을 경우 org.hibernate.AnnotationException: mappedBy reference an unknown target entity property 와 같은 에러가 발생할 수 있다.

 

💡 그렇다면 OneToOne 관계에서 어느 쪽을 주인으로 둬야 할까?

      🔹데이터 접근 패턴이나 주로 사용하는 쪽을 고려하여 외래 키를 두는 것이 좋다.

 



 

 

 

 

🔎단방향 vs 양방향

 

💡단방향이란?

 한쪽 엔티티만 다른 엔티티를 참고하는 구조를 뜻한다.

 

 예를 들어 Person이 IDCard를 참조하지만, IDCard는 Person을 참조하지 않을 때

 즉, 사용자를 가지고 주민번호는 검색하지만, 주민번호를 가지고 사용자를 찾진 않을 때 이를 '단방향' 매핑이라고 한다.

 

🔹단방향 매핑의 특징

  • 코드가 단순하고, 불필요한 관계를 줄일 수 있다.
  • 쿼리나 데이터 접근이 한 방향으로만 필요할 때 유리하다.
  • 엔티티 간 관계를 최소화해 유지보수가 쉽다.
  • 참조되는 엔티티에는 관계를 나타내는 정보가 없다. (@OneToOne과 같은 어노테이션을 쓰지 않는다.)

💡양방향이란?

 두 엔티티가 서로를 참조하는 구조를 뜻한다.

 

 예를 들어, Person이 IDCard를 참조하고, IDCard도 Person을 참조하는 경우를 의미한다. (사람으로 주민번호도 알고 싶고,   주민등록증을 통해 소유자를 찾고 싶다.)

 

🔹양방향 매핑의 특징

  • 양쪽에서 데이터를 접근하거나, 객체 그래프를 탐색할 때 편리하다.
  • 하지만 데이터 일관성을 유지하려면 양쪽 객체를 모두 업데이트해야 해서 복잡성이 늘어날 수 있다.
    • 예: Person에서 IDCard를 추가/삭제할 때, 양방향이라면 IDCard의 Person 참조도 업데이트해야 한다. 실수로 한쪽만 수정하면 데이터 불일치가 발생할 수 있다.
  • 지연 로딩(FetchType.LAZY)을 잘 설정하지 않으면 N+1 문제(불필요한 쿼리가 여러 번 실행됨)가 발생할 수 있다.
  • 양방항 참조시, 주인 엔티티의 반대되는 엔티티에 다음을 추가해야 한다.
@OneToOne(mappedBy = "주인 엔티티의 필드 이름") 
private [주인 엔티티] [변수명];

 




 

 

 

 

 

📌 @OneToMany / @ManyToOne (1:N / N:1 관계)

  • 한 엔티티가 여러 엔티티와 연결되거나, 여러 엔티티가 한 엔티티와 연결되는 경우
  • @OneToMany (현재 엔티티(1)가 다른 여러 엔티티(N)와 연결됨)

 

 

 

🔎특징

  • "N"쪽이 주인이다. (@ManyToOne 을 사용한다.) → 따라서 참조 엔티티엔 @OneToMany를 사용한다. 
  • 보통 “N”쪽에서 외래키를 가진다 (∵ 주인이므로) 
  • 기본적으로 지연 로딩을 갖는다. (FetchType.LAZY)
  • 예: 한 사람이 여러 주문을 할 수 있다(1명의 Person → 여러 Order)

 

 

 

 

 

 

🔎예시 (양방향)

 

한 사람이 여러 개의 주문을 한다.

Person(사람)에서 Order(주문)을 참조할 수도 있고, Order(주문)에서 Person(사람)을 참조할 수도 있다. 

@Entity
public class Person {
		@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long personId; // 기본 키

    private String name;
		//Person(1) : Order(N) = 한 명의 사람이 여러개의 주문 
    @OneToMany(mappedBy = "person") // Order 엔티티의 person 필드가 연관관계 주인
    private List<Order> orders = new ArrayList<>();
}




@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderId; // 기본 키

    private String orderName;

    @ManyToOne // 여러 Order가 한 Person에 연결
    @JoinColumn(name = "personId") // 외래 키, Person 테이블의 personId를 참조
    private Person person;
}

 

코드를 살펴보면

  1. 외래키가 Order 엔티티에 있으므로 “Order”가 연관관계의 주인이 된다. (@JoinColumn가 사용 됨)
  2. Order 엔티티에 personId로 외래키를 생성하고, Person엔티티의 기본키인 porsonId를 참조한다.
  3. 참조 엔티티인 Person에는 @OneToMany(mapped by = "") 를 사용한다. 

 

 

위의 코드를 DB 테이블로 나타내보면 다음과 같다.

 

<Person>

personId (PK) name
1 "홍길동"

 

<Order>

orderId (PK) orderName personId (FK)
1 "책 구매" 1
2 "옷 구매" 1

주인인 Order 테이블에 외래키 존재. 

 

 

🔎예시 (단방향)

1:N 관계에서 한 엔티티(1쪽)가 여러 엔티티(N쪽)를 참조하지만, N쪽 엔티티는 1쪽 엔티티를 참조하지 않는 관계

외래 키는 N쪽 테이블에 위치하며, 1쪽 엔티티에서만 N쪽 엔티티를 조회할 수 있다.

 

위의 양방향 매핑에선 참조테이블에 @OneToMany(mapped by = "") 를 입력해주었지만, 단방향 관계에선

mapped by를 입력하지 않아도 된다. 

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

    private String name;

    @OneToMany
    private List<Order> orders = new ArrayList<>();
}




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

    private String orderName;

    @ManyToOne
    @JoinColumn(name = "personId", nullable = false) // 외래 키가 Order 테이블에
    private Person person;
}

 

Person에서만 orders를 조회할 수 있으며, Order에서 Person을 조회하려면 별도의 쿼리나 로직이 필요하다.

💡1:N관계에선 N이 관계의 주인이므로 1:N 단방향 관계에서는 일반적으로 N이 1을 참조하는 방향만 존재한다.

 

 

 

위의 코드를 DB 테이블로 나타내면 다음과 같다.

personId (PK) name
1 "홍길동"
orderId (PK) orderName personId (FK)
1 "책 구매" 1
2 "옷 구매" 1



 

 

 

 

 

 

 

📌 @ManyToMany (N:N 관계)

여러 엔티티가 여러 다른 엔티티와 연결되는 경우



🔎특징

  • 반드시 중간 테이블 필요 (@JoinTable) ← 주인 엔티티에 작성해준다.
  • 양방향 또는 단방향으로 설정할 수 있지만, 보통 양방향으로 많이 사용된다.
  • 양방향 사용시 mappedBy를 사용해 연관관계 주인을 지정한다.
  • 기본적으로 FetchType.LAZY로 설정되어 있다.
  • 중간 테이블에 동일한 조합(예: 특정 학생-강의 쌍)이 중복 삽입되지 않도록 주의해야 한다. Set 대신 List를 사용할 경우 중복이 발생할 수 있으니, Set을 사용하는 게 안전하다.
  • N:N 관계는 여러 테이블(원본 테이블 + 중간 테이블)을 수정하므로, 트랜잭션 관리(예: @Transactional)가 중요



 

🔎예시

학생이 여러 강의를 수강하고, 한 강의는 여러 학생이 수강할 수 있다(Student ↔ Course).
@Entity
public class Student { //주인 엔티티 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long studentId; // 기본 키

    private String name;

    @ManyToMany
    @JoinTable(name = "Student_Course", // 중간 테이블
               joinColumns = @JoinColumn(name = "studentId"), // Student 테이블의 외래 키
               inverseJoinColumns = @JoinColumn(name = "courseId")) // Course 테이블의 외래 키
		private Set<Course> courses = new HashSet<>(); // Set으로 중복 방지
    // getter, setter
}




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

    private String courseName;

    @ManyToMany(mappedBy = "courses") // Student 엔티티의 courses 필드가 연관관계 주인
		private Set<Student> students = new HashSet<>(); // Set으로 중복 방지
    // getter, setter
}

 

Course 엔티티는 @ManyToMany(mappedBy = "courses")를 통해 Student 엔티티의 courses 필드를 참조하며, 연관관계의 주인은 Student임을 알 수 있다.

 

 

 

위 코드를 DB 테이블로 나타내면 다음과 같다.

 

<Student>

studentId (PK) name
1 "민수"
2 "지영"

 

 

<Course>

courseId (PK) courseName
1 "자바 프로그래밍"
2 "웹 개발"

 

<중간테이블>

studentId (FK) courseId (FK)
1 1
1 2
2 1

 

(1행 : 민수가 자바 프로그래밍 수강

 2행 : 민수가 웹 개발 수강 

 3행 : 지영이가 자바 프로그래밍 수강)

 

 

 

 

 

🔎JoinTable(중간 테이블) 

N:N 관계는 JPA가 자동으로 중간 테이블을 생성하거나, @JoinTable로 명시적으로 정의한다.

이 테이블은 두 엔티티의 기본 키를 외래 키로 포함한다

 

위의 예시에서 다음 테이블을 의미

 

<중간테이블>

studentId (FK) courseId (FK)
1 1
1 2
2 1



 

 

 

 

 

 

 

 

끝까지 읽어주셔서 감사합니다 :)

Have a good day🐱

 

 

 

📢 Notice

1. 개발자 준비생이 공부한 내용을 정리한 글입니다. 내용에 오류가 있을 수 있습니다.

2. 위와 같은 이유로 내용에 대한 지적과 조언은 감사하게 받습니다.

3. 이 글의 내용은 계속 공부함으로써 언제든지 추가/수정 될 수 있습니다.