개발기술/ORM

Spring JPA Entity 설정

bsh6226 2024. 10. 28. 19:53

Entity 생성하기

Entity 생성하기

  엔티티(Entity)란 JPA에서 데이터베이스 테이블과 매핑되는 클래스를 의미하며, 각 엔티티 객체는 데이터베이스에서 독립적으로 관리되는 레코드(행, row)를 나타냅니다. 

  • @Entity : JPA에서 해당 클래스가 데이터베이스 테이블과 매핑됨을 나타내는 필수 애너테이션
  • @Table(name="테이블명") : 클래스명과 테이블명을 다르게 지정하고 싶은 경우에는 @Table 어노테이션을 사용하여 원하는 테이블명을 명시

Entity의 Column 설정하기

  Table column들의 속성을 annotation을 통해서 셋팅해둔다..

  • @Id:  엔티티 클래스를 정의할 때는 기본적으로 기본 키(primary key)로 사용할 필드를 지정합니다 
    • @GeneratedValue(Strategy = ? ) : 기본 키의 자동 생성 전략을 설정합니다.
      • GenerationType.AUTO: JPA가 데이터베이스에 맞는 생성 전략을 자동으로 선택합니다.
      • GenerationType.IDENTITY: AUTO_INCREMENT와 같이 데이터베이스가 기본 키 값을 생성하도록 설정합니다.
      • GenerationType.SEQUENCE: 시퀀스를 사용하여 기본 키를 생성합니다. Oracle 등에서 주로 사용됩니다.
      • GenerationType.TABLE: 고유성을 보장하는 테이블을 이용해 기본 키를 생성합니다.
@Entity(name = "memo")
public class Memo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String text;
}

 

 

복합키 설정

Embedded Key는 JPA에서 @Embeddable 클래스를 이용해 복합 키를 객체로 묶어 표현한 것입니다.

  • @Embeddable : 복합 키를 하나의 값 객체(Value Object)로 표현
@Embeddable
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class StatsBySecondsId implements Serializable {
    private String yyyymmdd;
    private String hhmmss;
    private Long areaId; // area.id will be mapped to this
}
  • @EmbeddedId :  해당 객체를 엔티티의 복합키(Composite Primary Key) 로 사용
  • 복합키의 연관관계 문제 :   복합키 안에 FK 컬럼이 포함될 때, 같은 컬럼을 @EmbeddedId와 @ManyToOne 모두가 사용하면 @EmbeddedId 칼럼과 @ManyToOne칼럼 중 어느 칼럼을 기준으로 SQL Insert-update 처리할지 혼란.
    •  이 경우는 JPA가 어떤 필드를 기준으로 해당 컬럼 값을 설정할지 모호하기 때문에 반드시 명확히 해줘야 합니다. 
    • @Joincolumn(insertable = false, updateable = false)
      • FK 컬럼을 이미 @EmbeddedId 에서 관리하므로 @ManyToOne 쪽에는 읽기 전용(readonly) 으로 만들고,  실제 insert/update 는 EmbeddedId 쪽에서만 처리하도록 함.
    • 복합키의 연관관계 한계 :
      • 복합키(Composite Primary Key)에서 연관관계를 맺으면 가장 큰 문제는
        FK가 PK 안에 들어 있어서, 결국 update 가 불가능하다는 것
        입니다.
      • @EmbeddedId (복합키) + insertable = false, updatable = false 연관관계의 구조적 한계  : @JoinColumn(insertable = false, updatable = false) 로 설정하여 readonly로 만들었다면 pk를 변경해야만 fk를 변경할 수 있다 그러나 pk를 변경하면 새로운 record가 생성되므로 결국에는 부모 entity에서 orphanRemoval이나 cascade all로 관리해야함
@ManyToOne
@JoinColumn(name = "order_id", insertable = false, updatable = false)
private Order order;

 

  • @MapsId
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StatsBySeconds {

    @EmbeddedId
    private StatsBySecondsId id;

    private double areaCount;

    @MapsId("areaId") // maps to areaId in the embedded ID
    @ManyToOne
    private Area area;
}
  • @Embeddable : is used to define the composite key class.
@Entity
public class Order {

    @EmbeddedId
    private OrderId id; // ✅ Composite Primary Key
  • JPA를 통해서 EmbeddedID사용하기
public interface OrderRepository extends JpaRepository<Order, OrderId> {
    // Custom query: Count orders by a specific product
    long countByIdProductId(Long productId);
}

 

 

 

 

 

 

칼럼 설정

