JPA 기반 데이터 액세스 계층(2) - 엔티티 매핑

Backend kwon·2023년 9월 15일
0

엔티티와 테이블 간의 매핑

@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 attribute

  • name
    • 엔티티 이름을 설정할 수 있습니다.

@Table attribute

  • name
    • 테이블 이름을 설정할 수 있습니다.
    • @Table 애너테이션은 옵션이며, 추가하지 않을 경우 클래스 이름을 테이블 이름으로 사용합니다.
    • 주로 테이블 이름이 클래스 이름과 달라야 할 경우에 추가합니다.

 
추가적으로
@Table 애너테이션은 옵션이지만 @Entity 애너테이션과 @Id 애너테이션은 필수입니다.

파라미터가 없는 기본 생성자는 필수로 추가합니다.
Spring Data JPA의 기술을 적용할 때 기본 생성자가 없는 경우 에러가 발생하는 경우가 있기 때문에 기본 생성자는 습관적으로 추가하는 것이 좋습니다.

 

기본키 매핑

데이터베이스의 테이블에 기본키 설정은 필수라고 할 수 있습니다.

JPA에서 지원하는 기본키 생성 전략

  • 기본키 직접 할당
    애플리케이션 코드 상에서 기본키를 직접 할당해주는 방식입니다.

  • 기본키 자동 생성

    • IDENTITY
      • 기본키 생성을 데이터베이스에 위임하는 전략입니다.
      • 데이터베이스에서 기본키를 생성해 주는 대표적인 방식은 MySQL의 AUTO_INCREMENT 기능을 통해 자동 증가 숫자를 기본키로 사용하는 방식이 있습니다.
    • SEQUENCE
      • 데이터베이스에서 제공하는 시퀀스를 사용해서 기본키를 생성하는 전략입니다.
    • TABLE
      • 별도의 키 생성 테이블을 사용하는 전략입니다.

즉, 해당 방식들은 사용하는 DB에 의존합니다.
ex)MySQL은 IDENTITY 사용, Oracle은 SEQUENCE 사용

 
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로 지정해 주면 됩니다.

IDENTITY 전략은 데이터베이스에서 기본키를 자동으로 대신 생성해 줍니다.

 
2. 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)를 지정하면 됩니다.

엔티티가 영속성 컨텍스트에 저장되기 전에 데이터베이스가 시퀀스에서 기본키에 해당하는 값을 제공할 것입니다.

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

