JPA 의 기본 개념

Sejun Park·2022년 6월 10일
0

SPRING

목록 보기
2/2
post-thumbnail

JPA 는 Java Persistence Api 의 줄임말으로, 자바의 ORM 표준 기술이다.
ORM 은 Object-relational mapping(객체 관계 매핑)으로 객체를 관계형 데이터베이스에 연결해주는 역할을 한다.


기본 동작

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member member = new Member();
    member.setId(1L);
    member.setName("Hello");
    em.persist(member);

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}

emf.close();
  • EntityManagerFactory: 자바를 실행하면 단 하나의 객체만 존재하며 사용자의 모든 요청마다 EntityManager 를 만들어서 반환해준다.

  • EntityManager: 사용자의 요청마다 만들어지며, 사용 후에는 무조건 파기한다.

  • EntityTransaction: 데이터베이스 사용 중 오류가 발생 할 경우 롤백, 성공적으로 끝날 시 commit 을 하여 데이터베이스 사용에 안정적인 흐름을 제공한다.

특이점으로는 entityManager의 경우 사용자가 사용을 끝냈을 경우 종료를 해야하며 entityManagerFactory 는 서버가 꺼지기 직전에 종료를 해야한다.
(요즘 jpa 는 내부적으로 동작해준다.)


핵심 기능

영속성 컨텍스트

영속성 컨텍스트는 눈에 보이지 않는 하나의 공간이라고 생각하면 쉽다.
엔티티 매니저를 통해서 영속성 컨텍스트에 접근이 가능하며, 보통은 한 트랜잭션 단위로 만들어 진다.

엔티티의 생명 주기는

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속(managed): 영속성 컨텍스트에 관리가 되고 있는 상태
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리가 된 상태
  • 삭제(removed): 삭제가 된 상태
Member member = new Member(); // 비영속 상태
member.setId(1L);
member.setName("hello");

em.persist(member); // 영속 상태
em.detach(member); // 준영속 상태
em.remove(member); // 삭제 상태(데이터베이스에서 삭제)

영속성 컨텍스트의 장점 및 동작 방식

  • 1차 캐시: 영속성 컨텍스트안에 1차캐시가 존재해 영속이 되어 있는 엔티티 같은 경우 재검색 했을때 디비를 거치지 않고 바로 결과값을 반환해준다.

    • 영속 상태가 될때 무조건 id(시퀀스) 값을 가지고 있다.
  • 동일성 보장: 영속성 컨텍스트에서 이미 데이터가 들어가 있으면 같은 주소값을 가진 객체를 반환한다.

  • 트랜잭션을 지원하는 쓰기 지연: 쓰기 지연 SQL 저장소라는 곳에 따로 저장해두었다가 flush 가 발생하면 데이터베이스에 쓰기 실행

쓰기 지연을 사용안하고 바로 디비로 SQL 을 날리면 되지 않나요?

  • 많은 데이터가 있을 시 버퍼링 기능을 사용하여 디비와 한번만 커넥트함으로써 성능의 이점을 가져갈 수 있다.
  • 변경 감지: 변수.setName 을 통해 이름을 변경(flush)하게 되면 1차 캐시에 있는 스냅샷과 비교한다음 쓰기 지연 저장소에 update 문을 저장하게 된다.
Member findMember = em.find(Member.class, 1L); //영속 상태
findMember.setName("changeName"); // 변경 감지가 일어남
  • 지연로딩: 필요한 시점에서 쿼리를 날려 데이터를 가져오는 방식
Member findMember = em.find(Member.class, 1L);
findMember.getTeam(); // 지연 로딩

PROXY

  • 지연 로딩이 일어날 때 내부적으로 프록시 객체가 생김
  • 실제 클래스를 상속받아서 만들어짐
  • 상속 받은 것에 더해서 Target 이라는 필드가 생김
    • target 호출 시 지연 로딩이 발생함

em.find vs em.getReference

  • em.find: 데이터 베이스를 조회하여 가져옴
  • em.getReference: 프록시 객체를 가지고 옴
    • 진짜 데이터가 필요할 때 가져옴 (지연 로딩)

사용 방식

