250x250
jhs0129
프로그래밍
jhs0129
전체 방문자
오늘
어제
  • 분류 전체보기
    • 자격증
      • SQLD
      • 정보처리기사
    • 프로젝트
      • html csss js - todolist
      • JSP 방명록
      • 졸업작품
    • 공부기록
      • Java
      • Spring
      • Spring Security
      • Algorithm
      • JPA
      • DB
      • Servlet JSP
      • html
      • 기술공유
    • 잡다한 생각

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 스프링 프레임워크
  • oAuth2
  • spring
  • JPA
  • github
  • 스프링
  • EC2
  • Spring Security Login
  • Spring Security
  • codedeploy
  • spring boot
  • spring framework
  • cicd
  • NHN Cloud
  • nhn cloud 강의
  • 스프링시큐리티
  • spring data jpa
  • rest docs
  • AWS
  • 프로젝트

최근 댓글

최근 글

티스토리

반응형
hELLO · Designed By 정상우.
jhs0129

프로그래밍

졸작 도메인 구성
프로젝트/졸업작품

졸작 도메인 구성

2022. 6. 20. 21:09
320x100
반응형

개요

졸작을 위해 만든 프로젝트를 정리

사용기술

  • Spring boot
  • JPA
  • thymeleaf
  • MySQL
  • H2(테스트 용도)

도메인 설계

기본 시간 사용자 정보 작성


위 ERD를 보면 알 수 있듯이 저장되는 거의 모든 엔티티에 시간정보와 사용자 정보를 기본으로 입력을 하도록 설정을 하였다

모든 엔티티에 직접 작성하여 넣을 수도 있지만 하나의 클래스로 도출하여 값만 상속을 받을 수 있도록 설계를 하였다

하지만 어느 엔티티는 시간정보만 다른 엔티티느 시간, 사용자 정보둘다 필요로 하기 때문에 위 문제는 아래와 같이 두개의 클래스로 나누어서 사용을 하였다

@Getter
@MappedSuperclass
public class TimeInfo {

    @Column(name = "CREATED_AT", updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "MODIFIED_AT")
    private LocalDateTime modifiedAt;

    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createdAt = now;
        modifiedAt = now;
    }

    @PreUpdate
    public void preUpdate() {
        LocalDateTime now = LocalDateTime.now();
        modifiedAt = now;
    }
}

@Getter
@NoArgsConstructor
@MappedSuperclass
public class TimeAndPersonInfo extends TimeInfo{

    @Column(name = "CREATED_BY")
    private String createdBy;

    @Column(name = "MODIFIED_BY")
    private String modifiedBy;

    public TimeAndPersonInfo(String createdBy) {
        this.createdBy = createdBy;
        this.modifiedBy = createdBy;
    }
}

엔티티 작성

어려웠던 점

이전에 공부를 할 때는 항상 @GeneratedValue만을 사용하여 DB에서 정해주는 임의의 Auto Increment값(인조키)을 사용하여 공부를 해왔지만 이번에는 몇몇 엔티티에서는 자연키, 혹은 복합키를 사용하도록 설정을 하여 해당 부분에서 약간의 어려움이 있었다

자연키 설정 시 주의사항

우선 Spring data JPA를 사용하여 저장하는 로직을 살펴보면 하나의 문제점이 있다

public String join(User user) throws DuplicatedUserException {
    userRepository.save(user);
    return user.getId();
}

위 코드는 추가적인 검증로직은 제거하고 간단히 저장하고 아이디를 반환하는 서비스 계층의 로직이다

단순히 코드만 보면 저장을 하는 것 같아 보이지만 실행을 하게 되면 select 후 insert 쿼리가 나가는 것을 확인 할 수 있다

왜 이렇게 두개의 쿼리가 같이 나가는지 내부 코드를 확인해 보면 엔티티가 새로운게 아니라고 판단되어 persist가 아닌 merge(병합)이 이루워졌기 때문이다

select 쿼리 후 insert 쿼리 실행

@Transactional
@Override
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

간단하게 debug를 통해서 isNew가 어디서 실행이 되는지 확인을 해보자

debug isNew 실행시점 확인

