JPA

이번 미션에서는 JPA(Java Persistence API)에 대해 집중적으로 학습하기 위한 미션으로,
QnA 게시판의 백엔드 로직을 JPA 기반으로 구현하는 것이 목적이었다.

각 단계별로 엔티티 매핑, 연관 관계 매핑, 질문 삭제하기 리팩터링으로 구성되어 있었고,
엔티티 매핑, 연관 관계 매핑 단계에선 각 단계의 제목대로 요구사항이 주어졌다.

QnA 서비스는 User, Question, Answer, DeleteHistory 총 4개의 도메인으로 이루어져 있었으며,
마지막 질문 삭제하기 리펙터링의 경우 ‘본인이 작성한 질문과 답글만 삭제할 수 있고, 이 이력을 DeleteHistory에
저장해야 한다.’
라는 요구사항이었다.

하지만 삭제라고 해서 DB에서 row가 삭제되는 것이 아니라, 삭제 상태(deleted = true)로 만드는 것이라,
DB에서는 update가 되는 것이고, delete_history 테이블에는 insert가 되는것이므로 도메인의 비즈니스 로직에서 잘 처리해줘야 한다.
또한, 이전 단계에서 엔티티 매핑과 연관 관계 매핑을 잘 처리해놨어야 3단계에서 비교적 매끄럽게 진행이 가능하다.

3단계를 진행하면서 이전에 학습했던 TDD, Refactoring 방법등을 적극 활용하여
이번 미션을 무난하게 진행 할 수 있었다

회고

이번 미션에서 잘 한 점

엔티티 매핑 및 연관관계 매핑

QnA의 시스템 중에서 가장 중요한 연관관계 매핑을 간단히 그려보면 아래와 같다.

DeleteHistory라는 엔티티의 책임에 대한 고민이 많았다.
속성중에 content_id 라는 Question / Answer의 id를 가지고 있었으나,
이 엔티티의 역할로 보면 History라는 측면에선 연관 관계를 맺지 않았다.

이렇게 설계한 연관 관계가 3단계에서 꽤나 유연하게 설계할 수 있는 밑거름이 되었다.
엔티티 매핑과 연관관계 매핑 단계가 JPA를 잘 알았던 사람이라면 금방 끝낼 수 있는 미션이지만,
각 엔티티의 역할과 책임을 적절히 고려하여 연관관계 매핑을 해줘야 한다는 점 에서
꽤나 오랫동안 고민하며 설계했던 것이 도움이 많이 되었다.

어려웠던 점

엔티티 설계에 대한 깊은 고민

Question 엔티티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Table(name = "question")
public class Question extends BaseDateTimeEntity {
private static final boolean CONTENT_DELETED_FLAG = true;
private static final String EXCEPTION_MESSAGE_FOR_CANNOT_DELETE = "질문을 삭제할 권한이 없습니다.";
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
@Lob
private String contents;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer_id", foreignKey = @ForeignKey(name = "fk_question_writer"))
private User writer;
@Embedded
private Answers answers;
private boolean deleted = false;
...
}
Answer 엔티티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
@Table(name = "answer")
public class Answer extends BaseDateTimeEntity {
private static final boolean CONTENT_DELETED_FLAG = true;
private static final String EXCEPTION_MESSAGE_FOR_CANNOT_DELETE = "질문을 삭제할 권한이 없습니다.";
private static final String EXCEPTION_MESSAGE_FOR_DUPLICATION_QUESTION = "이미 다른 질문에 달려있는 답글입니다.";
@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;
@Lob
private String contents;
@Column(nullable = false)
private boolean deleted = false;
...
}

두 엔티티를 보면 공통적으로 content, writer, id, deleted 라는 속성을 가지고 있다.

