Spring/JPA

@Entity 설정 Annotation 정리

공대키메라 2022. 3. 17. 16:27

혼자 인터넷에서 뒤져가면서 내 공부용 미니 프로젝트를 위한 스키마를 생성하고 테이블들의 연관관계를 Java Persistence API를 이용해서 작업중이다. 

 

근데 작업하는 와중에 많은 기능이 있었다.

 

문제는 이것을 잘 사용해야 하는데 이번 기회에 다시 집고 넘어가려고 한다. 

 

필자는 다음과 같은 ERD를 이용해서 Mapping을 시도했다. 

 

readBookAndBuy (erdcloud.com)

 

 

1. @MappedSuperclass vs @Embedded & @Embeddable

@MapppedSuperClass는 공통 매핑 정보가 필요할 때 사용한다. 

 

쇼핑몰에서 우리가 물건을 구매할 때 회원 가입시 입력한 기본 주소를 그냥 쓸 수도 있지만 여러곳에서 물건을 수령할 수도 있다. 즉, 실제로 저장한 주소들과 내가 받을 곳의 주소들은 일치할 수도, 다를 수도 있다.

 

그래서 Delivery_Address라는 테이블을 만들어 주소를 넣어두었고, User_Orders라는 테이블을 만들어 주소를 넣어두었다. 

 

문제는 Address 주소 정보를 우편번호, 주소, 상세주소로 받도록 했는데 이것을 공통된 class로 묶어서 사용하고 싶은 것이다. 반복은 개발자가 굉장히 싫어하는 것이다!

 

그래서 JPA에서 제공하는 기능으로 @MappedSuperClass와 @Embedded & @Embeddable이 있다. 

 

@MappedSuperClass 사용법

 

1. 공통의 관심사 뽑아내기

package readBookAndBuy.readBookAndBuy.domain.entity;

import lombok.Getter;

import javax.persistence.MappedSuperclass;

@MappedSuperclass
@Getter
public class Address {

    private String userAddress1;
    private String userAddress2;
    private String userAddress3;

}

공통으로 사용할 class 위에 @MappedSuperclass라고 적어준다. 

 

 

2. 적용하길 원하는 곳에 상속하기 

package readBookAndBuy.readBookAndBuy.domain.entity.order;

import lombok.Getter;
import lombok.NoArgsConstructor;
import readBookAndBuy.readBookAndBuy.domain.entity.Address;
import readBookAndBuy.readBookAndBuy.domain.entity.Users;

import javax.persistence.*;

import static javax.persistence.FetchType.LAZY;

@Entity
@Getter
@NoArgsConstructor
public class UserOrders extends Address {

    @Id @GeneratedValue
    @Column(name = "user_order_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id")
    private Users user;

    private String receiverName;
    private String receiverPhone;

}

 

이렇게 extends 를 이용해서 Address를 상속하면 끝이다. 

 

@Embeddable & @Embedded 사용법

기능은 동일하지만 약간의 차이점이 있다. 

 

1. 밸류 클래스 생성

package readBookAndBuy.readBookAndBuy.domain.entity;

import lombok.Getter;

import javax.persistence.MappedSuperclass;

@Embeddable
public class Address {

    private String userAddress1;
    private String userAddress2;
    private String userAddress3;

}

 

2. 적용

package readBookAndBuy.readBookAndBuy.domain.entity.order;

import lombok.Getter;
import lombok.NoArgsConstructor;
import readBookAndBuy.readBookAndBuy.domain.entity.Address;
import readBookAndBuy.readBookAndBuy.domain.entity.Users;

import javax.persistence.*;

import static javax.persistence.FetchType.LAZY;

@Entity
@Getter
@NoArgsConstructor
public class UserOrders {

    @Id @GeneratedValue
    @Column(name = "user_order_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id")
    private Users user;

    private String receiverName;
    private String receiverPhone;
    
    @Embedded
    private Address address;

}

 

필자는 어떤 차이가 있는지 잘 몰라서 인터넷에서 찾아본 결과 다음과 같은 차이가 있었다.

출처: https://www.inflearn.com/questions/18578

 

JPA 강의에서 김영한 선생님께서 답변을 해주셨는데

 

상속의 경우 결합성이 높아져서 객체지향의 일반적인 법칙을 따르면 위임을 하는것이 편하지만

 

우리가 JPQL을 작성할 때에는 불편하기 때문에 상속을 사용한다고 한다. 

 

