스터디(2) - 3장영속성관리 ~4장 엔티티매핑

25131 단어 studyJPAJPA

인프런- 김영한님의
자바ORM 표준 JPA 프로그래밍 강의를 참고한 스터디입니다.
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

영속성 컨텍스트

  • 엔티티를 영구저장하는 환경
  • 눈에 보이지않는 논리적인 개념
  • 엔티티매니저를 통해 영속성 컨텍스트에 접근한다.

EntityManager.persist(entity)
위는 DB에 저장하는것이아니라 엔티티를 영속성 컨텍스트라는곳에 저장한다.

엔티티의 생명주기

  Member member = new Member();
  member.setId(2L);
  member.setName("HelloB");
  • 비영속
    => 영속성 컨텍스트와 전혀관계없음
  Member member = new Member();
  member.setId(2L);
  member.setName("HelloB");
  em.persist(member) // 영속성 컨텍스트에 엔티티를 저장한다.
  • 영속
    => 영속성 컨텍스트에 관리되는상태
  Member member = new Member();
  member.setId(2L);
  member.setName("HelloB");
  em.persist(member)
  em.detach(member) // 지우기
  • 준영속
    => 영속성 컨텍스트에 저장되었다가 분리된상태
em.remove(member);
  • 삭제

1차 캐시


영속성컨텍스트 안에는 1차캐시라는것이있다.(@Id, Entity)
Map처럼 키와 값이 있는 느낌이다.

조회할때, 영속컨텍스트에서 1차캐시를 뒤진다. DB를뒤지는것이 아니라
1차캐시에없는 객체는 1차캐시에 없는걸확인하고 DB를 조회하고, DB에서 가져와서 1차캐시에 저장해둔다.
그후에 조회하더라도 영속성컨텍스트를 조회한다.

      Member member = new Member();
      member.setId(2L);
      member.setName("HelloB");
            
      System.out.println("==before==");
      em.persist(member);
      System.out.println("==after==");
      Member findMember = em.find(Member.class, 1L); // DB에 접근하지않기떄문에 셀렉쿼리 안뜬다. 
      System.out.println("findMember.id =" + findMember.getId());
      System.out.println("findMember.name= " + findMember.getName());
      tx.commit();

find를 db에서 꺼내는것이 아닌 위에서 저장된 1차캐시에서 조회하기때문에
insert 쿼리만 뜨고 select쿼리는 안뜬다
비즈니스가 정말복잡할때는 쿼리가 줄어들수있지만, 현업에선 크게 도움을 주지않는다.
성능적인 이점보다는 컨셉적인이점(객체지향적 코드 )이 있다.

어차피 요청 하나 당 하나의 DB 트랜잭션에 대해서만 관리되기 때문에 요청이 끝나면 삭제되게 되기 때문에?

영속 엔티티의 동일성 보장

동일성 = 같은 인스턴스임을 보장한다.
동등성 = 인스턴스가 다를수있지만 같은 값을 보장한다.

Member member1 = em.find(Member.class, 2L);
Member member2 = em.find(Member.class, 2L);
System.out.println(member1==member2); // true

객체에서의 동일성과 관계형 DB에서의 동일성의 패러다임불일치를 해결할수있다는 장점
그러나 자바는 객체 식별(a==b) 과 객체 동일성(a.equals(b)) 을 모두 정의한다.

궁금점 Springboot에서의 Mybatis 동일성 보장?

여러 블로그글에서 JPA는 동일성을 보장하나, Mybatis나 JDBC를 사용할경우 동일성이 보장이 안된다는 이야기를 보고나서 Mybatis가 동일성이 보장이안됬었나? 하고 테스트를 해보니 보장이된다.

Mybatis를 부트가 아닌 Spring-framework에서 사용하는 경우가 많아서 그런건가 라는 의문을 가지고 스터디를 들어가서
스티원분들과 이야기를 나누었다.

1. DB마다 다른 트랜잭션의 격리수준

한분이 말씀해주신 이야기이다. 현재 Mysql환경에서 테스트했었고 Mysql의 격리수준은 REAEATABLE READ(트랜잭션 내에서 한번 조회한 데이터를 반복해서 조회해도 결과는 동일하다) 로 되어있다.
의도적으로 2단계 낮춘 READ UNCOMMITTED(트랜잭션내에 커밋하지않은 데이터에 다른 트랜잭션의접근이 가능) 로 격리수준을 변경한후 테스트했다.

그런데, 결과는 그대로였다.

2. Mybatis자체에서의 Cache기능

