[Section 3] JPA의 Entity Mapping

Kim·2022년 11월 2일
0

Boot Camp

목록 보기
41/64
post-thumbnail

JPA Entity Mapping

엔티티 매핑은 객체와 테이블 간의 매핑, 기본키 매핑, 필드(멤버 변수)와 컬럼 간의 매핑, 엔티티 간의 연관 관계 매핑 등으로 나눌 수 있다.

엔티티와 테이블 간의 매핑

@Entity
@Table
public class Member {
    @Id
    private Long memberId;
}

@Entity 매핑 에너테이션을 이용해 엔티티 클래스와 테이블을 매핑할 수 있다.
클래스 레벨에 @Entity 애너테이션을 붙이면 JPA 관리 대상 엔티티가 된다.

@Entity(name = "USERS") // (1)
@Table(name = "USERS") // (2)
public class Member {
    @Id
    private Long memberId;
}

@Entity 애너테이션

(1), (2) 처럼 name 애트리뷰트를 사용해 엔티티 이름과 테이블 이름을 변경할 수 있다.
name 애트리뷰트를 설정하지 않으면 기본값으로 클래스 이름을 엔티티명으로 사용한다.

@Table 애너테이션

@Table 애너테이션도 name 애트리뷰트를 사용해 테이블명을 설정할 수 있다.
@Table 애너테이션은 옵션이고 추가하지 않을 경우 클래스 이름을 테이블명으로 사용한다. 주로 테이블명이 클래스명과 달라야 할 때 추가한다.

📢 주의 사항

  • @Table 애너테이션은 옵션이지만, @Entity@Id 애너테이션은 필수이다.

  • @Entity@Id 애너테이션은 함께 사용하자.
    @Entity 애너테이션만 추가하고 식별자 역할을 하는 필드(멤버 변수)에 @Id 애너테이션을 추가하지 않으면 에러가 발생한다.

  • 피라미터가 없는 기본 생성자는 필수로 추가하자.
    Spring Data JPA의 기술을 적용할 때, 기본 생성자가 없는 경우 에러가 발생하는 경우가 있다.

  • 중복되는 엔티티 클래스가 없으며 테이블명과 클래스명이 같은 경우, @Entity@Table 애너테이션에 name 애트리뷰트를 지정하지 않고 클래스명으로 사용하는 것을 권장한다.

기본키 매핑

DB 테이블에 기본키 설정은 필수인데, JPA에서는 기본적으로 @Id 애너테이션을 추가한 필드가 기본키 칼럼이 된다.

JPA 키본키 생성 전략

  • 기본키 직접 할당: 애플리케이션 코드 상에서 기본키를 직접 할당해주는 방식
  • 기본키 자동 생성
    IDENTITY: 기본키 생성을 DB에 위임하는 전략
    SEQUENCE: DB에서 제공하는 시퀀스를 사용해 기본키를 생성하는 전략
    TABLE: 별도의 키 생성 테이블을 사용하는 전략 Reference : Using the table identifier generator

기본키 직접 할당 전략

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id // (1)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

(1)과 같이 @Id 애너테이션만 추가하면 기본적으로 기본키 직접 할당 전략이 적용된다.

@Configuration
public class JpaIdDirectMappingConfig {
    ...

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        ...

        return args -> {
            tx.begin();
            em.persist(new Member(1L)); // (1)
            tx.commit();
            Member member = em.find(Member.class, 1L);

            ...
        };
    }
}

(1)과 같이 기본키를 직접 할당해 엔티티를 저장한다. (기본키 없이 엔티티를 저장하면 에러 메시지를 출력한다.)

기본키 자동 생성 전략

IDENTITY 전략

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // (1)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

IDENTITY 기본키 생성 전략을 사용하려면 @GeneratedValue 애너테이션의 strategy 애트리뷰트의 값을 GenerationType.IDENTITY로 지정하면 된다.
Member 엔티티에 IDENTITY 전략이 적용되었기 때문에 Member 엔티티에 별도의 기본키 값을 할당하지 않아도 DB에서 기본키를 대신 생성해준다.

