모르지 않다는 것은 아는것과 다르다.

Spring Jpa

JPA 란

채마스 2022. 2. 27. 00:53

JPA 가 필요한 이유

  • SQL 중심적인 개발의 문제점을 해결해준다.
  • CRUD와 같은 반복적인 코드를 지원해준다.
  • 개발자는 객체지향적으로 개발을 할 수 있다.
  • 필드값이 추가된다거나 변경되는 상황이 발생했을때, 쿼리문을 전부 바꿔주는 작업을 줄여준다.
  • 패러다임의 불일치를 해결해준다.
  • 1차 개시와 동일성 보장 --> 신뢰도 보장
  • 트랜잭션을 지원하는 쓰기 지연 지원
  • 지연 로딩 지원 --> 성능 보장




객체 vs 관계형 데이터베이스

  • 상속 관점 ( 객체 상속 관계 vs Table 슈퍼타입 서브타입 관계)
  • 연관관계 관점 ( 단방향 vs 양방향)
  • 데이터 타입 관점
  • 데이터 식별 방법 ( 같은 참조값 기준 같은 값 vs 다른 값)




패러다임의 불일치를 해결해 줄 수 있는 기술 "JPA"

  • 위에서 말한 객체 와 관계형 데이터베이스 사이에서 발생하는 패러다임의 불일치를 해결해 줄 수 있는 무기이다.
  • Java Persistence API의 약자로 자바 진영의 ORM 기술 표준이다.
  • JAVA 애플리케이션 --> JPA --> JDBC API --> DB --> 결과 반환 순으로 동작한다.




JPA는 어떻게 만들어 졌을까?

  • EJB로 부터 하이버네이트로 발전되면서 JPA가 만들어졌다.
  • JPA 표준 인터페이스는 대표적으로 3가지 구현체를 가진다.
    • Hibernate
    • EclipseLink
    • DataNucleus




지연로딩 vs 즉시로딩

  • 지연로딩
    • 객체가 실제 사용될 때 로딩
    • 장점: 사용될 때 로딩하기 때문에 로딩시간이 짧다.
    • 단점: 매번 사용된다면 쿼리문이 2번 발생함으로 즉시로딩보다 비효율적이다.
  • 즉시로딩
    • JOIN SQL로 한번에 연관된 객체까지 미리 조회
    • 장점: 자주 사용될 경우 Join문으로 쿼리 한번에 호출함으로 지연로딩보다 성능이 좋을 수 있다.
    • 단점: 객체를 사용하지 않을 때도 로딩하기 때문에 성능이슈가 발생할 수 있다.
    • 하지만 N+1문제가 발생할 수 있다.
  • 실무에서는 즉시로딩보다 지연로딩을 선호한다.




실습

  • pom.xml
    ?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>jpa-basic</groupId>
    <artifactId>ex1-hello-jpa</artifactId>
    <version>1.0.0</version>
    <dependencies>
    <!-- JPA 하이버네이트 -->
    <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.3.10.Final</version>
    </dependency>
    <!-- H2 데이터베이스 -->
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.199</version>
    </dependency>
    </dependencies>
  • persistence.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <persistence version="2.2"
    xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
    <properties>
    <!-- 필수 속성 -->
    <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
    <property name="javax.persistence.jdbc.user" value="sa"/>
    <property name="javax.persistence.jdbc.password" value=""/>
    <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
    <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
    <!-- 옵션 -->
    <property name="hibernate.show_sql" value="true"/>
    <property name="hibernate.format_sql" value="true"/>
    <property name="hibernate.use_sql_comments" value="true"/>
    <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
    </properties>
    </persistence-unit>
    </persistence>




데이터베이스 방언

  • 방언 : SQL 표준을 지키지 않는 특정 데이터베이스만의 고유한 기능
  • hibernate.dialect 속성에 지정
    • H2 : org.hibernate.dialect.H2Dialect
    • Oracle 10g : org.hibernate.dialect.Oracle10gDialect
    • MySQL : org.hibernate.dialect.MySQL5InnoDBDialect
  • 하이버네이트는 40가지 이상의 데이터베이스 방언 지원