Mybatis에서는 기본설정값으로 로컬캐싱기능을 이용하는데 localCacheScope의 디폴트값이 SESSION이다
SESSION은 하나의 SqlSession을 가지고 동일한 SQL을 반복호출시 캐싱된 동일한결과를 받는다.
캐시의 생존범위는 session이 유지되는 순간까지 (트랜잭션이 끝나거나 commit 이나 rollback) 혹은 insert,update,delete 가 실행되면 로컬 캐시가 보유하고있던 캐시정보는 폐기된다.
(이게 원인인듯하다)
STATEMENT는 동일한 SqlSession이라도 각 SQL조회마다 캐싱이 이루어진다.

localCacheScope을 STATEMENT로 주니

그제서야 동일성 보장이 안된다..

동일한 insert문을 날렸을때 Mybatis는 예외가 발생한다.

Mybatis에서 캐시라는 개념이 존재하기때문에, 조회시 캐싱된 결과를 받기때문에 동일성은 보장이 된다.
하지만, 데이터 삽입,수정,삭제시에는 JPA처럼 쓰기버퍼 개념이 존재하지않기때문에, 동일한 객체라도 각각 쿼리문이 2번실행된다.

내가생각한 결론(JPA가 동일성을 보장한다는 이야기에대한)

  1. JPA는 테이블과 객체의 동일성에대한 패러다임을 일치시켜줄수있다.
    =>테이블은 기본키라는개념으로, 객체는 동등성(equals(), ),동일성(==, 실제 인스턴스가 같은지) 을 모두 정의한다.

  2. Mybatis에서는 조회시 동일성을 보장하긴하지만, 삽입,삭제,수정에서는 동일성을 보장해주지못한다.insert,update,delete가 실행되고나면 로컬캐시가 보유하고있던 캐시정보가 폐기되기때문이다.

다음주에 스터디원분들과 한번더 이야기나눠보고 추가할예정

트랜잭션을 지원하는 쓰기지연

em.persist(memberA) // 이순간 JPA가 엔티티를 1차캐시에 넣어두고 분석하여 sql을 생성하고, 쓰기지연 sql저장소라는곳에 생성해둔다
em.persist(memberB) // memberB도 1차캐시에 넣어놓고 쓰기지연 저장소에 넣는다 

tx.commit(); // 쓰기지연 sql저장소에있던 애들이 flush되고 커밋된다. 

커밋직전에 INsert sql 하진않고 커밋하는순간 DB에 보낸다

변경감지

            Member member = em.find(Member.class, 150L);
            member.setName("ZZZZZ"); 
//            em.persist(member); // 이부분호출해야되는거 아니야? 아니다 리스트,컬렉션에서 값을 변경하고 다시 집어넣냐? 아니거든
            tx.commit();

update쿼리문이 나간다.
그이유는 영속성 컨텍스트에있다.

1차 캐시안에선 스냅샷이라는것도 존재한다.
값을 읽어온 최초시점의 상태를 스냅샷으로 뜨게됩니다.

커밋되는시점에 JPA가 비교한다. Entity랑 스냅샷이랑
바뀐걸 감지하면 update쿼리를 쓰기지연 sql저장소에 넣어둔다.
그후 커밋된다.

플러시

영속성 컨텍스트의 변경내용을 데이터베이스에 반영

  • 변경감지
  • 수정된 엔티티를 쓰기지연 sql저장소에 등록
  • sql저장소의 쿼리를 db에 보냄
            Member member = new Member(200L, "member200");
            em.persist(member);

            em.flush(); //강제로 호출 이시점에 insert쿼리가 나감
            System.out.println("=======");
            
            tx.commit();
  • 영속성 컨텍스트를 비우지않음

준영속상태

persist 로집어넣으면 영속상태가 되지만,
find로 조회를 했을때, 1차캐시에없으면 db에서 가져와서 조회하는데 이때도 영속상태가 된다.

em.detach(membeR) // 영속성 관리x , jpa에서 관리를안한다.

객체와 테이블 매핑

@Entity

  • JPA가 관리하는 엔티티

  • JPA를 사용해서 매핑할 클래스는 필수

  • 기본생성자 필수(JPA스펙상)
    => 라이브러리들이 동적으로 리플렉션으로 객체를 프록싱할때 필요하다

  • final,enum,interface,inner클래스는 사용할수없다

  • 저장할 필드에 final 사용 x

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

  • 애플리케이션 로딩시점에 create문으로
  • 방언을활용해서 db에맞는 적절한 ddl생성
  • 개발장비에서만사용 (운영에선 사용 x)

주석되어있는 프로퍼티를 주석해제하면 기존의 테이블을지우고 생성하게된다.

update로 value를 주면 변경부분만(alter문) 실행된다
validate로 값을주면 엔티티와 테이블이 정상 매핑되었는지 확인한다.

운영단계에서는 create,create-drop
(생성후 끝나면 테이블 삭제),update 절대 사용하면안된다
테스트서버,개발서버도 가급적 쓰지않는것이좋다.
그나마 validate정도는 괜찮은거같다

