[JPA] 연관관계 매핑

Coastby·2023년 5월 24일
0

연관관계 매핑이란?

조인은 데이터베이스에서 테이블 간의 관련된 데이터를 결합하는 작업이며, JPA의 연관관계 매핑은 객체 지향 프로그래밍에서 객체 간의 관계를 표현하는 것입니다. 이러한 연관관계 매핑을 통해 객체 간의 관계를 사용하여 데이터베이스 테이블 간의 조인을 자동으로 처리할 수 있습니다.

연관관계 매핑의 종류

JPA에서 연관관계 매핑을 위해 다양한 어노테이션을 제공합니다. 가장 일반적으로 사용되는 어노테이션은 다음과 같습니다:

  1. @OneToOne: 일대일 관계를 매핑할 때 사용됩니다. 예를 들어, 한 명의 사용자(User)가 하나의 프로필(Profile)을 갖는 경우에 사용됩니다.
  2. @OneToMany: 일대다 관계를 매핑할 때 사용됩니다. 예를 들어, 한 개의 부서(Department)에 여러 명의 직원(Employee)이 속하는 경우에 사용됩니다.
  3. @ManyToOne: 다대일 관계를 매핑할 때 사용됩니다. 예를 들어, 여러 명의 주문(Order)이 한 명의 고객(Customer)에 속하는 경우에 사용됩니다.
  4. @ManyToMany: 다대다 관계를 매핑할 때 사용됩니다. 예를 들어, 여러 명의 학생(Student)이 여러 개의 과목(Subject)을 수강하는 경우에 사용됩니다.

연관관계 매핑의 방향

연관관계 매핑에는 단방향(One-way)과 양방향(Two-way) 매핑 두 가지 방향성이 있습니다. 이를 예시를 통해 설명해보겠습니다.

  1. 단방향 매핑 (One-way Mapping):
    • 예시: 주문(Order)과 상품(Product) 간의 관계
    • 설명: 주문과 상품은 일대다 관계입니다. 즉, 한 개의 주문은 여러 개의 상품을 포함할 수 있습니다. 이 경우, 주문 엔티티에는 상품과의 관계를 설정하고 매핑합니다. 주문(Order) 엔티티에는 @OneToMany 어노테이션을 사용하여 상품과의 관계를 매핑할 수 있습니다. 이렇게 하면 주문 엔티티에서는 상품 엔티티에 대한 참조가 가능하지만, 상품 엔티티에서는 주문에 대한 참조가 없습니다. 이는 단방향 매핑입니다.
  2. 양방향 매핑 (Two-way Mapping):
    • 예시: 게시글(Post)과 댓글(Comment) 간의 관계
    • 설명: 게시글과 댓글은 일대다 관계이면서 동시에 다대일 관계입니다. 한 개의 게시글에 여러 개의 댓글이 달릴 수 있고, 한 개의 댓글은 하나의 게시글에 속합니다. 이 경우, 양방향 매핑이 필요합니다. 게시글(Post) 엔티티에는 @OneToMany 어노테이션을 사용하여 댓글과의 관계를 매핑하고, 댓글(Comment) 엔티티에는 @ManyToOne 어노테이션을 사용하여 게시글과의 관계를 매핑합니다. 이렇게 하면 게시글 엔티티에서는 댓글에 대한 참조가 가능하고, 댓글 엔티티에서는 속한 게시글에 대한 참조가 가능합니다. 이는 양방향 매핑입니다.

ManyToOne 단방향

Board와 User의 관계

  1. Board entity, BoardRepository 만들기

🔖 Board

@Entity
@NoArgsConstructor
@Getter
@Builder
@AllArgsConstructor
public class Board {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(nullable = false)
  private String title;
  @Column(nullable = false)
  private String contents;
  @ManyToOne
  private User author;

}

🔖 BoardRepository

