본문 바로가기

백엔드 학습 과정/Section 3 [Spring MVC, JDBC, JPA, RestDo

#5. JPA

[JPA]

Java에서 사용하는 ORM (Object-Relational Mapping) 기술의 표준 사양.

이는 Java 인터페이스로 사양이 정의되어 있기에 JPA라는 표준 사양을 구현한 구현체는 따로 있다. = eg.Hibernate ORM

 

[데이터 액세스 계층의 구조]

 

JPA는 데이터 엑세스 계층에서의 상단에 위치하며, 데이터 저장, 조회 등의 작업은 JPA를 거쳐

JPA의 구현체인 Hibernate ORM을 통해 이루어지며 내부적으로 JDBC API를 이용해 데이터 베이스에 접근하게 된다.

 

[영속성 컨텍스트 (Persistence Context) ]

JPA는 테이블과 매핑되는 엔티티 객체 정보영속성 컨텍스트 라는 곳에 보관하며,

이렇게 보관된 엔티티 정보는 DB 테이블에 저장, 수정, 조회, 삭제하는 데 활용된다.

영속성 컨텍스트 공간에는 1차 캐시, 쓰기 지연 SQL 저장소 라는 영역이 있고, 이는 Entity Manager에 의해 관리된다.

 

[코드로 JPA API 사용하기]

1. JPA API 의존 라이브러리 설정.

build.gradle 파일의 Dependencies 안에 하기 코드 추가.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // (1)

2. JPA 설정. (application.yml)

spring:
  h2:
    console:
      enabled: true
      path: /h2     
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
      ddl-auto: create  # (1) 스키마 자동 생성
    show-sql: true      # (2) SQL 쿼리 출력

(1) : JPA에서 사용하는 엔티티 클래스를 정의하고, 애플리케이션 실행 시, 엔티티와 매핑된 테이블을 DB에서 자동 생성.

(2) : JPA의 동작 과정을 이해하기 위해 JPA API를 통해서 실행되는 SQL 쿼리를 로그로 출력해준다.

 

3. 샘플 코드 실행을 위한 Configuration 클래스 생성.

// (1)
@Configuration
public class JpaBasicConfig {
    private EntityManager em;
    private EntityTransaction tx;

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

        return args -> {
            // (3) 이 곳에 학습할 코드를 타이핑합니다.
        };
    }
}

(1) : @Configuration 애너테이션을 붙이면 Spring에서 해당 클래스를 Bean 검생 대상으로 간주한다.

(2) : @Bean 메서드를 검색하여 리턴하는 객체를 Spring Bean으로 등록한다.

 

4. 영속성 컨텍스트에 저장할 엔티티 클래스 생성.

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

    private String email;

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

(1) : @Entity 애너테이션을 설정하여 엔티티 클래스임을 명시.

(2) : 엔티티 클래스와 매핑할 테이블의 식별자가 될 변수에 @Id, @GeneratedValue 추가

 

5. JPA 영속성 컨텍스트에 등록된 객체를 테이블에 저장

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

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

        return args -> {
            example02();
        };
    }
    private void example02() {
        // (2)
        tx.begin();
        Member member = new Member("hgd@gmail.com");
        // (3)
        em.persist(member);
        // (4)
        tx.commit();
        // (5)
        Member resultMember1 = em.find(Member.class, 1L);
        Member resultMember2 = em.find(Member.class, 2L);
        
        System.out.println(resultMember2 == null);
    }
}

(2) : 트랜잭션을 시작하겠다 의미.

(3) em.persist() : 영속성 컨텍스트에 등록. (1차 캐시 O, 쓰기 지연 SQL 저장소 O)

(4) tx.commit(); : 영속성 컨텍스트에 등록한 데이터를 DB로 저장. (1차 캐시 O, 쓰기 지연 SQL 저장소 X)

(5) em.find(Member.class, 1L); : 입력한_엔티티클래스(member)에서 식별자1 을 가진 데이터를 가져온다.

 

