@ManyToOne, @OneToMany

이진욱·2024년 12월 27일

이론

목록 보기
20/24

@OneToMany@ManyToOne은 JPA에서 객체 관계 매핑을 설정할 때 사용되는 애너테이션으로, 데이터베이스 테이블 간의 관계를 객체 모델에서 표현하는 방법입니다. 각각의 차이점과 사용해야 하는 상황을 설명하겠습니다.

1. @OneToMany

  • 의미 : 한 엔티티가 여러 다른 엔티티와 관계를 가질 때 사용합니다.

  • 예: 부모 엔티티가 자식 엔티티를 여러 개 가질 수 있음.

  • 매핑 방향

    • 단방향: 부모 엔티티에서 자식 엔티티 리스트를 참조.
    • 양방향: 부모 엔티티가 자식 리스트를, 자식 엔티티가 부모를 참조.
  • 사용해야 하는 상황

    • 부모-자식 관계
      예: 부모(Parent)가 여러 자식(Child)을 가짐.

    • 컬렉션 데이터를 필요로 할 때
      예: 한 사용자(User)가 여러 게시글(Post)을 작성할 때.

2. @ManyToOne

  • 의미 : 여러 엔티티가 하나의 엔티티와 관계를 가질 때 사용합니다.

  • 예 : 여러 자식 엔티티가 같은 부모 엔티티를 참조할 수 있음.

  • 매핑 방향

    • 단방향: 자식 엔티티가 부모를 참조.

    • 양방향: 자식 엔티티가 부모를, 부모 엔티티가 자식 리스트를 참조.

  • 사용해야 하는 상황
    • N:1 관계에서 자식이 부모를 참조
      • 예: 여러 게시글(Post)이 한 카테고리(Category)에 속할 때.
    • 단순 참조만 필요할 때
      • 예: 댓글(Comment)이 특정 게시글(Post)에 속할 때.

어떤 상황에서 써야 할까?

@OneToMany를 사용해야 하는 경우

  • 부모 엔티티에서 자식 엔티티의 목록(리스트)을 자주 조회해야 할 때.
    • 예: 게시판에서 한 게시글(Post)이 여러 댓글(Comment)을 포함할 때.

@ManyToOne을 사용해야 하는 경우

  • 자식 엔티티에서 부모 엔티티를 참조할 일이 많을 때.
    데이터베이스 구조가 단순하고, 자식 → 부모 참조가 더 효율적일 때.
    • 예: 댓글(Comment)이 게시글(Post)을 참조할 때.

차이점

실무 팁

양방향 매핑은 객체 간의 관계를 보다 직관적으로 표현할 수 있지만, 관리해야 할 코드 복잡도가 늘어날 수 있습니다.
→ 정합성 유지를 위해 연관 관계 편의 메서드를 작성하세요.

단순한 참조가 필요하다면 @ManyToOne 단방향 매핑을 고려하세요.

Lazy Loading을 기본적으로 설정하고, 필요한 경우에만 데이터를 Fetch하여 성능 문제를 방지하세요.

@JoinColumn

@JoinColumn(name = "parent_id")를 사용하면 JPA는 자식 테이블parent_id라는 외래 키 컬럼을 생성합니다. 이 컬럼은 부모 엔티티(Parent)의 기본 키 값을 저장합니다.

역할

  • 외래 키 컬럼 지정

@JoinColumn은 외래 키가 저장될 컬럼 이름을 명시적으로 설정합니다.
외래 키는 자식 테이블에 생성되며, 부모 테이블의 기본 키를 참조합니다.

  • 기본값 제공

@JoinColumn을 생략하면 JPA는 기본적으로 부모 엔티티의 이름 + _id 형식으로 외래 키 컬럼 이름을 생성합니다.

  • 테이블 간 관계 매핑

외래 키를 통해 부모-자식 관계를 매핑하고, 이를 통해 두 엔티티 간의 참조를 설정합니다.

