JPA

대영·2023년 11월 20일
3

Spring

목록 보기
3/16

🙏내용에 대한 피드백은 언제나 환영입니다!!🙏

📌JPA란?

자바 프로그램에서 관계형 데이터베이스에 접근하는 방식을 명세화한 인터페이스이다.

JPA는 ORM(Object-Relational Mapping) 기술을 구현한 스펙으로, 개발자가 SQL 쿼리를 직접 작성하지 않고도 자바 객체와 관계형 데이터베이스 간의 매핑을 자동으로 처리할 수 있도록 도와준다.

JPA 구현체로는 Hibernate가 쓰인다. Hibernate는 자바 기반의 ORM 프레임워크로, 객체와 관계형 데이터베이스 간의 매핑을 지원한다.

👉내부 동작 방식

전체적 내부 동장 방식

아래의 사진에서 보듯이, JPA는 Application과 JDBC 사이에서 동작한다.(JPA는 JDBC를 기반으로 사용된다.) 그리고. JPA는 구현체 Hibernate 라이브러리 사용하여 객체와 관계형 데이터베이스(RDBMS)와 연결한다.

<출처 - 나무소리 유튜브>

JPA 내부 동작 방식

JPA는 트랜잭션이 시작되면 영속성 컨텍스트(Persistence Context)에서 commit이 될때까지 관리를 한다. 데이터가 관리되는 공간을 "1차 캐시"라고 한다.
commit이 된다면, SQL을 데이터베이스에 날려 영속성 컨텍스트에 있던 데이터를 데이터베이스에 보낸다. 즉, 트랜잭션이 실행 중이라면 데이터베이스로 보내는 과정은 없다.

그렇기에, 엔티티매니저를 통해서 데이터를 가지고 올때, 1차캐시에 없다면 데이터베이스에서 데이터를 가지고 오게 된다.
이러한 형식으로, 데이터베이스의 접근 횟수를 줄여 성능을 향상시켰다.

@ 영속성 컨텍스트(Persistence Context) : 고유 ID를 갖는 모든 영속 객체 인스턴스에 대한 집합
@ 1차 캐시 : 영속성 컨텍스트 내에서 엔터티의 상태를 관리.

<출처 - blog>

추가내용 (1) - 영속석 컨텍스트의 이점
1. 1차캐시에서 데이터 조회. (commit하기 전까지 데이터베이스에 데이터를 보내지 않음.)
2. 영속 엔티티 동일성 보장. (트랜잭션 범위 내에서 엔티티의 변경을 관리하기에.)
3. 쓰기지연 (commit하기 전까지 데이터베이스에 데이터를 보내지 않음.)
4. 변경감지 (아래 JPA의 CRUD 중 Update)

추가내용 (2) - flush()
commit이 일어나기 전에 영속성 컨텍스트의 내용을 데이터베이스에 동기화하려면 'flush()'사용. but, 동기화되는 것이지, 데이터가 반영이 되는 것이 아님. (반영된다면 commit이 된 것이기에 rollback을 할 수 없음.)

👉JPA 장점, 단점

장점

  • 자바 객체와 데이터베이스 테이블 간의 매핑을 자동으로 처리하므로, 객체 지향 프로그래밍에 높은 수준의 일관성을 제공.(생산성 향상)
  • JDBC와는 달리 데이터베이스 필드 변화에도 직접 처리해야하는 작업이 없음.(유지보수에 좋음)
  • 트랜잭션안에서는 같은 엔티티를 반환하기 때문에 데이터베이스와의 통신 횟수를 줄일 수 있음. 또한, 트랜잭션을 commit하기 전까지 메모리에 쌓고 한번에 SQL을 데이터베이스에 전송.(성능측면)

단점

  • 객체지향적인 모델과 데이터베이스 간의 매핑을 단순화하려는 목적으로 설계되었기 때문에, 복잡한 통계 작업에서는 SQL 을 직접 작성하는 것이 더 좋다.(데이터의 집계, 그룹화, 여러 테이블 간의 연산 등)