6. 영속성 컨텍스트와 테이블에 엔티티 업데이트

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

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

        return args -> {
             example04();
        };
    }

    private void example04() {
       tx.begin();
       em.persist(new Member("hgd1@gmail.com"));    // (1)
       tx.commit();    // (2)


       tx.begin();
       Member member1 = em.find(Member.class, 1L);  // (3)
       member1.setEmail("hgd1@yahoo.co.kr");       // (4)
       tx.commit();   // (5)
    }
}

(1). 영속성 컨텍스트에 Member 객체 (이메일)로 생성하여 등록함.

(2) : DB 테이블에 저장.

(3) : 영속성 컨텍스트에 저장된 데이터 중 조회할 엔티티 클래스타입, 식별자 번호의 데이터를 불러와서 member1 에 할당.

(4) : member1을 setEmail()메서드를 활용해서 이메일 주소를 변경함.

(5) : setter 메서드로 이메을을 변경한 member1을 commit처리하여 DB에 저장함.

 

7. 영속성 컨텍스트와 테이블에 엔티티 삭제

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

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

        return args -> {
            example05();
        };
    }

    private void example05() {
        tx.begin();
        em.persist(new Member("hgd1@gmail.com"));  // (1)
        tx.commit();    //(2)

        tx.begin();
        Member member = em.find(Member.class, 1L);   // (3)
        em.remove(member);     // (4)
        tx.commit();     // (5)
    }
}

(1) : Member 클래스 객체를 만들어서 이메일로 데이터 생성 후 영속성 컨텍스트에 등록.

(2) : 등록한 Member 객체 DB에 저장.

(3) : Member 엔티티 클래스에 식별자1번을 가진 데이터를 호출하여 member 변수에 할당함.

(4) : em.remove(member) : member라는 변수를 EntityManager에 의해 삭제함.
(5) : 삭제 이후 다시 commit함.

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

 

JPA를 이용해 DB 테이블과 상호작용 (저장, 수정, 조회, 삭제)를 위해

제일 필요한 것은 DB 테이블과 엔티티 클래스간의 매핑 작업이다. 

 

[매핑 작업]

1. 엔티티 클래스와 테이블간의 매핑

2. 기본키 매핑

3. 객체 필드와 테이블 컬럼의 매핑

4. 엔티티 클래스들의 연관 관계 매핑

 

 

[매핑작업] - 1. 엔티티 객체와 테이블 간의 매핑

package com.codestates.entity_mapping.single_mapping.table;

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

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

(1) : @Entity 매핑 애너테이션 사용. 옵션으로 @Entity(name="원하는_엔티티_이름")

(2) : @Table은 옵션 이며, 추가하지 않을 경우, 클래스 이름을 테이블 이름으로 사용한다.

(3) : @NoArgsConstructor, 파라미터가 없는 기본 생성자는 습관적으로 추가하기.

> @Table은 옵션,  하지만 @Entity + @Id 애너테이션은 필수

 

[매핑작업] - 2. 기본키 매핑

A. 기본키 직접 할당 

A-1 : @Entity 클래스의 필드 변수에 @Id 애너테이션 지정으로 기본키 직접 할당

A-2 : tx.bein()

         em.persist(new Member(1L)); 

         tx.commit()

영속성 컨텍스트에 저장할 때 객체 생성하며 기본키 직접 할당.

 

B. 기본키 자동 생성

B-1 : IDENTITY : 테이블에 데이터를 저장한 후, 기본키 값이 자동으로 생성되어 할당됨.

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

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

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY) 사용.

 

B-2 : SEQUENCE : 엔티티를 영속성 컨텍스트에 저장하기 전에 DB에서 시퀀스 값(자동번호생성)을 조회하여 할당.

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

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

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE) 사용.

 

B-3 : AUTO : JPA가 DB의 Dialect에 따라서 적절한 전략을 자동으로 선택.

* Dialect : 표준 SQL 등이 아닌 특정 DB에 특화된 고유한 기능.

package com.codestates.entity_mapping.single_mapping.id.sequence;

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

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

 

[매핑작업] - 3. 객체 필드와 테이블 컬럼의 매핑

@Column : 앤티티 클래스 필드와 테이블 컬럼을 매핑.

 

@Column의 Attribute

1. @Column (nullable = true/false) : 컬럼에 빈 값(null) 허용 여부, default : true