결국 상속과 위임의 차이라고 하는데 상속? 위임? 그게뭐지? 이 차이점에 대해서도 잘 몰랐기에 찾아서 읽어보았다.

 

참고 : https://sorjfkrh5078.tistory.com/282

참고 : https://soft.plusblog.co.kr/89

 

2. Auditing

JPA에서 자동적으로 값을 넣어주는 기능이다.

audit 뜻이 감사하다인데 해야할 일을 자동으로 처리해줘서 감사한건가? 하여간... 

 

이 기능은 언제 생성했는지, 언제 수정했는지, 누가 수정하고 생성했는지를 넣어준다.

 

많은 테이블에서 위에서 언급한 정보를 공통적으로 사용하기에 상속을 통해(그게 직관적이니까) 공통된 정보를 넣어주는데 이것도 그렇게 생성이 가능하다. 

 

다만 거기에 더해 몇가지만 더 적용하면 된다. 

 

BaseTimeEntity.java

package readBookAndBuy.readBookAndBuy.domain.entity.base;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

}​

@CreatedDate 어노테이션은 spring data jpa에서 제공해주는 기능으로 

작성일을 자동으로 데이터 생성시 넣어준다.

 

그냥 JPA의 기능으로도 구현 가능한데 궁금한분은 찾아보시길 바란다.

 

작성일의 경우 누군가 수정하면 안되기 때문에 @Column 설정에 updatable 을 false로 해주면 된다. 

 

@LastModifiedDate는 알아서 수정시에 수정 날짜를 넣어준다. 

 

BaseEntity.java생성

package readBookAndBuy.readBookAndBuy.domain.entity.base;

import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity{

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;

}


위에서 선언한 BaseTimeEntity를 상속해서 다시 작성자와 수정자가 누구인지 받도록 설정했다. 

 

@CreatedDate 혹은 @LastModifiedDate의 경우에는 @EnableJpaAuditing만 넣어주면 알아서 날짜가 들어가지만 CreatedBy와  @LastModiefdBy는 하나를 더 설정해줘야 한다. 


ReadBookAndBuyApplication.java

package readBookAndBuy.readBookAndBuy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import java.util.Optional;
import java.util.UUID;

@SpringBootApplication
@EnableJpaAuditing
public class ReadBookAndBuyApplication {
   public static void main(String[] args) {
      SpringApplication.run(ReadBookAndBuyApplication.class, args);
   }

   @Bean
   public AuditorAware<String> auditorProvider(){
      return () -> Optional.of(UUID.randomUUID().toString());
   }

}

 

auditorProvider에서 주어진 값을 @CreatedBy 혹은 @LastModifiedBy에 들어가게 된다. 

 

해당 어노테이션이 붙어있으면 Bean으로 등록된 값을 찾아서 넣어주는 것 같다. 

 

여기서는 UUID로 우선 임의적으로 작성해 놓았지만 spring-secuirty를 사용할 때는 session에서 사용자의 정보를 꺼내와서 전달해주면 될 것이다. 

 

3. @ManyToOne vs @OneToMany

 

readBookAndBuy (erdcloud.com)

이것을 하는 방법은 간단하다. 필자는 Lend와 User를 예로 들겠다.

 

Lend.java

package readBookAndBuy.readBookAndBuy.domain.entity.book;

import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import readBookAndBuy.readBookAndBuy.domain.entity.Users;
import readBookAndBuy.readBookAndBuy.domain.entity.base.BaseTimeEntity;
import readBookAndBuy.readBookAndBuy.domain.entity.book.BookMark;

import javax.persistence.*;
import java.time.LocalDateTime;

import static javax.persistence.FetchType.*;

@Entity
@Getter
@NoArgsConstructor
public class Lend extends BaseTimeEntity {

    @Id @GeneratedValue
    @Column(name = "lend_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id")
    private Users user;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "book_id")
    private Book book;

    @OneToMany
    @JoinColumn(name = "bookmark_id")
    private BookMark bookMark;

    private LocalDateTime lendDate;

    private LocalDateTime expireDate;

}

 

Users.java

package readBookAndBuy.readBookAndBuy.domain.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;
import readBookAndBuy.readBookAndBuy.domain.entity.base.BaseTimeEntity;
import readBookAndBuy.readBookAndBuy.domain.entity.role.Roles;
import readBookAndBuy.readBookAndBuy.domain.enums.UserGradeEnum;

