백엔드/Spring Boot

5. 데이터베이스 조작이 편해지는 ORM (2) - JPA와 하이버네이트, 스프링 데이터와 스프링 데이터 JPA

두개의 문 2023. 7. 18. 22:30

3. JPA와 하이버네이트 

◉ DBMS에도 여러 종류가 있는 것처럼, ORM에도 여러 종류가 있음 

 

▪︎ 자바에서는 JPA( Java Persistence API )를 표준으로 사용함 

▪︎ JPA : 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스 

  → 실제 사용을 위해서는 ORM 프레임 워크를 추가로 선택해야 함 

  ⇒ 대표적으로 Hibernate를 많이 사용함 

 

▪︎ 하이버네이트

  - JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크

  - 내부적으로 JDBC API 사용 

    → 하이버네이트의 목표 : 자바 객체를 통해 데이터베이스 종류에 상관없이 데이터베이스를 자유자재로 사용할 수 있게 해줌 

 

🗣️ JPA와 하이버네이트의 역할 ★★★

  - JPA : 자바 객체와 데이터베이스를 연결해 데이터를 관리 

               객체 지향 도메인 모델과 데이터베이스의 다리 역할 

  - 하이버네이트 : JPA의 인터페이스를 구현, 내부적으로 JDBC API 사용 

 

 

 

◉ JPA의 중요한 컨셉 - 엔티티 메니저 / 영속성 컨텍스트 ★★★

 

1) 엔티티 매니저란?

 

  ▪︎ 엔티티 (entity) 

    • 데이터베이스의 테이블과 매핑되는 객체를 의미 

    • 본질적으로 자바 객체이므로 일반 객체와 다르지 않으나, 데이터베이스의 테이블과 직접 연결된다는 특징으로 구분됨 

       → 객체이긴 하지만, 데이터베이스에 영향을 미치는 쿼리를 실행하는 객체 

  

  ▪︎ 엔티티 매니저  

    • 엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 등의 역할 

    • 엔티티 매니저 팩토리(entity manager factory)에서 생성됨 

    • ( 예시 )

       회원 2명이 동시에 회원가입을 하려는 경우,

       회원 1의 요청에 대해서 가입 처리할 엔티티 메니저를 엔티티 매니저 팩토리에서 생성하면,

       이를 통해 가입 처리해 데이터베이스에 회원 정보를 저장 / 회원 2도 동일하게 진행 

       회원 1, 2를 위해 생성된 엔티티 매니저는 필요한 시점에 데이터베이스와 연결한 뒤에 쿼리함 

 

 

 

◉ 스프링부트에서도 직접 엔티티 매니저 팩토리를 만들어서 관리할까? 사실 그렇지 않음! 

  ▪︎ 스프링 부트에서 엔티티 매니저 팩토리를 하나만 생성해서 관리

    → @PersistenceContext 또는 @Autowired 애너테이션을 사용해서 앤티티 매니저를 사용함 

 

// * 스프링 부트가 엔티티 매니저를 사용하는 방법 예
@PersistenceContext
EntityManager em;	// 프록시 엔티티 매니저. 필요할 때 진짜 엔티티 매니저 호출

  ▪︎ 스프링 부트에서 빈은 하나만 생성해서 공유 → 동시성 문제 발생할 수 있음 

   - 실제 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용함 

   - 필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 매니저를 호출함 

 

   ※ 엔티티 매니저는 Spring Data JPA에서 관리하므로, 직접 생성하거나 관리할 필요 없음 

 

 


2) 영속성 컨텍스트란?

 

◉ 영속성 컨텍스트

 