  • @Enumerated: Enum 타입의 필드를 데이터베이스에 저장할 때 어떤 형태로 저장할지 지정합니다. 
    • EnumType.ORDINAL: Enum의 순서를 정수 값으로 저장합니다. (0, 1, 2 등)
    • EnumType.STRING: Enum 이름 자체를 문자열로 저장합니다.

 

  • @Column: 열의 속성을 설정하는 데 사용됩니다.
    • unique = true: 해당 열에 유일값을 허용하여 중복을 방지합니다.
    • columnDefinition = "POINT": 특정 SQL 데이터 타입을 명시적으로 지정할 때 사용됩니다. 예를 들어, POINT 타입은 위치 데이터를 저장할 때 사용합니다.
    • nullable = true: NULL 값을 허용합니다.
    • name = "name" DB에서의 column name을 설정해준다.
@Column(unique = true, nullable = false, columnDefinition = "POINT")
private String location;
  • @Table(uniqueConstraints = {...}):  단일 컬럼의 유니크 제약 조건은 @Column(unique = true) 로 지정할 수 있지만, 두 개 이상의 컬럼 조합에 대해 유니크 제약 조건을 설정하려면 @Table(uniqueConstraints = {...}) 를 사용해야 합니다.
    • @UniqueConstraint(columnNames = {...}): 유니크 제약 조건을 적용할 컬럼을 지정하는 역할을 합니다
    • @UniqueConstraint(columnNames = {"companyId", "date"}) 를 설정했기 때문에, 데이터베이스에서 (companyId, date) 조합이 중복되지 않도록 자동으로 검사합니다.
@Entity
@Table(
    name = "dividend", // 테이블명 지정
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"companyId", "date"}) // companyId + date 조합을 유니크 키로 설정
    }
)
public class DividendEntity {

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

    private Long companyId;

    private String date;

    private Double dividendAmount;
}
  • @Transient : DB에 저장하지 않지만 entity 객체에 저장하고 싶은 내용을 저장
// A list to hold each status without persisting it to the database
@Transient
private List<Enum<?>> statuses;

// You can initialize this list in the constructor or a method
public void initializeStatuses() {
    this.statuses = List.of(participationStatus, depositStatus, ratingStatus);
}

 

  • @Convert : customize how entity attributes are stored in the database by specifying a converter class.
@Table(name = "auth_menu")
public class AuthMenu extends BaseEntity {

    @EmbeddedId
    private AuthMenuKey id;

    @Convert(converter = YesNoConverter.class)
    @Column(name = "create_yn", length = 1)
    @Comment("생성 권한")
    private YesNoType createYn;
  • @comment : used for database column comments.

 

Entity생성시 고려사항

  • Entity의 값을 변경하는 중요한 로직은  Entity class 외에서 실행하더라도, Entity내에서 메소드를 정의하고 이것을 호출하는 형식으로 진행할 필요가 있음. (Encapsulation, Static Factory)
  • Entity의 자동생성ID외의 식별자는 일반적으로 UUID를 통한 유일한 생성자 생성에 많이 사용한다. ;
import java.util.UUID;

@Entity
public class User {
    @Id
    private String id; // UUID를 문자열로 저장

    private String name;

    protected User() {} // JPA 기본 생성자

    // 정적 팩토리 메서드 사용 (UUID 자동 생성)
    public static User create(String name) {
        User user = new User();
        user.id = UUID.randomUUID().toString();
        user.name = name;
        return user;
    }
}

테이블 관계설정 

Collection과 Entity의 연결

  • @ElementCollection : 필드로 값 타입(Value Type) 컬렉션을 저장할 때 사용하는 JPA 애너테이션. Entity와 컬렉션의 값들을 맵핑하기 위한 추가 테이블을 생성하여 관계를 관리함.
    • 이 추가 테이블의 개별행은 기본 키로는 원본 엔티티의 기본 키(PK)와 컬렉션 값 칼럼을 조합한 복합키(Composite Key)로 구성됩니다.
    •  @CollectionTable : @ElementCollection 필드에 대한 테이블의 이름과 원본 엔티티에서 외래키로 사용할 칼럼을 설정
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ElementCollection
    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role")
    private List<String> roles;
}

 

user 테이블 (User 엔티티)

id (PK) name
1 Alice
2 Bob

 

user_roles 테이블 (@ElementCollection으로 생성된 추가 테이블)

