@ManyToOne 더 깊게 파악하기

김소연·2026년 4월 20일

@ManyToOne

이전에 @ManyToOne으로 모든 연관관계를 풀어낼 수 있다고 했습니다.
이를 잘 이해하려면 데이터베이스의 테이블의 구조에 대해 한 번 생각해보면 됩니다.

  • 다시 Team과 Member의 양방향 매핑 코드를 보겠습니다.

    @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(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
        // ...
    }

    이 두 엔티티 클래스 코드에서 Member 엔티티에는 Team team이 존재하고,
    Team 엔티티에는 List<Member> members가 존재한다.

    ❌ 데이터베이스의 데이터(한 칸)에 객체나 리스트의 데이터가 들어갈 수 있을까?
    → 불가능하다!

    그런데 이 말에 따르면, List<Member> members 라는 건 코드 상으로는 가능하지만, 데이터베이스의 입장에서는 불가능한 일입니다.
    실제 데이터베이스에서는 양방향이라는 건 존재하지 않습니다. 오직 단방향만 존재합니다.
    따라서,

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

    @ManyToOne을 사용하는 이 단방향만 있으면 모든 연관관계 매핑을 개발할 수 있는 것입니다.

    [ 이와 관련한 @OneToMany 이야기 ]
      1. @ManyToOne이 존재해야만 @OneToMany가 가능(단방향이 있어야만 양방향이 존재)
      2. 양방향은 실제로는 존재하지 않지만, JPA가 양방향처럼 동작하게 만듬
      3. 실무에서는 대부분 양방향 매핑을 쓰지 않는 방향으로 개발을 진행

@ManyToMany를 @ManyToOne으로 풀어내기

@ManyToMany를 사용하면 가운데 테이블을 암묵적으로 만들어내는 것을 배웠습니다.
이를 @ManyToOne으로 풀어내는 건 간단합니다. 중간 테이블을 직접 생성해주면 됩니다!.
오히려 @ManyToOne으로 풀어내는 것이 더 명시적이고 쉬울 수 있습니다.

  • 기존의 @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<>();
    }
  • @ManyToOne으로 개선한 코드

    @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;
    }

    이렇게 직접 ‘BookAuthor’ 테이블을 만들어서 명시적으로 중간 테이블을 관리할 수 있습니다.


@ManyToOne 옵션 (@ManyToOne, @JoinColumn)

  • (아래의 예시를 기반으로 진행)

    @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;
    
        // ...
    }

1. @ManyToOne

  • fetch = FetchType.LAZY: 연관 엔티티(Team)를 프록시로 지연 로딩(쿼리 최적화/순환 로딩 방지)

  • optional: 이 연관관계가 필수인지(=null 허용 X) 여부를 정의

    • optional = false → 필수 관계. JPA에서 null을 허용하지 않음을 의미.
    • optional = true(default 값) → 선택 관계. null 허용.

2. @JoinColumn

  • name: FK 컬럼명 (ex. @JoinColumn(name = "team_id") )

  • unique: 유니크 제약 부여 여부(기본: false).

    • true로 설정하면 여러 Member가 같은 Team을 가리킬 수 없게 되어 사실상 1:1 제약이 됩니다.

      DB 입장에서 @OneToOne이라는 건, 본질적으로는 존재하지 않습니다.
      실제로는 이 또한 다대일 관계입니다. 다만 하나 있느냐, 여러개 있느냐의 차이입니다.
      하나 있는 경우 편의성을 위해 JPA에서 @OneToOne이라는 도구를 제공해주고 있습니다.
      즉, @JoinColumn(unique=true)를 사용하면 @OneToOne과 동일한 기능을 하게 됩니다.

  • nullable: FK 컬럼의 NULL 허용(기본: true).

    • @ManyToOne(optional = false)와 항상 짝을 맞추세요.
  • foreignKey: FK 제약 메타데이터.

    • @ForeignKey(name = "fk_member_team", value = ConstraintMode.XXX) 형식.
    • name: DB에 생성될 FK 제약 이름
    • value: FK 생성 모드.
      • ConstraintMode.CONSTRAINT(default 값)→ 실제 FK 제약 생성
      • ConstraintMode.NO_CONSTRAINT → 실제 FK 제약 미생성(논리적 연관만; 다만 무결성 보장은 애플리케이션/배치로 책임져야 함)

  • @JoinColumn 모든 옵션 사용 예시

    @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;
    
        // 생성자, 기타 메서드 등...
    }

핵심 정리

  • @ManyToOne는 항상 LAZY (선택권)
  • FK가 필수가 아니라면 optional=falsenullable=false를 사용하고 아니라면 설정하지 않는 것을 권장

✅ FK가 null이면 안되는 경우

@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", nullable = false)
    private Team team;
    // ...
}

✅ FK가 null이어도 되는 경우

@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;
    // ...
}

0개의 댓글