필드와 컬럼 매핑

  • @Entity: JPA 가 관리하겠다고 선언 하는 것
    • 기본 생성자 필수(파라미터x)
  • @Id: 기본 키 매핑
    • @GeneratedValue 를 이용해 시퀀스 자동 할당 가능
      • identity 일 경우 persist(영속)하는 순간 insert 쿼리를 날림
      • 나머지의 경우에 commit 시 쿼리를 날림
    • @SequanceGenerator 를 이용해 시퀀스 설정 가능(sequance 전략 시 사용)
      • allocationSize: 시퀀스 한번 호출에 증가하는 수, 디비에서 미리 땡겨와 네트워크 사용을 줄이고 메모리에서 사용하는 방식(initialValue = 1 필수)
      • name: 식별자 이름, GeneratedValue 와 같은 식별자 이름을 사용
      • sequanceName: 데이터베이스에 등록되어 있는 시퀀스
      • initialValue: 초기 시작값
    • @TableGenertor: 테이블 전략 시 사용
  • @Column: 필드와 컬럼 매핑

  • @ManyToOne, @OneToMany: 연관관계 매핑

  • @Lab: 문자의 경우 BLOB, 나머지는 CLOB

  • @Enumerated: enum 타입 매핑 (EnumType.STRING 만 사용할 것!)

  • @Transient: 특정 필드를 무시

@Entity
public class Member {
	
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name="member_name")
    private String name;
    
    @ManyToOne
    private Team team;
    
    @Lab
    private String description;
    
    @Enumerated(EnumType.STRING)
    private MemberType type;    
}

스키마 자동 생성

xml 일 경우 <property name="hibernate.hbm2ddl.auto" value="create" />
yml 일 경우

spring:
  hibernate:
    ddl-auto: create

옵션

  • create: 기존 테이블 삭제 후 다시 생성
  • create-drop: 서버 시작 시 생성 후 서버 내려갈 때 디비 삭제
  • update: 기존 테이블은 유지, 변경점만 적용
  • validate: 엔티티와 테이블이 서로 매핑되는지만 확인
  • none: 사용하지 않음

개발할 때만 create, update 등 사용, 실제 서버에서는 절대로 사용을 하지 않습니다.(none 적용)

연관 관계

객체는 참조를 이용해 연관된 객체를 찾는 반면에 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾는다.
기존에는 id 값을 통해서 테이블을 검색했지만 Jpa 에서는 객체를 통해 검색하는 것처럼 행동할 수 있다.

  • @ManyToOne: 1:N 중에서 1 쪽에 붙이는 어노테이션이다.
    • @JoinColumn(name="외래키"): id 값으로 컨트롤 하는 것이 아니라, 객체를 컨트롤 할 수 있게 도와준다.

둘의 차이점

외래키로 양쪽 둘다 검색을 할수 있는 반면 객체는 둘다 사용할 수 없다.
하지만 @OneToMany 를 사용해 객체에서도 양방향(단방향 2개)를 만들 수 있다.

  • @OneToMany: 1:N 중에서 N 쪽에 붙이는 어노테이션이다.
    • mappedBy: 옵션으로 mappedBy를 넣어야 하며, 반대편에서 선언한 객체명을 적어준다.
@Entity
public class Member {
	..
    
    @ManyToOne
    @JoinColumn(name="team_id")
    private Team team;
}

@Entity
public class Team {
	..
    
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
}

연관관계의 주인

  • 외래키를 가지고 있는 쪽이 주인이다.
  • 주인이 아닌쪽은 읽기만 가능하다.
  • mappedBy 속성으로 주인을 지정

연관관계의 주인이란 실제로 값을 변경하고 등록하는 쪽을 의미하고, 주인이 아닌 쪽은 읽기만 가능하다.

//member
@ManyToOne
@JoinColumn(name="team_id")
private Team team;

//team
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();

members.add(member); //실제 데이터 베이스에 적용되지 않음

setTeam(team); // 연관관계의 주인이기 때문에 team 이 성공적으로 잘 들어감

연관관계 편의 메서드

