이전에 새로 설계한 모델에 맞춰서 JPA 의 엔티티도 새로 구현하였다.
사실 DDL 을 먼저 작성해서 인덱스나 UNIQUE 제약조건 부터 확실하게 정하고 가는게 맞아보이긴 하지만 아직 물리적 모델링을 배운 상태가 아니라서 일단은 엔티티부터 구현하였다.
인덱스는 JPA 엔티티 구현에서는 크게 신경쓰지 않아도 되고, UNIQUE 제약 조건도 굳이 JPA 엔티티에 모두 표현하기보다는 단일 컬럼에 대한 UNIQUE 제약조건 정도만 표현해놓는 편이라서 구체적인 사항들은 나중에 DDL 을 정의하면서 결정해도 충분하다고 판단하였다.
엔티티 설계
첫번째로는 JPA 엔티티의 핵심적인 설계들을 설명하겠다.
일단 엔티티의 내용들은 설계했던 모델과 똑같으므로 굳이 다시 설명하지는 않고, JPA 엔티티를 구현하면서 바뀐 부분 위주로 설명하겠다.
- updated_at 컬럼 NOT NULL 로 변경
- Spring Data JPA의 Auditing 기능을 활용하면 어차피 기본적으로 엔티티가 생성될 때 생성일, 업데이트일 모두 그 생성된 날짜로 초기화 된다.
- 그래서 updated_at 에 NULL 이 들어갈 일이 없으므로 더 명확하게 NOT NULL 로 변경하였다.
- 소개와 관련된 컬럼(bio, description) NOT NULL 로 변경
- 이 부분은 아래에서 따로 더 자세하게 설명하므로 간단한 이유만 말하겠다.
- 소개들은 선택적 필드이긴 하지만 굳이 NULL 과 빈 문자열이 엄격하게 구분될 필요는 없다. 그래서 비어있는 경우라면 단순하게 빈 문자열을 저장하도록 변경하였다.
- 이렇게 되면 소개 관련 컬럼들에 NULL 이 들어갈 일이 없으므로 더 명확하게 모두 NOT NULL 로 변경하였다.
- 유저의 자기소개 컬럼명을 bio 로 변경
- 처음 설계에서는 description 으로 되어있었지만, JPA 엔티티를 구현하면서 다시 보니 조금 어색한 느낌이 있었다.
- 보통 유저의 자기소개는 bio 라는 줄임말로 표현하는 경우가 많고, description 은 상품이나 소설 같은 사람이 아닌 특정 물건의 진짜 설명이라는 느낌이 좀 강해서 더 명확하게 유저의 소개 컬럼은 bio 라는 이름으로 변경하였다.
또한 모델의 설계는 아니지만 코드 입장에서도 약간 변경이 있었다.
- DateEntity 의 이름을 DateAuditEntity 로 변경
- 기존에는 created_at, updated_at, deleted_at 컬럼을 가지는 공통 부모 엔티티를 DateEntity 라는 이름으로 사용하고 있었지만, 다시보니 Date 이라는 이름이 약간 날짜를 가지는 엔티티 라는 느낌이라 너무 추상적인 느낌이 들었다.
- 그래서 조금 더 명확히 날짜에 대한 감사 컬럼을 가지는 엔티티라는 의미로 DateAudit 으로 변경하였다.
- DateEntity 의 deletedAt 필드 다른 클래스로 분리
- DateEntity 에 있던 deletedAt 필드를 SoftDeleteEntity 라는 이름의 클래스를 만들고 그곳으로 이동시켰다.
- 생각해보면 created_at, updated_at 정도의 컬럼을 가지는 모델은 흔하지만, deleted_at 컬럼을 가지고 soft delete 를 구현해야 하는 모델은 다른 날짜 컬럼보다는 조금 더 선택적으로 나오는 경우가 많다.
- 그래서 완전히 공통적인 날짜 감사 컬럼이라고 보기에는 조금 애매하다고 보고, 아예 새로운 클래스로 독립적으로 분리하였다.
- 애초에 역할을 따져보았을때도 deleted_at 은 감사를 위한 컬럼이라기 보다 soft delete 를 구현하기 위한 목적이 더 먼저이므로 책임 분리 관점에서도 분리하는 것이 맞다고 판단하였다.
- 또한 현재 공통 부모 엔티티가 종류별로 DateAuditEntity, SoftDeleteEntity, PublicEntity 3개가 있고, 상속관계는 DateAuditEntity → SoftDeleteEntity → PublicEntity 로 상속되고 있다.
사실 이런 상속을 이용한 공통화는 좀 유연하지는 않아서 나도 VO 기반으로 변경을 하거나 인터페이스 기반으로 변경을 할까 생각은 해보았지만, 일단 인터페이스로 구현하게되면 해당 컬럼의 설정이나 Auditing 기능을 각 엔티티에서 직접 구현해야 하므로 공통화 라는 의미가 많이 사라진다고 생각하였다.
그렇다고 VO 로 만들게 되면, 유연하긴 하지만 그 객체의 필드가 아니라 객체 자체를 갈아끼워야 변경감지가 제대로 작동하므로 오히려 복잡도만 늘리고 버그가 일어날 가능성이 더 크다고 생각하였다.
또한 현실적으로 대부분의 모델들은 created_at, updated_at 정도는 기본으로 가지게 되고, 만약 deleted_at 을 가지는 모델이라면 나머지 날짜 감사 컬럼도 기본으로 가지는 경우가 더 흔하다.
또 public_id 를 가질정도의 모델이라면 서비스에서 꽤 중요한 도메인이라는 의미이므로 대체로 created_at, updated_at, deleted_at 을 모두 가지는 경우가 많다.
그래서 지금 설계로도 충분히 공통화는 가능하다고 판단하였고, 대신 특정 모델들은 created_at 만 가지거나, updated_at 만 가지거나 하는식으로 공통 엔티티를 상속하지 못하는 상황도 있긴 하다.
그러나 이런 경우는 정말 소수기 때문에 그런 모델들에는 그냥 직접 컬럼과 Auditing 설정을 구현해주는게 적절한 균형이라고 생각하였다.
문자열 필드가 비어있을때 NULL VS 빈 문자열
이전에 엔티티 모델 재설계 1편에서는 선택적 문자열 필드가 비어있다면 NULL 로 표현하는 것이 좋겠다고 말했는데, 좀 더 고민해보니 이 부분도 상황에 따라 조금씩 달라질 것 같았다.
일단 소설/회차의 소개, 유저의 자기소개 같은 컬럼은 사실 비어있냐 비어있지 않냐 정도만 판단되면 충분하다. 만약 설명이 비어있는 소설의 수치를 파악해 설명을 작성하도록 유도하는 문구를 넣을 지 말지 결정해야 한다고 해도 그냥 빈 문자열의 수치를 계산하면 된다.
하지만 만약 추천인 코드나 가입시에 특별 코드를 입력하면 포인트를 더 준다거나 하는 비즈니스 요구사항이 있으면, 존재하지 않는다는 것을 단순하게 빈 문자열로 표현하는 것 보다는 NULL 로 표현하는게 더 명확할수도 있다.
예를 들어 !specialCode.isEmpty() 보다는 specialCode != null 이 조금 더 코드가 존재한다는 것을 드러내기 좋다.
그래서 이런식으로 문자열은 상황에 따라 조금 다를 것 같다고 생각하였다. 지금 내 서비스의 소설, 회차, 유저의 소개들은 굳이 NULL 과 빈 문자열을 구분하여 표현할 정도로 비어있다 라는 상태가 중요하지는 않다고 보고, 그냥 비어있으면 무조건 빈 문자열을 넣도록 결정하였다.
사실 이런 부분을 생각하게 된 이유는 선택적 업데이트를 지원하는 도메인 메서드를 만들면서 생긴 문제 때문이었다. 보통 클라이언트에서 수정 요청을 보낼 때 설명이 수정되지 않았다면 요청에 포함하지 않고, 수정되었다면 NULL 혹은 빈 문자열로 필드를 설정해 보내게 된다.
이럴 때 선택적 업데이트를 구현하기 위해 메서드의 description 인자가 업데이트 가능한 상태면 갱신해주고, 아니라면 무시하는 조건문을 넣어야 하는데, != null 조건으로 검사하기에는 설명을 수정하지 않아야 하는 상황에서 description 필드가 NULL 일수도 있어 단순히 비어있는지, 업데이트 하지 않아야 하는지 판단하기 애매하고, 그렇다고 .isBlank 조건으로 검사하기에는 또 업데이트가 되어야 하는 값이지만 비어있는 값일수도 있어 이 조건으로도 애매하다는 생각이 들었다.