public interface BoardRepository extends JpaRepository<Board, Long> {

}
  1. 프로젝트 실행 (ctrl + R) 후 테이블 확인하기
    테이블 생성 쿼리


  2. 테스트 코드 작성
    BoardRepository - cmd + N - Test…. - Ok
    UserRepositoryTest와 동일하게 어노테이션을 추가하고 User도 등록해야 하므로 UserRepository도 DI해줍니다. @DataJpaTest 어노테이션을 이용하면 기본적으로 @Transactional이 적용되기 때문에 영속성 컨텍스트에만 반영이 되고 쿼리가 다 실행되지는 않습니다. 따라서 쿼리를 확인하기 위해 @SpringBootTest로 진행합니다.

@SpringBootTest
@ActiveProfiles(“test”)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BoardRepositoryTest {
  @Autowired
  private UserRepository userRepository;
  @Autowired
  private BoardRepository boardRepository;

  @Test
  @DisplayName("게시글 작성")
  void 게시글작성 (){
     //유저 등록
     User saved = userRepository.save(User.builder().userName("이름").password("비밀번호").build());
     assertEquals("이름", saved.getUserName());

     //게시글 등록
     Board board = boardRepository.save(Board.builder().title("제목").contents("내용").author(saved).build());
     assertEquals("이름", board.getAuthor().getUserName());

     //게시글 확인
     Optional<Board> optionalBoard = boardRepository.findById(board.getId());
     assertEquals("이름", optionalBoard.orElseThrow(RuntimeException::new).getAuthor().getUserName());

  }

}

쿼리 확인
Board 객체를 생성할 때는 User 객체를 참조했지만, INSERT 쿼리에서는 user의 id가 들어가는 것을 확인할 수 있습니다.

OneToMany 단방향

Board와 Comment 매핑

  1. Comment entity, CommentRepository 만들기

🔖 Comment Entity

@Entity
@NoArgsConstructor
@Getter
@Builder
@AllArgsConstructor
public class Comment {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String contents;
  @ManyToOne
  private User author;
  @Column(name = "board_id")
  private Long boardId;

}

🔖 Comment Repository

public interface CommentRepository extends JpaRepository<Comment, Long> {
 
}
  1. Board entity에서 Comment OneToMany 단방향 매핑하기
public class Board {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(nullable = false)
  private String title;
  @Column(nullable = false)
  private String contents;
  @ManyToOne
  private User author;
  @OneToMany(fetch = FetchType.EAGER)
  @JoinColumn(name = "board_id")
  @Builder.Default
  private List<Comment> comments = new ArrayList<>();
}
  1. 실행하고 DB의 테이블 확인하기

  2. 테스트 코드 작성
    BoardRepositoryTest에 CommentRepository DI 추가합니다.
class BoardRepositoryTest {
  @Autowired
  private UserRepository userRepository;
  @Autowired
  private BoardRepository boardRepository;
  @Autowired
  private CommentRepository commentRepository;
  
  ………
}

댓글 생성 테스트 코드 작성

@Test
@DisplayName("댓글 생성 및 게시글의 댓글 조회하기")
void 댓글생성조회() {
  //게시글 등록
  Board board = boardRepository.save(Board.builder().title("제목").contents("내용").build());
  assertEquals("제목", board.getTitle());

  //댓글 등록
  Comment comment = commentRepository.save(Comment.builder().contents("댓글내용").boardId(
        board.getId()).build());
  assertEquals("댓글내용", comment.getContents());

  //게시글의 댓글 조회
  List<Comment> commentList = boardRepository.findById(board.getId()).get().getComments();
  assertEquals(1, commentList.size());
  assertEquals("댓글내용", commentList.get(0).getContents());

}

실패 - comment가 조회되지 않는다.

다시 작성 - Comment를 먼저 작성하고 게시글에 등록한다.