- 엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장함 

  - 영속성 컨텍스트

     • 엔티티를 관리하는 가상의 공간 

     • 데이터를 생성한 프로그램이 종료되더라도 사라지지 않는 데이터의 특성을 의미 

       ( 영속성을 갖지 않는 데이터와 비교 : 단지 메모리에서만 존재 → 프로그램 종료 시 모두 잃어버리게 됨 )

      → 영속성 컨텍스트로 인해 데이터베이스에서 효과적으로 데이터를 가져올 수 있고, 엔티티를 편하게 사용할 수 있음 

 

 

 ◉ 영속성 컨텍스트의 기본적인 특징

 

  ① 1차 캐시 

    • 영속성 컨텍스트 : 내부에 1차 캐시를 가지고 있음 

    • 캐시의 키 : 엔티티의 @Id 애너테이션에 달린 기본키 역할을 하는 식별자 

       캐시의 값 : 엔티티 

    • 엔티티 조회 시, 1차 캐시에서 조회 → (값이 없으면) 데이터베이스에서 조회 

       ( 데이터베이스에서 조회 후 1차 캐시에 저장한 다음 반환 )

    • 캐시된 데이터를 조회할 때에는 데이터베이스를 거치지 않음 → 매우 빠르게 데이터 조회 가능 

    

  ② 쓰기 지연 (tranactional write-behind)

    • 트랜잭션을 커밋하기 전까지는 데이터베이스에 실제로 질의문을 보내지 않고 모음

       → 트랜잭션 커밋하는 시점에 모았던 쿼리를 한꺼번에 전송 

    • 적당한 묶음으로 쿼리 요청 가능 → 데이터베이스 시스템의 부담을 줄일 수 있음 

 

  ③ 변경 감지 

    • 트랜잭션 커밋 → 1차 캐시에 저장되어 있는 엔티티의 값과 현재 엔티티의 값을 비교해서 변경된 값이 있다면 변경사항을 감지, 변경된 값을 데이터베이스에 자동으로 반영함  

    • 쓰기 지연과 동일하게 적당한 묶음으로 쿼리 요청 가능 → 데이터베이스 시스템의 부담을 줄일 수 있음 

 

  ④ 지연 로딩 (lazy loading)

    • 쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라 필요할 때 쿼리를 날려 데이터 조회 

    ↔︎ 즉시 로딩 : 조회할 때 쿼리를 보내 연관된 모든 데이터를 즉시 조회 

 

 ⇒ 이러한 특징으로 인해 데이터베이스의 접근을 최소화시켜 성능 향상이 가능함 

 

 


3) 엔티티의 상태 

 

◉ 엔티티의 상태 - 4가지 

  - 분리(detached) 상태 : 영속성 컨텍스트가 관리하고 있지 않는 상태 

  - 관리(managed) 상태 : 영속성 컨텍스트가 관리하는 상태 

  - 비영속(transient) 상태 : 영속성 컨텍스트와 전혀 관계가 없는 상태 

  - 삭제된(removed) 상태 

  ※ 엔티티의 상태는 특정 메서드를 호출해 변경 가능 → 필요에 따라 엔티티의 상태를 조절해 데이터를 올바르게 유지하고 관리 가능 

 

// * 엔티티 상태 변경 예
public class EntityManagerTest {
	@Autowired
    EntityManager em;
    
    public void example(){
    	// ① 엔티티 매니저가 엔티티를 관리하지 않는 상태 (비영속 상태)
        Member member = new Member(1L, "홍길동");
        
        // ② 엔티티가 관리 상태가 됨 (관리 상태)
       	em.persist(member);
        
        // ③ 엔티티 객체가 분리된 상태가 됨 (분리 상태)
        em.detach(member);
        
        // ④ 엔티티 객체가 삭제된 상태가 됨 (삭제 상태)
        em.remove(member);
    }    
}
① 엔티티를 처음 만들면, 엔티티는 비영속 상태가 됨 
② persist( ) 메서드를 이용해 엔티티를 관리 상태로 만들 수 있음 → Member 객체는 영속성 컨텍스트에서 상태가 관리됨  
③ 엔티티를 영속성 컨텍스트에서 관리하고 싶지 않을 경우, detach( ) 메서드를 사용해 분리 상태로 만들 수 있음 
④ 더 이상 객체가 필요 없다면, remove( ) 메서드를 사용해 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제 가능  

 

 


4. 스프링 데이터와 스프링 데이터 JPA 

 

▪︎ 스프링 데이터 

  - 엔티티의 상태 직접 관리 / 필요한 시점에 커밋 실행 등 개발자가 신경써야 할 부분이 많음 

     → 스프링 데이터 이용하여 비즈니스 로직에 더 집중 가능 

  - 스프링 데이터 : 데이터베이스 사용 기능을 클래스 레벨에서 추상화 → 인터페이스를 통해서 스프링 데이터 사용 가능

  - 이 인터페이스의 경우,

    • CRUD를 포함한 여러 메서드가 포함되어 있음

     알아서 쿼리를 만들어줌 

     페이징 처리 기능

    • 메서드 이름으로 자동으로 쿼리 빌딩하는 기능 제공 

 

 

1) 스프링 데이터 JPA란?

 

  - 스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술 

  - 스프링 데이터의 인터페이스인 PagingAndSortingRepository를 상속받아 JpaRepository 인터페이스를 만들었으며, JPA를 더 편리하게 사용하는 메서드를 제공함  

 

 

▪︎  기존에 엔티티 상태를 바꾸는 방법 

  → 메서드 호출로 엔티티 상태를 바꿨음 

// * 메서드 호출로 엔티티 상태 변경 예시
@PersistenceContext
EntityManager em;

public void join(){
    // 기존에 엔티티 상태를 바꾸는 방법 (메서드 호출을 해서 상태 변경)
    Member member = new Member(1L, "홍길동");
    em.persist(member);
}

 

 

스프링 데이터 JPA 사용하면,

   리포지토리 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있음