로컬서버에서만 자유롭게..

필드와 컬럼 매핑

엔티티에 기본생성자가 필수인이유

JPA구현체 Hibernate가 내부적으로 리플렉션을 이용해 객체를 생성하기때문



public class Application {

    public static void main(String[] args){
        
          Class<?> userClass = Class.forName("git.demo.domain.member.Member");
        Constructor<?> constructor = userClass.getConstructor(); // 런타임 에러지점 
        Member member =(Member) constructor.newInstance(); // User의 인스턴스생성
        System.out.println(member);
        
    }
}

자바 리플렉션은 생성자의 인자정보들을 가져올수없다. 생성자없이 파라미터가있는 생성자만 존재하면 리플렉션이 객체를 생성할수없다.

@Column

insertable, updateable

등록,변경 가능
updateable를 false로해두면 변경되지않음

nullable = false

not null 조건이 걸린다

unique = true

유니크 제약조건을 걸게됨 그런데 잘 안쓴다.

@Enumerated 기본값이 ordinal 이다
따라서 String으로 바꿔주는게 좋다

왜? enum에 값이 하나 추가된다면 enum값 자체의 순서가 변경되기때문이다.

LOB

필드타입이 문자면 CLOB, 나머지는 BLOB

기본키 매핑

여러가지 전략중 어느걸 써야되나

PK는 비즈니스랑 전혀상관없는 생성키(대체키)를 사용하는것이 좋다 + Long형
(주민번호가 PK였다가 강사님의 주민번호를 받으면 안되는 포털회사근무에서의 예시)

기본키 직접할당

@ID

기본키 자동생성전략

@GeneratedValue = 기본키 자동생성, 전략을 설정할수있다.

IDENTITY : DB에 위임
-> 커밋하기전에 insert쿼리를 날린다

@Entity
public class Board{

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    ...
}


public static void main{
	Board board = new Board();
    em.persist(board);
    print("board.id= " + board.getId()); //1출력 
}

SEQUENCE : DB시퀀스를 사용해 기본키 할당
-> 시퀀스 전략인걸 확인하면 영속성컨텍스트에 값을넣으려고 할때 DB시퀀스를 사용해 식별자를 조회하고 조회한 식별자를 엔티티에 할당한후, 영속성컨텍스트에 저장한다.

IDENTITY : 엔티티를 db에저장한후 식별자를 조회해서 엔티티의 식별자에 할당한다.
SEQUENCE: DB시퀀스를 사용해 식별자를 조회하고, 조회한식별자를 엔티티에 할당한후에 영속성컨텍스트에 저장한다.

AUTO = 방언에 맞게 자동선택, DB변경해도 코드수정할필요가없음

@SequenceGenerator(name = "MEMBER_SQE_GENERATOR",sequenceName = "MEMBER_SEQ"
                    ,initialValue = 1 , allocationSize = 50)
/**
 * 미리 50개를 땡긴다. 50개를 db에 미리올려놓고, 메모리에서 그 개수만큼 쓰는방식
 * 여러was가있어도 동시성이슈가없고
 * 두번호출시
 * SEQ = 1  // 문제가있나보네?라고 인식하고 한번더 호출
 * SEQ = 51  // 애플리케이션이 2를쓰고
 * SEQ = 51 //  3을쓸것이다.
 * em.persist(member1) // 1이 처음 호출되고 재호출해서 51을 쓰게되고
 * em.persist(member2) // 51//2 메모리에서 사용
 * em.persist(member3) // 51//3 메모리에서 사용
 * 51번을 만나는순간 50개를 더 떙겨야되기때문에 next call이 호출된다
 * 이것은 allocationSize에 따라 달라진다. 
 *
 */



  /**
     * GeneratedValue = 
     
     * IDENTITY = 기본키 생성을 DB에 위임 난 모르겠고 db너가 알아서해줘라
     * => 이 케이스에서는 특이하게 커밋하는시점에 쿼리가 날라가는 일반상태와달리 커밋전 바로 insert쿼리가 날아간다.(영속성 관리하려면 pk값이 필요한데, pk값을 db에위임하기때문에)
     * SEQUENCE = DB시퀀스 오브젝트 사용 , 오라클은 @SequenceGenerator 가필요하다
     * => 시퀀스전략을 확인하면, 영속성컨텍스트에 넣으려고할때 db에서 값을얻어와서 id값을 넣어준다. 그후 영속성컨텍스트에 저장한다.
     
     allocationSize 
     *
     * */
    
    /**
     * Long을쓰는게 좋은이유 : 데이터가 많아져 10억이 넘는경우 그때 타입을 바꾸는것보다 Integer를 써야하는데 Long을쓰는상황이낫다
     *
     */

좋은 웹페이지 즐겨찾기