Spring/괴발개발

[괴발개발] 덕지덕지 개발記 (#2)

오잎 클로버 2023. 2. 20. 11:07
728x90

본격적으로 데이터베이스 설계를 해보고자 한다. 엔티티는 크게 유저(member)와 메모지(memo)로 2개이다.

하지만, 현재 S3와 같이 외부 스토리지가 따로 준비가 되어있지 않은 관계로 유저의 배경화면을 따로 저장을 할 수 없다.

그렇기에 이를 데이터베이스에 따로 저장하여 사용할 계획이다. (배경 엔티티가 추가로 생긴다.)

위와 같이 ERD(엔티티 관계 다이어어그램)으로 표현할 수 있다. 물론 JPA 를 사용하는 경우, Join Table 이나 프록시 등으로 브릿지 테이블은 개발자가 직접 구현하지 않아도 된다. (실수로 전부 비식별키로 해버렸다.)

여기서 메모지 엔티티를 보면 칼럼이 굉장히 많으며, 몇몇 칼럼들이 부분적 종속이 존재한다.

(뭔가 더 깔끔하게 ERD를 작성하고 싶었지만, ERDCloud 에는 양방향을 표현하는 방법이 없어서 위와 같이 작성하는 것이 최선이 것 같다.) (그렇다고 위와 같이 구성하면 다대다로 보이기도 한다...) (다음부터는 단방향으로만 구현하고 메모하는 편이 훨 나을 것 같다.)

이제 직접 JPA를 사용하여 엔티티를 작성해보자.

@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(indexes = @Index(name = "unq_email", columnList = "email", unique = true))
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true, updatable = false)
    private String email;

    @OneToMany(mappedBy = "member", orphanRemoval = true)
    private List<Memo> memos = new ArrayList<>();

    @OneToOne(mappedBy = "member", orphanRemoval = true)
    private Background background;

}

email은 자주 조회되는 데 사용될 것이고, 논리 식별은 보통 email로 사용할 것 같아서 unique index 로 설정하였다.

(물론 상황에 따라 보조 인덱스가 필요할 수도 있지만, 이메일 칼럼은 유일성이 보장되어야 하기에 unique index 로
    설정하였다.)

그리고 메모하고는 일대다 양방향으로 매핑해주었다. 원래는 메모 엔티티로만 다대일 단방향을 생각하였으나,
  회원 엔티티가 부모 엔티티가 될 필요가 있다고 생각이 들어 양방향으로 바꾸었다.

(※일대다 양방향과 다대일 양방향은 개인적으로 유사하다고 생각한다. 주체가 달라지면 달라질 뿐이다.)

그리고 배경 엔티티와는 일대일 양방향이다. 이 또한 단방향으로 설정하고자 했으나, 메모 엔티티와 같은 이유로 양방향
  매핑을 택했다.

@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Background {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 128)
    private String directory;

    @Column(nullable = false, length = 64)
    private String filename;

    @OneToOne(optional = false)
    private Member member;

}

배경 엔티티는 회원 엔티티와 일대일 양방향이자, 주인이며 식별관계이다. 회원 엔티티 없이는 생성하지 못한다.

@Entity
@Getter
@Builder
@EntityListeners(AuditingEntityListener.class)
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Memo {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 10000)
    private String content;

    @LastModifiedDate
    private LocalDate lastModifiedAt;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Member member;

    @OneToOne(mappedBy = "memo", orphanRemoval = true)
    private MemoDetail memoDetail;

    @OneToOne(mappedBy = "memo", orphanRemoval = true)
    private MemoDate memoDate;

}

EntityListeners 를 사용하여 insert, update, delete 를 감지하여, 특정 행동을 하도록 구현이 가능하다.

이를 통해 @LastModifiedDate 어노테이션으로 insert 혹은 update가 발생할 때 날짜를 갱신할 수 있다.

회원을 식별관계로서 사용하고 있으며, 지연로딩을 통해 가져오도록 하였다.

메모의 디테일한 설정들은 MemoDetail 엔티티에서 관리하고 있으며, 날짜 설정은 MemoDate 엔티티를 통해 관리한다.

디테일 설정들이나 날짜들은 굳이 있을 필요가 없기에 비식별관계이다. 반대로 디테일 설정 엔티티와 날짜 엔티티들은 메모를 식별관계이다.

또, 부모 엔티티인 메모 엔티티가 삭제될 경우, 자식 엔티티(MemoDetail.class, MemoDate.class)들이 자동으로 삭제된다.

@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemoDate {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @MapsId
    @OneToOne(optional = false)
    @JoinColumn(name = "memo_id")
    private Memo memo;

    @Column(nullable = false)
    private LocalDate startDate;

    private LocalDate endDate;

}
@Entity
@Getter
@Builder
@SQLDelete(sql = "update memo_detail set deleted = true where id = ?")
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemoDetail {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @MapsId
    @OneToOne(optional = false)
    @JoinColumn(name = "memo_id")
    private Memo memo;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private Font font;

    @Builder.Default
    @Column(nullable = false)
    private boolean deleted = false;

    public void changeFont(Font font) {
        this.font = font;
    }
}

@MapsId 어노테이션은 @Id로 지정한 칼럼에 일대일 혹은 다대일 관계를 매핑시켜주는 역할을 한다.

@SQLDelete 어노테이션은 해당 엔티티에게 삭제 쿼리가 발생했을 때, 삭제 대신 행동할 쿼리를 작성할 수 있는
  어노테이션이다. 이를 통해 soft-delete를 진행한다. 그리고 Spring Scheduler 를 통해 한 번에 삭제할 수 있도록 한다.

 

WARNING

 WARN 26372 --- [           main] org.hibernate.orm.deprecation            : HHH90000021: Encountered deprecated setting [javax.persistence.sharedCache.mode], use [jakarta.persistence.sharedCache.mode] instead

이라는 로그가 남았다. 스프링은 기존 2.X.X 버전까지는 javax 패키지를 기본으로 사용하였는 데, 그 여파로 3.X.X 가 되어서 deprecated가 되어 jakarta를 사용하라는 경고 로그이다. 해결방안은 설정해주면 된다.

spring:
  jpa:
    properties:
      jakarta:
        persistence:
          sharedCache:
            mode: ALL

자동완성이 되지 않으나, 필자의 IntelliJ Ultimate 에서는 제대로 작성하니 오류가 발생하지 않았다.

 

 

이상입니다.