// * 기본 CRUD 메서드를 사용하기 위한 JpaRepository 상속 예
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}

 ▪︎ JpaRepository 인터페이스를 우리가 만든 인터페이스에서 상속받고,

   제네릭에는 관리할 <엔티티 이름, 엔티티 기본키의 타입>을 입력하면, 기본 CRUD를 위해 만든 메서드 사용 가능 

 

 


2) 스프링 데이터 JPA에서 제공하는 메서드 사용해보기 

 

 

▪︎ MemberService.java

package org.choongang.service;

import java.util.List;
import java.util.Optional;
import org.choongang.domain.Member;
import org.choongang.domain.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service 
public class MemberService {
	// 데이터베이스와 작업은 리포지토리 또는 모델
	@Autowired
	MemberRepository memberRepository;
	
	public void test() {
		// ❶ 생성(Create) - JPA에서 데이터 추가 : save()
		memberRepository.save(new Member(1L, "A"));
		
		// ❷ 조회(Read) - JPA에서 데이터 검색 2가지 
		//  - 컬럼별 레코드 검색 : findById()
        	//    → 기본키 Id로 검색 
		//  - java.util.Optional<T> 클래스 ( 아래 설명 참고 )
		Optional<Member> member = memberRepository.findById(1L);
		//  - 전체 레코드 검색 : findAll()
		List<Member> allMembers = memberRepository.findAll();
		
		// ❸ 삭제(Delete) : deleteById()
		memberRepository.deleteById(1L);
	}
}
❶ save( ) 메서드 호출해 데이터 객체 저장 가능 
   전달 인수로 엔티티 Member를 넘기면 반환값으로 저장한 엔티티를 반환받을 수 있음 

❷ 단건 조회 : findById( ) 메서드에 id를 지정해 엔티티 하나를 조회 
     전체 조회 : findAll( ) 메서드를 통해 전체 엔티티 조회 

❸ deleteById( ) 메서드에 id를 지정해 엔티티 삭제 가능 

 


▪︎ java.util.Optional<T> 클래스 

  - 'T타입의 객체'를 감싸는 래퍼 클래스로, 모든 타입의 객체를 담을 수 있음 

       존재할 수도 있지만, 안할 수도 있는 객체 
           즉, null이 될 수도 있는 객체를 감싸고 있는 일종의 Wrapper 클래스 

public final class Optional<T> {
	private final T value;	// T 타입의 참조변수 
    	...
}

  - 최종 연산의 결과를 그냥 반환하는 게 아니라 Optional 객체에 담아서 반환하면,

     반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리 가능 

 - Optional<T> 이용 시, 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는, 보다 간결하고 안전한 코드 작성 가능 

 

 

+ 추가적인 개념 

[ 참고 ] https://www.daleseo.com/java8-optional-after/

▪︎ Optional이란?

 - 존재할 수도 있지만, 안할 수도 있는 객체

    즉, null이 될 수도 있는 객체를 감싸고 있는 일종의 wrapper 클래스 

 - 직접 다루기에 위험하고 까다로운 null을 담을 수 있는 특수한 그릇으로 생각하자 

 

▪︎ Optional의 효과

 - NPE를 유발할 수 있는 null을 직접 다루지 않아도 됨 

 - null 체크를 직접 하지 않아도 됨 

 - 명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현할 수 있음 

 

▪︎ Optional 기본 사용법 

 - java.util.Optinal<T> 클래스 이용 

 - Optional 변수 선언 

  • 제네릭 제공 → 변수 선언 시 명기한 타입 파라미터에 따라서 감쌀 수 있는 객체의 타입이 결정됨 

   Optional<Member> member ;                  // Member 타입의 객체를 감쌀 수 있는 Optional 타입의 변수 

  변수명의 경우, 보통 클래스 이름을 사용하기도 하지만, ‘opt’ 같은 접두어를 붙여 Optional 타입의 변수라는 것을 명확히 나타내기도  

 

 


5. 예제 코드 살펴보기 

 

◉ 지금까지 배운 애너테이션이 어떤 역할을 하는지 확인해보자 

 

▪︎ Member.java

package org.choongang.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access=AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter	// 데이터가 한번 만들어지면 고칠 수 없도록 설정 (VO 형식)
@Entity // 모델 클래스와 데이터 클래스를 1:1로 동기화함 
	// 데이터베이스 테이블의 구조와 동등 -> JPA가 만듦 
public class Member {
	@Id		
    	// 일련번호로 Primary Key를 사용함 
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	// GeneratedValue : 자동생성된다는 의미 
	// strategy : 만드는 방식, 전략 -> 자동으로 일련번호를 만들어서 사용하라 
	@Column(name="id", updatable=false)
	private Long id;
	// 테이블의 'id' 컬럼과 매핑 
	
    @Column(name="name", nullable=false)
	private String name; 
}

 

