지금까지는 JPA_TEMPLATE 파일을 분석해보면서 JPA에 대해 학습해보았습니다. 이제부터는 지금까지 배운 내용을 토대로 Controller-Service-Repository를 생성해보고 직접 API까지 작성해봅시다. 또한 엔티티 간의 관계 매핑하는 방법에 대해서도 알아보겠습니다. 이번 포스팅에서 만들어 볼 Entity는 Board Entity입니다.
1. 멤버와 게시글 간의 매핑관계
1) @ManyToOne
한 명의 멤버가 여러 개의 게시글을 작성할 수 있기 때문에 멤버:게시글=1:N 관계임을 알 수 있다. 여기는 게시판을 정의하는 클래스이므로, Board 클래스 입장에서는 다대일 즉, JPA 어노테이션으로 @ManyToOne이라 해야 할 것이다.
2) 관계 매핑
관계 매핑이 되어있는 테이블에 대해 두가지를 설정해주어야 하는데, 바로 fetch 타입과 join column이다.
① fetch타입
- 특별한 상황이 아니라면 Lazy Loading으로 지정해야 한다.
- Lazy Loading이란, Eager Loading과 반대되는 개념으로 필요한 데이터를 실제로 사용하기 전까지 로드를 지연시키는 방식이다. 초기 로딩속도가 빠른 반면 쿼리의 수가 많아져 성능저하가 발생할 수 있다.
- Eager Loading은 초기 쿼리 실행 시에 관련된 모든 데이터를 한 번에 로드하는 방식이다. 쿼리의 수는 줄어들지만, 관련 데이터를 전부 가져오기 때문에 불필요한 데이터를 로드할 수도 있다.
② join column
- 두 개의 테이블을 결합할 때 사용되는 기준 열을 의미한다. 이는 테이블 간의 관계를 설정하는 요소에 해당한다.
- name의 속성값으로 매핑할 외래키의 컬럼명을 넣는다. 여기서 name 속성의 값이 멤버 테이블의 필드명이어야 하는 줄로 잘못 알고 있는 사람도 많다. name은 단순히 board 테이블에 멤버 id 필드를 포함할건데, 그 컬럼명을 무엇으로 지정할지 설정하는 것이다. 이해를 돕기 위해 Member 테이블에서는 필드명으로 id를 사용했으나 @JoinColumn에서는 member_id를 사용했다.
- 그러면 Board 테이블에서 외래키로 사용할 Member 테이블의 필드를 지정하지 않았다는 것인데 어떻게 컴파일이 되었을까? 원래대로라면 @JoinColumn의 속성으로 referenceColumnName을 사용하여 어떤 필드를 외래키로 사용할지 지정해주어야 한다.
- 다만, 이를 생략할 경우 참조하는 테이블의 기본키를 default로 가져온다. 어차피 Member 테이블의 기본키를 외래키로 사용할 것이었기 때문에 굳이 이 속성을 포함하지 않았던 것이다. 이 어노테이션이 붙어야 할 필드는 당연히 관계매핑할 Member 테이블이 되겠다.
3) 주종 관계
연관관계에서 중요한 개념이 또 있는데 바로 주종관계이다. 연관관계의 주인은 외래키를 포함하는 테이블이다. 설명이 이해가 안 간다면, 단순히 일대다관계에서 항상 '다'가 연관관계의 주인이라고 생각하면 된다. 즉, 여기서는 Board 테이블이 주인인 것이다.
이는 조금만 생각을 해도 이해할 수 있을 것이다. 멤버가 주인이라면 어떨까? 멤버의 필드에 Board의 ID가 있어야 하는데, 지금처럼 Board 밖에 없는 경우야 큰 차이가 없겠지만, 만약 Board 같은 테이블이 많이 생긴다면, Member에서 이를 관리하기 어려워질 것이다. 또한 데이터를 한눈에 알아보기도 어려울 것이다. 다시 말해 관계형 데이터베이스의 이점을 거의 이용하지 못하고 있는 것이다.
따라서 Borad를 주인으로 한다는 의미에서 Member 테이블에 mappedby 설정을 해주어야 한다. mappedby를 굳이 해석하자면, "이 테이블은 주인이 아닙니다"라는 뜻이다.
Member 클래스 입장에서 Board와의 관계는 일대다이므로 여기서는 @OneToMany를 사용했다.
① mappedBy
- 여기서 주의할 점은 mappedBy="member"의 member는 Member 테이블이 아니라, Board 테이블의 member 필드라는 것이다.
- 즉, mappedby의 속성 값은 주인 테이블에서 외래키를 가져올 테이블을 가리키는 필드명이다. 아래 캡쳐본에서 보이는 member가 그것이다.
② cascade
- cascade는 엔티티간 관계매핑에 사용되는 옵션을 말하는데, 그 중 CascadeType.All은 특정 엔티티의 변경 작업이 연관된 다른 엔티티에도 영향을 주도록 설정한다는 의미이다. 다시 말해, 부모 엔티티의 모든 변경 작업(추가, 수정, 삭제)이 자식 엔티티에도 영향을 준다는 것이다.
- cascade타입도 특별한 상황이 아니라면 항상 CascadeType.All로 설정하면 된다.
③ orphanRemoval
- 부모를 잃어버린 자식 엔티티를 지울 것인지 여부를 설정한다.
- cascade를 설명하면서 잠깐 부모 자식관계에 대해 언급은 했지만, 아직 자세히 설명은 하지 않았다. Board 클래스가 부모일까? Member 클래스가 부모일까? 주종관계에서는 Board가 주인이기 때문에 Board가 부모이지 않을까 생각할 수도 있지만, 1:N 관계매핑에서 부모는 항상 1이다. 즉, Member 클래스가 부모이다.
- 상식적으로 이해해보자. Board가 부모라면 게시글이 지워질 때 멤버도 삭제할 것인지를 orpahnRemoval로 설정한다는 것인데, 상식적으로 말이 안 된다. 멤버가 삭제됐을 때 게시글도 지울지에 대해 설정하는 것이 훨씬 자연스럽다.
- 또한 한 부모가 여러 자식을 갖는 건 가능하지만, 한 자식이 여러 부모를 가질 수 없다는 이치도 적용할 수 있겠다.
- 즉, 1:N관계 매핑에서 주인은 N, 부모는 1이 된다. 우리는 orphanRemoval속성을 true로 설정함으로써 유효하지 않은 데이터의 존재 가능성을 낮추고, 데이터의 일관성을 유지할 수 있게 된다.
2. create board API
지금까지의 내용을 모두 이해했다면 아래의 코드 내용은 쉽게 이해할 수 있을 것이다. 아래는 BoardRepository의 코드를 나타낸다.
아래는 BoardController의 코드이다.
PostBoardReq는 아래와 같이 정의되었다.
즉, email과 title, content를 요청 body에 Json타입으로 입력해주어야 한다. 만약 Size와 NotBlank를 지키지 않으면, @Validated에 의해 실행되지 않는다. 설명을 간략히 하기 위해 service의 코드는 직접 확인해보기 바란다. 아래는 API의 테스트 결과이다.
title의 size가 20을 초과하여 API가 동작하지 않는다. 이를 형식적 Validation이라 한다. validation을 통과하지 못했을 때 반환 값을 설정하는 방법도 있는데, 궁금하다면 직접 찾아보기 바란다.
제목과 내용 모두 유효성을 만족할 때에만 API가 동작하는 것을 확인할 수 있다.
게시글의 조회, 수정, 삭제에 대한 API는 다음 포스팅에서 다루기로 하자.