Spring 입문 (JPA 연관관계)

KimGwangmin·2026년 9월 8일

JPA 연관관계 매핑

@OneToOne

  • 1:1 대응 관계
  • 회원 멤버와 프로필 사진
  • 어느 쪽이 주도 테이블이 될지 정해야 함 (둘 중 어느 쪽에 FK를 둘까?)
    • 멤버-프로필 사진이라면, 멤버 쪽 테이블에 프로필 사진을 FK로 두는 것이 자연스럽다
    • @JoinColumn으로 FK 관리
@Getter
@Entity
@Table(name = "members")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "profile_id")
    private Profile profile;
    // ...
}

@Getter
@Entity
@Table(name = "profiles")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Profile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String imageUrl;
    // ...
}

@ManyToOne

  • N:1 대응 관계
  • Member->Team (멤버는 여럿, 팀은 하나)
  • 다수 쪽에서 단일 쪽을 참조
  • 다수 쪽이 연관관계의 '주인'이 된다!
    • Member 테이블에 Team을 FK로 갖는다
  • 모든 연관관계는 @ManyToOne만으로 정의할 수 있다!
@Getter
@Entity
@Table(name = "members")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
    // ...
}

@Getter
@Entity
@Table(name = "teams")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // ...
}

@OneToMany

  • 단일 쪽에서 다수 쪽을 참조
  • @ManyToOne이 반드시 정의되어 있어야 함
    • 즉, 양방향 참조만 가능
  • 주인은 @ManyToOne
  • DB에서는 하나의 속성에 객체나 리스트가 들어갈 수 없다
    • Team이 Members를 참조하는 건 DB 입장에선 불가능한 일!
  • 양방향은 실제로 존재하지 않는다. JPA가 그렇게 동작하는 것처럼 만드는 것이다.
  • 실무에서는 양방향 매핑을 지양한다.
// members 쪽은 위와 동일
@Getter
@Entity
@Table(name = "teams")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    // ...
}

@ManyToMany

  • 다대다 관계
  • 한 권의 책은 여러 저자를 가질 수 있다
  • 한 명의 저자는 여러 책을 집필할 수 있다
  • 실무에서든 연습에서든, 절대 쓰지 않는다!
@Getter
@Entity
@Table(name = "books")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // Book이 관계의 주인 (조인 테이블 생성)
    @ManyToMany
    @JoinTable(
        name = "book_author", // 자동으로 생성되는 조인 테이블
        joinColumns = @JoinColumn(name = "book_id"), // 현재 엔티티의 FK
        inverseJoinColumns = @JoinColumn(name = "author_id") // 반대쪽 엔티티의 FK
    )
    private List<Author> authors = new ArrayList<>();
}

@Getter
@Entity
@Table(name = "authors")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();
}

다대다 관계의 ERD를 보면, 만들지 않은 테이블이 생성되고, 작성하지 않은 SQL이 실행된다.
유지보수성이 떨어진다!

  • 다대다 관계를 만들어야 한다면, 저 중간 테이블을 명시적으로 직접 생성해주면 된다.
  • 다대다를 다대일 + 다대일로 풀어내는 것!

개선한 코드

@Getter
@Entity
@Table(name = "authors")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

@Getter
@Entity
@Table(name = "books")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
}

@Getter
@Entity
@Table(name = "book_author")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BookAuthor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
}

JPA 조회 전략 (FetchType)

FetchType.EAGER: 즉시 로딩

  • 부모 엔티티 조회 시, 연관된 자식 엔티티까지 한 번에 모두 조회

FetchType.LAZY: 지연 로딩

  • 부모 엔티티를 조회할 때는 부모 엔티티만 조회
  • 자식 엔티티는 필요한 시점에 조회
  • Proxy Initalization
    • 부모 엔티티에 속한 자식 엔티티 필드는 Proxy 객체가 매핑되었다가, 실제로 사용할 때 실제 객체를 내부적으로 로드하게 됨

@ManyToOne 옵션

  1. @ManyToOne

    • fetch = FetchType.LAZY: 연관 엔티티를 프록시로 지연 로딩 (EAGER는 안쓴다고 생각하기)
    • optional: 이 연관관계가 필수인지 여부, nullable처럼 생각해도 됨 (기본값: true)
  2. @JoinColumn

    • name: FK 컬럼명
    • unique: 유일 제약 여부 (기본값: false)
      • true로 설정하면 여러 Member가 같은 Team 가리킬 수 없음 -> 사실상 일대일 제약
      • @OneToOne도 이걸로 대체할 수 있음
    • nullable: FK 컬럼의 null 값 허용 (기본값: true)
    • foreignKey: FK 제약 메타데이터
      • @ForeignKey(name="str", value=ContraintMode.XXX) 형식
      • name: DB에 생성될 FK 제약 이름
      • ConstraintMode.CONSTRAINT(기본값): 실제 FK 제약 생성
      • ConstraintMode.NO_CONSTRAINT: DB에는 실제 제약 미생성, 무결성 보장은 애플리케이션에서 책임져야 함
        • 예: 글-댓글 관계는 댓글이 주인(Many). 그럼 댓글이 있는 글 삭제 불가(제약 조건)
        • 이런 제약 조건을 회피할 때 사용해볼 수 있음

사용 예시

@Getter
@Entity
@Table(name = "members")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(
        name = "team_id",               // FK 컬럼명
        unique = true,                  // 유니크 제약 부여 (1:1 관계와 동일한 효과)
        nullable = false,               // FK 컬럼의 NULL 불허
        foreignKey = @ForeignKey(
            name = "fk_member_team",    // DB에 생성될 FK 제약 이름
            value = ConstraintMode.CONSTRAINT // 실제 FK 제약 생성
        )
    )
    private Team team;

    // 생성자, 기타 메서드 등...
}

FK는 연관관계 설정 뿐 아니라, DB 무결성을 위한 제약 조건으로 생각할 수 있다.

0개의 댓글