위에서 setTeam 을 통해 넣어줬을때 members 에 보면 데이터가 아무것도 없다. 다음과 같은 메서드로 양방향으로 세팅을 해줘야 한다.

//member
changeTeam(Team team) {
	this.team = team;
    team.getMembers().add(this);
}

프로젝트를 개발할 때 단방향으로 먼저 개발할고 필요할 때 양방향으로 세팅하는 것이 좋다.

즉시 로딩과 지연 로딩

  • fetch = FetchType.EAGER: 즉시 로딩, 가져올 때 데이터를 한번에 가져옴
  • fetch = FetchType.LAZY: 지연 로딩, 필요할 때 데이터를 가져 옴
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;

즉시 로딩을 사용하면 예상하지 못한 sql 들이 나가기 때문에 실무에서는 반드시 지연 로딩을 사용하는 것을 추천드립니다.

  • @ManyToOne, @OneToOne 은 기본 설정이 Eager 이기 때문에 Lazy 로 설정
  • 즉시 로딩을 사용하면 JPQL 을 사용 할 때 N + 1 문제가 발생하게 됨

cascade

  • 영속성 전이라고 불리며, 부모 객체를 영속 상태나 삭제 상태를 진행하게 될 때 자식까지 전이가 되는 것 의미 함.
  • cascade = CascadeType.
    • ALL: 저장, 삭제 모두 적용
    • PERSIST: 영속에만 사용
    • REMOVE: 삭제에만 사용
    • MERGE: 머지에만 사용
Parent parent = new Parent();
Child child1 = new Child();

parent.addChild(child1); // child1 을 persist 하지 않아도 영속 상태가 됨
em.persist(parent)

고아 객체

  • 부모와 연결이 끊어진 자식 객체를 데이터베이스에서 자동 삭제하는 기능
  • orphanRemoval = true
Parent parent = new Parent();
Child child1 = new Child();
parent.addChild(child1);

em.flush();
em.clear();

Parent findParent = em.fine(parent.getId(), Parent.class);
findParent.getChildren().remove(0); // 객체에서만 삭제되는게 아니라 디비에서도 삭제 됨
  • 고아객체와 cascade 는 참조하는 곳이 하나일 때만 사용할 것
  • 특정 엔티티가 소유할 때만 사용
  • @OneToOne, @OneToMany 에 사용 가능

임베디드 타입

  • @Entity 에서 공통된 속성들을 하나의 클래스에 공유해서 쓰는 타입
  • 기본 값들을 모아서 만들었기 때문에 복합 값 타입이라고도 불림
  • @Embeddable: 정의해둔 곳에 사용
  • @Embedded: 사용하는 곳에 표시
  • 임베디드 타입은 소유한 엔티티의 생명 주기에 의존함
  • 불변객체로 설계하기(setxxx 막아두기)
  • 비교할땐 @Equals, @hashCode 구현
@Embeddable
public class Address {
	private String city;
    private String street;
    private String zipcode;
    public Address() { // 기본생성자 필수
    }
    ...
}

@Entity
public class Memeber {
	@Id @GeneratedValue
    private Long id;
    
    @Embedded
    private Address address;
    ...
    
}

값 타입 컬렉션

임베디드 타입이나 값 타입을 컬렉션으로 jpa 에서 사용할 수 있도록 도와주고 있다.

  • @ElementCollection, @CollectionTable 사용
  • 새로운 테이블을 만들어서 생성
  • 컬렉션은 자동으로 지연 로딩이 적용
  • 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 가지고 있음
  • 추적이 불가능하기 때문에 사용을 거의 하지 않음
@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    @ElementCollection
    @CollectionTable(name = "friend_name", joinColumns =
    	@JoinColumn(name = "member_id")
    )
    @Column(name = "friend_name") // 여기 이름으로 테이블이 생성됨
    private Set<String> friendNames = new HashSet<>();
    
    @ElementCollection
    @CollectionTable(name = "address", joinColumns =
    	@JoinColumn(name = "member_id")
    )
    private List<Address> addressList = new ArrayList<>();
}