외래 키 저장의 원리

  • JPA는 자식 엔티티의 parent 필드에 매핑된 부모 엔티티의 id 값을 찾아 parent_id 컬럼에 저장합니다.
  • @JoinColumn을 지정하지 않으면 기본적으로 부모 엔티티의 클래스 이름 + _id 형식으로 외래 키 컬럼 이름이 결정됩니다.

    예: Parent 엔티티 → parent_id.

외래 키는 주로 자식 테이블에 존재하며, 부모 테이블의 기본 키(Primary Key)를 참조하는 방식으로 데이터베이스 관계를 유지합니다. 부모 엔티티는 외래 키 값을 직접 저장하지 않고, 자식 엔티티를 참조하는 역할만 합니다.

외래키는 왜 자식 테이블에 저장이 되나요?
  • 관계의 방향성

    • 관계형 데이터베이스에서는 외래 키를 통해 두 테이블 간의 관계를 정의합니다.
    • 외래 키는 자식 테이블에서 부모 테이블의 고유 값을 참조하므로 "N:1 관계"에서 자식이 부모를 식별할 수 있도록 합니다.
  • 부모-자식 관계에서의 책임 분리

부모는 자신이 가지고 있는 데이터만 관리하고, 자식 테이블은 외래 키를 통해 부모와의 관계를 관리합니다.
이는 데이터베이스 설계에서 효율적이고 직관적인 관계 설정 방식입니다.

  • 정규화 원칙

관계형 데이터베이스의 정규화 원칙에 따라, 외래 키는 관계를 정의하는 데 필요한 최소한의 정보를 사용하여 테이블 간 중복을 방지합니다.

주요 속성

  • 부모 엔티티에서 다른 속성들도 많고 명시한 부분도 없는데 왜 id를 왜 참조하게 되나요?

    JPA에서는 엔티티 간의 연관 관계를 설정할 때 기본적으로 @Id가 지정된 필드를 참조하도록 설계되어 있습니다.

다른 컬럼을 참조하고 싶을 때

만약 @id가 아닌 다른 컬럼을 참조하고 싶다면, @JoinColumnreferencedColumnName 속성을 사용하여 참조할 컬럼을 명시적으로 지정해야 합니다.

예제: User 엔티티의 username 컬럼을 참조하도록 설정

@ManyToOne
@JoinColumn(name = "user_username", referencedColumnName = "username")
private User user;
  • name = "user_username": 외래 키 컬럼 이름을 지정합니다.

  • referencedColumnName = "username" : User 엔티티의 username 컬럼을 참조하도록 설정합니다.

  • 생성되는 테이블
    Post 테이블의 외래 키 컬럼은 user_username으로 생성됩니다.
    외래 키는 User 테이블의 username 컬럼을 참조합니다.

예제

1. User Entity

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List posts = new ArrayList<>();

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class User {

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

    private String username;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Post> posts = new ArrayList<>();

    // Helper method to manage bidirectional relationship
    public void addPost(Post post) {
        posts.add(post);
        post.setUser(this);
    }

    public void removePost(Post post) {
        posts.remove(post);
        post.setUser(null);
    }
}

2. Post 엔티티

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Post {

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

    private String title;

    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
}

3. Repository 설정

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

public interface PostRepository extends JpaRepository<Post, Long> {
}

4. Service 예제

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PostRepository postRepository;

    @Transactional
    public void createUserWithPosts() {
        // User 생성
        User user = new User();
        user.setUsername("John Doe");

        // Post 생성
        Post post1 = new Post();
        post1.setTitle("First Post");
        post1.setContent("This is the first post.");

        Post post2 = new Post();
        post2.setTitle("Second Post");
        post2.setContent("This is the second post.");

        // 관계 설정
        user.addPost(post1);
        user.addPost(post2);

        // 저장
        userRepository.save(user);
    }
}

5. Controller 예제

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/create")
    public String createData() {
        userService.createUserWithPosts();
        return "User and posts created successfully!";
    }
}
profile
열심히 하는 신입 개발자

0개의 댓글