양방향 매핑(Bidirectional Mapping)은 객체 관계 매핑(ORM, Object-Relational Mapping)에서 두 엔티티 간의 관계를 양쪽에서 참조할 수 있도록 설정하는 것을 의미합니다. 즉, 한 엔티티가 다른 엔티티를 참조할 수 있고, 반대로 그 엔티티도 처음의 엔티티를 참조할 수 있는 관계입니다.
1. 양방향 매핑 설정
양방향 매핑은 보통 **JPA(Java Persistence API)**나
Hibernate
같은 ORM 프레임워크에서 사용되며,
1:11, N:1, N
과 같은 관계에서 적용할 수 있습니다. 이러한 양방향 관계를 설정하면 두 엔티티 간의 데이터 일관성을 유지하고, 더 효율적으로 데이터를 조회할 수 있습니다.

2. Entity가 아니라 DTO로 받기
Service에서 Entity로 받아도 되지만 Entity로 받게 되면 불필요한 필드들을 추가로 들고 오기 때문에 데이터 공간을 쓸데없이 차지하게 된다. 따라서 Repository에서 받아온 데이터에서 쓸려고 하는 데이터들만 추출해서 사용하기 위해선 DTO를 따로 만들어서 담아오는 것이 효율적이다.
Controller
@GetMapping("/board/{id}")
public String detail(@PathVariable("id") Integer id, HttpServletRequest request) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DetailDTO model = boardService.게시글상세보기(sessionUser, id);
request.setAttribute("model", model);
return "board/detail";
}Service
public BoardResponse.DetailDTO 게시글상세보기(User sessionUser, Integer boardId){
Board board = boardRepository.mFindByIdWithReply(boardId)
.orElseThrow(() -> new Exception404("게시글이 없습니다."));
BoardResponse.DetailDTO boardPS = new BoardResponse.DetailDTO(board, sessionUser);
return boardPS;
}Repository
package org.example.springv3.board;
public interface BoardRepository extends JpaRepository<Board, Integer> {
@Query("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id=:id")
Optional<Board> mFindByIdWithReply(@Param("id") int id);
// @Query(value = "select * from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id=?", nativeQuery = true)
@Query("select b from Board b join fetch b.user u where b.id= :id")
Optional<Board> mFindById(@Param("id") int id);
@Query("select b from Board b order by b.id desc")
List<Board> mFindAll();
}BoardResponse
package org.example.springv3.board;
import lombok.Data;
import org.example.springv3.reply.Reply;
import org.example.springv3.user.User;
import java.util.ArrayList;
import java.util.List;
public class BoardResponse {
@Data
public static class DetailDTO {
private Integer id;
private String title;
private String content;
private Boolean isOwner;
private String username;
// 댓글들
private List<ReplyDTO> replies = new ArrayList<>();
public DetailDTO(Board board, User sessionUser) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isOwner = false;
if (sessionUser != null) {
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true; // 권한체크
}
}
this.username = board.getUser().getUsername();
for(Reply reply : board.getReplies()){
replies.add(new ReplyDTO(reply, sessionUser));
}
}
@Data
class ReplyDTO {
private Integer id;
private String comment;
private String username;
private Boolean isOwner;
public ReplyDTO(Reply reply, User sessionUser) {
this.id = reply.getId();
this.comment = reply.getComment();
this.username = reply.getUser().getUsername();
this.isOwner = false;
if (sessionUser != null) {
if (reply.getUser().getId() == sessionUser.getId()) {
isOwner = true; // 권한체크
}
}
}
}
}
}DetailDTO에 필요한 필드들만 담아서 데이터를 뽑아내도록 설정해준다.
Repository의 쿼리를 이용해 데이터를 들고 올 경우 중복 데이터를 가져오게 된다.
ex) 게시물 1, 댓글 1 / 게시물 1, 댓글 2 / 게시물 1, 댓글 3 이런 식으로 게시물 1 이 중복으로 표시된다.
이러한 데이터를 효율적으로 묶기 위해서는 뒤의 댓글들을 List에 담는 것이 좋다. 그러기 위해
List<ReplyDTO> 로 새로운 DTO를 생성하여 데이터를 담는다. 이렇게 DTO를 묶어서 데이터를 보내면 다음과 같이 데이터가 깔끔하게 보내진다.
@ResponseBody로 받을 때 JSON 형식으로 변환되어 화면에 나타난다
{
"id": 5,
"title": "제목5",
"content": "내용5",
"createdAt": "2024-09-05T03:23:41.958+00:00",
"user": {
"id": 2,
"username": "cos",
"email": "cos@nate.com",
"createdAt": "2024-09-05T03:23:41.958+00:00"
},
"replies": [
{
"id": 1,
"comment": "댓글1",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T03:23:41.958+00:00"
}
},
{
"id": 2,
"comment": "댓글2",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T03:23:41.958+00:00"
}
},
{
"id": 3,
"comment": "댓글3",
"user": {
"id": 2,
"username": "cos",
"password": "1234",
"email": "cos@nate.com",
"createdAt": "2024-09-05T03:23:41.958+00:00"
}
}
]
}** Test 관련 팁)


Test를 할 때, JpaRepository를 상속한 클래스는 @import를 안해도 된다
하지만 nativeQuery를 사용할 경우 import가 필요하다

@JsonIgnoreProperties({””})를 이용해서 양방향 맵핑으로 일어나는 오류 + 불필요한 필드를 가져오지 않게 설정이 가능하다
** 하지만 이렇게 설정하는 것보다 화면에 필요한 데이터들을 모은 클래스 DTO를 만들어서 관리하는 것이 더 편하다.
이렇게 나온 데이터를 model에 담아 화면에 전송해서 다음과 같이 담아서 사용한다
{{>layout/header}}
<div class="container p-5">
<!-- 수정삭제버튼 -->
{{#model.isOwner}}
<div class="d-flex justify-content-end">
<a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a>
<form action="/api/board/{{model.id}}/delete" method="post">
<button class="btn btn-danger">삭제</button>
</form>
</div>
{{/model.isOwner}}
<div class="d-flex justify-content-end">
<b>작성자</b> : {{model.username}}
</div>
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr/>
<div class="m-4 p-2">
{{{model.content}}}
</div>
</div>
<!-- 댓글 -->
<div class="card mt-3">
<!-- 댓글등록 -->
<div class="card-body">
<form action="/reply/save" method="post">
<textarea class="form-control" rows="2" name="comment"></textarea>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
</div>
</form>
</div>
<!-- 댓글목록 -->
<div class="card-footer">
<b>댓글리스트</b>
</div>
<div class="list-group">
{{#model.replies}}
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-primary text-white rounded">{{username}}</div>
<div>{{comment}}</div>
</div>
{{#isOwner}}
<form action="#" method="post">
<button class="btn">🗑</button>
</form>
{{/isOwner}}
</div>
{{/model.replies}}
</div>
</div>
</div>
{{>layout/footer}}
Board와 Reply 는 1:N의 관계이다. 하나의 게시글에 여러 개의 댓글을 달 수 있기 때문에 Reply가 N이 되는 것이다.
그렇기에 Board 엔티티에서 Reply 를 객체로 받기 위해서 @OneToMany 그리고 반대로 Reply에서는 @ManyToOne으로 객체를 설정해준다.
Share article