SEQUENCE 전략

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE) // (1)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

SEQUENCE 전략을 사용하려면 @GeneratedValue(strategy = GenerationType.SEQUENCE)를 지정하면 된다.

@Configuration
public class JpaIdIdSequenceMappingConfig {
    ...

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        ...

        return args -> {
            tx.begin();
            em.persist(new Member());  // (1)
            Member member = em.find(Member.class, 1L);
            System.out.println("# memberId: " + member.getMemberId());
            tx.commit();
        };
    }
}

(1)과 같이 Member 엔티티 객체를 생성하면서 별도의 기본키 값을 전달하지 않았지만, SEQUENCE 전략을 사용하도록 지정했으므로 엔티티가 영속성 컨텍스트에 저장되기 전에 데이터베이스가 시퀀스에서 기본키에 해당하는 값을 제공할 것이다.

AUTO 전략

마지막으로 @Id 필드에 @GeneratedValue(strategy = GenerationType.AUTO)를 지정하면 JPA가 데이터베이스의 Dialect에 따라서 적절한 전략을 자동으로 선택한다.

💡 Dialect는 표준 SQL 등이 아닌 특정 DB에 특화된 고유한 기능을 의미한다.
만약 JPA가 지원하는 표준 문법이 아닌 특정 DB에 특화된 기능을 사용할 경우 Dialect가 처리해준다.

필드(멤버 변수)와 칼럼 간의 매핑

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

	// (1)
    @Column(nullable = false, updatable = false, unique = true)
    private String email;
		...
		
    public Member(String email) {
        this.email = email;
    }
}

@Column 애너테이션은 필드와 컬럼을 매핑해주는 애너테이션이다.
만일 @Column 애너테이션이 없고 필드만 정의되어 있다면 JPA는 기본적으로 이 필드가 테이블의 컬럼과 매핑되는 필드라고 간주한다. 또한, @Column 애너테이션에 사용되는 애트리뷰트의 값은 디폴트 값을 적용한다.

@Column 애너테이션

  • nullable
    칼럼에 null값을 허용할지 여부를 지정한다.
    디폴트 값은 true이다.
  • updatable
    칼럼 데이터를 수정할 수 있는지 여부를 지정한다.
    디폴트 값은 true이다.
  • unique
    하나의 칼럼에 유니크 제약 조건을 설정한다.
    디폴트 값은 false이다.

📢 주의 사항

@Column 애너테이션이 생략된 경우 디폴트 값이 적용되어 nullable=true이다.
필드의 데이터 타입이 int, long 같은 Java의 원시 타입인 경우, null값을 입력할 수 없다. null은 객체 타입인 경우에만 적용되기 때문이다.

만약, Java의 원시 타입 필드에서 @Column 애너테이션이 없거나, @Column 애너테이션이 있지만 애트리뷰트를 생략한 경우에는 nullable=false를 설정해야 에러를 방지할 수 있다.

🔑Key Summary

  • @Entity 애너테이션을 클래스 레벨에 추가하면 JPA의 관리대상 엔티티가 된다.

  • @Table 애너테이션엔티티와 매핑할 테이블을 지정한다.

  • @Entity 애너테이션과 @Id 애너테이션은 필수로 추가해야 한다.

  • JPA는 IDENTITY, SEQUENCE, TABLE, AUTO 전략 같은 다양한 기본키 생성 전략을 지원한다.
    ◼ IDENTITY 전략 : 기본키 생성을 데이터베이스에 위임하는 전략

    ◼ SEQUENCE 전략 : 데이터베이스에서 제공하는 시퀀스를 사용해서 기본키를 생성하는 전략

    ◼ TABLE 전략 : 별도의 키 생성 테이블을 사용하는 전략

    ◼ AUTO 전략 : JPA가 데이터베이스의 Dialect에 따라서 적절한 전략을 자동으로 선택

  • java.util.Date, java.util.Calendar 타입으로 매핑하기 위해서는 @Temporal 애너테이션을 추가해야 한다.
    반면에 LocalDate, LocalDateTime 타입일 경우, @Temporal 애너테이션은 생략 가능하다.

  • @Transient 애너테이션을 필드에 추가하면 JPA가 테이블 컬럼과 매핑하지 않겠다는 의미로 인식한다.

  • 테이블에 이미 저장되어 있는 enum 순서 번호와 enum에 정의되어 있는 순서가 일치하지 않게 되는 문제가 발생하지 않도록 EnumType.STRING을 사용하는 것이 좋다.