@Test
@DisplayName("댓글 생성 및 게시글의 댓글 조회하기")
void 댓글생성조회() {
  //댓글 등록
  Comment comment = commentRepository.save(Comment.builder().contents("댓글내용").build());
  assertEquals("댓글내용", comment.getContents());

  //게시글 등록
  Board board = Board.builder().title("제목").contents("내용").comments(new ArrayList<>()).build();
  board.getComments().add(comment);
  Board savedBoard = boardRepository.save(board);
  assertEquals("제목", board.getTitle());

  //게시글의 댓글 조회
  List<Comment> commentList = boardRepository.findById(board.getId()).get().getComments();
  assertEquals(1, commentList.size());
  assertEquals("댓글내용", commentList.get(0).getContents());

}

📌 @Builder.Default

@Builder.Default는 Lombok에서 제공하는 어노테이션 중 하나로, 빌더 패턴을 사용할 때 기본 값을 설정하는 용도로 사용됩니다.
@Builder 어노테이션을 클래스에 적용하면, 해당 클래스에 대한 빌더 클래스가 생성됩니다. 일반적으로 빌더 클래스는 각 필드의 값을 설정하는 메서드 체인을 통해 객체를 생성하며, 필드의 값을 설정하지 않을 경우 해당 필드는 기본값으로 설정됩니다.
하지만 때로는 특정 필드의 기본값을 직접 지정하고 싶을 때가 있습니다. 이때 @Builder.Default 어노테이션을 사용하면 해당 필드의 기본값을 명시적으로 지정할 수 있습니다.
예를 들어, 다음과 같은 클래스가 있다고 가정해봅시다:

@Getter 
@Builder 
public class Person { 
  private String name; 
  @Builder.Default private int age = 18; 
  private String address; 
}

위의 예시에서 age 필드에 @Builder.Default 어노테이션을 사용하여 기본값을 18로 지정했습니다. 이제 빌더를 사용하여 객체를 생성할 때, age 필드에 값을 지정하지 않으면 자동으로 기본값인 18이 설정됩니다.

Person person = Person.builder() 
    .name("John")
    .address("123 Main St")
    .build();
 System.out.println(person.getAge()); // 출력: 18

이와 같이 @Builder.Default 어노테이션을 사용하면 빌더 패턴을 사용할 때 특정 필드의 기본값을 설정할 수 있습니다.

OneToMany 양방향

  1. Comment entity에서 BoardId에 @ManyToOne 어노테이션을 추가하고 타입, 변수을 Board로 변경한다.
public class Comment {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String contents;
  @ManyToOne
  private User author;
  @ManyToOne
  private Board board;

}
  1. Board의 @OneToMany에 mappedBy 속성을 추가합니다.
public class Board {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(nullable = false)
  private String title;
  @Column(nullable = false)
  private String contents;
  @ManyToOne
  private User author;
  @OneToMany(mappedBy = "board", fetch = FetchType.LAZY)
  @Builder.Default
  private List<Comment> comments = new ArrayList<>();
}
  1. 실행 후 테이블 구조 확인 -> 이전과 동일

  2. 테스트 코드로 확인

@Test
@DisplayName("댓글 생성 및 게시글의 댓글 조회하기")
void 댓글생성조회() {
  //게시글 등록
  Board board = boardRepository.save(Board.builder().title("제목").contents("내용").build());
  assertEquals("제목", board.getTitle());

  //댓글 등록
  Comment comment1 = commentRepository.save(Comment.builder().contents("댓글내용1").board(
        board).build());
  Comment comment2 = commentRepository.save(Comment.builder().contents("댓글내용2").board(
        board).build());
  Comment comment3 = commentRepository.save(Comment.builder().contents("댓글내용3").board(
        board).build());
  assertEquals("댓글내용1", comment1.getContents());
  assertEquals(board.getId(), comment1.getBoard().getId());

  //게시글의 댓글 조회
  List<Comment> commentList = boardRepository.findById(board.getId()).get().getComments();
  assertEquals(3, commentList.size());
  assertEquals("댓글내용1", commentList.get(0).getContents());

}
단방향에서 통과했던 테스트 (board 객체의 comments 필드에 comment 객체를 넣는 방법)은 테스트하지 않는 것도 확인할 수 있습니다.
profile
훈이야 화이팅

0개의 댓글