본문 바로가기
가치관 쌓기/개발 돌아보기

[우아한테크코스Pro]QnA 서비스(JPA)[2/9]

by simplify-len 2021. 6. 6.

 

이번 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

 

[Step1] 엔티티 매핑 by LenKIM · Pull Request #22 · next-step/jwp-qna

안녕하세요. 리뷰어님, JPA 1단계 코드 리뷰 요청드립니다 😃 학습테스트를 작성하라는 요구사항이 있는데, 어떤 테스트 코드를 작성해야 하는지 몰라, 간단히 Repository 에 잘 저장되는지 확인하

github.com

[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

 

[Step2] 연관 관계 매핑 by LenKIM · Pull Request #58 · next-step/jwp-qna

안녕하세요 ㅎㅎ 리뷰어님. JPA을 학습하면서 하려니, 생각만큼 개발의 속도가 붙지 않네요. 보내주신 피드백 감사합니다 : ) 이번 코드의 피드백도 잘 부탁드립니다. 이번에 개발하면서 일부 어

github.com

 

[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

 

[Step3] 질문 삭제하기 리팩터링 by LenKIM · Pull Request #93 · next-step/jwp-qna

안녕하세요 : ) 벌써 3단계 질문 삭제하기 리팩터링이네요. 아래 TODO 리스트를 작성하고 최대한 맞쳐서 개발하고자 했습니다. 사용되지 않는 getter / setter 제거함으로써 최대한 변경에 닫혀있도록

github.com

 

알게된 부분

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 전략 기억하기

https://joont92.github.io/jpa/CascadeType-PERSIST%EB%A5%BC-%ED%95%A8%EB%B6%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0/

 

[jpa] CascadeType.PERSIST를 함부로 사용하면 안되는 이유

엔티티의 자식에 CascadeType.PERSIST를 지정할 경우 JPA에서 추가적으로 수행하는 동작이 있고, 이 때문에 예상치 못한 사이드 이펙트가 발생할 수 있으므로 이를 남겨두고자 한다. 일단 기본적으로 c

joont92.github.io

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

 

Spring Boot: Soft Delete functionality with Hibernate

In the previous article, we have discussed the Web and Stereotype annotations and seen the examples with code. In this article, I will…

levelup.gitconnected.com

 

[참고자료]

https://gmlwjd9405.github.io/2019/08/12/primary-key-mapping.html

 

[JPA] 기본키(PK) 매핑 방법 및 생성 전략 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

댓글