📑 엔티티와 테이블 매핑 권장 사용 방법

  • 클래스 이름 중복 등의 특별한 이유가 없다면 @Entity@Id 애너테이션만 추가한다.
    엔티티 클래스가 테이블 스키마 명세의 역할을 하길 원한다면 @Table 애너테이션에 테이블명을 지정해줄 수 있다.

  • 기본키 생성 전략은 데이터베이스에서 지원해주는 AUTO_INCREMENT 또는 SEQUENCE를 이용할 수 있도록 IDENTITY 또는 SEQUENCE 전략을 사용하는 것이 좋다.

  • @Column 정보를 명시적으로 모두 지정하는 것은 번거롭지만, 다른 사람이 엔티티 클래스 코드를 확인해도 테이블 설계가 어떤식으로 되어 있는지 한눈에 알 수 있다는 장점이 있다.

  • 엔티티 클래스 필드 타입이 Java의 원시타입일 경우, @Column 애너테이션을 생략하지 않고 nullable=false 설정을 하는게 좋다.

  • @Enumerated 애너테이션을 사용할 때 EnumType.ORDINAL 을 사용할 경우, enum의 순서가 뒤바뀔 가능성도 있으므로 처음부터 EnumType.ORDINAL 대신에 EnumType.STRING 을 사용하는 것이 좋다.


엔티티 간의 연관 관계 매핑

Spring Data JDBC에서 테이블 설계와 클래스 다이어그램 설계를 통해 회원과 주문, 주문과 커피와의 관계를 도출했었던 것처럼 엔티티 클래스 간의 관계를 만들어주는 것이 바로 연관 관계 매핑이다.

연관 관계 매핑은 참조하는 방향성을 기준으로 생각했을때 단방향 연관 관계와 양방향 연관 관계로 구분할 수 있다.
그리고, 엔티티 간에 참조할 수 있는 객체 수에 따라 일대일(1:1), 일대다(1:N), 다대일(N:1), 다대다(N:M)의 연관 관계로 나눌 수 있다.

단방향 연관 관계

Member 클래스가 Order 객체를 원소로 포함하고 있는 List 객체를 가지고 있으므로, Order를 참조할 수 있다. 따라서 Member는 Order의 정보를 알 수 있다.
하지만 Order는 Member의 정보를 알 수 없다.

Order 클래스가 Member 객체를 가지고 있으므로, Member 클래스를 참조할 수 있다. 따라서 Order는 Member의 정보를 알 수 있다.
하지만 Member 클래스는 Order 클래스에 대한 참조 값이 없으므로 Member 입장에서는 Order 정보를 알 수 없다.

이렇게 한쪽 클래스만 다른 쪽 클래스의 참조 정보를 가지고 있는 관계를 단방향 연관 관계라고 한다.

양방향 연관 관계

Member 클래스가 Order 객체를 원소로 포함하고 있는 List 객체를 가지고 있으며 Member 클래스를 참조할 수 있다. 따라서 Order는 Member의 정보를 알 수 있다.

Order 클래스 역시 Member 객체를 가지고 있으므로 Member 클래스를 참조할 수 있다.

두 클래스 모두 서로의 객체를 참조할 수 있으므로 Member는 Order 정보를 알 수 있고, Order도 Member 정보를 알 수 있다.
이렇게 양쪽 클래스가 서로의 참조 정보를 가지고 있는 관계를 양방향 연관 관계라고 한다.

일대다 단방향 연관 관계

일대다의 관계란 1에 해당하는 클래스가 N에 해당하는 객체를 참조할 수 있는 관계이다.