제약 사항 및 대안

  • 값 타입은 엔티티와 다르게 식별자가 없으므로 변경 시 추적이 어렵다.
    • 변경이 발생하면 주인 엔티티와 관련된 데이터를 모두 삭제하고, 새롭게 다시 저장한다.
    • 여기서 발생하는 문제때문에 사용을 하지 않는 것을 추천 드립니다.
  • 사용할려면 기본키를 구성해서 일대다 를 사용하는 것이 좋습니다.
@Entity
@Table(name = "address")
public class AddressEntity {
	@Id @GeneratedValue
    private Long id;
    
    private Address address;
}

@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    @OneToMany(cascade = CASCADETYPE.ALL, ohparnRemoval = true)
    @JoinColumn(name = "member_id")
    private List<AddressEntity> addressList = new ArrayList<>();
}

쿼리 언어

jpa 로 전체를 해결할 수 없으므로 쿼리 언어를 사용한다.
여러가지가 있지만 실무에서 추천받은 언어들은 다음과 같다.

  • JPQL
  • queryDSL

JPQL

  • query 를 테이블이 아닌 엔티티 기준으로 사용
  • 객체 중심으로 설계를 했기 때문에 jpa 와 잘 맞음
  • 별칭 필수
select m from Member m where m.id = 10

결과 조회 api

  • query.getResultList(): 결과를 리스트에 담아서 반환(없을 경우 빈 리스트)
  • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException

파라미터 바인딩

  • 이름 기준: (:이름)
String username = "hello";
em.createQuery("SELECT m FROM Member m where m.username=:username", Member.class)
	.setParameter("username", username)
    .getResultList();
  • 번호 기준: 사용 비추천
String username = "hello";
em.createQuery("SELECT m FROM Member m where m.username=?1", Member.class)
	.setParameter(1, username)
    .getResultList();

프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입
    ex) SELECT m FROM Member m

페이징API

  • setFirstResult(int startPosition): 조회 시작 위치(default 0)
  • setMaxResults(int maxResult): 조회할 데이터 수
em.createQuery("SELECT m FROM Member m", Member.class)
	.setFirstResult(50)
    .setMaxResults(100)
    .getResultList();

경로 표현식

  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드(m.username)
  • 연관 필드(association field): 연관관계를 위한 필드
    • 단일 값 연관 필드: @ManyToOne, @OneToOne, 엔티티(m.Team)
    • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 컬렉션(t.members)

연관 필드를 이용하면 묵시적으로 내부 조인이 발생함(판별하기 어려움) - 실무에서 비추천하는 방식
ex)

select o.member from Order o // JPQL
select m.* from Orders o inner join Member m on o.member_id = m.id //SQL
  • 명시적 조인을 사용하여 한눈에 sql 을 알아볼 수 있도록 하기

fetch join

  • SQL 조인 X, JPQL 에서만 사용
  • 성능 최적화를 위해서 사용하는 join 기능
  • 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능

ex)

select m from Member m join fetch m.team; //JPQL
select m.*, t.* from member m inner join Team t on m.team_id=t.id; //SQL
  • 컬렉션으로 조회시 중복된 값을 가져 옴
  • 이걸 해결하기 위한 것이 distinct
  • distinct 가 sql 문 뿐만 아니라 어플리케이션에서도 중복을 제거해 줌

fetch join 특징과 한계

  • 페치 조인 대상에는 별칭을 사용하지 않는 것이 좋다.
    • 별칭을 이용하여 데이터를 가져올 때 컬렉션에서 전체가 아닌 일부만 가져오기 때문이다.
  • 둘 이상의 컬렉션은 페치 조인할 수 없다.(데이터가 맞지 않는 경우가 다수)
  • 컬렉션을 페치 조인하면 페이징API 를 사용할 수 없다.
    • 일대다나 다대다의 경우에 중복 결과가 있기 때문이다.
    • 사용할 경우 전체 데이터를 가져와서 메모리에서 페이징해서 주기 때문에 사용하면 안됨!
  • 컬렉션에서 lazy 로딩할 때 하나씩 가져오는 것을 batch_size 를 통해 여럿을 한번에 가져오도록 해결하는 경우가 많음

여러 테이블을 조인할 경우 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO 로 반환하는 것이 좋음

profile
백엔드 개발자

0개의 댓글