나는 처음에 중복 코드를 최대한 배제하자 라는 생각으로 공통된 속성을 하나의
BaseContentEntity라는 @MappedSuperclass로 분리하여, 이를 상속받는 구조로 구현을 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@MappedSuperclass
public abstract class BaseContentEntity extends BaseDateTimeEntity {
private static final boolean CONTENT_DELETED_FLAG = true;
private static final String EXCEPTION_MESSAGE_FOR_CANNOT_DELETE = "질문을 삭제할 권한이 없습니다.";
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Lob
private String contents;
@Column(nullable = false)
private boolean deleted = false;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer_id")
private User writer;
...
}

하지만, 리뷰어님의 피드백은 BaseContentEntity는 과도한 책임을 가지고 있는 슈퍼클래스가 되어 버렸고,
추가 확장에 유연하지 못할 것 같다는 의견을 주시며 내 생각이 어떠한지 물어보셨다.😅

각 속성에 대해 다시 한번 고민 해 본 결과

  • id : 변경에 유연하지 못하고, MappedSuperclass 보다는 이를 상속받는 근본 Entity가 가지고 있는 것이 더 바람직하다.
  • writer : Question / Answer의 writer는 나중에 다른 비즈니스 로직이 적용될 가능성이 높다.
  • deleted : 이미 3단계 미션에서 요구사항이 다름(삭제에 필요한 비즈니스 로직이 다르게 전개됨).
    따라서, 만약 공통 속성으로 뺀다면 content만 남게 된다.

그렇다면 추가로 고민해볼 것이, content가 공통 속성으로 뺄만한 가치가 있는가?
–> 그렇지 않다는게 나의 결론이었다. content는 공통 속성으로 뺄만한 복잡한 비즈니스 로직이 없거니와,
만일 뺀다고 했을때 구현 복잡도가 오히려 증가한다는 결론을 내리고, BaseContentEntity를 아예 삭제하는 식으로 다시 원복했다..😭

이제와서 다시 생각해보면, 단일 책임의 원칙으로 고려해봤을 때
BaseContentEntitycontent와 관련된 역할책임만 가지고 있어야 됐다.

느낀점

이번 미션을 진행하면서 가장 크게 느낀점은 JPA는 DataBase를 위한 기술이 아니라, 객체지향설계를 위한 기술 이라는 것이다.

  • 각 속성을 어떻게 매핑하는지
  • 연관 관계는 어디서 어떤 방향으로(DB의 관계에선 방향이 없다.) 매핑해야 하는지
  • 시스템의 설계 상 어떤 임의의 도메인이 다른 도메인과 자주 사용되는지에 따라 변경이 예상되는 부분을 고려
    하는 등등.. 여러 가지 측면에서 봤을 때, JPA도 결국은 객체지향을 위한 기술 이라는 것이었다.

이 부분을 크게 간과하고 이번 미션에 임했던 것이 많은 시행착오를 야기시켰다.
JPA 사용 시 엔티티 매핑, 연관 관계 매핑단계 에서는 반드시 객체지향적인 설계를 동반해야 한다!

생각해보자

이번 미션을 완료 한 시점에 ‘좋은 객체지향 설계란 무엇인가?’ 라는 의문이 생겼다.
이 질문에 대한 좋은 답을 내고 있는 책을 찾게 되었다.
바로 ‘객체지향의 사실과 오해 - 조영호 지음(위키북스)’ 이다.

개인적으론 이 책을 읽으며 책의 제목 그대로
내가 객체지향에 대해 잘못 알고 있는 부분이 많았다는 것이다.
위 책에 대한 독후감은 다른 포스트에서 좀 더 자세히 다뤄 볼 예정이다.

또한, 기술적으로 바로 도입해볼 수 있는 것들은..
적어도 어떤 개발이라도 객체지향의 5대 원칙(SOLID)를 체크리스트로 만들어서,
원칙을 잘 지키고 있는지를 체크하고 넘어가도 좋을 것 같다는 생각이 든다.🧐

JPA를 학습하기 위한 How에 대한 추천