한명의 회원이 여러 건의 주문을 할 수 있으므로 Member와 Order는 일대다 관계이다. 그리고 Member만 List 객체를 참조할 수 있으므로 단방향 관계이다.

그런데, 일대다 단방향 매핑은 잘 사용하지 않는다.

위 그림은 MEMBER 테이블과 ORDERS 테이블의 관계를 나타내는 다이어그램이다.

테이블 간의 관계에서는 일대다 중에서 N에 해당하는 테이블에서 1에 해당하는 테이블의 기본키를 외래키로 갖는다. 따라서 ORDERS 테이블이 MEMBER 테이블의 기본키인 member_id를 외래키로 갖게 된다.

그런데, Order 클래스가 테이블 관계에서 외래키에 해당하는 MEMBER 클래스의 참조값을 가지고 있지 않아, 일반적인 테이블 간의 관계를 정상적으로 표현하지 못한다.
따라서, Order 클래스의 정보를 테이블에 저장하더라도 외래키에 해당하는 MEMBER 클래스의 memberId 값이 없는채로 저장된다. 이러한 문제로 인해 일대다 단방향 매핑은 잘 사용하지 않는다.

그렇지만 일대다 단방향 매핑을 하는 방법을 알아야한다.
그 이유는 다대일 단방향 매핑을 먼저 하고, 필요한 경우에 일대다 단방향 매핑을 추가하여 양방향 연관 관계를 만드는 것이 일반적이기 때문이다.

다대일 연관 관계

다대일의 관계란 N에 해당하는 클래스가 1에 해당하는 객체를 참조할 수 있는 관계이다.

한 명의 회원이 여러 주문을 할 수 있으므로 Order와 Member는 다대일 관계이다. 그리고 Order만 Member 객체를 참조할 수 있으므로 단방향 관계이다.

ORDERS 테이블이 MEMBER 테이블의 member_id를 외래키로 가지듯, Order 클래스가 Member 객체를 외래키처럼 갖고 있다.
즉, 다대일 단방향 매핑은 테이블 간의 관계처럼 자연스러운 매핑 방식이기 때문에 JPA의 엔티티 연관 관계 중에서 가장 기본으로 사용되는 매핑 방식이다.

🔑Key Summary

  • Spring Data JDBC는 엔티티 간에 단방향 매핑만 지원하지만 JPA는 단방향과 양방향 매핑을 모두 지원한다.

  • JPA는 엔티티 간에 일대일, 일대다, 다대일, 다대다 연관 관계 매핑을 지원한다.

  • 일대다 관계는 외래키를 가지고 있어야 할 엔티티에 외래키 역할을 하는 객체 참조가 없기때문에 가급적 사용하지 않는 것이 좋다.

  • 다대일 매핑(@ManyToOne)은 다대일에서 N에 해당하는 엔티티에서 사용한다.
    @JoinColumn 애너테이션은 다대일 매핑(@ManyToOne)에 사용한다.
    @JoinColumn 애너테이션의 name 애트리뷰트 값에는 테이블 조인시 사용되는 외래키가 저장되는 칼럼명을 지정한다.

  • 일대다(@OneToMany) 양방향 매핑은 다대일에서 1에 해당하는 엔티티에서 사용한다.
    @OneToManymappedBy 애트리뷰트의 값으로 외래키 역할을 하는 객체의 필드명을 지정한다.

  • 다대다 연관 관계 매핑은 두 개의 다대일 단방향 매핑을 적용하고, 필요한 경우 양방향 매핑을 적용한다.

  • 일대일 연관 관계 매핑 방식은 @OneToOne 애너테이션을 사용한다는 것 외에 @ManyToOne 단방향 방식, 양방향 방식과 동일하다.

📑 엔티티와 테이블 매핑 권장 사용 방법

  • 일대다 매핑은 사용하지 않는다.

  • 먼저 다대일 단방향 매핑부터 적용한다.

  • 다대일 단방향 매핑을 통해 객체 그래프 탐색으로 조회할 수 없는 정보가 있을 경우, 그때 양방향 매핑을 적용한다.


참고 자료

📄 Fetching
📄 Cascading entity state transitions

0개의 댓글