티스토리 뷰

728x90
반응형

 

 

 

지난 글에서 결국 JPA의 목적은

 

  • Object ↔ DB Table 매핑 (설계)
  • 영속성 컨텍스트 (JPA 내부 동작)

라고 정리하고 영속성 컨텍스트에 대해 알아봤었다.

 

2022.08.31 - [개발/Spring] - [Spring]JPA(Java Persistence API)

 

[Spring]JPA(Java Persistence API)

지난 글에서 JDBC의 단점을 극복하며 나온 ORM에 대해 다루었다. 2022.08.26 - [개발/Spring] - [Spring]SQL Mapper vs. ORM [Spring]SQL Mapper vs. ORM 지난 글에선 웹 앱의 계층 구조와 JDBC Workflow에 대해..

gnidinger.tistory.com

이번 글에서는 JPA 매핑 애너테이션을 이용해 엔티티와 테이블 사이의 매핑을 진행한다.

 

 

Entity Mapping

 

엔티티 매핑 작업은 크게 네 부분으로 나눌 수 있다.

 

  1. 객체와 테이블 간의 매핑
  2. 기본 키 매핑
  3. 필드와 컬럼 간의 매핑
  4. 엔티티 간의 연관 관계 매핑

이 글에서는 4번을 제외한 단일 엔티티 매핑에 대해 살펴보겠다.

 

Entity ↔ Table Mapping

package com.gnidinger.single_mapping;

import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@NoArgsConstructor // 필수
@Entity(name = "USERS") // 필수, 괄호 안은 생략 가능
@Table(name = "USERS") // 생략 가능
public class Member {
    @Id // 필수
    private Long memberId;
}

먼저 새로운 패키지에 엔티티 클래스를 생성했다.

 

지난 글에도 봤듯이 클래스 레벨에 @Entity 애너테이션을 붙이면 JPA 관리 대상 엔티티로 등록된다.

 

또한 @Entity와 @Id만 붙여도 자동으로 엔티티 클래스와 테이블 간의 매핑이 이루어지며, 괄호 안은 생략 가능하다.

 

@Table은 테이블 이름과 클래스 이름을 다르게 붙이고 싶을 경우에 사용하며, 생략할 경우 클래스 이름 = 테이블 이름이 된다.

 

중요한 점은 에러 방지를 위해 @Id와 @NoArgsConstructor를 반드시 사용해주어야 한다는 것이다.

 

Primary Key Mapping

 

JPA에서는 기본적으로 @Id를 추가한 필드가 기본 키가 되는데, 이 기본 키를 생성하는 방식에 몇 가지 전략이 있다.

 

  • 기본키 직접 할당 - 코드에서 기본키를 직접 할당해주는 방식
  • 기본키 자동 생성

    • IDENTITY

      • 기본키 생성을 데이터베이스에 위임하는 전략
      • 대표적으로 MySQL의 AUTO_INCREMENT 기능을 통한 자동 증가 숫자를 기본키로 사용하는 방식이 있음
    • SEQUENCE

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

      • 별도의  생성 테이블을 사용하는 전략

별도의 테이블을 이용하는 방법은 성능 면에서 좋지 않기 때문에 생략한다.

 

기본키 직접 할당 전략

package com.gnidinger.single_mapping;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.Id;

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    private Long memberId;

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

단순히 @Id를 추가하는 것으로 적용할 수 있다.

package com.gnidinger.single_mapping;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

@Configuration
public class JpaIdDirectMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            em.persist(new Member(1L));  // 기본 키 직접 할당
            tx.commit();
            Member member = em.find(Member.class, 1L);

            System.out.println("# memberId: " + member.getMemberId());
        };
    }
}

Configuration 클래스에서 persist() 요청을 보낼 때 기본 키를 직접 할당하는 것을 확인할 수 있다.

 

IDENTITY 전략 

package com.gnidinger.single_mapping;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

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

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

@GeneratedValue의 속성에서 strategy = GenerationType.IDENTITY로 지정하면 된다.

 

이 방법은 데이터베이스에서 기본 키를 대신 생성해주는 전략이다.

package com.gnidinger.single_mapping;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

@Configuration
public class JpaIdIdentityMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

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

            System.out.println("# memberId: " + member.getMemberId());
        };
    }
}

persist() 메서드에 기본 키를 직접 할당하던 부분을 삭제한 것을 확인할 수 있다.

 

이대로 앱을 실행시켜보면

Hibernate: insert into member (member_id) values (default)
# memberId: 1

위와 같은 로그를 볼 수 있다.

 

SEQUENCE 전략

package com.gnidinger.single_mapping;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

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

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