Persistable을 상속받은 JpaPersistableEntityInformation에서 실행이 되는 것을 확인 할 수 있고 아래와 같은 코드가 실행이 되는 것을 확인 할 수 있다

@Transient
@Override
public boolean isNew() {
    return null == getId();
}

EntityInformation을 어떤 객체로 주입할지 담당하는 메소드
Persistable을 구현하면 구현한 Entity객체로 판단

@SuppressWarnings({ "rawtypes", "unchecked" })
public static <T> JpaEntityInformation<T, ?> getEntityInformation(Class<T> domainClass, EntityManager em) {
    Metamodel metamodel = em.getMetamodel();

    if (Persistable.class.isAssignableFrom(domainClass)) {
        return new JpaPersistableEntityInformation(domainClass, metamodel);
    } else {
        return new JpaMetamodelEntityInformation(domainClass, metamodel);
    }
}

그럼 우리는 Persistable을 직접 상속을 받아서 isNew()를 재정의 하여 사용을 하면된다

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Table(name = "USERS")
@ToString(of = {"id", "name"})
public class User extends TimeInfo implements Persistable<String> {

    @Id
    @Column(name = "USER_ID")
    private String id;

    @Column(name = "PASSWORD")
    private String password;

    @Column(name = "NAME")
    private String name;

    @Override
    public boolean isNew() {
        return getCreatedAt() == null;
    }
}

결과적으로 위와 같은 코드가 된다

createAt값은 TimeInfo클래스에 정의되어 있는데 @PrePersist를 통해서 엔티티메니저에 영속 상태가 되기전 즉 save함수에서 em.persist()가 실행이 되기 직전에 값을 할당하기 때문에 isNew()가 실행 될때는 null값을 가지게 되어 새로운 엔티티로 인식이 되게 된다

@PrePersist
public void prePersist() {
    LocalDateTime now = LocalDateTime.now();
    createdAt = now;
    modifiedAt = now;
}

select 쿼리 나가는 문제 해결

복합키 작성

@EmbeddedId와 @IdClass두가지 방식이 있다고 하는데 현재 프로젝트에서는 @EmbeddedId를 사용하여 구현을 하였다

찾아보니 @EmbeddedId는 객체지향에 가깝고 @IdClass는 DB에 더 가깝다고 하는데 후자는 사용을 해보지 않아서 아직은 차이를 자세히는 모르겠다 추후 두개를 비교 정리를 하겠다

이 부분은 크게 어렵다고는 느껴지진 않았지만 처음써보는 것이고 약간의 해매이는 부분이 없지는 않았다

@Getter
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class FriendId implements Serializable {

    @Column(name = "USER_ID")
    private String userId;

    @Column(name = "FRIEND_ID")
    private String friendId;
}

친구에 관련된 복합키이다 복합키를 사용하는데 있어서는 약간의 규약? 규칙이 있다

  • Serializable 인터페이스 구현
  • @Embeddeable 어노테이션 사용
  • equals, hashCode 구현
  • 기본 생성자, 클래스 public

복합키를 사용한 클래스는 다음과 같다

@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(of = {"userId", "friendId"})
@Table(name = "FRIEND")
public class Friend {

    @EmbeddedId
    private FriendId id;

    @MapsId("userId")
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "USER_ID")
    private User userId;

    @MapsId("friendId")
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "FRIEND_ID")
    private User friendId;
}

식별관계로 엔티티가 구성이 되어있기 때문에 @MapsId를 통해서 연관관계를 지정을 해주면 된다

다음으로 어떻게 사용되는지에 대해서 알아 보자

  • save
User userA = userRepository.findById("userA").get();
User userB = userRepository.findById("userB").get();


FriendId id = new FriendId(userA.getId(), userB.getId());  
Friend friend = new Friend(id, userA, userB);  
friendRepository.save(friend);
  • find
User userA = userRepository.findById("userA").get();
User userB = userRepository.findById("userB").get();

Friend friend = friendRepository.findById(new FriendId(userA.getId(), userB.getId())).get();
320x100
반응형
    jhs0129
    jhs0129
    공부기록 남기기

    티스토리툴바