인프런 스프링 JPA 강의 기초인 자바 ORM 표준 JPA 프로그래밍을 완강한 후 후기를 남겨보려고 합니다.
[목차]
1. 강의 느낀점
저번 스프링부트와 JPA 활용1편을 들은 후, 이번에 가장 기초강의인 자바 ORM 표준 JPA 프로그래밍을 수강했습니다.
원래 이것부터 들어야 하지만, 실제로 JPA 활용1편을 먼저 들으면서 일단 따라해보고 다시 기초강의를 듣는 방식도 상당히 효과가 좋은 것 같습니다. 좀 더 익숙해진 상태로 기초강의를 들으니 빠르게 들을 수 있었네요.
JPA 활용1편과 다른점은 예제보다 이론에 좀 더 초점을 맞춘 강의인 것 같습니다. 활용1편에서는 나오지 않는 개념이 꽤 많이 나오네요. 그 중 가장 중요한 것은 fetch join 개념인 것 같습니다. 이건 활용1편에서 제 기억에는 나오지 않았고 언급만 하신 것으로 알고 있습니다.
그리고 활용1편은 보통 중요한 개념을 다시 언급은 해주시지만, 그런 중요한 개념들에 대해 자세히 설명을 해주십니다. 더티체킹, 엔티티매니저, LAZY로딩, 1차캐시, 영속상태, 연관관계 주인등에 대해 자세히 설명해주시네요.
그리고 JPQL의 문법에 대해 후반부에 아주 자세히 설명을 해주십니다. 이 부분은 나중에 실무를 하거나 토이 프로젝트를 할 때 필요하면 다시 찾아듣는 방식을 쓰면 좋을 것 같네요.
또한 나머지 도메인 모델링이나 엔티티 모델링은 활용1편과 거의 유사하게 쇼핑몰로 가져가기 때문에 이해하기 훨씬 수월했습니다.
어쨋든 아주 알찬 JPA 기초 강의였습니다!
2. 강의에서 기억할 것1
기억할 것이 정말 많긴 하지만, 전반적으로 머리에 꼭 넣어야 할 부분들을 추려봤습니다. (생략된 내용이 많아서 해당 요약으로는 강의 내용을 다 공부할 수 없습니다)
강의에서 기억할 것1에서는 JPQL 이전을 다루고 강의에서 기억할 것2에서는 JPQL관련을 다룹니다.
- EntityManagerFactory(이하 emf)는 전역 선언 가능하지만 EntityManager(이하 em)는 하나의 트랜젝션 안에서 관리 된다(되어야 한다). 쓰레드간 공유가 안 된다.
- em이 생성되면 영속성 컨텍스트라는 논리적 공간이 생성된다. 여기서 엔티티의 생명주기가 관리된다.
- JPQL은 엔티티 객체를 대상으로 쿼리하며 SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.
- 1차캐시는 영속성 컨텍스트 내에 존재하는 개념이다. persist()로 저장시에 우선 여기 저장된다. 그리고 commit() 시점에 DB에 저장된다.
- 조회시 1차캐시에 ID와 매핑되는 값 있으면 해당 값 가져온다.(DB조회X)
- persist()를 하면 1차캐시에 저장되는 동시에 쓰기 지연 SQL 저장소에 저장되어 SQL이 생성된다. 이후 커밋 시점에 SQL이 DB로 날라간다.
- 더티체킹의 예는 setName()등으로 객체를 변경할 때 자동으로 update 쿼리가 나가는 것이다. 1차캐시에 스냅샷이 있는데 최초 1차캐시에 값이 들어올 때 스냅샷에 값을 복사해두고, 이후 update시 스냅샷과 다르다면 update SQL을 만들어 쓰기지연저장소에 넣어둔 후 커밋시점에 DB에 날린다.
- 엔티티 생명주기에서 준영속 상태는 영속상태였던 객체를 영속상태가 아닌 객체로 변경하는 것이다. em.detach(entity)는 특정 엔티티를 준영속화 한다.
- em.clear()는 영속성 컨텍스트를 초기화하고 em.close()는 영속성 컨텍스트를 종료한다
- 엔티티에서 쓰이는 어노테이션들
- @Transient: 이거 쓰면 DB와 매핑 안되는 프로퍼티(객체 내에서만 사용/메모리에서만 사용)
- @Lob: 큰 컨텐츠 넣을때(CLOB-String, BLOB-byte등)
- @Column: 컬럼관련 정보 넣을 수 있음
- columnDefinition: 컬럼정보 직접 줄 수 있음
- @Enumerated: enm쓸 때(옵션으로 ORDINAL말고 STRING 써야함)
- @Temporal: 최신버전은 그냥 LocalDate, LocalDatetime 타입 쓰면 됨(LocalDate=>date타입 / LocalDatetime=>timestamp 타입)
- @GeneratedValue: 엔티티 pk의 시퀀스 생성방법 제시
- strategy=GenerationType.IDENTITY: 기본키 생성 DB에 위임( ex: Mysql의 AUTO_INCREMENT)
- strategy=GenerationType.SEQUENCE: 시퀀스 오브젝트 가져온 후 값 세팅
- SEQUENCE전략과 IDENTITY 전략의 중요한 차이점
- identity전략을 쓰면 레코드 삽입(insert)시 PK 시퀀스 증가된 값을 할당한다.
- 그래서 persist()후 바로 id(pk) 값을 알 수 없다. commit()후에 알 수 있다.
- 그래서 예외적으로 identity전략 사용시 persist()단계에서 바로 insert문을 날려서 pk값을 알 수 있다.
- @SenquenceGenerator: SEQUENCE 제네레이트 전략 사용시 테이블별로 이름 지정한 시퀀스 오브젝트로 사용가능(allocationSize 전략 등 필요시 다시 공부)
- @MappedSuperclass: 공통속성(create_date)등을 BaseEntity로 만들어서 사용
- 연관관계 매핑관련
- 단방향 매핑
- Member, Team 다대일 가정. Member 조회시 Team을 조회하는데 Team조회시 Member를 안 쓴다고 가정하면 member->Team 단방향. 이때 Team에 List<Member>만들고 mappedby 쓸 필요 없음.
- 사실 양방향으로 만드는 것이 좋음. DB는 fk만 있으면 알아서 양방향이니..
- 양방향매핑
- 연관관계 주인만 외래키 관리. 등록, 수정 등 가능. 주인 아닌 쪽은 ONLY READ.
- 연관관계 주인은 FK있는곳(다대일 중 다)에 걸자.
- 다대일, 일대다, 일대일 등 개념
- 일대다에서 일 쪽에 연관관계 주인 설정은 거의 안쓴다. 엔티티가 관리하는 외래키가 다른 테이블에 있다.
- 일대일은 fk를 주테이블에 관리할 것인지 대상테이블에 관리할 것인지 선호(김영한님은 조회의 주대상이 되는 쪽에 거는걸 선호. member와 locker가 있으면 member에)
- 다대다는 실무에서 쓰면 안 됨. 중간테이블이 알아서 만들어지는데 거기에 컬럼추가 등 불가함
- 상속관계 매핑은 Item을 상속한 Movie, Book, Album등 예시. extends Item으로 상속만 받으면 기본 설정은 싱글테이블 전략(한 테이블(ITEM)에 다 떄려넣는 방식)
- SINGLE_TABLE, JOINED, TABLE_PER_CLASS 방식
- 지연로딩(LAZY)은 즉시로딩(EAGER)와 반대되는 개념이다. Member와 Team이 다대일 관계라고 가정했을 때, 즉시로딩은 Member조회시 Team도 조회되는 반면, 지연로딩은 Member 조회시 Team조회 안 된다. getTeam()등으로 team관련 정보 조회시점에 쿼리가 나간다.
- 지연로딩을 제대로 알려면 프록시 개념을 알아야 한다. 프록시는 em.getReference()로 가져올 수 있다. Member를 상속한 프록시 객체를 내부적으로 만들어 반환한다. 내부에 프로퍼티로 Entity target이 있는데, getName()등이 호출될때 실제로 불러온다.
- 프록시 객체는 처음1번만 초기화된다. 그리고 원본을 상속받는다.
- N+1문제
- 즉시로딩시 select m from Member m만 조회해서 2명의 멤버 데이터가 나오면, team 테이블도 2번 조회쿼리가 나간다. 즉, Member 조회(1)만 하려고헀는데 그 조회데이터만큼 team테이블도 n번 나가는 문제가 발생한다.
- LAZY로 설정하면 실제 team이 필요시에 쿼리가 조회되어 N+1같은 의도치 않은 문제 발생확률이 적어진다(fetch join설명때 LAZY로딩도 N+1문제가 있다는 것을 보면 LAZY만으로 완벽한 해결법은 아닌것같다)
- 우선 LAZY 설정후, 나중에 필요한 경우 fetch join으로 가져오는 게 좋다고 한다.
- 영속성전이-CASCADE
- 부모엔티티 저장시 자식엔티티도 저장할 때 사용
- CascadeType.ALL 많이 쓴다. parent, child가 있을 때(1:N) parent persist()시 childList도 persist()를 자동으로 찌르게 한다.(참고로 child만 parent에 종속적으로 쓰일 때 써야한다. 안그러면 난리가 난다고 한다..)
- 고아객체 옵션(orphanRemoval=true)을 주면 childList.remove(a) 등을 호출시 delete쿼리가 나간다.
- parent를 지우면 childList도 다 지워진다.
- CascadeType.ALL + orphanRemoval=true를 같이 쓰면 부모엔티티를 통해 자식 생명주기 관리가 가능하다.
- 값타입
- 기본타입(int등)
- 임베디드타입(@Embedded)
- 컬렉션타입
- 기본타입은 값을 복사하기 때문에 공유가 안 됨
- 임베디드타입
- 임베디드 타입(@Embedded)은 Address 엔티티 예시. 주소, 번지, 위치 등이 모인 객체가 있다고 가정. member, order등 여러군데서 필요하다면 임베디드타입을 만들어 공유할 수 있다. 그러나
- 임베디드 타입도 이를 소유한 엔티티 생명주기에 의존한다.
- 불변객체로 가져갈 수 있게 생성자에 값 세팅하고 setter는 안 만드는 것이 좋다.
- 임베디드 타입은 값 공유시 위험하다. 공유할거면 엔티티로 만드는 게 나음
- 값타입의 비교
- 동일성 vs 동등성 비교
- 동일성 비교는 ==로 인스턴스 참조값 비교
- 동등성 비교는 인스턴스의 값을 비교(equals)
- equals()를 오버라이딩 해서 값비교만 할 수 있게 하자. 그리고 hashCode도 같이 오버라이딩하여 재구현한다(컬렉션을 이용해 사용시 올바르게 동작하고 성능상 이점)
- 값타입 컬렉션(@ElementCollection)
- 값타입도 컬렉션으로 만들 수 있다. List<String> favorite Foods 등..
- @CollectionTable(name="FAVORITE_FOODS")등으로 테이블생성시 지정가능
- 값타입 컬렉션도 멤버(엔티티)에 의존하며 값타입을 따로 persist할 필요가 없다.
- 기본적으로 지연로딩사용
- 단방향 매핑
3. 강의에서 기억할 것2
- JPQL
- em.createQuery(쿼리, [클래스명].class)
- createQuery로 조회하면 영속성컨텍스트에서 관리되고 이 시점에 flush된다.
- 임베디드 타입도 조회가능(em.createQuery("select o.address from Order o", Order.class).getResultList())
- 단건조회(getSingleResult)는 null이면 NPE오류 발생함
- 스칼라타입(username등)을 받을 때 DTO로 받으면 깔끔하다.
- em.createQuery("select new hellojpa.MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList()
- 페이징API : setFirstResult, setMaxResult
- FROM절에서 서브쿼리 불가능(조인으로 해결). SELECT 절은 가능
- ENUM은 패키지명 모두 포함해서 쓸 수 있음
- SQL처럼 EXISTS, IN, AND, OR, NOT 등 다 쓸수 있음
- JPQL 사용자정의함수 따로 등록해서 써야함
- 경로표현식
- 상태필드: m.useranme / 탐색 더 못함. 경로의 끝
- 연관필드: 연관관계를 위한 필드
- 단일값 연관 필드: m.team t / @OneToOne, @ManyToOne / 묵시적 내부조인 발생하며 탐색 가능
- 컬렉셕값 연관 필드: m.orders o / @ManyToMany, @OneToMany / 묵시적 내부조인 발생하며 탐색 불가능
- select m.team ..과 같이 쓰면 묵시적 내부조인이 발생할 수밖에 없다. 즉, join team을 할 수 밖에 없다.
- 묵시적 내부 조인이 발생하도록 짜면 안 된다!
- 명시적 조인을 쓰자
- select t,name, m.username from Team t join t.members m
- fetch join
- fetch join은 연관관계에 있는 엔티티들을 한 번의 SQL쿼리로 조회할 수 있음
- select m from Member m join fetch m.team은 m.*, t.*를 같이 가져옴
- fetch join은 즉시로딩과 비슷하게 작동하며 fetch join을 쓴 JPQL에서만 LAZY로딩보다 우선적용된다.(세밀한 제어)
- 1:N관계(team, member)에서 데이터 뻥튀기 가능. distinct명령어로 처리 가능하지만 하이버네이트6부터 자동으로 distinct 처리가 된다.
- 일반join과의 차이점
- fetch join은 연관된 엔티티 값들도 select시 가져온다. 반면 그냥 join은 연관된 엔티티 값들을 가져오지 않는다. 예를들어 team과 member를 join으로 가져온다면 member는 초기화 안 된다(LAZY로딩일시)
- fetch join시 알아둬야할 점들
- 별칭 쓰면 안 된다. 별칭으로 where 제어등을 하면 JPA에서 원하는 방향,사상과 안 맞게 작동한다.
- 2개 이상의 컬렉션은 fetch join쓰면 안 된다. 뻥튀기x2가 될 수도 있다.
- 컬렉션을 fetch join하면 페이징API를 쓸 수 없다.
- 그냥 1:N을 N:1로 바꿔쓰자. 그러면 문제가 사라진다.
- 아니면 Team만 조회 후, Team엔티티의 Member에 @BatchSize(또는 Persistence.xml 설정)을 주자. 그러면 Team조회후 member조회 쿼리 작동시 BatchSize에 지정한 개수만큼 모든 팀에 맞는 member가 조회된다.
- 실무에서 많이쓰이기 때문에 100%이해해야함. 나중에 다시 보자.
- fetch join은 연관관계에 있는 엔티티들을 한 번의 SQL쿼리로 조회할 수 있음
- 다형성 쿼리의 TYPE, TREAT를 알자. Item과 Item을 상속한 Movie, Album, Book을 가정.
- TYPE은 조회대상 특정 자식을 한정한다.
- JPQL의 select i from Item i where type(i) IN ('MOVIE', 'ALBUM)은 select * from Item where DTYPE IN ('M', 'A') SQL과 동일하다.
- TREAT는 자바의 타입캐스티와 비슷하며 부모타입을 특정 자식 타입으로 다룰 때 사용한다.
- JPQL의 select i from Item i where TREAT(i as BOOK).author='kim'는 select * from Item i where i.DTYPE='B' AND i.AUTHOR='kim' SQL과 동일하다.
- TYPE은 조회대상 특정 자식을 한정한다.
- 엔티티 직접사용하여 매핑하는 것은 결국 pk 매핑과 동일하다
- select count(m) from Member m == select count(m.id) from Member m
- 엔티티는 where 절에서도 파라미터 사용 가능하며 역시 pk로 처리된다.
- Named쿼리는 미리 엔티티에 쿼리 지정해두어 재사용할 수 있는 쿼리다. 정적쿼리만 가능하고 최초 애플리케이션 로딩시점에 초기화 되어 재사용되며, 로딩 시점에 쿼리 검증을 할 수 있다.
- 벌크연산은 update, delete같은 JPQL을 날리는 것이다. update를 예로들면 모두 조회해서 for문으로 update를 날리면 성능상 좋지 않기 때문에 벌크연산이 필요하다.
- 벌크연산시 영속성 컨텍스트 무시하여 DB에 반영한다. 정합성이 안 맞을 수 있다.
- 벌크연산을 먼저해서 영속성컨텍스트에 아무것도 없을 때 처리하거나, 영속성컨텍스트에 뭐가 있다면 우선 벌크연산 수행 후 영속성 컨텍스트를 초기화한다.
4. 이후 계획
이제 김영한님의 야생형(?)을 적용하여 빠르게 활용1편을 다시 듣고 활용2편으로 넘어가야겠습니다. 10월 말 또는 11월 초까지 다 듣고 정리하는 것을 목표로 하고 있습니다. 그리고 사내의 가벼운 기능적용시 JPA를 적용하거나 간단한 토이프로젝트로 체득을 해놔야될 것 같습니다.
공부할수록 어렵지만 재밌는 기술인 것 같습니다. 최근에는 대부분 회사가 JPA를 활용하는데 좀 늦게 공부한 감이 없지않아 있네요. 예전에 가볍게 공부했을 때 뭘 모르고 공부해서 그런지 머리에 하나도 안 남았는데, 지금 다시 곱씹으며 공부하니 그래도 정리가 어느정도 되고 있는 것 같습니다.
감사합니다!
'개발자 일지 > Spring' 카테고리의 다른 글
필터와 인터셉터 차이, 개념, 예제 (0) | 2024.09.20 |
---|---|
[스프링]@ResponseBody 역할, 쓰는이유, 대체 어노테이션(@RestController) 알아보기 (0) | 2024.09.10 |
[개인학습]스프링부트 + Swagger + JPA + MySQL 설정 및 테스트 (2) | 2022.10.10 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(7) (3) | 2022.07.03 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(6) (0) | 2022.06.18 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(5) (0) | 2022.06.14 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(4) (0) | 2022.06.04 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(3) (2) | 2022.05.31 |