strategy를 SEQUENCE로 변경했다.

 

이 전략은 DB의 시퀀스를 이용하는 전략이다.

package com.gnidinger.single_mapping;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

@Configuration
public class JpaIdIdSequenceMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

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

        };
    }
}

Configuration 클래스는 이름 말고는 바뀐 부분이 없다.

Hibernate: create sequence hibernate_sequence start with 1 increment by 1

Hibernate: call next value for hibernate_sequence
# memberId: 1
Hibernate: insert into member (member_id) values (?)

로그를 확인하면 DB에 시퀀스 생성 후 조회해서 할당하는 것을 볼 수 있다.

 

Auto 전략

 

@GeneratedValue(strategy = GenerationType.AUTO)로 지정하면

 

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

 

Dialect는 표준 SQL 등이 아닌 특정 데이터베이스에 특화된 고유한 기능과 그에 따른 쿼리를 말하는데,

 

JPA는 Hibernate에서 지원하는 Dialect 클래스를 상속받으면 그에 맞는 쿼리를 자동으로 생성해준다.

 

 

Field ↔ Column Mapping

 

마지막으로 엔티티 필드와 테이블 컬럼 간의 매핑을 알아보자.

 

Member Entity Class Field ↔ Column Mapping

 

package com.gnidinger.single_mapping;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

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

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

@Column은 필드와 컬럼을 매핑해주는 애너테이션이다.

 

만약 @Column이 없이 필드만 정의되어 있다면 JPA는 기본적으로 이 필드가 테이블의 컬럼과 매핑되는 필드라고 간주한다.

 

생략했을 경우 속성이 전부 기본값으로 정해지지만, 필드가 원시 타입일 경우엔 최소한 nullable=false는 해주는 것이 좋다.

 

@Column의 속성은 아래와 같다.

 

  • nullable - 컬럼에 null 값을 허용할지 여부를 지정. 디폴트 값은 true
  • updatable - 컬럼 데이터를 수정할 수 있는지 여부를 지정. 디폴트 값은 true
  • unique - 하나의 컬럼에 unique(중복 X) 제약 조건을 설정. 디폴트 값은 false
package com.gnidinger.single_mapping;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

@Configuration
public class JpaColumnMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
//            testEmailNotNull();   // (1)
//            testEmailUpdatable(); // (2)
//            testEmailUnique();    // (3)
        };
    }

    private void testEmailNotNull() {
        tx.begin();
        em.persist(new Member());
        tx.commit();
    }

    private void testEmailUpdatable() {
        tx.begin();
        em.persist(new Member("gni@gmail.com"));
        Member member = em.find(Member.class, 1L);
        member.setEmail("gni@naver.com");
        tx.commit();
    }

    private void testEmailUnique() {
        tx.begin();
        em.persist(new Member("gni@gmail.com"));
        em.persist(new Member("gni@gmail.com"));
        tx.commit();
    }
}

(1) - email 필드에 아무 값도 입력하지 않고 데이터를 저장하는 메서드. 에러가 발생해야 함.

 

(2) - 등록한 email 주소를 수정하는 메서드. 수정되지 않아야 함.

 

(3) - 등록된 emial 주소를 한번 더 등록하는 메서드. 에러가 발생해야 함.

 

하나씩 주석을 해제해가며 테스트하면 위에 쓰인 대로 작동하는 것을 확인할 수 있다.

 

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

 

엔티티 클래스에서 발생한 예외는 API 계층의 GlobalExceptionAdvice 에서 받아 처리할  있다.

 

매핑할 필드와 애너테이션을 추가한 전체 코드는 아래와 같다.

package com.gnidinger.single_mapping;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

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

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

    @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();   // 등록 날짜 및 시간

    @Column(nullable = false, name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt = LocalDateTime.now(); // 수정 날짜 및 시간
    
    @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;
    }
}

@Transient를 사용하면 해당 필드는 테이블 컬럼과 매핑되지 않는다.

 

주로 임시 데이터를 메모리에서 사용하기 위한 용도로 쓰인다.

 

Coffee Entity Class Field ↔ Column Mapping

 

package com.gnidinger.single_mapping;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

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

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Coffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long coffeeId;

    @Column(nullable = false, length = 50)
    private String korName;

    @Column(nullable = false, length = 50)
    private String engName;

    @Column(nullable = false)
    private int price;

    @Column(nullable = false, length = 3)
    private String coffeeCode;

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

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

 

Order Entity Class Field ↔ Column Mapping

 

package com.gnidinger.single_mapping;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

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

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

    @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 이름을 테이블에 저장(권장됨)
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함