이번 2주차 미션은 JPA를 학습하는 한 주였습니다.
매번 느끼는 거지만, JPA 는 학습한다고해서 아는게 아닌것 같습니다.
JPA는 진짜 백문이불여일타 의 대표적인 모범예제이지 않을까 싶습니다.
작업 내역
[1단계] 엔티티 맵핑하기
create table answer
(
id bigint generated by default as identity,
contents clob,
created_at timestamp not null,
deleted boolean not null,
question_id bigint,
updated_at timestamp,
writer_id bigint,
primary key (id)
)
위와 같은 SQL 을 그대로 엔티티 만들기
@Entity
@Where(clause = "deleted = false")
public class Answer extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer_id",
foreignKey = @ForeignKey(name = "fk_answer_writer"))
private User writer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "question_id",
foreignKey = @ForeignKey(name = "fk_answer_to_question"))
private Question question;
@Embedded
private Contents contents;
@Embedded
private Deleted deleted = new Deleted(false);
...
}
https://github.com/next-step/jwp-qna/pull/22
[2단계] 연관관계 맵핑
연관관계 SQL 보고 JPA 맵핑하기
alter table answer
add constraint fk_answer_to_question
foreign key (question_id)
references question
alter table answer
add constraint fk_answer_writer
foreign key (writer_id)
references user
alter table delete_history
add constraint fk_delete_history_to_user
foreign key (deleted_by_id)
references user
alter table question
add constraint fk_question_writer
foreign key (writer_id)
references user
@Entity
@Where(clause = "deleted = false")
public class Answer extends BaseEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer_id",
foreignKey = @ForeignKey(name = "fk_answer_writer"))
private User writer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "question_id",
foreignKey = @ForeignKey(name = "fk_answer_to_question"))
private Question question;
@Embedded
private Contents contents;
...
}
@Where(clause="deleted=false") 라는 키워드는 Soft delete 할 수 있게 해주는 Tips 같은 느낌..
https://github.com/next-step/jwp-qna/pull/58
[3단계] 질문 삭제하기 리팩토링
대망의 3단계 리팩토링
@Transactional
public void deleteQuestion(User loginUser, Long questionId) throws CannotDeleteException {
Question question = findQuestionById(questionId);
if (!question.isOwner(loginUser)) {
throw new CannotDeleteException("질문을 삭제할 권한이 없습니다.");
}
List<Answer> answers = question.getAnswers();
for (Answer answer : answers) {
if (!answer.isOwner(loginUser)) {
throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.");
}
}
List<DeleteHistory> deleteHistories = new ArrayList<>();
question.setDeleted(true);
deleteHistories.add(new DeleteHistory(ContentType.QUESTION, questionId, question.getWriterId(), LocalDateTime.now()));
for (Answer answer : answers) {
answer.setDeleted(true);
deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriterId(), LocalDateTime.now()));
}
deleteHistoryService.saveAll(deleteHistories);
}
위 코드는 Question, Answer, DeleteHistory 의 역할이 deleteQuestion 메소드 한 곳에 집중되어, 테스트 하기 어려운 코드입니다. 3번째 미션은 위 코드를 각 도메인별로 책임을 분배하고, 그렇게 함으로써 테스트하기 좋은 코드를 작성하는 것을 목표합니다.
https://github.com/next-step/jwp-qna/pull/93
알게된 부분
1. MappedBy 의 활용
이전에는 MappedBy 라는 것을 @ManyToOne 맵핑관계를 가진 엔티티라면 무조건 사용해야되는 거라고 생각했습니다. 또한 MappedBy 를 사용하면 주인을 정하는 것? 이라는 말을 많이 했는데, 사실 주인을 정한다는 말보다, 해당 관계의 외래키를 누가 관리할 것인가가 더 맞는 표현인것 같습니다.
mappedBy를 사용하지 않으면 어떻게 될까요?
JPA에 의해서 생성되는 DDL 을 살펴보면, 아무런 차이도 없습니다. `mappedBy` 는 양방향 연관 관계를 할 수 있도록도와주는 키워드입니다.
연관관계의 주인을 왜 정할까요?
객체 지향과, DBMS 에서 오는 차이점에서 연관관계의 주인을 정해야 한다는 니즈가 발생합니다. 객체 지향에서는 양방향 연관 관계라는 것이 없습니다. 그저, 서로 다른 단방향 연관 관계 2개를 양방향인 것처럼 보이게 할뿐입니다.
또한, 연관관계의 주인(외래키의 관리자)만이 데이터베이스 연관 관계와 매핑되고 외래 키를 등록, 수정, 삭제할 수 있습니다. 주인이 아닌 쪽은 읽기만 할 수 있습니다.
데이터베이스 테이블의 @ManyToOne, @OneToMany 에서는 항상 Many 이 외래 키를 갖습니다.
2. @Embedded 사용할 때 주의사항
개발하면서 겪었던 부분인데, 마침 https://jojoldu.tistory.com/559 여기에서도 다루고 있어 공유드립니다.
@Embedded 를 사용하면서, 초기값을 설정하기 위해 다음과 같은 코드를 작성하는 경우가 있었습니다.
@Embeddable
public class Answers {
public static Answers EMPTY = new Answers();
@OneToMany(mappedBy = "question",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true
)
@Column(name = "answers")
private List<Answer> value = new ArrayList<>();
...
}
위 코드 중 EMPTY 와 같이 널 객체(Null Object) 패턴을 오용하는 사례이죠
@Entity
@Where(clause = "deleted = false")
public class Question extends BaseEntity {
@Embedded
private Answers answers = new Answers(new ArrayList<>());
@Embedded
private Answers answers = Answers.EMPTY;
...
}
Answers 를 사용하는 클라이언트 입장에서는 Question 의 new Answers(new ArrayList<>()) 와 Answers.EMPTY; 보면 차이를 확 알수 없지만, 문제는 Answers.EMPTY; 으로 등록된 Answers 를 JPA 로 등록하게 되면 문제가 발생한다.
어떤 문제가 발생할까?
Anwers.EMPTY 가 static 으로 되어있엇기 때문에, Anwers를 가진 Question Entity를 A,B 를 선언한 뒤에 A에 Answers 에 대한 값을 Set 한 뒤에 A 를 세이브하자. 이때까지는 문제가 없다. 그러나, B 엔티티에서는 Answers 에 대한 값을 저장하지도 않았지만- B의 값을 세이브하려고 보면 A에서 Set 한 값이 저장되어있는 것을 발견할 수 있습니다.
3. cascade 전략 기억하기
4. @Where(clause = "deleted = false") 활용하기
@Entity
@Where(clause = "deleted = false")
public class Answer extends BaseEntity {
...
}
Soft Delete 라고 해서, 이렇게 하면 데이터 조회시 FindById 로 ID 를 특정지어 조회하는 것을 제외하고, findAll 과 같은 경우 데이터가 필터링되서 조회된다.
https://levelup.gitconnected.com/spring-boot-soft-delete-functionality-with-hibernate-f5ee8c24c99f
[참고자료]
https://gmlwjd9405.github.io/2019/08/12/primary-key-mapping.html
'가치관 쌓기 > 개발 돌아보기' 카테고리의 다른 글
[우아한테크코스Pro] 그럴듯한 서비스 만들기(network) - 1 [4/9] (0) | 2021.06.17 |
---|---|
[우아한테크코스Pro]인수테스트 주도 개발[3/9] (0) | 2021.06.15 |
[우아한테크코스Pro]로또 구현(테스트 주도 개발) - 못다한 이야기 (0) | 2021.06.03 |
[우아한테크코스Pro]로또 구현(테스트 주도 개발)[1/9] (1) | 2021.05.28 |
우아한테크코스Pro 프리코스 후기 (1) | 2021.05.17 |
댓글