(Dialect는 표준 SQL 등이 아닌 특정 데이터베이스에 특화된 고유한 기능을 의미합니다.
만일 JPA가 지원하는 표준 문법이 아닌 특정 데이터베이스에 특화된 기능을 사용할 경우 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 attribute

  • nullable
    • 열에 null 값을 허용할지 여부를 지정합니다.
    • 디폴트 값은 true입니다.
    • email 주소는 일반적으로 회원 정보에서 ID로 많이 사용되며, 따라서 필수 항목이기 때문에 nullable 값을 false로 지정합니다.
  • updatable
    • 열 데이터를 수정할 수 있는지 여부를 지정합니다.
    • 디폴트 값은 true입니다.
    • 여기서는 email 주소가 사용자 ID 역할을 한다고 가정하고 한번 등록되면 수정이 불가능하도록 하기 위해서 updatable 값을 false로 지정했습니다.
  • unique
    • 하나의 열에 unique 유니크 제약 조건을 설정합니다.
    • 디폴트 값은 false입니다.
    • email의 경우 고유한 값이어야 하므로 unique 값을 true로 지정했습니다.

 
@Column 애너테이션이 생략되었거나 애트리뷰트가 기본값을 사용할 경우 주의 사항
int나 long 같은 원시 타입일 경우, @Column 애너테이션이 생략되면 기본적으로 nullable=false입니다.

그러나 nullable에 대한 명시적인 설정 없이 단순히 @Column 애너테이션만 추가하면 nullable=true가 기본값이 되기 때문에 테이블에는 int price not null로 열이 설정되는 것이 아니라 int price와 같이 설정이 될 것입니다.

따라서 개발자가 의도하는 바가 int price not null일 경우에는 @Column(nullable=false)라고 명시적으로 지정하든가 아예 @Column 애너테이션 자체를 사용하지 않는 것이 권장됩니다.

엔티티 클래스에서 발생한 예외 처리

계층은 다르지만 엔티티 클래스 필드의 설정으로 인해 발생한 예외는 API 계층의 DTO 클래스에서 발생한 예외와 마찬가지로 일종의 유효성(Validation) 검증이라고 볼 수 있습니다.

엔티티 클래스에서 발생한 예외는 API 계층까지 전파되므로 API 계층의 GlobalExceptionAdvice에서 캐치(catch) 한 후, 처리할 수 있습니다.

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

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

		// (1)
    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();   // (2)

    // (3)
    @Column(nullable = false, name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt = LocalDateTime.now();

    // (4)
    @Transient
    private String age;

		public Member(String email) {
        this.email = email;
    }

	  public Member(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }
}

@Column 애너테이션의 추가 attribute

  • length
    • 열에 저장할 수 있는 문자 길이를 지정할 수 있습니다.
    • 디폴트 값은 255입니다.
  • name
    • name 애트리뷰트를 생략하면 엔티티 클래스 필드의 이름으로 열이 생성되지만 (3)과 같이 name 애트리뷰트에 별도의 이름을 지정해서 엔티티 클래스 필드명과 다른 이름으로 열을 생성할 수 있습니다.

 
(2)는 회원 정보가 등록될 때의 시간 및 날짜를 매핑하기 위한 필드입니다.

  • java.util.Date, java.util.Calendar 타입으로 매핑하기 위해서는 @Temporal 애너테이션을 추가해야 LocalDateTime 타입일 경우, @Temporal 애너테이션은 생략 가능합니다.
  • LocalDateTime은 열의 TIMESTAMP 타입과 매핑됩니다.
  • 회원 정보가 등록되는 시간 정보를 필드에 전달하기 위해 createdAt 필드에 LocalDateTime.now() 메서드로 현재 시간을 입력하고 있습니다.

 
(4)와 같이 @Transient 애너테이션을 필드에 추가하면 테이블 열과 매핑하지 않겠다는 의미로 JPA가 인식합니다.

  • 따라서 데이터베이스에 저장도 하지 않고, 조회할 때 역시 매핑되지 않습니다.
  • @Transient은 주로 임시 데이터를 메모리에서 사용하기 위한 용도로 사용됩니다.

 

@NoArgsConstructor
@Getter
@Setter
@Entity(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    // (1)
    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt = LocalDateTime.now();

    public enum OrderStatus {
        ORDER_REQUEST(1, "주문 요청"),
        ORDER_CONFIRM(2, "주문 확정"),
        ORDER_COMPLETE(3, "주문 완료"),
        ORDER_CANCEL(4, "주문 취소");

        @Getter
        private int stepNumber;

        @Getter
        private String stepDescription;

        OrderStatus(int stepNumber, String stepDescription) {
            this.stepNumber = stepNumber;
            this.stepDescription = stepDescription;
        }
    }
}

@Enumerated 애너테이션

  • enum 타입과 매핑할 때 사용하는 애너테이션입니다
  • EnumType.ORDINAL : enum의 순서를 나타내는 숫자를 테이블에 저장합니다.
  • EnumType.STRING : enum의 이름을 테이블에 저장합니다.

주의할것은
EnumType.ORDINAL로 지정할 경우, 기존에 정의되어 있는 enum 사이에 새로운 enum 하나가 추가된다면 그때부터 테이블에 이미 저장되어 있는 enum 순서 번호와 enum에 정의되어 있는 순서가 일치하지 않게 되는 문제가 발생합니다.

따라서 처음부터 이런 문제가 발생하지 않도록 EnumType.STRING을 사용하는 것을 권장하고 있습니다.

profile
백엔드개발자를 향해서

0개의 댓글