entityManagerFactory, entityManager

  • JPA 구동 방식 : Persistence --> EntityManagerFactory --> EntityManager
  • entityManagerFactory
    • 엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유
    • 만드는 비용이 상당히 큼
    • 한 개만 만들어서 어플리케이션 전체에서 공유하도록 설계
    • 여러 스레드가 동시에 접근해도 안전, 서로 다른 스레드 간 공유 가능
  • entitManaer
    • 여러 스레드가 동시에 접근하면 동시성 문제 발생
    • 스레드간 절대 공유하면 안된다
    • 데이터베이스 연결이 필요한 시점까지 커넥션을 얻지 않는다
  • JPA의 모든 데이터 변경은 트랜잭션 안에서 실행




JPQL

  • JPQL : Java Persistence Query Language
  • 왜 필요한가? : a.getB().getC() 이런식으로 찾는 것은 한계가 있다
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다
  • 그래서 JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다
  • SQL과 문법이 유사하고, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN등을 지원한다
  • JPQL은 엔티티 객체를 대상으로 쿼리를 질의하고
  • SQL은 데이터베이스 테이블을 대상으로 쿼리를 질의한다.




영속성 컨텍스트

  • 영속성 컨텍스트란? : 엔티티를 영구 저장하는 환경을 뜻한다.
  • 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다.
  • 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
  • 엔티티 메니저를 생성해서 영속성 컨텍스트에 접근, 관리할 수 있다.




엔티티의 생명주기

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
    • 엔티티 객체를 생성했지만 아직 영속성 컨텍스트에 저장하지 않은 상태
      Member member = new Member();
  • 영속(managed): 영속성 컨텍스트에 저장된 상태
    • 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장한 상태
    • 영속성 컨텍스트의 관리를 받는다
      em.persist(member);
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
    • 영속성 컨텍스트가 관리하던 영속 상태의 엔티티 더이상 관리하지 않으면 준영속 상태가 된다
    • 1차 캐시, 쓰기지연, 변경감지, 지연로딩 기능이 적용되지 않는다.
    • 준영속 상태로 전환하는 3가지
      em.detach(member); //영속성 컨텍스트에서 분리시킴
      em.claer(); // 영속성 컨텍스트를 비움
      em.close(); // 영속성 컨텍스트를 종료시킴
  • 삭제(removed): 삭제된 상태
    • 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제
      em.remove(member);




영속성 컨텍스트의 장점

  • 1차 캐시
    • 영속성 컨텍스트 내부에는 캐시가 있는데 이를 1차 캐시라고 한다. 영속 상태의 엔티티를 이곳에 저장한다.
      Member member = em.find(Member.class, "member1");
    • 만약 위와 같이 객체를 조회한다 했을때 아래와 같은 로직으로 객체를 찾는다
      1. 1차 캐시에서 엔티티를 찾는다
      1. 있으면 메모리에 있는 1차 캐시에서 엔티티를 조회
      1. 없으면 데이터베이스에서 조회
      1. 조회한 데이터로 엔티티를 생성해 1차 캐시에 저장)
      1. 조회한 엔티티를 반환
  • 동일성 보장
    • 영속성 컨텍스트는 엔티티의 동일성을 보장한다
      Member a = em.find(Member.class, "member1");
      Member b = em.find(Member.class, "member1");
      System.out.print(a==b)
    • 위와 같이 같은 식별자로 객체를 조회하는 경우 같은 객체를 보장해준다.
  • 쓰기 지연 지원
    • 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 INSERT SQL을 모아둔다
    • 트랜잭션을 커밋하는 시점에서 모아둔 쿼리를 한번에 DB에 보냄
  • 변경 감지 지원
    • JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터를 변경
    • 개발자는 변경 후 다시 저장하는 작업을 할 필요가 없다
    • 변경 감지의 흐름은 아래와 같다.
        1. 트랙잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시가 호출된다.
        1. 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.
        1. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장한다.
        1. 쓰기 지연 저장소의 SQL을 플러시한다.
        1. 데이터베이스 트랜잭션을 커밋한다.