user_id (FK, PK) role (PK)
1 ADMIN
1 USER
2 USER

 

Entity 연관관계

  • JPA에서 연관관계는 엔티티 간의 참조(association)를 정의하는 것이며,  연관 엔티티를 본 엔티티의 필드로 삼고 JPA어노테이션(@ManyToOne, @OneToMany, @ManyToMany, @OneToOne)을 사용하여 두 엔티티간의 다중성과 외래키 관계생성을 포함한 연관관계를 정의함
    • ManyToOne, OneToMany의 경우 OwningTable이 명확하여 @JoinColumn을 생략해도 Many사이드의 Pk로 FK가 자동으로 생성
    • OneTOne, ManyToMany의 경우 OwningTable이 명확하지 않아 @JoinColumn 혹은 @JoinTable로 FK관리를 명확화 해야함
  • 연관관계는 엔티티 간의 상호 참조가능성을 기준으로 단방향 또는 양방향으로 설계될수 있음. 기본은 단방향이며, 양방향 관계를 만들려면 반대편에 mappedBy 를 선언해야 한다
  •  cascade = ALL, orphanRemoval = true로 부모를 설정하고 부모-자식 생명주기를 정의할 때 사용하는 설정이다.

 

@JoinColumn

  • 연관관계의 주인(owning side) 엔티티에서 외래 키(FK)를 명시할 필드에 적용되며, FK 컬럼의 이름(name)과 연관 엔티티 내에서 참조할 컬럼(referencedColumnName)을 지정할 수 있습니다.
  • name을 생략하면 자동으로 필드명 + _id로 FK 컬럼이 생성되고, referencedColumnName을 생략하면 참조 대상 엔티티의 PK가 기본값으로 사용됩니다.
    • (name = "저장할 FK의 칼럼명",)
    • (referencedColumnName = " FK로 사용할 연관 테이블의 칼럼 지정") 
    • (unique : Ensures that each value in the foreign key column is unique. used in one-to-one relationships.)
    • nullable :  foreign key column to be non NULL. 테이블간 연관관계가 깨지지않게 방지하기 위해서 사용)
@Entity
public class Order { // ✅ Many 쪽
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id") // 외래 키는 여기 (Many 쪽) 에 생김
    private Member member;
}

 

연관관계 맵핑의 mapped By 옵션 : 양방향 연관관계에서 mapped by가 있어야 피소유 테이블을 지정한다.

@Entity
public class Member { // One 쪽
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "member")
    private List<Order> orders;
}
@Entity
public class Locker {
    @Id
    private Long id;

    @OneToOne(mappedBy = "locker") // 반대편: 외래 키 없음
    private User user;
}

 

부모와 자식관계 변경

 

 

일대다 관계

@Entity
public class OrderEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false) // FK 설정
    private UserEntity user; // 부모 테이블(UserEntity) 참조
}
  • @ManyToOne : 적용되는 연관엔티티가 원본 엔티티와 다대일 관계(자식-부모관계)임을 설정
@Entity
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "user") // 연관관계의 주인은 OrderEntity
    private List<OrderEntity> orders = new ArrayList<>();
}
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderEntity> orders = new ArrayList<>();
  • @OnetoMany : 일대다 관계를 설정
    • (mappedBy = entityName) 속성을 사용하여 피소유 엔티티에서 소유엔티티를 필드로 명시하여 참조할 때 사용, 
UserEntity user = new UserEntity();
OrderEntity order = new OrderEntity();
order.setUser(user);

user.getOrders().add(order);
userRepository.save(user); // ✅ This will also save `order` automatically
user.getOrders().remove(order); // ✅ This will delete the order from DB when user is saved
userRepository.save(user);

 