>> 필드 타입이 String, int, char, long 등과 같이 원시 타입이면 nullable = false 설정할 것.

2. @Column (updatable = true/false) : 컬럼이 수정의 허용 여부, default : true

3. @Column (unique = true/false) : 컬럼이 중복 값 허용 여부, default : false

4. @Column (length = 0~255) : 컬럼에 빈 값(null) 허용 여부, default : 255

5. @Column (name = ) : 컬럼에 별도의 이름을 지정해 필드명과 테이블 컬럼명을 다르게 설정.

 

@Transient

추가된 필드는 테이블의 컬럼에 포함시키지 않겠다 라는 의미.

 

[매핑작업] - 4. 엔티티 클래스들의 연관 관계 매핑

A. 참조하는 방향성 기준.

A-1 : 단방향 연관 관계

A-2 : 양방향 연관 관계

 

B. 참조할 수 있는 객체 수 기준.

B-1 : [1:N 관계]

B-2 : [N:N 관계]

B-3 : [N:1 관계]

B-4 : [1:1 관계]

 

 

A. 참조하는 방향성 기준.

A-1 : [단방향 연관 관계] : 한쪽 엔티티 클래스에서만 상대 엔티티 클래스의 참조 객체를 가지고 있는 관계

                                                                          OR

 

A-2 : [양방향 연관 관계] : 양쪽 엔티티 클래스가 서로의 참조 객체를 가지고 있어 서로 참조가 가능한 관계

1:N 관계

1클래스 : N클래스의 객체를 List/Set <N클래스명> 타입으로 필드를 가진다.

N클래스 : 1클래스의 객체를 필드로 가진다.

 

 

B. 참조하는 객체 수 기준

B-1 : [1:N 관계] = 단방향 연관 관계

1 클래스가 N클래스를 List/Set <N클래스명> 필드를 가진다.

하지만 테이블의 관계에서는 1테이블의 PKN테이블에서 FK로 가진다.

즉 클래스 관계에서의 필드와 테이블의 컬럼간에 관계가 맞지 않으므로 단독의 1:N 매핑은 사용되지 않는다.

 

B-2 : [N:1 관계]

N클래스가 1클래스의 객체를 필드로 가지는 관계.

 

<코드로 보는 N:1 연관 관계 매핑>  // Member : Order 중 N에 해당하는 Order 클래스

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

    @ManyToOne   // (1)
    @JoinColumn(name = "MEMBER_ID")  // (2)
    private Member member;

    public void addMember(Member member) {
        this.member = member;
    }
    
    
    @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;
        }
    }
}

(1) : @ManyToOne : N:1 관계임을 나타내며, N클래스에서 1클래스 객체 필드에 사용한다.

(2) : @JoinColumn(name = "") : N테이블에서의 FK 컬럼명 (=1테이블의 PK 컬럼명)

 

<N:1 연관 관계 매핑을 이용한 회원과 주문 정보 저장>

<Configuration 클래스 생략..>

@Configuration
public class JpaManyToOneUniDirectionConfig {
    private EntityManager em;
    private EntityTransaction tx;
    @Bean
    public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();
        return args -> {
            mappingManyToOneUniDirection();
        };
    }

    private void mappingManyToOneUniDirection() {
        tx.begin();
        // (1)
        Member member = new Member("hgd@gmail.com", "Hong Gil Dong",
                "010-1111-1111");

	// (2)
        em.persist(member);
		
        // (3)
        Order order = new Order();
        // (4)
        order.addMember(member);    
        // (5)
        em.persist(order);           
		
        // (6)
        tx.commit();

		// (7)
        Order findOrder = em.find(Order.class, 1L);

        // (8)
        System.out.println("findOrder: " + findOrder.getMember().getMemberId() +
                        ", " + findOrder.getMember().getEmail());
    }
}

(1) : 회원 객체 member 생성.

(2) : 생성한 회원 객체 member, 영속성 컨텍스트에 등록

(3) : 신규 주문 객체 order 생성

(4) : 주문 객체 order에 생성한 회원 객체 member 추가.

= 주문 정보에 특정 회원의 정보를 부여함으로써 주문에 회원정보가 포함된다.

(5) : 주문 객체 영속성 컨텍스트에 추가. 