import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import static javax.persistence.CascadeType.ALL;
import static javax.persistence.FetchType.LAZY;

@Entity
@NoArgsConstructor
@Getter
public class Users extends BaseTimeEntity {

    @Id @GeneratedValue
    @Column(name = "user_id")
    private Long id;

    @OneToOne(fetch = LAZY)
    @JoinColumn(name="role_id")
    private Roles roles;

    @Email
    private String email;
    private String nickname;

    @Pattern(regexp = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{6,20})")
    private String password;

    @Pattern(regexp = "\\d{2,3}-\\d{3,4}-\\d{3,4}")
    private String phone;

    private String connectionIp;
    private String companyName;

    @Pattern(regexp = "\\d{2,3}-\\d{3,4}-\\d{3,4}")
    private String companyNumber;

    @Enumerated(EnumType.STRING)
    private UserGradeEnum userGrade;
    private LocalDate birthDay;

    @OneToMany(mappedBy = "user", cascade = ALL)
    private List<DeliveryAddress> deliveryAddresses = new ArrayList<>();

    @OneToMany(mappedBy = "user", cascade = ALL)
    private List<Subscribe> subscribes = new ArrayList<>();

    public void addDeliveryAddress(DeliveryAddress address){
        deliveryAddresses.add(address);
    }

}

 

현재 작업중이고 어떤 연관관계 편의 메서드를 만들어야 하는지 감도 안잡히기에 부족해도 너그러이 이해 부탁한다옹... ㅠㅠ

 

외래키를 가지고 있는 테이블에서 연관관계의 주인이라 생각하고 작성했다. 

 

만약 두개의 테이블에서 서로 접근하듯이 객체를 매핑하고 싶다면 mappedBy를 설정해 주면 된다. 

 

매핑을 하면서 느낀점은 이게 어느정도까지 정보를 허용해줄 것이냐다. DB의 입장에서는 외래키를 하나만 다른 테이블에서 가지고 있어도 그 외래키를 바탕으로 양쪽에서 접근이 가능하다. 

 

하지만... 우리 JPA에서 객체끼리 mapping을 할 경우 양쪽에서 접근이 가능하도록 하기 위해서는 양방향 mapping을 해줘야 한다. 

 

이 부분에서 큰 고민이 생긴다. 도대체 어디까지 이것을 허용해야 하는가? 

 

Lend에서 ManyToOne으로 Mapping을 한다고 생각하자. 

 

User에서 별도로 Lend에 대한 정보를 Mapping해주지 않으면 User로는 Lend 에 접근이 불가능하다. 

 

어디까지 그것을 끊어주고 이어줄지를 아직은 잘 모르겠다. 그래서 우선 대부분은 양방향이 아닌 다대일 단방향으로 대부분 설계를 진행했다.

 

작업을 하다가 무언가 불편하거나 필요하다 싶을 때 다시 돌아와서 수정할 계획이다. 

 

 

4. @Converter

 

@Converter

entity에 어떤 값을 넣어야 하는지 고민하고 있는데 필자는 boolean값을 이용해 TRUE와  FALSE를 넣어주고 싶었다. 

그래서 찾다보니 Converter를 이용하면 이것이 가능하다고 한다. 

 

말 그대로 특정 값을 변환해주는 것이다. 

 

자신이 직접 customizing도 가능하고, 기존에 지원해주는 기능을 사용할 수 있다. 

 

출처 : https://memostack.tistory.com/194

출처 : https://www.baeldung.com/jpa-attribute-converters

 

BookOrdersDetail.java

package readBookAndBuy.readBookAndBuy.domain.entity.order;

import lombok.Getter;
import lombok.NoArgsConstructor;
import readBookAndBuy.readBookAndBuy.domain.entity.book.Book;

import javax.persistence.*;

import java.util.ArrayList;
import java.util.List;

import static javax.persistence.FetchType.*;

@Entity
@Getter
@NoArgsConstructor
public class BookOrdersDetail {

    @Id @GeneratedValue
    @Column(name = "book_orders_detail_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_orders_id")
    private UserOrders userOrders;

    @OneToMany(mappedBy = "bookOrdersDetail")
    private List<BookOrderRefund> bookOrderRefundList = new ArrayList<>();

    @Convert(converter = AttributeConverter.class)
    private boolean isRefundable;

}