❶ @Entity 

   - 엔티티 : 데이터베이스의 테이블과 매핑되는 객체 

   - Member 객체를 JPA가 관리하는 엔티티로 지정 

      → Member 클래스와 실제 데이터베이스의 테이블을 매핑시키는 역할 

   - @Entity의 name 속성 이용 시, name의 값을 가진 테이블 이름과 매핑

      name을 지정하지 않으면, 클래스 이름과 같은 이름의 테이블과 매핑됨 (현재는 후자에 해당)

 

❷ @NoArgsConstructor(access=AccessLevel.PROTECTED) 

   - 엔티티는 반드시 기본 생성자가 있어야 함 

   - 접근 제어자의 경우, public 또는 protected로 지정 ( public보다는 protected가 더 안전 )

 

❸ @Id 

   - JPA 엔티티 객체의 식별자로 사용할 필드에 적용 

   - Long 타입의 id 필드를 테이블의 기본키로 지정 

 

❹ @GeneratedValue 

   - 기본키의 생성 방식을 결정 

     • AUTO : 선택한 데이터베이스 방언(dialect)에 따라 방식을 자동으로 선택(기본값)

     • IDENTITY : 기본 키 생성을 데이터베이스에 위임

                           → id값을 null로 하면 DB가 알아서 AUTO_INCREMENT 해줌 

     • SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당하는 방법 → 오라클에서 주로 사용 

     • TABLE : 키 생성 테이블 사용 

 

❺ @Column 

   - 데이터베이스의 컬럼과 필드를 매핑해주는 역할 

   - 속성 

속성 기능 기본값
name 필드와 매핑할 컬럼 이름 
설정하지 않으면, 필드 이름으로 지정함 
* Java - 카멜표기법 권장 / DB - 스네이크표기법 권장
   ⇒ name 속성으로 매핑 
객체의 필드 이름
nullable DDL 생성 시 null 값의 허용 여부를 설정함 
false로 설정하면 Not null 제약조건이 붙음 
true
updatable 엔티티 수정 시 이 필드도 같이 수정함 
false로 설정하면 데이터베이스에 수정하지 않음 
false 옵션은 읽기 전용일 때 사용함 
true
unique 컬럼의 유일한 값 여부
설정하지 않으면 false
false 
columnDefinition 컬럼 정보 설정 
default값을 줄 수 있음 
자바 필드의 타입과 데이터베이스 방언 설정 정보를 사용해 적절히 생성 

 

 

▪︎ MemberRepository.java

package org.choongang.domain;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long>{

}

 - 리포지토리 : 엔티티에 있는 데이터들을 조회하거나 저장, 변경, 삭제를 할 때 사용하는 인터페이스

                       스프링 데이터 JPA에서 제공하는 인터페이스인 JpaRepository 클래스를 상속받아 간단하게 구현 가능 

 - 단순히 인터페이스를 생성 후, JpaRepository< Entity 클래스, PK 타입 > 를 상속하면 기본적인  CRUD 메서드가 자동 생성됨

   → 이 레포지토리 사용 시, JpaRepository에서 제공하는 여러 메서드 사용 가능 

 

 


 - JpaRepository에 대한 추가 설명 

   • JpaRepository : PagingAndSortingRepository, QueryByExampleExecutor 인터페이스를 상속받고 있음

   

   • PagingAndSortingRepository : CrudRepository 인터페이스를 상속받고 있음

   • CrudRepository : 기본적인 CRUD 메서드 제공 

                                    → save( ), findById( ), existsById( ), count( ), deleteById( ), delete( ), deleteAll( )

 

   • QueryByExampleExecutor : 더 다양한 CRUD 메서드 제공 

                                                     → findOne( ), findAll( ), count( ), exists( )

 

 

 


 

  1. ORM : 객체와 데이터베이스를 연결하는 프로그래밍 기법 

  2. JPA : 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스 

      - 엔티티 : 영속성을 가진 객체 ( 데이터베이스의 테이블과 매핑되는 객체 ) 

      - 엔티티 매니저 : 엔티티를 관리하며 조회, 삭제, 수정, 생성하는 역할을 함 

      - 엔티티 매니저를 만드는 곳이 엔티티 매니저 팩토리 

      - 엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장함 

      - 영속성 컨텍스트의 특징 : 1차 캐시 / 쓰기 지연 / 변경 감지 / 지연 로딩 

      - 엔티티의 상태 : 분리 / 관리 / 비영속 / 삭제 상태 

  3. 하이버네이트 : JPA의 구현체 중 대표적인 구현체로, 자바 언어를 위한 ORM 프레임워크

  4. 스프링 데이터 JPA : JPA를 쓰기 편하게 만들어 놓은 모듈 

                                           → 데이터베이스에 접근해 CRUD 작업을 수행하는 메서드를 제공하는 인터페이스 : JpaRepository