flush (플러시)

  • 플러시란? : 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영
  • 플러시하는 방법
      1. em.flush()
      1. 트랙잭션 커밋시 자동 호출
      1. JPQL 쿼리 실행시 자동 호출
  • 플러시가 진행되는 흐름
      1. 변경 감지가 동작해서 스냅샷과 비교해서 수정된 엔티티를 찾는다.
      1. 수정된 엔티티에 대해서 수정 쿼리를 만들거 SQL 저장소에 등록한다.
      1. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.




엔티티 매핑

  • 객체와 테이블 매핑
    • @Entity
    • @Table
  • 필드와 컬럼 매핑
    • @Column
  • 기본 키 매핑
    • @Id
  • 연관관계 매핑
    • @ManyToOne
    • @JoinColumn




데이터베이스 스키마 자동 생성

  • 데이터베이스에 맞는 적절한 DDL생성
    <property name="hibernate.hbm2ddl.auto" value="create"/>
    https://gmlwjd9405.github.io/2019/08/11/entity-mapping.html
  • 옵션
    • create : 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
    • create-drop : create와 같으나 종료 시점에 테이블 DROP
    • update : 변경 분만 반영
    • validate : 엔티티와 테이블이 정상 매핑되었는지만 확인
    • none : 아무 속성도 사용하지 않음
    • 운영 시점에서는 create, create-drop은 절대 사용하면 안된다.




필드와 컬럼 매핑

  • @Column
    • @Column(name = "name") : DB 컬럼명 설정
    • @Column(updatable = false) : 변경되어도 DB에 반영도지 않는다.
    • @Column(nullable = false) : NOT NULL 제약조건
    • @Column(columnDefinition = "varchar(100) default 'EMPTY'") : 직접 컬럼 정보를 작성
  • @Enumerated
    • EnumType.ORDINAL : enum 순서를 데이터베이스에 저장
    • EnumType.String : enum 이름을 데이터베이스에 저장
    • 거의 필수적으로 EnumType.String을 사용한다
  • @Temporal
    • java.util.Date, java.util.Calender
    • TemporalType enum class는 아래와 같이 세가지 타입이 존재
      • DATE: DB의 date타입과 매핑 (2021-07-07)
      • TIME: DB의 time타입과 매핑 (10:33:32)
      • TIMESTAMP : DB의 timestamp와 매핑(2020-07-07 10:33:32)
      • Java 8이상인 경우 LocalDate(date), LocalDateTime(timestamp)로 생략이 가능하다.
  • @Lob
    • DB에서 varchar를 넘어서는 큰 내용을 넣고 싶은 경우에 사용
    • DB의 BLOB, CLOB과 매핑
      • BLOB : byte[], java.sql.BLOB
      • CLOB : String, char[], java.sql.CLOB
  • @Transient
    • 특정 필드를 컬럼에 매핑하지 않음
    • DB에 관계없이 메모리에서만 사용하고자 할때 사용
    • 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
    • DB에 저장, 조회되지 않는다.





연관관계 매핑

  • 객체 연관관계 vs 테이블 연관관계
    • 객체는 참조로 연관관계를 맺습니다
    • 테이블은 외래 키로 연관관계를 맺습니다
    • 객체의 연관관계는 단방향입니다
    • 테이블의 연관관계는 양방향입니다
  • 다대일 단방향 매핑
    • 가장 많이 사용하는 연관 관계
    • 다대일의 반대는 일대다
  • 다대일 양방향 매핑
    • 외뢰 키가 있는 쪽이 연관관계 주인
    • 양쪽을 서로 참조
  • 일대다 단방향 매핑
    • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
    • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
    • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
    • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)
    • 단점
      • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
      • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
  • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자!!
  • @JoinColumn
    • name : 매핑할 외래 키 이름
    • referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
    • foreignKey(DDL) : 외래 키 제약조건을 직접 지정