📌JPA 동작 방식

👉Entity

Entity란 데이터베이스의 테이블에 대응하는 클래스.
실제로 JPA의 관리를 통해서 데이터가 들어가고 쓰는 객체임을 말한다.
@Entity 어노테이션을 붙여서 지정한다.
또한, 엔티티 클래스의 주요 식별자(primary key)를 지정하는 데 사용되는 어노테이션은 @Id이다.

👉persistence.xml

persistence.xml는 영속성 유닛을 설정하는 파일이다. 이곳에는 데이터베이스와의 연결을 위한 정보, 엔터티 클래스 등의 내용이 담겨있다.

👉EntityManagerFactory

EntityManagerFactory는 엔티티 매니저 인스턴스를 관리하는 주체.(영속성 유닛을 생성하는 팩토리) persistence.xml과 연결함.
이것은 어플리케이션 실행 시 한개만 만들어지고, 사용자로부터 요청이오면 EntityManagerFactory로부터 EntityManager가 생성된다.

👉EntityManager

EntityManager는 영속 컨텍스트에 접근하여 엔티티에 대한 데이터베이스 작업을 제공.
JPA를 통해 객체를 영속화 하기 위해서는 EntityManager 객체가 필요하며, EntityManager의 인스턴스 객체는 EntityManagarFactory 객체를 통해 얻는다.

👉EntityTransaction

EntityTransaction은 JPA에서 트랜잭션을 관리하기 위한 인터페이스.
이 인터페이스를 사용하여 JPA에서 트랜잭션을 시작하고 커밋 또는 롤백하는 등의 작업을 수행한다.

아래는 설명한 내용을 바탕으로 작성한 코드이다.

		EntityManagerFactory emf = Persistence.createEntityManagerFactory("관리 주체");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {
        	// 과정.
            tx.commit();        // 끝나면 commit()
        } catch (Exception e) {
            tx.rollback();     // 문제가 생기면.
        } finally {
            em.close();
        }
        emf.close();

📌JPA의 CRUD

EntityManager은 바로 위 코드를 통해 만들어 졌다고 가정.
Entity로 지정된 class는 MyEntity라고 하겠다.

👉Create(저장)

저장을 위해서는 persist()를 이용한다.
이것은 영속성 컨텍스트에 있는 1차 캐시에 저장하는 것이다.
그리고, commit() 시점에 insert 쿼리를 DB에 날려 저장한다.

	tx.begin();
    try {
		MyEntity myEntity = new MyEntity();  // 엔티티 객체 생성.
		myEntity.setName("John Doe");
		myEntity.setAge(25);
		entityManager.persist(myEntity);     // 1차 캐시에 저장.
		tx.commit();
	} catch (Exception e) {
		tx.rollback();
	}

👉Read(조회)

조회를 하기 위해서는 find()를 사용한다.
find를 할때는 고려할 경우가 있다.
1. 1차 캐시에 데이터가 있을 경우.
2. 캐시에 데이터가 없을 경우.

1번과 같은 경우에는 영속성 컨테이너 1차 캐시에 있는 데이터를 그대로 가지고 온다.
2번과 같은 경우에는 DB에 쿼리를 날려서 데이터를 가지고 와 1차 캐시에 저장한다.

2번과 같은 경우를 보자.
JPA에서는 ORM 프레임워크인 Hibernate를 통해서 객체와 데이터베이스를 매핑한다. 그러면, Hibernate를 통해서 데이터베이스의 데이터를 가지고 올 것이다.
하지만, 여기서 문제점이 생긴다. JPA에서는 Entity의 클래스의 타입을 알 수 없다. 이런 상황에서는 데이터를 받는데 문제가 생긴다.

그렇다면 어떻게 데이터를 들고 올 것인가? '리플랙션(Reflection)'을 이용한다.
리플랙션은 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드와 타입 그리고 변수들을 접근할 수 있도록 해주는 자바 API이다. 실행 시간에 동적으로 특정 클래스의 정보를 추출한다.

