문제 상황
일대다 관계인 Registry와 Comment 연관관계 매핑을 마치고 Comment에 있던 registryId와 registryNickname 필드를 없앴다.
👉🏻 연관관계 적용2(refactoring)
(매핑으로 인해 Comment.getRegistry().getIdx()
하면 되므로)
그래서 해당 필드를 없애면서 수정해야 할 코드들을 고치고 있다가 궁금한 점이 생겼고 정리해봤다.
의문점(postComment)
댓글을 post할 때 js에서 form 형태로 보냈다.
1
2
3
4
5
6
// js
let form_data = newFormData()
form_data.append("comment", $("#comment").val())
form_data.append("nickname", nickname)
form_data.append("registryId",$("#RegistryId").html())
form_data.append("registryNickname",$("#user").text())
위 코드를 아래로 변경했는데 registryId
로 보내던 것을 registry
로 보내고 dto를 엔티티로 수정했다.
1
2
3
4
5
// js
let form_data = new FormData()
form_data.append("comment", $("#comment").val())
form_data.append("nickname", nickname)
form_data.append("registry",$("#RegistryId").html()) // 👈🏻
dto
1
2
3
4
5
6
7
public class CommentDto {
private String nickname;
private String comment;
private Registry registry; // 👈🏻 연관관계 매핑 후 수정
// private Long registryId;
// private String registryNickname;
}
Comment
1
2
3
4
5
6
7
public Comment(CommentDto commentDto) {
this.nickname = commentDto.getNickname();
this.comment = commentDto.getComment();
this.registry = commentDto.getRegistry();
// this.registryId = commentDto.getRegistryId();
// this.registryNickname = commentDto.getRegistryNickname();
}
그런데 이렇게 RegistryId
로 보내던 것을 registry
로 보내고 dto에 Registry로 담아봤는데 성공했다. (😮?)
나는 이 부분이 의문이 들었고 왜 그런건지 알고 싶었다.
id가 pk라서 알아서 해당 id에 해당하는 db를 가져오는걸까?
어떻게 id인줄 아는걸까??
참고로 보통은 dto에 엔티티를 넣지 않는다고 한다. (이 부분은 아래에서 다룰 예정!)
🐣 dto에 엔티티를 넣지 않고 id를 받아서 서버에서 해당 id를 조회하고 유효성 검사한 다음에 저장할 때 넣어준다.
❓ 왜 dto에 entity를 넣으면 안되는걸까?
dto에 객체를 넣어도 되지만 entity는 넣어주면 안된다.
Entity와 Dto를 분리하기 위해서 작성하면 안된다.
Entity와 Dto를 분리하는 이유는
DB와 View 사이의 역할 분리를 위해서
테이블에 매핑되는 정보와 실제 View에서 요청되는 정보가 다를 경우
테이블에 필요한 정보에 맞게 데이터를 변환하는 로직이 필요할 수 있는데,
해당 로직이 Entity에 들어가게 되는 것은 일반적으로 생각해도 깔끔하지 못하다.
DB로부터 조회된 Entity를 그대로 View로 넘기게 되었을 때 불필요한 정보 및 노출되면 안 되는 정보까지 노출될 수 있고,
이를 막기 위한 로직을 따로 구현해야 한다.
Dto가 일회성으로 데이터를 주고받는 용도로 사용되는 것과 다르게 Entity의 생명주기(Life Cycle)도 전혀 다르다.
DTO(Data Transfer Object)는 Entity 객체와 달리 각 계층끼리 주고받는 우편물이나 상자의 개념이다.
순수하게 데이터를 담고 있다는 점에서 Entity 객체와 유사하지만,
목적 자체가 전달이므로 읽고, 쓰는 것이 모두 가능하고, 일회성으로 사용되는 성격이 강하다.
JPA를 이용하게 되면 Entity 객체는 단순히 데이터를 담는 객체가 아니라 실제 데이터베이스와 관련된 중요한 역할을 하며,
내부적으로 EM(EntityManager)에 의해 관리되는 객체다.
출처
역할 분리를 위한 Entity, DTO 개념과 차이점
문제를 해결하면서 2가지 방법으로 보낼 수 있다는 것을 알게 되었다.
- 위 처럼 registry로 보내기
- registry의 id값으로 보내기
먼저 1번을 얘기하면서 궁금증을 해결해본다.
registry로 보내기
front
1
2
3
4
5
6
7
8
let form_data = new FormData()
form_data.append("comment", $("#comment").val())
form_data.append("nickname", nickname)
form_data.append("registry", $("#RegistryId").html())
for (let key of form_data.keys()) {
console.log(key, ":", form_data.get(key));
}
front에서 3개의 데이터를 보낸다.
CommentController에서 받는 값을 출력시켜봤다.
front에서 id값만 보냈는데 dto에서는 객체로 보낸다.
Postman으로 실행하기
위 처럼 데이터를 주고 실행을 시켜보니 콘솔에 아래와 같이 찍혀있었다.
콘솔을 통해 바인딩 할때(form에서 객체로 바꿔줄 때) 객체(registry)로 보내면 알아서 id값을 체크한다.
그래서 알아서 db를 갖고 올수 있었던 것이었다.
이를 활용해서 코드를 작성하면 ServiceImpl에서 findById같은 코드를 생략해도 되는 것이다.
→ repository 의존도를 낮춰줄 수 있다.
1
2
Registry registry = registryRepository.findById(comment.getRegistry().getIdx()).orElseThrow();
comment.setRegistry(registry);
그렇게 작성하기 위해선 몇가지 코드가 추가로 작성되어야 한다.
Dto에 Entity를 넣어야 하기 때문에 RequestDto와 ResponseDto가 있어야 하고
RequestDto에는 사용자에게 보여지는 Dto가 아니기 때문에 Entity를 넣어줘도 상관이 없을 듯하다.
또한 예외처리가 필수적으로 이루어져야한다.
게시글이 없는 경우엔 댓글을 작성할 수 없다. → db에 없는 게시글 id를 줄 경우 예외 처리
또한 데이터가 없는 경우에도 댓글을 작성할 수 없다. → id값을 front에서 주지 않을 때 예외 처리
이 경우를 예외처리를 해야 정상적으로 동작하는 것을 볼 수 있을 것이다.
어떻게 작성하냐 선택의 차이인 것 같다.
아래는 registry가 아닌 registry의 id값으로 보내는 방법이다.
registry의 id값으로 보내기
js에서 registry의 id값으로 명시해서 보냈다.
1
2
3
4
5
// js
let form_data = newFormData()
form_data.append("comment", $("#comment").val())
form_data.append("nickname", nickname)
form_data.append("registry.idx",$("#RegistryId").html())
controller
1
2
3
4
@PostMapping("/comment")
public Comment setComment(@ModelAttribute CommentDto commentDto) {
return commentService.setComment(commentDto);
}
@ModelAttribute
는 클라이언트로부터 일반 HTTP 요청 파라미터나 multipart/form-data 형태의 파라미터를 받아 객체로 사용하고 싶을 때 사용한다.
@ModelAttribute
로 form에서 registry.idx
로 보내면 해당 값을 받기 위해서 setter를 작성해야한다.
1
2
3
4
// Registry.java
public void setIdx(Long idx) {
this.idx = idx;
}
그 외의 필드 값은 생성자를 작성해야 한다.
registry.idx
로 보냈을 때 성공하려면 id에 대한 setter가 있어야 하며
registry.idx 말고 registry(객체)로만 줬을 때 바인딩 시켜주는 것은 생성자 역할이다.
→ 필드명으로 주고 싶으면 setter고 객체 자체로 주려면 생성자 역할이다 라는 얘기
🐣 id값은 생성자 작성 안해도 된다.( id는 자동으로 생성되는 auto_increment 쓰므로)
registry.idx: 100
과 같이 넣어주려면 Registry클래스에 setter가 있어야 한다.(Comment.java 아님)
👉🏻 Commemt의 외래키에 registry id 넣게 하기 위함
test
idx와 nickname에서는 setter가 맞는지 확인해보자
setter를 작성하고 test를 했더니 아래와 같이 나왔다.
1
2
3
4
5
6
7
public void setIdx(Long idx) {
this.idx = idx;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
1
2
3
4
5
6
//js
let form_data = newFormData()
form_data.append("comment", $("#comment").val())
form_data.append("nickname", nickname)
form_data.append("registry.idx",$("#RegistryId").html())
form_data.append("registry.nickname",$("#user").text())
post는 되었지만 registry 객체에 title, main이 null로 뜨는 것을 볼 수 있다.
setter로 id, nickname 값만 넣어서 나머지 필드는 null로 뜨는 것이다.
registry.~~
는 setter로 동작해서 값을 넣어준다.라는 것을 알게 되었다.
만약 idx에 대해서만 setter를 설정하면 js에서 idx값과 nickname을 전달해도 idx 값만 받아온다.
수정
db에는 fk값인 registryId
만 있으면 되므로 권장하는 방향인 Dto에 Entity가 아닌 Long으로 받게 수정해본다.
js에서 전달할 값을 registryIdx
로 설정했다.
1
2
3
4
let form_data = new FormData()
form_data.append("comment", $("#comment").val())
form_data.append("nickname", nickname)
form_data.append("registryIdx",$("#RegistryId").html()) // 👈🏻
CommentDto
이를 받을 dto에서도 똑같이 registryIdx
로 작성한다.
1
2
3
4
5
public class CommentDto {
private String nickname;
private String comment;
private Long registryIdx; // 👈🏻
}
Comment
entity에서 dto에 대한 생성자를 수정한다.
필드에 registryIdx
가 없기 때문에 this를 사용하지 않는다.
1
2
3
4
5
public Comment(CommentDto commentDto) {
this.nickname = commentDto.getNickname();
this.comment = commentDto.getComment();
Long registryIdx = commentDto.getRegistryIdx(); // 👈🏻
}
idx를 가지고 comment에 set 해줘야 하므로 Registry에 idx값만 담는 생성자를 작성해야겠지? 라고 생각하면 안된다🙅🏻♀️
id는 생성자 만들지 않는다.
Repository를 이용해서 findById()
후 해당 id의 Registry를 가져와서 comment에 set 해주면 된다.
find vs get
그런데 findById()
보다 더 나은 방법이 있다는 것을 해당 블로그 글을 보고 알게 되었다.
findById vs getReferenceById 글을 따로 정리했다.
요약하자면 getReferenceById()
는 실제 테이블을 조회하는 대신 프록시 객체만 가져오기 때문에
ID값을 제외한 나머지 값을 사용하기 전까지는 SELECT 쿼리가 발생하지 않는다.
Optional<T> findById(ID id)
: (탐색함) 탐색 결과가 없을 수 있고 내부 예외 발생이 없다.
T getReferenceById(ID id)
: (가져옴) 가져오려는 대상이 없을 시, 내부에서 예외 발생한다. (EntityNotFoundException)
getReferenceById는 EntityManager#getReference 를 사용하며, 조회된 entity 내부값 접근 전까지 lazy loading 처리한다.
ServiceImpl
1
2
3
4
5
6
Registry registry = registryRepository.getReferenceById(commentDto.getRegistryIdx());
Comment comment = Comment.builder()
.comment(commentDto.getComment())
.nickname(commentDto.getNickname())
.registry(registryId)
.build();
마치면서…
왜 js에서 registry로 값을 보냈을 때 어떻게 id인지 알고 db를 갖고오는지 정확히 알지 못했는데
객체로 보낼 때 id값을 받아서 db를 가져오는 것을 보고 2가지 방법으로 댓글을 post하는 방법을 알아냈다.
또한, 이번 기회에 findById()
말고도 getReferenceById()
가 있다는 것을 알게 되었다.
reference
JPA 의 getById() vs findById()
[JPA] 연관 관계를 가진 엔티티 저장 방식 개선 (불필요한 select문 제거)
find vs get (네이밍 컨벤션과 JPA에서의 내부 동작 차이)
연관관계 목차