연관 관계의 주인

  • 연관 관계의 주인이란? :두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.
  • 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다.
  • 반면에 주인이 아닌 쪽은 읽기만 할 수 있습니다.
  • 주인은 mappedBy 속성을 사용하지 않는다.
    주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
  • 연관관계의 주인은 외래 키가 있는 곳으로 설정해야 한다.
  • 따라서 @ManyToOne은 항상 연관관계의 주인이 된다. @OneToMany는 주인이 되지 않는다.
  • 그러므로 @ManyToOne에는 mappedBy 속성이 없습니다. mappedBy속성은 @OneToMany에서 가진다.




편의 메소드

  • 편의 메소드? : 양방향 연관에서 어느 한쪽만 동기화 되지 않도록 편의 메소드를 구현해 두는 것이 좋다. (맴버에 팀을 등록하면 팀에도 맴버를 등록해줘야된다)
  • 보통 주인쪽에 구현해둔다.
  • 예시는 아래와 같다.
  • public class Member{ private Team team; public void setTeam(Team team) { if(this.team != null) { this.team.getMembers().remove(this); } this.team = team; team.getMembers().add(this); } }
  • 위와 같이 Member에 setTeam이라는 편의 메소드를 구현해 둠으로써
  • 연관된 객체들 끼리의 동기화를 보장해 준다.





프록시

  • 프록시란? : JPA는 객체지향언어와 관계형 데이터베이스의 패러다임 불일치를 해결하는 기술이다.
  • 객체와 다르게 RDB의 경우 참조하는 외래키를 이용해서 테이블간의 조인 쿼리를 작성해서 탐색해야 한다.
  • 그렇기 때문에 연관된 객체를 쓰일지 안 쓰일지도 모르는 체 전부다 가져와야하는 문제가 있다.
  • 이러한 문제를 프록시라는 기술로 해결한다.
  • 프록시는 실제 연관된 객체를 즉시 join을 통해 DB로부터 가져오는 것이 아니라 앞서 프록시 객체를 두어서 실제 사용되는 시점에 DB를 조회해서 가져오는 기술이다.
  • 프록시 객체는 실제 객체에 대한 참조를 보관하고 있다가 객체가 사용돌 때 프록시 객체가 실제 객체의 메소드를 호출하는 식으로 사용된다.
  • 프록시 객체가 실제 객체에 대한 참조가 없을 때에는 영속성 컨택스트에 실제 객체를 조회하고 영속성 컨택스트에도 없다면 DB조회를 해서 실제 객체를 생성해준다.
  • 프록시 객체의 초기화 : 프록시객체가 실제 엔티티 객체를 생성하는 작업을 프록시 객체의 초기화라고 한다.




프록시의 특징

  • 처음 사용할 때 한 번만 초기화 된다.
  • 프록시 객체는 원본 엔티티를 상속받는 객체이므로 타입 체크를 잘해야한다.
  • 영속성 컨텍스트에 찾는 객체가 있으면 DB를 조회할 필요가 없기 때문에 getReference()로 프록시를 가져오려고 해도 실제 엔티티가 조회된다.
  • 초기화는 영속성 컨텍스트의 도움을 받아야한다. 만약 준영속상태의 프록시를 초기화하면 예외가 발생한다.
  • PersistenceUnitUtil.isLoaded(object entitry) 메소드로 프록시 객체의 초기화 여부를 알 수 있다.




즉시로딩

  • @ManyToOne(fetch=FetchType.EAGER)이렇게 속성을 정의하면 된다.
  • 이렇게하면 member 테이블에서 조회따로 team테이블에서 따로 2회 조회쿼리가 실행될거같지만 JPA구현체에 의해서 join되어 쿼리가 1번만 요청된다.
  • 즉시로딩은 참조하는 외래키가 null일 수 있기 때문에 outer join을 사용하지만 이는 쿼리 최적화에 좋지 않다. 그렇기 때문에 아래와 같이 nullable = false를 통해서 null이 아님을 보장해준다.
    @ManyToOne(fetch = FetchType.EAGER) 
    @JoinColum(name = "TEAM_ID", nullable = false) 
    private Team team;
  • 이렇게 하면 null이 아님이 보장되기 때문에 inner join을 수행한다.




