조인은 데이터베이스에서 테이블 간의 관련된 데이터를 결합하는 작업이며, JPA의 연관관계 매핑은 객체 지향 프로그래밍에서 객체 간의 관계를 표현하는 것입니다. 이러한 연관관계 매핑을 통해 객체 간의 관계를 사용하여 데이터베이스 테이블 간의 조인을 자동으로 처리할 수 있습니다.
JPA에서 연관관계 매핑을 위해 다양한 어노테이션을 제공합니다. 가장 일반적으로 사용되는 어노테이션은 다음과 같습니다:
연관관계 매핑에는 단방향(One-way)과 양방향(Two-way) 매핑 두 가지 방향성이 있습니다. 이를 예시를 통해 설명해보겠습니다.
@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;
}
public interface BoardRepository extends JpaRepository<Board, Long> {
}
프로젝트 실행 (ctrl + R) 후 테이블 확인하기
테이블 생성 쿼리
테스트 코드 작성
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가 들어가는 것을 확인할 수 있습니다.
@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;
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
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<>();
}
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는 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 어노테이션을 사용하면 빌더 패턴을 사용할 때 특정 필드의 기본값을 설정할 수 있습니다.
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String contents;
@ManyToOne
private User author;
@ManyToOne
private Board board;
}
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<>();
}
실행 후 테이블 구조 확인 -> 이전과 동일
테스트 코드로 확인
@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());
}