(6) : DB에 저장.

(7) : 저장된 데이터중 Order 엔티티 클래스타입과 식별자 1을 가진 데이터를 Order findOrder로 할당.

(8)

findOrder.getMember().getMemberId() : findOrder객체에서 Member 객체를 찾아서 포함된 MemberId를 가져오기.

findOrder..getMember().getEmail() : findOrder객체에서 Member 클래스에 Email이라는 변수값을 가져오기.

 

 

<N:1 연관 관계 매핑에 1:N 매핑 추가>  Member : Order

: 현재 주문은 회원의 정보를 조회할 수 있지만, 회원은 주문정보를 조회할 수 없다. 

 

추가하는 1:N 매핑에서는 B-1과 같이 1클래스에서 N클래스 객체를 List/Set<N클래스>로 가진다.

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;
    
    // (1)
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();


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

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

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

    public void addOrder(Order order) {
        orders.add(order);
    }
}

(1) @OneToMany(mappedBy = "")

N:1 매핑 후 1:N 추가의 경우1클래스에 가지는 N클래스의 필드에 사용.

mappedBy = ""에는 N클래스에서 @ManyToOne 애너테이션 가지고있는 필드 명.

 

<N:1 연관 관계 매핑에 1:N 매핑 추가하여 주문 정보 조회>  Member : Order

<Configuration 클래스 생략..>

private void mappingManyToOneBiDirection() {
        tx.begin();
        // (1)
        Member member = new Member
        ("hgd@gmail.com", "Hong Gil Dong", "010-1111-1111");
        // (2)
        Order order = new Order();

	// (3)
        member.addOrder(order);
        // (4)
        order.addMember(member);
	// (5)
        em.persist(member);   
        em.persist(order);    
	// (6)
        tx.commit();

	// (7)
        Member findMember = em.find(Member.class, 1L);

        
        findMember
        		// (8)
                .getOrders()
                .stream()
                .forEach(findOrder -> {
                    System.out.println("findOrder: " +
                    		// (9)
                            findOrder.getOrderId() + ", "
                            + findOrder.getOrderStatus());
                });
    }
}

(1) : 회원 정보 생성

(2) : 주문 정보 생성

(3) : 회원 정보에 주문 정보 추가

(4) : 주문 정보에 회원 정보 추가

(5), (6) : 회원, 주문 정보 저장

(7) : DB에서 회원 타입의 데이터중 식별자1을 가진 데이터를 findMember에 할당.

(8) : findMember.getOrders()로 findMember 변수에 포함되는 주문 정보를 불러온다.

(9) : 불러온 주문 정보에서 OrderId, OrderStatus 정보를 불러온다.

 

 

B-3 : [N:N 관계]

테이블의 관계에서는 <중간 객체>를 생성하여 1:N, N:1 관계로 만든다.

<중간 객체>는 양쪽의 객체를 FK로 가지고 있으므로, 테이블을 기반으로

엔티티 클래스간의 매핑을 진행하면된다. 

 

1. N:1 관계 먼저 매핑

N클래스

@ManyToOne : N클래스에서 1클래스 객체를 필드로 작성 = N:1 관계 명시

@JoinColumn(name="1 테이블의 PK 컬럼명")

 

1클래스

@OneToMany (mappedby="N클래스에서 가지고있는 1클래스의 필드명")

N:1 관계에서 1클래스에서 작성하며, N클래스를 List/Set 형태로 필드 선언 후 애너테이션 추가.

 

 

2. N:1 매핑이 종료된 이후, 1쪽에서 N쪽의 정보를 참조할 기능이 필요하다면 1:N 추가하여 양방향 처리.

1클래스

@OneToMany (mapped ="N클래스에서 필드로 선언한 1클래스의 객체명")

N클래스를 List/Set<N클래스명> 형태로 필드 선언

 

 

B-4 : [1:1 관계]

양쪽의 엔티티 클래스 필드에 상대 클래스 객체 선언.

@OneToOne(mappedby="상대클래스 필드에 선언된 본인 클래스명", cascade = CascadeType.PERSIST)

@JoinColumn(name="상대1클래스_테이블의_PK컬럼명)

상대_1클래스_필드