다대다관계

  • @ManyToMany  :  다대다(Many-to-Many) 관계를 나타냄. 학생(Student)과 수업(Course)의 관계에서 한 학생이 여러 수업에 참여할 수 있고, 한 수업에는 여러 학생이 참여할 수 있음. 
    •  @JoinTable  :  다대다(Many-to-Many) 관계에서 중간 조인 테이블을 설정할 때 사용합니다. 이 조인 테이블을 명시적으로 정의한 엔티티 쪽이 연관관계의 주인(owning side) 이 됩니다.
      • 비소유 엔티티는 양방향 관계를 갖으려면 mappedby를 사용해야합니다.
      • name: 조인 테이블의 이름을 지정합니다. 특별한 설정이 없다면 JPA가 자동으로 이름을 생성합니다.
      • joinColumns: 주도권을 가진 엔티티의 외래 키 컬럼을 설정합니다. 
      • inverseJoinColumns: 반대쪽 엔티티의 외래 키 컬럼을 설정합니다.
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(name = "student_course",
            joinColumns = @JoinColumn(name = "student_id"),
            inverseJoinColumns = @JoinColumn(name = "course_id"))
    private List<Course> courses = new ArrayList<>();
}
@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students = new ArrayList<>();
}

 

 

  • @ManyToMany를 사용하면 발생하는 문제점
    • RDB의 상태와 일치하지 않음. RDB에서는 다대다를 구현하는 것이아니라 중간테이블을 생성하여 1:N + N:1 관계로 풀어냄
    • 두 테이블의 FK외에 추가적인 정보 저장 불가하기때문 연관관계가 복잡해질 경우 확장성이 떨어짐 (ex : 수강 신청 날짜(enrollmentDate), 성적(grade) 같은 필드)
    • JPA의 변경 감지(Dirty Checking)와 연관관계 관리 문제 : @ManyToMany는 자동으로 중간 테이블을 관리하지만, 변경 감지가 어렵고 성능 최적화가 어려움. remove(student.getCourses().get(0))을 해도 변경 사항이 DB에 제대로 반영되지 않을 수 있음. 이는 @ManyToMany가 중간 테이블을 직접 관리하지 않기 때문에 발생함. 

다대다 관계 @OneToMany + @ManyToOne으로 풀어내기 (선호되는 방식)

 

실질적으로 ManyToMany 테이블은 많이 사용되지 않고  중간 테이블을 만들어 일대다(@OneToMany) 관계가 두 개 포함

된 엔티티를 형성하는 방식이 더 선호됩니다. 이 방식이 더 유연하고 관리가 용이하기 때문입니다.

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "student")
    private Set<StudentCourse> studentCourses = new HashSet<>();
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "course")
    private Set<StudentCourse> studentCourses = new HashSet<>();
}

@Entity
public class StudentCourse {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "student_id")
    private Student student;

    @ManyToOne
    @JoinColumn(name = "course_id")
    private Course course;

    // 예: 관계에 대한 추가적인 속성
    private LocalDateTime enrollmentDate;
}
  • student.getStudentCourses()로 중간 엔티티에 접근하고, 그 안에서 getCourse()로 타겟 엔티티 조회

 

일대일관계

두 엔티티가 1:1로 연결되는 관계입니다. 둘이 동등한 관계이기때문에 @Joincolumn으로 주인 엔티티를 설정하고 FK를 지정함.

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToOne
    @JoinColumn(name = "profile_id") // FK
    private UserProfile profile;
}
@Entity
public class UserProfile {
    @Id @GeneratedValue
    private Long id;

    private String address;

    @OneToOne(mappedBy = "profile") // inverse side
    private User user;
}

 

 

부모자식 관계정립

  • cascade = CascadeType.ALL : 부모가 저장되거나 삭제될 때 자식도 함께 저장/삭제되게 함
  • orphanRemoval = true :  부모-자식 관계가 끊긴 자식은 고아 객체로 간주되어 자동 삭제
  • Cascade All이나 orphanremoval이 아니라면 owning entity에서만 두 entity 관계가 조정될 수 있음 

JPA의 로딩 전략

  • JPA는 ResultSet (쿼리 결과)를 받아서 Entity 객체로 매핑해주는 기술
  • Entity 간에 연관관계가 있으면, 단순 매핑뿐 아니라 객체 안의 객체(그래프)까지 채워야함
    • Lazy Loading으로 필요한 순간마다 추가 쿼리로 채울 수도 있지만, eager loading이나 fetch join은 이걸 "한 번에" 가져와서 그래프를 완성하는 최적화 기법
전략 설명 실제 SQL 쿼리
LAZY 객체는 먼저 로딩, 연관 객체는 나중에 (필요할 때) N+1 문제 발생 가능
EAGER 객체와 연관 객체를 즉시 로딩 한 번에 join 쿼리 날림 (자동 fetch join 비슷)
fetch join Lazy여도 명시적으로 한 번에 join해서 가져오도록 명령 개발자가 직접 조절하는 "즉시 로딩"

 

EAGER Fetching:

  • Loads related entities immediately when the parent entity is fetched.
    • meaning if select a record from a table, it will also fetch the related rows from the other table
    • Occurs even if the related entity is never accessedRisk of unnecessary data loading (Performance Issue).
    • Default for @ManyToOne and @OneToOne.