환불 가능 상품인지, 아닌지를  boolean으로 받고 싶었는데 이것을 DB에는 "true" 혹은 "false"로 저장하고 싶었다.

 

AttributeConverter.class는 기본적으로 제공해주는 class이고 이것말고 스스로 만든 converter를 적용하고 싶으면 다음과 같이 하면 된다. 

 

BooleanConverter.java

package readBookAndBuy.readBookAndBuy.domain.entity;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class BooleanConverter implements AttributeConverter<Boolean, String> {

    /**
     * Boolean 값을 Y 또는 N으로 convert
     *
     * @param attribute boolean 값
     * @return String true 인 경우 Y 또는 false인 경우 N
     */
    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null && attribute) ? "Y" : "N";
    }

    /**
     * Y 또는 N 을 Boolean 으로 convert
     * @param yn
     * @return
     */
    @Override
    public Boolean convertToEntityAttribute(String yn) {
        return "Y".equalsIgnoreCase(yn);
    }
}

 

5. @Enumerated

@enumerated는 enum으로 선언한 데이터를 DB에서 어떻게 저장될 수 있는지 선택하는 기능을 준다.  

 

우선 이것을 사용하기 전에는 어떻게 활용을 했는지 보고 가겠다. 

 

EnumTestModel.java (공용 인터페이스 생성)

package miniproject.demo.enums;

public interface EnumModel {
    String getKey();
    String getValue();
}

 

EnumTestType.java(EnumTestModel.java 상속 클래스 생성)

package miniproject.demo.enums;

public enum ContentInfoType implements EnumModel{
    LIKED("LIKED"),
    FOLLOWING("FOLLOWING"),
    FOLLOWED("FOLLOWED"),
    LIKE("LIKE");

    private String value;

    ContentInfoType(String value){
        this.value = value;
    }

    @Override
    public String getKey() {
        return name();
    }

    @Override
    public String getValue() {
        return value;
    }
}

 

이것이 Code에 따라서 Value가 세팅될 경우에 사용할 수 있는 방법이다. 하지만 이러한 경우 문제점은 무언가 ENUM을 하나 만들때 마다 너무 귀찮게도 파일일 계속 만들어 줘야 한다는 것이다.

 

굳이 table에 code에 따른 value를 설정해 준다면 이러한 방법도 필요 없을 뿐더러 굳이 table을 생성하지 않더라도

 

enum에서만 간단히 끝날 일도 많다. 무조건 table을 만들지 않아도 되는 경우도 있다는 말이다. 

 

그런데 이렇게 class가 자꾸 할 때 마다 두배로 늘어나면 정말 짜증날 것이다. 어짜피 쓰는건 딱 하나인데...

 

이렇한 고민을 해서 spring에서 사실 이 기능을 만든지는 모르겠지만 필자는 이 기능을 쓰면서 너무 편했다.

 

Roles.java

package readBookAndBuy.readBookAndBuy.domain.entity.role;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Roles {

    @Id @GeneratedValue
    @Column(name = "role_id")
    private Long id;

    @Enumerated(EnumType.STRING)
    private RoleTypeEnum roleType;

}

 

Roles.java의 필드에 RoleTypeEnum을 성분으로 받았다. 

 

그리고 @Enumerated 기능을 활용한다면 이것을 위의 번거로운 작업이 없이 바로 사용이 가능하다. 

 

 

결국에 쓰이는건 이 두개밖에 없다. 

 

한 번 이 내용에 대해서 확인을 해봤다.

/*
 * Copyright (c) 2008, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Linda DeMichiel - 2.1
//     Linda DeMichiel - 2.0

package javax.persistence;

/**
 * Defines mapping for enumerated types.  The constants of this
 * enumerated type specify how a persistent property or
 * field of an enumerated type should be persisted.
 * 
 * @since 1.0
 */
public enum EnumType {
    /** Persist enumerated type property or field as an integer. */
    ORDINAL,

    /** Persist enumerated type property or field as a string. */
    STRING
}

 

린다 데미치엘님... 기여자로서 정말 감사합니다....

 

이런 식으로 필자는 전부 매핑을 했다.

 

 

 

 

Entity에 따라서 table이 생성된 것을 확인할 수 있다. 

'Spring > JPA' 카테고리의 다른 글

DB 개념 + JPA concurrency control  (2) 2022.03.14