지연로딩

  • @ManyToOne(fetch = FetchType.LAZY) 이렇게 속성을 정의하면 된다.
  • member만 필요할 때는 member만 조회하고 team은 프록시객체로 가지고 있다가 추후에 member.getTeam()으로 팀이 요구될 때 실제 객체를 영속성컨텍스트에서 찾아온다.
  • 만약 영속성컨텍스트에 이미 team객체가 있으면 프록시객체가 아닌 실제 객체를 사용한다.




즉시로딩 vs 지연로딩

  • 지연로딩이 즉시로딩보다 장점이 많은 것은 사실이다.
  • 하지만 즉시로딩 또한 어떤점에서는 최적화에 있어서 유리하다.
  • 그렇기 때문에 개발단계에서 모든 연관관계에 지연로딩을 사용하고, 출시버전배포 이전에 필요한 부분에 즉시로딩을 적용하면 최적화된다.




CASCADE

  • 영속성 전이가 필요한 이유? : 만약 Team이라는 엔티티가 있고, Member 20개가 속해 있다고 생각해 보자. 이럴 경우 어떠한 수정이 발생하면 20개의 쿼리를 날려야 한다. 그렇기때문에 자동적으로 영속성 켄텍스트에 관리될 수 있도록 전이시키는 작업이 필요하다.
  • 종류
    • ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH 가 있다.
    • 여러 속성을 지정할 수도 있다. --> cascade = {CascadeType.PERSIST, CascadeType.REMOVE}




고아 객체

  • JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이 있다.
  • 부모엔티티의 컬렉션에서 자식엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 하는 것이다.
  • 특정 엔티티가 다른 엔티티를 개인 소유할 때만 사용해야한다.




Spring Data Jpa

Spring Data JPA란?

  • Spring Data JPA는 Spring에서 제공하는 모듈이다.
  • JPA를 한 단계 추상화시킨 Repository라는 인터페이스를 제공함으로써 이루어진다.
  • Spring Data JPA의 Repository의 구현에서 JPA를 사용하고 있다.
  • CRUD 처리를 위한 공통 인터페이스 제공한다.
  • 공통 인터페이스 종류는 대략 아래와 같다.
  • count, delete, deleteAll, deleteAll, deleteById, existsById, findById, save.
  • 공통 메소드가 아닐 경우에도 스프링 데이터 JPA가 메소드 이름을 분석해서 JPQL을 실행할 수 있다.




JPA vs Data JPA

JPA만으로 Repository를 구현한다면?

@Repository
public class TeamJpaRepository {
    @PersistenceContext
    private EntityManager em;

    public Team save(Team team) {
        em.persist(team);
        return team;
    }
    public void delete(Team team) {
        em.remove(team);
    }

    public List<Team> findAll() {
        return em.createQuery("select t from Team t", Team.class)
                .getResultList();
    }
    public Optional<Team> findById(Long id) {
        Team team = em.find(Team.class, id);
        return Optional.ofNullable(team);
    }
    public long count() {
        return em.createQuery("select count(t) from Team t", Long.class)
                .getSingleResult();
    }
}
  • 위와 같이 JPA만으로 Repository를 구현할 수 있다.
  • 이 방식도 나쁘진 않다.
  • 하지만 Data JPA의 힘을 빌리면...




Reference

'Spring Jpa' 카테고리의 다른 글

조회 성능 최적화  (0) 2022.02.27
값타입  (0) 2022.02.27
OSIV  (0) 2022.02.27
Spring Data Jpa  (0) 2022.02.27
영속성 전이 (CASCADE)  (0) 2022.02.27