그래서, 하이버네이트는 동적으로 처리하기 위해 리플랙션을 통해서 데이터를 가지고 온다.
그렇지만, 여기서도 고려할 점은, 리플랙션은 매개변수 정보를 가지고 올 수 없다.
그래서, 엔티티 클래스에 대한 인스턴스화를 시키려면 '디폴트 생성자'가 필요한 것이다.

	tx.begin();
    try {
		MyEntity myEntity = entityManager.find(MyEntity.class, entityId);   // 데이터를 가지고 오기.
        System.out.println("Name: " + myEntity.getName());
		System.out.println("Age: " + myEntity.getAge());
		tx.commit();
	} catch (Exception e) {
		tx.rollback();
	}

👉Delete(삭제)

삭제 또한, Read(조회)가 필요하다.
find()를 통해서 데이터를 가지고 오고, remove를 통해서 영속성 컨텍스트에 있는 데이터를 삭제한다.

	tx.begin();
    try {
		MyEntity myEntity = entityManager.find(MyEntity.class, entityId);   // 데이터를 가지고 오기.
        entityManager.remove(myEntity);     // 삭제
		tx.commit();
	} catch (Exception e) {
		tx.rollback();
	}

👉Update(수정) - 변경감지(Dirty Checking) 사용

업데이트를 위해서는 Read(조회)가 필요하다.
find()를 통해서 데이터를 가지고 오고, setter를 통해서 데이터의 값을 변경한다.
그리고, 따로 처리해주는 것 없이 commit을 하면 update쿼리를 날려 업데이트 된다.
이렇게 되는 이유는, find()를 통해 데이터를 EntityManager가 관리하는 영속성 컨텍스트에 가져왔기 때문이다.

	tx.begin();
    try {
		MyEntity myEntity = entityManager.find(MyEntity.class, entityId);   // 데이터를 가지고 오기.
        myEntity.setName("Updated Name");   // 수정
		myEntity.setAge(30);				// 수정
		tx.commit();						// commit시 update 쿼리 날려 수정.
	} catch (Exception e) {
		tx.rollback();
	}

👉Merge(병합)

준영속상태(현재 영속상태가 아님)에서 영속상태로 변환할 때 Merge를 사용한다.
merge가 호출되면 우선 해당 엔티티를 1차 캐시에서 먼저 조회하고, 없다면 식별자로 DB에서 엔티티을 검색해 가져와 준영속 상태의 엔티티 값을 대입 받는다.
아래 예시는 간단히 적어 보았다.

	Member member = new Member();

	member.setId(form.getId());
	member.setName(form.getName());

	em.merge(member);

주의사항 1)
위 예시에서 준영속 상태의 member 자체를 영속성 컨텍스트에 넣는 것이 아니다.
merge는 준영속 객체를 영속하는 것이 아니라, 준영속 객체의 식별자와 일치하는 엔티티를 데이터베이스에서 가져와 값을 대입 받고, 해당 그 엔티티를 반환하는 것이다.

따라서 위 코드에서 member는 준영속 상태 그대로이고, 만약 db에서 가져온 영속된 엔티티를 받고 싶다면 merge의 리턴 값을 받아서 사용해야 한다.

Member realMember = em.merge(member);

주의사항 2)
merge는 엔티티 모든 필드를 그대로 변경한다.
만약 member의 name만 변경하고자 member만 세팅하고 merge한다면
member의 나머지 필드는 기존의 값을 잃고 null이 대입된다.

결론) merge보다는 find를 통해서 가져오는 변경감지(Dirty Checking)를 사용하는게 안전한 것 같다.

💡느낀점

객체와 데이터베이스간의 매핑을 자동으로 처리해주어 SQL쿼리문을 작성하는 수고를 덜 수 있다. 그렇기 때문에 JDBC방식보다는 생산성이 향상하고 유지보수에 좋다고 느꼈다. 이 후에 이것의 이점을 더욱 극대화시켜주는 '스프링 데이터 JPA'를 배우며 이것에 대해 더욱 알아가보겠다.

profile
Better than yesterday.

0개의 댓글