LAZY Fetching:

  • Loads related entities only when accessed (on-demand).
    • meaning JPA will fetch the related entities on demand (i.e., when you explicitly access the relationship property)
    • Prevents unnecessary data fetching, making queries more efficient.
    • Default for @OneToMany and @ManyToMany.
@ManyToOne(fetch = FetchType.LAZY)

Best Practice는 기본적으로 모두 lazyloading으로 설정하고 필요에따라 커스컴 메소드를 eager fetching으로 변경하는 것.

 

N+1 Problem Case (ORM Specific Problem)

  •  The N+1 problem occurs in JPA when fetching entities that have lazy-loaded relationships, leading to multiple additional queries being executed instead of a single optimized one. 
    •  usually happens when parent entity select child entity sequentially 
      • A collection of parent entities is fetched (List<UserEntity> users)
      • Each parent entity accesses its child entities (user.getOrders()) inside a loop
      • The child entities are lazily loaded (FetchType.LAZY)
  • Simple case
List<UserEntity> users = userRepository.findAll(); // 1번 실행 (User 조회)

for (UserEntity user : users) {
        System.out.println(user.getOrders()); // N번 실행 (Order 조회)
        }
SELECT * FROM users;  -- (1 Query for Users)

SELECT * FROM orders WHERE user_id = 1;  -- (N Queries for Orders)
SELECT * FROM orders WHERE user_id = 2;
SELECT * FROM orders WHERE user_id = 3;
SELECT * FROM orders WHERE user_id = 4;
SELECT * FROM orders WHERE user_id = 5;

 

 

Fetching strategies application at two levels

  1. Static Fetching Strategy (Defined at Entity Level)
    • Applies to entity relationships (@OneToMany, @ManyToOne, etc.).
    • Configured via FetchType.EAGER or FetchType.LAZY.

@Entity
public class Order {

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

    @ManyToOne(fetch = FetchType.EAGER)  // Default for @ManyToOne
    private Customer customer;
}

 

2. Dynamic Fetching Strategy (Applied Per Query, Join Fetch) 

  • Overrides default fetch behavior for specific queries.
  • Done using JPQL (JOIN FETCH), @EntityGraph, Criteria API, or Native Queries.

B. JPQL로 select문을 사용하여 eagerloading 전용의 Method를 설정할 수 있다.

@Query("SELECT m FROM MemberEntity m JOIN FETCH m.visitedHeritages WHERE m.memberId = :memberId")
Optional<MemberEntity> findByMemberIdWithVisitedHeritages(@Param("memberId") String memberId);

 

C. Entity를 사용한 관계테이블 생성이아니라 별도의 관계형 Repository를 생성하여 해당 Repository로부터 직접 Fetch를 한다.

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VisitedHeritageEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false)
  private String memberId;

  @Column(nullable = false)
  private String heritageId;
}

Modifying Strategies of Related Entity

cascading (cascade) and orphan removal (orphanRemoval) control how child entities behave when the parent entity is modified.

  • cascade in JPA
    • cascade attribute allows operations performed on a parent entity to propagate to its child entities.
      • There are multiple types of Cascade Operation
        • Saves child entities when parent is saved.
        • Updates child entities when parent is updated.
        • Deletes child entities when parent is deleted.
        • Refreshes child entities when parent is refreshed.
        • Detaches child entities when parent is detached from the persistence contex
    • 조심히 사용해야하는 편의기능이며 성능과는 무관하다. 단점으로는 아래와 같은 것이 있기에 수정할때 관련 레코드는 순차적으로 직접 수정할것같다.
      • 코드의 가독성이 저하될수 있다
      • 한번에 많은 양의 삭제로 성능문제가 발생할 수 잇다.
      • 실수로 데이터 손실이 발생할 수 있다.
@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders;
}
User user = new User();
Order order1 = new Order(user);
Order order2 = new Order(user);

user.getOrders().add(order1);
user.getOrders().add(order2);

entityManager.persist(user);  // 🚀 Saves both user and orders

 

  • orphan removal in jpa
    • If a child entity is removed from the parent's collection, it is automatically deleted from the database.
    • It is different from CascadeType.REMOVE, which deletes children only when the parent is deleted.
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders;
User user = entityManager.find(User.class, 1L);
user.getOrders().remove(0);  // 🚀 Order is also deleted from the database!