면접직전이라 JAVA ORM표준 JPA프로그래밍을 이틀만에 급 읽어서 정리; 집중안돼서 너무 힘들었지만 평소에 알고싶었던 개념들이라 지금이라도 정리해서 다행이다..
기본 내부구조
ORM이란?
- 데이터베이스와 자바 간 패러다임의 불일치를 해결하기 위한 도구. 개발자들이 비즈니스 로직에 집중할 수 있게 도와준다.
- 패러다임 불일치 해결
- 반복되는 SQL 자동 생성
- 자바에서의 상속을 데이터베이스와 매핑해서 객체지향적인 구조를 유지할 수 있게 도와줌
JPA의 장점?
- 패러다임의 불일치 해결
- 연관된 객체 탐색을 보다 간편하게 해줌
- 동등성 관리에 도움(근데 캐시와 프록시가 끼어드니 더 복잡하게 만드는 것 같아서 참조보다는 값을 통한 비교가 나을 수도 있음)
- 1차 캐시(영속성 컨텍스트)를 통한 도움
- 내부적인 트랜잭션 관리 가능
- 데이터베이스에 다녀오지 않아도 이미 영속성 컨텍스트에서 가지고 있는 데이터는 곧바로 가져올 수 있음
그래도 Mybatis를 사용하는 이유?
- 지연 로딩같은게 JPA의 장점일 수 있는데, 사실 사용할 객체라면 한번에 join으로 로딩해오는 것이 더 빠를 수 있음
- 어쩌피 현업에서 매우 단순한 쿼리는 잘 사용하지 않아서 성능도 고려하면 대부분의 쿼리는 개발자가 직접 생성해야 함
- 제대로 이해하지 못하고 사용하면 추상화되어있기 때문에 문제를 파악하기 어려움 ex)N+1문제
그래도 JPA가...
- mybatis에서 리턴받은 데이터베이스 오브젝트를 일일히 xml을 통해 자바의 객체와 매핑하는 것이 너무 불편; 패러다임의 불일치 극복 비용이 너무 많이 든다..
엔티티 매니저
- 엔티티와 관련된 모든 일을 처리(CRUD).
- 서버 요청당 하나일 수도(OSIV), 트랜잭션당 하나일 수도 있고 다양함.
- 가장 근본적인 원칙은 앤티티 매니저 팩토리는 스레드 세이프하지만 엔티티 메니저는 스레드 간에 공유가 안됨
영속 컨텍스트
- 1차 캐시 관리
- 내부에 있는 엔티티들은 모두 식별자를 가지고 있음 -> 따라서 Identity전략(mysql에서 사용)트랜잭션에서 write을 할 경우, 식별자를 알아야지 영속 컨텍스트에 넣을 수 있으므로 쓰기 지연이 발생하지 않음
엔티티의 생명주기
- 비영속: 순수한 자바 객체 상태
- 영속: 영속성 컨텍스트에 들어간 자바 객체.
- 객체의 변경이 추적되어 자동적으로 데이터베이스에 반영된다.
- 1차 캐시
- 동일성 보장
- 트랜잭션 쓰기 지연: flush전에는 데이터베이스에 직접 락을 걸지 않으므로 장점
- 지연 로딩
- 준영속: 트랜잭션의 종료 또는 명시적인 영속성 컨텍스트 종료 등으로 영속성이 끊어진 상태. 거의 비영속 상태인데 차이는 식별자 값을 가진 것
- 삭제
관련 어노테이션
@PersistenceContext
- 순수 자바 어플리케이션과는 달리, 스프링에서는 EntityManager를 알아서 관리해주기에, 직접 EntityManagerFactory에서 주입받지 않더라도 주입받을 수 있음
@Repository
다대일 양방향
- member * - 1 team일 경우
- member가 foreign key를 관리할 수 있도록 설정하는 것이 편리. -> @ManyToOne
- team는 member의 외래키를 이용해서 List< member >를 생성하는 것이므로, 연관관계의 실제 주인인 member의 team속성을 넣어줌-> @ManyToOne(mappedBy="team")
- 왜넣어주지? 싶었는데 List< member >이 많을 경우 ex) 학교 팀 멤버들, 동네 팀 멤버들... 이렇게 있을 때 구분해주기 위해서인듯
- 주의사항
- 한쪽에만 데이터를 넣거나 삭제하면 다른 쪽에는 반영되지 않기 때문에, 양쪽에 넣어주는 함수 만들어 사용하면 편함(단, 영속성 전이를 사용하면 양쪽에 반영할 수 있음)
상속관계
- 부모, 자식들을 각자 다른 테이블로 만듬
- 장
- 테이블이 정규화됨
- 효율적인 데이터 사용
- 부모, 자식에서 필요한 칼럼들을 모아서 하나의 테이블로 만듬
- 장
- 성능을 높일 수 있음
- 조회 쿼리 단순
- 부모에서 필요한 칼럼들을 모두 자식 테이블에 넣음
- 장
- 메모리 공간 절약 + 부모 테이블에 조인할 필요성 없음
식별관계 vs 비식별관계
- 식별관계: 부모 테이블의 기본 키를 받아서 자식 테이블의 기본키 + 외래키로 사용하는 방식
- 비식별관계: 부모 테이블의 기본 키를 자식 키에서 외래키로 사용하는 것
로딩
지연 로딩
- 한번에 모든 데이터들을 가져오는 것은 매우 비효율적이어서 정말 사용할 때 가져오는 방식
- JPA에서는 바이트 코드를 로딩할때 조작하는 로드타임 위빙 방식과 프록시 방식을 사용
프록시 방식
- 객체를 getReference로 조회하면 엔티티 그대로의 객체가 오는 것이 아니라, 프록시로 한번 더 감싸진 객체가 온다. (영속성 컨텍스트에 존재하면 이미 로딩되었다는 것이므로 불필요하게 프록시로 감싸져있는게 아닌 실제 엔티티를 가져옴)
- getReference로 얻은 프록시 객체는 식별자를 가지고 있기에 AcessType이 프로퍼티인 경우에는 getId를 해도 엔티티가 초기화되지 않음(?어떻게.....? Mysql은 디비에 넣어야지 식별자를 알 수 있을텐데... 임시식별자인가?)
- 실제 데이터를 얻기 위해 get메서드를 사용할 때 초기화를 진행한다.
- 영속성 컨텍스트의 범위에서 벗어나 있는 경우(ex 컨트롤러단, 영속성 컨텍스트 종료)에 해당 속성에 접근하려고 하면 LazyInitializationException이 뜬다.
Spring Data JPA
사용 이유
- 별다른 구현 클래스 없이 인터페이스만 만들어놓으면 네이밍 규칙만 맞추면 자동으로 쿼리를 생성해준다 (JpaRepository를 상속받아야 함)
- 이미 몇가지 findAll같은 인터페이스는 이미 JpaRepository에 들어있음
트랜잭션
영속성 컨텍스트
- 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용
준영속 상태의 지연 로딩 문제
컨트롤러에서 추가적으로 엔티티를 초기화할 필요가 있을수도 있음. 하지만 컨트롤러는 트랜잭션의 범위를 이미 넘어가서 엔티티가 비영속 상태이고, 더이상 초기화가 불가능해짐.
- 해결방법1: 페치 전략을 즉시로딩으로 수정
- 단: N+1문제가 발생, 불필요한 엔티티를 로딩해둠
- 해결방법2: OSIV를 사용
- 이전 OSIV: 필터 또는 인터셉터에서 트랜잭션을 시작해 요청당 하나의 영속성 컨텍스트를 사용 -> 컨트롤러에서 뷰에 보여지기 위해 임시로 엔티티의 데이터를 수정하면, 영속성 컨텍스트 내부에 있으므로 결과적으로 요청을 반환할 때 DB에 반영되는 문제
- 해결책: 엔티티를 읽기 전용으로 전달(getter만 있는 인터페이스 타입으로 전달. 매번 인터페이스를 작성해야 한다는 불편함), DTO로 전달, wrapper로 전달 다 비슷한 단점
- 현재 OSIV: 필터 또는 인터셉터에서 영속성 컨텍스트를 만들어 영속성 컨텍스트는 요청 전체로 유지를 하되, 트랜잭션은 서비스 계층만으로 국한시킴. -> 서비스 호출이 끝나고 컨트롤러에서 데이터를 수정하여도, flush는 이미 하였으니 디비에는 반영되지 않을 것. flush를 명시적으로 호출하여도 트랜잭션의 범위 밖이라서 데이터 변경 불가 에러 발생(그런가보다..)
하지만 service -> 컨트롤러에서 수정 -> service하면 결국 flush하니 디비에 반영되는디...? -> 그러지 마라....
롤백 문제
트랜잭션이 롤백되어도, 영속성 상태의 객체는 이미 수정되어서 남아있음. 따라서 명시적으로 em.clear같이 초기화를 해줘야 함
-> 그런데 스프링에서는 내부적으로 롤백발생하면 알아서 초기화해줌
추가적인 문제들
엔티티 동등성
- 영속성 컨텍스트가 같으면 같은 엔티티는 같은 참조를 가짐
- 반면 영속성 컨텍스트가 다를 때는 엔티티의 동일성 비교(==)는 실패하므로, 되도록이면 미스하지 않도록 동등성 비교(equals)쓰는게 좋음
프록시 객체 동등성
- getReference하고 find하면? -> 영속성 컨텍스트에서는 find에다가 이미 찾아놓은 프록시 객체 돌려줌
- find -> getReferenct하면? -> 영속성 컨텍스트에서는 getReference에다가 이미 로딩해놓은 엔티티 객체 돌려줌
- 동등성 지켜짐
- 심화문제있는데 나중에 657 이해해보자
성능 최적화
N+1문제
지연 로딩으로 인해 객체들을 찾아오고, 가지고있는 외부참조 속성들을 하나씩 초기화하게 되면 요청이 매 객체마다 날라가는 문제 발생
락
낙관적 락
- 트랜잭션 대부분이 충돌이 발생하지 않는다고 가정
- JPA가 제공하는 버전 관리 기능 이용
- 동시에 두 데이터를 트랜잭션에서 변경할 때, 최초 커밋만 인정하도록 구현
- Version사용: 트랜잭션이 데이터를 변경하거나, 심지어는 조회만 했을 때(경우에 따라 다르게 설정가능) 버전을 하나씩 높임. 칼럼으로 하나 둬서 JPA가 관리. 만약 커밋을 할 때 내가 가지고 있는 버전과 다르면 다른 트랜잭션에서 이미 접근했다는 의미이므로 에러를 발생시키고 반영하지 않음.
- NONE: 데이터 변경시 버전 높여서 두번의 갱신으로 인해 데이터가 유실되는 것을 방지
- OPTIMISTIC: 조회시 버전을 높여서 다른 트랜잭션들의 non repeatable read를 방지
비관적 락
- 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 검
- 데이터베이스가 제공하는 락 기능 사용(ex. for update)
2차 캐시
- 2차 캐시를 추가로 둘 수 있음.
- 1차 캐시에서 없으면 2차 캐시를 한번 더 찾아서 네트워크 비용을 줄일 수 있도록 함
- 동시성을 극대화하기 위해 객체를 그대로 반환하지 않고 복사본을 반환