우아한테크코스/레벨3
JPA 기초
nauni
2021. 6. 27. 17:01
JPA 왜 사용하는가?
- 패러다임 불일치(객체지향 VS 관계형 데이터베이스)
- 객체는 방향성이 있는데, 테이블은 방향성이 없음
- 객체답게 모델링할수록 RDB에 저장하기 위해 매핑작업만 늘어남
- 엔티티 신뢰문제
- 원하는 내부 필드를 다 가지고 있는지 확신할 수 없다. 널포인터 익셉션이 일어날 수 있다!
- 자바 컬렉션에 저장하듯이 DB에 저장할 수는 없을까?
- JPA(Java Persistence Api)
- ORM(Object-Relational Mapping, 객체 관계 매핑)JPA를 사용하면?
- SQL 중심적인 개발에서 객체 중심적인 개발이 가능
- 생산성 향상 (비지니스 로직에 더 집중가능)
- 유지보수
- 신뢰할 수 있는 엔티티
- 패러다임 불일치 문제 해결
JPA의 핵심개념
- 영속성 컨텍스트, 연관관계 매핑
JPA 사용에 필요한 기초지식
@Entity
: JPA가 관리할 객체@Id
: DB의 PK와 매핑할 필드- persistence.xml: JPA 설정파일 (SpringBoot 없이 사용하려면 필수임)
- META-INF 폴더 아래애 persistence.xml로 생성
- 실행시 설정정보를 조회하고, EntityManagerFactory에서 EntityManager를 생성함
- 트랜잭션 단위로 매번 EntityManger를 만듦
- EntityManagerFactory는 하나만 생성하여 애플리케이션 전체에 공유
- EntityManager는 스레드간에 공유하면 안됨 -> DB 커넥션이 물릴 수 있다.
- JPA 모든 데이터 변경은 트랜잭션 안에서 실행
- flush: 쓰기지연 sql 저장소의 쿼리를 데이터베이스에 전송(영속성 컨텍스트를 비우진 않음, 동기화가 목적)
- 직접호출: em.flush()
- 자동호출: 트랜잭션 커밋
- 자동호출: JPQL 쿼리 실행
- commit: DB에 영구반영 (flush 호출되고 commit 호출)
DDL
hibernate.hbm2ddl.auto
로 옵션을 설정- create: 기존 테이블 삭제 후 다시 생성(create+drop) → 개발초기단계
- create-drop: create와 같으나 종료시점에 테이블 DROP → 테스트코드
- update: 변경분만 반영(운영 DB에서는 사용하면 안 됨) → 개발초기단계, 테스트서버
- validate: 엔티티와 테이블이 정상 매핑되었는지만 확인 → 테스트서버, 스테이징, 운영
- none: 사용하지 않음 → 스테이징, 운영
1. 영속성 컨텍스트
- 엔티티를 영구 저장하는 환경
- 논리적 개념
- 엔티티 매니저를 통해 영속성 컨텍스트에 접근
- 일단은 엔티티 매니저와 영속성 컨텍스트가 비슷한 개념임
- 하나의 트랜잭션 단위에서 작동
- 서로 다른 트랜잭션에서는 캐시등이 적용되지 않음
- DB 커넥션도 들고 있음
상태
- 비영속: 객체 생성만함, JPA 안넣음
- 영속: 객체 생성 후, 넣음(관리대상!) → em.persist(member);
- persist 메소드는 영속상태로 만든다는 의미
- 준영속: 관리하고 있었는데 잠시 떼어놓은 상태(트랜잭션 범위를 벗어나는 경우도 해당)
- 변경감지 등이 되지 않음
- em.clear()
- em.detach(entity)
- em.close()
- 삭제
장점
- 1차캐시
- 트랜잭션 단위의 엄청 짧은 주기의 캐시
- 맵 같은 느낌으로 ID를 key 값으로 entity를 value 값으로 저장함
- 스냅샷도 저장함
- 동일성 보장
- 1차 캐시로 repeatableRead 등급의 트랜잭션 격리수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
- 트랜잭션을 지원하는 쓰기지원(buffer 기능)
- flush: DB에 보내는 시점(sync를 맞추기 위함)
- commit: flush and commit, DB에 영구반영
- 엔티티 수정(변경감지, DirtyChecking)
- 1차 캐시되는 시점에 스냅샷도 같이 저장함
- flush, commit 시점에 스냅샷을 비교하여 변경사항은 DB에 update 보냄
- 마치 컬렉션에서 참조 객체를 수정하는 것처럼 관리됨
- 엔티티 삭제
- 트랜잭션 커밋시점에!
em.remove(entity);
- 트랜잭션 커밋시점에!
Lazy로딩: fetch = FetchType.Lazy
- 실제 필요할 때 실제 값으로 교체
- 생성시에는 프록시객체로 가지고 있다가 필요할 때 실제 값으로 교체
- lazy 로딩을 사용하려면 final class를 하지 말아야 한다.
- 가급적 지연로딩을 적용하는 것이 좋다.
- 즉시로딩(FetchType.EAGER)을 사용하면 예상치 못한 SQL이 발생하여 N+1 발생가능
@ManyToOne
,@OneToOne
: 기본이 즉시로딩@OneToMany
,@ManyToMany
: 기본이 지연로딩- 지연로딩을 하려면 프록시객체가 있어야 하고, 즉 영속성 컨텍스트가 있어야 함
- 준영속상태에서는 지연로딩이 되지 않음
2. 연관관계 매핑
- 객체를 데이터 의존적이게 모델링하면 협력관계를 만들 수 없다.
- 객체와 테이블간의 연관관계 설정의 괴리를 줄여주는 역할
단방향 매핑
Member에서 Team 필드에
@JoinColumn(name = "team")
@ManyToOne(fetch = FetchType.LAZY)
양방향 매핑Team 에서 List<\Member>를 갖는다면?
@OneToMany(mappedBy = "team")
객체는 양방향이 아니라 단방향이 2개 있는 것!
- 객체를 양방향으로 참조하려면(Team -> List
, Member-> Team) 단방향을 2개 만듦
- 객체를 양방향으로 참조하려면(Team -> List
객체참조와 외래키는 성격이 다르므로 mappedBy가 필요
mappedBy
- 연관관계의 주인을 설정하기 위해 사용, 주인이 아닌 쪽에서 mappedBy 사용
- 연관관계의 주인만이 외래키를 관리(등록, 수정 등)
- 주인이 아닌 쪽은 읽기만 가능
- 나는 주인이 아님 -> mappedBy
- 주인임 -> mappedBy 속성 사용하지 않음
- 외래키가 있는 곳을 주인으로 정함
주의사항
연관관계 주인에 값을 입력해야함 (1번말고 2번처럼 해야함)
team.getMembers().add(member) // 1. 외래키가 null로 들어감 member.setTeam(team) // 2. ok
1,2 코드 둘다 사용하는 것이 권장(객체사용의 의미로 보면)설계
- 먼저 단방향으로만 설계한다
- 이후 필요하다면 양방향 매핑을 해준다(조회를 편하게 하기 위한 부가기능)
기타
- 모든 영속성컨텍스트는 트랜잭션 내에서 동작
- propagation에 따라 트랜잭션이 동작하는게 다름
- Id를 IDENTITY로 auto-increment로 설정하면 DB의 도움이 필요하므로 save 하는 순간 insert query 동작
- 테스트코드에서는 어차피 rollback 할 것이라 쿼리를 안 날림JPQL
- SQL을 추상화한 쿼리언어
- 객체지향 SQL
- 엔티티 이름을 사용함
- 별칭은 필수
- 프로젝션
- 일부 데이터만 가져오는 방법
- fetchJoin
- 자주 같이 사용되는 객체에 설정
- N+1 문제 해결
- namedQuery
- 애플리케이션 로딩 시점에 SQL 오류를 잡을 수 있음
Spring Data JPA
- 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결
- 개발자는 인터페이스만 생성
Query DSL
- SQL, JPQL 문제점: 해당 로직이 실행전까지 작동여부 확인 불가, Type-check 불가
- SQL, JPQL을 코드로 작성하게 해줌
- 컴파일 시점에 문법오류 발견가능
- IDE를 통해 코드 자동완성 도움
- 동적쿼리(분기문등) 작성용이
- 자바코드이기 때문에 extractMethod 하여 가독성을 높일 수 있음
참고
- 김영한님 T-academy 강의
- 우아한테크코스 수업