[JAVA ORM 표준 JPA 프로그래밍] 총정리

Junho Bae·2021년 3월 12일
6

SPRING JPA

목록 보기
2/5

인프런 김영한님의 Java ORM 표준 JPA 프로그래밍을 듣고 제가 나중에 보려고 정리한 내용입니다. 들어보시기를 강추합니다! 구어체 주의, 오타 주의

Java 표준 JPA 프로그래밍

Chapter 1. jpa 소개

-모던 어플리케이션개발 : 객체지향으로 개발
-현재 db : 관계형 db

객체를 관계형 db에 관리해야함. (sql 중심)
1) 무한 반복, 지루한 코드
A. CRUD 쿼리를 다 짜야되고 java를 sql로 바꾸고 sql을 java로 바꾸고.
B. 테이블 하나 만들 때 마다 crud 수십개 짜야됨.
ex) 전화번호 없이 개발했는데 갑자기 나중에 연락처 추가되면… sql 존나 다시 써야됨…
-> Sql에 의존적인 개발을 피하기가 어려움.

2) 패러다임의 불일치 : 객체 vs 관계형 데이터베이스
A. 애초에 관계형 db와 객체의 사상 자체가 다름.
B. 객체는 결국 잘 캡슐화(추상화,정보은낙, 상속,다형성) 하는게 목표, rdbms는 데이터를 정교하게 짜는 것이 목표
C. 결국 개발자 = sql 매퍼?

3) 객체와 rdbms의 차이
A. 상속? Rdbms는 없음
B. 연관관계? Rdbms는 pk와 fk로 조인해서 찾아야됨. 테이블의 슈퍼타입과 서브타입이 그나마 유사함.

  • 객체답게 모델링 할수록 매핑 작업만 늘어난다.. 엄청나게 코드를 많이 작성해야함…
  • 객체를 자바 컬렉션에 저장 하듯이 rdbms에 저장할 수는 없을까? => jpa.

Chapter1-2. jpa 소개

ORM? : Object-relational mapping(객체 관계 매핑), 객체는 객체대로, db는 db대로 설계를 하고, orm 프레임워크가 알아서 매핑을 해주겠다.

  • 개발자가 직접 jdbc api를 쓰는게 아니라, jpa를 쓰는 것.
  • 쿼리를 jpa가 만들어준다.

패러다임의 불일치를 해결해준다.

1) Jpa 표준 명세 :

  • Jpa는 인터페이스의 모음
  • Jpa 2.1 표준 명세를 구현한 3가지 구현체 : 거의 다 hibernate를 쓴다.

2) Jpa를 왜 써야 하는가
A. Sql 중심적인 개발에서 객체 중심으로 개발
B. 생산성 : 마치 자바 컬렉션에다가 넣었다 뺐다 하는 것 처럼.
1)저장 : jpa.persist(member)
2)조회 : Member member = jpa.find(memberId)
3)수정 : member.setName(“변경할 이름”)
C. 유지 보수 – 기존에는 필드 변경시 쿼리문을 전부 수정해야함.

(1) JPA와 상속 :
A. 지연로딩 : 객체가 실제 사용될 때 로딩됨.
B. 즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회.

ex) 멤버를 가져올 때는 팀을 항상 가져와(즉시로딩) VS 멤버 가져왔다가 나중에 필요할 때 팀만 가져와! => 같이 쓰는게 많은 경우에는 즉시로딩, 따로 쓰는 경우가 많을 경우에는 지연로딩.

  • 개발할 때는 지연로딩으로 쭉 가져와서 최적화 하기도함
  • ORM은 객체와 RDB 두 기둥위에 있는 기술이다!! 둘 다 정말 잘해야됨. 사실 RDB가 제일 중요하다.

Chapter3. 영속성 컨텍스트

jpa에서 가장 중요한 2가지?

  • 객체와 관계형 데이터베이스 매핑하기(정적인 느낌, 디비를 어케 설계하고 객체를 어케 설계해서 어케 매핑할거야?) & 영속성 컨텍스트(실제 jpa가 내부에서 어떻게 동작해? = 메커니즘!)

1) 엔티티 매니저 팩토리와 엔티티 매니저
웹 어플리케이션이 시작하면 엔티티 매니저 팩토리가 생성 -> 요청에 따라서 엔티티 매니저를 팩토리가 만들어줌. -> 각각의 매니저가 db 커넥션을 이용해 처리함.

2) 영속성 컨텍스트? Jpa를 이해하는데 가장 중요한 용어. “엔티티를 영구 저장하는 환경” 이라는 뜻.

  • EntityManager.persist(entity) : 객체를 db에 저장하는구나? -> “엔티티를 영속화한다” -> 사실은 db에 저장하는게 아니라, 영속성 컨텍스트에 저장하는 거임.
  • 영속성 컨텍스트는 논리적인 개념. 눈에 보이지 않음. 엔티티 매니저를 통해서 영속성 컨텍스트에 접근.
  • 엔티티 매니저를 생성하면, 영속성 컨텍스트라는 공간이 생긴다고 생각.

3) 엔티티의 생명주기

  • 비영속(new,transient) : 최초의 멤버 객체를 생성한 상태. 영속성 컨텍스트와 관계없는 아예 새로운 상태
  • 영속(managed) : 영속성 컨텍스트에 관리되는 상태 (persist(member)) 하는 느낌.
  • 준영속, 삭제

4) 비영속

  • 멤버객체를 생성을 하고, 엔티티 매니저에 안 넣은 상태
  • ex) Member member = new Member(); member.setId(“memberID”) -> JPA와 아예 관계가 없음
    5) 영속
  • em.persist(member)를 통해서, 영속성 컨텍스트라는 곳에 들어가 있는 상태.
  • 멤버가 이제 관리가 된다라는 의미.
  • 사실 이 때 db에 저장되는게 아님.
  • persist()메서드를 쓸 때 쿼리가 날아가는게 아님.
  • **tx.commit()을 하는 때에 쿼리가 날아가게 됨.

6) 영속성 컨텍스트의 이점
(1) 1차 캐시. 약간 중간에 있는 느낌. 버퍼링도 가능하고, 캐싱도 가능함.

  • 사실상 1차 캐시가 영속성 컨텍스트. 스프링에서는 조금 달라지긴 하지만 일단은.
  • 엔티티 매니저(영속성 컨텍스트) 안에 1차 캐시가 있음. Persist 하면 1차 캐시에 pk-엔티티 이렇게 저장
  • 조회를 하면, db를 바로 뒤지는게 아니라 1차캐시에서 조회함
  • 만약 1차 캐시에 없으면, db에서 조회하고, 이 값을 1차캐시에 다시 저장하고, 이걸 반환.

-> 사실 큰 도움은 안됨. 사실 em은 트랜잭션 단위로 만들고 끝나면 지워버리기 때문. 굉장히 찰나의 순간에만 이득이 됨. 여러 명의 고객이 사용하는 캐시가 아님. 어플리케이션에서 공유하는 캐시는 2차 캐시라고 함.
-> 비즈니스 로직이 굉장히 복잡하면 도움이 되겠징?
-> 디비에서 한번 가져오고, 계속해서 영속성 컨텍스트에다가 저장.
-> 성능적인 이점 보다는 컨셉이 주는 이점. 객체지향적인 느낌.

7) 영속 엔티티의 동일성 보장.
”== “ 비교하면 어떻게 되니? 같은 객체로 취급됨. 마치 자바 컬렉션에서 꺼낸 것 처럼. 1차 캐시가 있기 때문에 가능.

  • 1차 캐시로 “반복 가능한 읽기(repeatable read) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 어플리케이션 차원에서 제공한다.

8)엔티티 등록

  • 영속성 컨텍스트 안에는 1차캐시 말고도, “쓰기 지연 sql 저장소”가 있음.
  • persist 하면, jpa가 엔티티를 분석을 해서 “쓰기 지연 sql 저장소”에다가 쿼리문을 보관해 놓음. 차곡 차곡 여기다가 쿼리문을 저장해 놓음
  • 이 쿼리가 언제 날라가냐? Transaction.commit()할 때 날라가는 거임. 플러쉬라고 함. 이 때 실제 db에 커밋됨.
  • 추가 : entity 객체는 기본 생성자가 있어야 함.
  • 이렇게 하면 버퍼링이라는게 가능. 하이버네이트 같은 경우에는 옵션이 있음.
    <”hibernate.jdbc.batch_size, value=””> 이 속성. 쿼리를 모았다가 디비에 한방에 보내는 것.
    -실전에서는 딱히 크게 얻을 이점이 많지는 않지만, 이러한 최적화의 여지가 생김. 마이바티스 이런걸로 쿼리를 모아서 커밋 직전에 넣는다..? 이런거 존나 빡침

9) 엔티티 수정 : 변경감지(더티 체킹)
jpa의 목적은 마치 자바 컬렉션에 넣은 것처럼 다루는 거임. 만약에 컬렉션 값을 수정 할 때 그 값을 꺼내서 수정하고 다시 그걸 저장함? 안함. 딱 찾아온 다음에 변경만 함. 즉, em.find() 해서 가져온 값을 member.set() 해서 수정 하고, 다시 em.persist 하면 안됨. 마치 자바 컬렉션에서 값 바꾸듯이 바꼈는데 sql이 날라감.

  • 어케 동작하누? 커밋을 하면, flush()가 호출됨. 무슨 일이 벌어지냐, 엔티티와 스냅샷을 비교함.db에서 가져오든 해서 최초로 영속성 컨텍스트에 들어온 상태를 “스냅샷”을 띄워 둠. 만약 memberA를 변경하고 flush를 하면, 그 순간 엔티티와 스냅샷을 쫙 비교를 함. 비교를 해보고 어 memberA가 바꼈네? 하면 쓰기 지연 SQL 저장소에다가 UPDATE 쿼리를 저장해 두고, 이걸 DB에 날려줌. 신기하하넹

10) 엔티티 삭제
Em.remove() 하면 딜리트 쿼리 생성. 커밋 시점에 날라감.
*결론 : jpa는 값을 바꾸면 트랜잭션 커밋 시점에 알아서 업데이트 쿼리가 날아가는 구나. 라고 생각하고, 따로 persist를 안하는게 정답임.

Chapter4. 플러시

플러시? 영속성 컨텍스트의 변경내용을 데이터베이스에 반영. 보통 db 트랜잭션이 커밋 될 때 플러시가 일어남. 쌓아놓은 sql문이 날라가는 것. 영속성 컨텍스트의 쿼리들을 db에 쭉 날려주는 것.

  • 플러시 발생 ? : commit 되면 자동으로 발생.
  • 변경 감지(더티 체킹), 수정된 엔티티를 쓰기 지연 sql 저장소에 등록함.
  • 쓰기 지연 sql 저장소의 쿼리를 db에 전송 (등록,수정,삭제 쿼리)를 쫙 보내는거임.
  • 플러시가 발생한다고 해서 커밋 이 발생하는건 아니고..

영속성 컨텍스트를 플러시 하는 방법 : em.flush() – 직접 호출.. 진짜로 이렇게 쓰는 경우는 없음

  • 트랜잭션 커밋 : 플러시 자동 호출
  • Jpql 쿼리 실행 : 플러시 자동 호출
  • 플러시를 하면 1차 캐시가 지워지나요? 아니요. 오직 쓰기 지연 sql 저장소에 있는 쿼리들만 db로 날려버리는 거임. 뭔가 바뀐거 이런것만 데이터베이스에 반영이 되는 것.

  • jpql에서 플러시가 자동으로 호출되는 경우? 만약 persist만 한 상태에서 중간에 jpql로 조회하는 쿼리를 날린다면, 아무것도 안날라올 수도 있음. 따라서, jpql은 무조건 플러시를 한번 날리고 시작함. 아 그래서 날라가는 구나 라고 생각하면 됨.

  • 플러시 모드 옵션? 딱히 쓸 일은 없음. FlushModeType.COMMIT – 커밋 할 때만 플러시, 쿼리에는 플러시를 안한다. 위 같은 경우에 만약 JPQL을 하는데 뭐 아예 다른 테이블을 가져오고 그런 경우에는 굳이 플러시를 할 필요가 없겠지? 근데 굳이 플러시를 하고 싶지 않을 수도 있음. 정 원하면 플러시 모드를 커밋으로 바꾸라 -> 큰 도움이 되지는 않습니다. 걍 AUTO로 쓰세요. 손대지 말고.

플러시는 영속성 컨텍스트를 비우는게 아니라, 영속성 컨텍스트를 DB에 반영 하는 것.

트랜잭션이라는 작업 단위가 중요함. 커밋 직전에만 동기화 하면 됨. 어쨌든 트랜잭션 커밋 직전에만 날려주면 되니까!! JPA는 결국 이런 동시성 관련 이슈는 다 DB 트랜잭션에 위임하기 때문에.

CHAPTER5. 준영속 상태

크게 이해하기가 쉽지는 않다. 나중에 실전에서 웹 어플리케이션을 만들 때 자세하게 설명할 거구요, 지금은 약간 이런게 있다, 정도만 알면 됩니다.

영속 -> 준영속

  • 만약, FIND를 했는데 영속성 컨텍스트에 없으면 DB에서 가져와서 1차 캐시에 올려놓음. 즉, 영속 상태가 됨. 근데 만약에 이거 영속성 컨텍스트에서 관리하고 싶지가 않아, 하면 detach()를 씀.

  • 사실 쓸 일이 많지는 않고, 웹 어플리케이션 실제 개발 할 때.

  • Em.detach() 특정 컨텍스트만 준영속으로 만들 떄.

  • Em.clear() em이라는 영속성 컨텍스트 안에 있는 걸 전부 날려버림. 따라서 쿼리가 안날라감.

  • 테스트 케이스 작성하고 이럴 때 도움이 될 수도 있음.

  • Em.close() 하면 em을 닫아버림. Jpa가 관리가 안되겠지?

  • 근데 뭐… 네… 이거에 대해서 깊이있게 이해는 할 필요 없고.. 실제 개발하는 단계에서 응용하는 것.

Chapter6. 엔티티 매핑

*jpa에서 제일 중요하게 볼거는 1)메커니즘, 2) 매핑

  • 객체와 테이블 매핑

1) 엔티티 매핑 소개

  • 객체와 테이블 매핑 : @Entity, @Table
  • 피륻와 컬럼 매핑 : @Column
  • 기본 키 메핑 : @Id
  • 연관관계 매핑 : @ManyToOne, @JoinColumn

2) @Entity

  • @Entity가 붙은 클래스는 jpa가 관리, 엔티티라고 한다.
  • 기본 생성자가 필수적임 -> 스펙상 그렇다.
  • final 클래스, enum, interface, inner 클래스 사용 불가능.
  • 저장할 필드에 final 사용하면 안됨.
  • 속성 : name, -- @Entity(name=”member”), 그냥 기본 값 쓰는게 정신건강에 좋음

3) @Table

  • @Table(name=”MBR”) 로 하면 DB의 “MBR”테이블로 쿼리가 나감.
  • catalog,schema, unique 등등..

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

jpa에서는 어플리케이션 로딩 시점에 db 테이블 생성하는 기능을 지원해줌. 로컬 pc에서 개발하고 이럴 때 도움. 운영 때 쓰면 안됨!!

  • 테이블 막 만들고 그럴 필요가 없음. 알아서 ddl 만들어줌.
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 ddl 생성.

** 이렇게 생성된 ddl은 개발 장비에서만 사용!! 운영 서버에서는 사용하지 말고, 적절히 다듬은 후에 사용하는 것을 권장

  • <”Hibernate.hbm2ddl.auto”, value = “create”>
  • @Entity가 있는 애들을 보고서 테이블을 싹 만들어줌.
  • 처음에 같은 이름이 있는 테이블을 지우고, 테이블 새로 생성.
  • db에서 막 크리에이트문, alter문 이런거 막 쓰면서 지랄하지 않고 바로 생성 가능.
  • 옵션 종류 :

1) Create-drop : 종료 시점에 table drop
2) Update : 컬럼을 하나 추가를 하고 싶은데, 드랍 테이블 하고 싶지 않고 alter 테이블 하고 싶을 떄. 추가만 가능. 지우는건 안됨., 테이블 컬럼 날라가면 큰일..
3) Validate : 엔티티와 테이블이 정상 매핑되었는지 확인해줌. 만약 이상한 컬럼이 추가되고 실행하면 에러가 남. 스키마에 이런 컬럼 없는데? 이런. 엔티티와 테이블이 정상 매핑 되었는지 확인
4) None : 사실 none은 없어요. 걍 sdh fkjsndfjkdsn이렇게 아무렇게나 쓰는거랑 같음. 매칭되는게 없어서 실행이 안됨.

주의 :

  • 데이터 베이스 방언 별로 달라지는 것을 확인!
  • 운영 장비에는 절대로 create, create-drop 이런거 절대로 절대 절대 절대 절대 쓰면 안됩니다…
  • 개발 초기 단계에는 create, update 사용..
  • 테스트 서버(개발서버)에는 update, validate… create 써놓으면 다른 개발자가 만약 서버 다시 배포하고 실행하면 데이터 다 날라가버림.
  • 스테이징과 운영 서버에는 validate or none.
  • 사실 테스트서버나 개발 서버에도 쓰지 마세요.. 운영서버에는 절대로 쓰면 안되고.. 개발서버나 스테이징 서버에도 validate 정도 쓰는 걸 추천.
  • 운영 서버에는 데이터가 만약 몇천만 건 있는데 alter 잘못하면 중단되버릴 수도… 굉장히 위험해요. Alter table 직접 만든 스크립트를 테스트 서버에다가 직접 쳐서 쓰는 것을 추천 합니다… update를 하더리도 만약 alter 쿼리가 날라가면 db 락이 걸려서 서비스가 중지됨… 운영 장비에는 저어어얼대 쓰지 마세요.

DDL 생성 기능
1) jpa에서 재밌는 기능. @Column(name=”username”, unique=true, length=10) 이런 식으로 추가 가능.
2)어플리케이션에 영향을 주는게 아닌, db에 직접 영향을 주는 것. 그냥 ddl 생성만 딱 도와주는 것.

Chapter 8. 필드와 컬럼 매핑

  1. 매핑 어노테이션
  • @Colimn(name= “name”) : 필드 이름과 컬럼 네임이 다를 경우 매핑 가능.
  • 객체에서 enum 타입을 쓰고 싶을 때, db에는 enum 타입이 없잖아. 그럴 때는 @Enumerated(EnumType.STRING)으로 쓰면 됨
  • @Temporal(TemporalType.TIMESTAMP) : date, time, or timestamp
  • DB에 varchar를 넘는 큰 컨텐츠 @Lob
  • @Transient : 특정 필드가 디비랑 관계 없이 쓰고 싶을 때. 디비에 반영 안됨.
  1. @Column
  • Name : 필드와 매핑할 테이블의 컬럼이름
  • Inertable = true/false, updatetable = true/false : 디폴트 값이 true이기 때문에, fasle로 져 놓으면 절대로 변경되지 않음
  • Nullable : false로 하면 not null 제약이 걸림.
  • Unique : 유니크 조건을 걸어줌. 근데 잘 안씀. 왜? 오히려 @Table(unique조건)으로 wwla
  • columnDefinition = “varchar(100) default ‘EMPTY’’ : 이런 식으로 쓰면 컬럼 정의를 직접 쓸 수 있음.
  • Precision, scale : bigint나 소수점 쓸 때 쓰면 됨.
  1. @Enumerated
  • EnumTyoe.ORDINAL: enum 순서를 디비에 저장 -> 0,1,2,3 integer 타입으로 추가됨.
  • EnumTyoe.STRING: 이넘 이름을 디비에 저장.
  • *ORDINAL쓰면 안됨 : 운영상에서 굉장히 위험함. 해결할 수 없는 버그가 됨.. 이넘 순서 바뀔 수도 있잖아..
  • 몇자 아끼려고 숫자 쓸 바에는 어차피 요새 디비 빵빵하니까 걍 값으로 하는게 적절함.
  1. @Temporal : 지금은 딱히 안필요함.. java8부터 LocalDate, LocalDateTime 쓰면 됨.
  • Private LocalDate createDate; 로 하면 db에 알아서 써짐. 과거 버전을 써야 하는 경우에만 사용!
  1. @Lob
  • 지정할 수 있는 속성 없음! 만약 매핑하는 필드타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑.
  1. @Transient : 디비에 쓰기 싫을 때!

Chpater9. 기본 키 매핑

*db에 기본 키 매핑을 어떻게 하는지 알고 있어야 함.

  1. 기본 키 매핑 방법
  • 직접 내가 PK를 할당해 준다 : @Id만 사용
  • 자동 생성 : mysql의 경우auto-increment(db가 값을 자동으로 할당하는 경우)
    -> @GeneratedValue까지 같이 사용
  1. @GeneratedValue
  • strategy = GenerationType.IDENTITY : 기본 키 생성을 데이터베이스에 위임. (MYSQL의 경우 AUTO-INCREMENT가 이 경우) “나는 모르겠고 디비야 니가 알아서 해조..”
  • strategy = GenerationType.SEQUENCE : 필드의 데이터 타입을 Long으로 하는 것을 추천.
    @SequneceGenerator()로 시쿠너스를 만들어서 사용해도 댐. -> ddl로 나감.
  1. Table 전략 : 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략. 모든 디비에 적용이 가능함. 단점) 성능
    -클래스어노테이션에@TableGenerator()=로만들고, @GeneratedValue(strategy=GenerationType.TABLE) 일케 써야됨
  • 운영에서는 아무래도 쓰기 좀 부담스러움.

권장하는 식별자 전략

  • DB의 PK 조건 ? Not null, unique, 변하면 안됨
  • 이 중 변하면 안되는 조건이 제일 어려움.. 정말 먼 미래까지, 몇 년이 지속되던 간에 이 식별자는 바뀌면 안됨.
  • 미래까지 이 조건을 만족하는 자연키(비즈니스적 필요 – 주민등록, 등등)는 찾기가 어려움. 대리키를 사용!!
  • 주민등록번호도 기본 키로 적절하지가 않음..
  • Long형 + 대체키 + 키 생성전략 사용!!
  • 결론적으로 auto-increment나 sequence or 회사 별 rule 사용. 비즈니스를 키로 끌고오는 것은 절대 좋지 않다!

어려운 내용)
1) @GeneratedValue(startegy = GenerateType.IDENTITY)의 특징

  • NULL로 날리면, DB에 들어가 봐야 PK를 알 수가 있음!
  • 근데 영속성 컨텍스트에는 PK가 있어야 된단 말이제…?
  • 영속상태라는건 1차캐시에 PK-ENTITY로 매핑이 되어 있어야 하는데, 이 상황에서 JPA는 PK를 모르기 때문에..
  • 따라서 울며 겨자먹기로, 이런 경우에만 EM.PERSIST()를 호출하는 시점에 쿼리가 날라간다(원래는 COMMIT할 때만 날라갔는데). 쿼리를 날려봐야 PK값을 알 수가 있기 때문에..
  • JDBC에서 이런 경우에 디비에 인서트 하는 경우 그 값을 리턴하는 기능이 있기 때문에 바로 PK값을 알아서 영속성 컨텍스트에 기록해 놓음.
  • 뭐,,,기능적으로 아주 그렇게 나쁘지는 않기 때문에,, 한 트랜잭션 안에서 인서트 쿼리가 여러 차례로 날라간다고 해서 그렇게 성능에 영향을 주지는 않음.,,

2)시퀀스 전략일 경우에는, 디비에서 시퀀스 값을 가져온 다음에 영속성 컨텍스트에다가 넣고, 그걸로 영속성 컨텍스트에다 넣어놓음. (call next value query), 물론 이런 경우에 인서트 쿼리는 날아가지 않고 커밋에서 날림.

  • 근데 어쨌든 간에 쿼리를 날려야 되니까 성능이 나빠지지 않냐?
  • 그래서 allocationsize를 지정해서 한번에 50개씩 땡기고 그런 식..그럼 디비에는 처음에 1-> 51로 되고, 메모리에서 1,2,3,……이렇게 쓰는 것.
  • 더미로 1로 셋팅되기 때문에 쿼리가 한번 더날라가는걸 볼 수 있음. 그 다음부터는 디비로 콜이 가지 않고 메모리에서 콜이 됨.
  • 그러면 이론적으로 @SequenceGenerator(allocationSize=50) 정도로 세팅하는게 적당.
  • 에티블 전략의 allocationSize로 이와 같은 전략임.
  • 서버 여러 개여도 문제가 없음. 웹 서버 열대가 동시에 호출하면 값이 그만큼 쭈우우욱 올라가겠지?

Chapter 10. 단방향 연관관계

*객체의 참조와 테이블의 외래키를 매핑하는 방법!

”객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.”

사실 orm이 어려운게 아니라, ‘객체지향스럽게 설계하는 것이 무엇인지’ 부터 아는 것이 더 중요함.

Chapter 11. 양방향 연관관계와 연관관계의 주인

*jpa에서 딱 어려운 두가지 : 영속성 컨텍스트 매커니즘 & 양방향 연관관계와 연관관계의 주인!

-> why? 객체와 테이블이 두개가 패러다임의 차이가 있지. 객체는 참조를 사용하고 테이블은 외래키로 조인을 하는데 이 둘 간의 차이가 뭔지란 차이에서 오는걸 이해를 해야함.

1) 양방향 매핑 : 객체에서는 양방향으로 서로 참조할 수 있게 하더라도, 정작 테이블에서는 큰 차이가 없음. 멤버의 입장에서는 TeamID로 조인하면 되고, Team에서도 자기 PK로 조인하면 됨.

=> 테이블에서는 양방향이란 개념이 없음. 그냥 FK 하나 넣으면 양방향으로 조인 되는거임. 문제는 객체. 객체에서는 멤버에서 한번에 팀으로 갈 수가 없었음. 그래서 팀에다가 리스트 멤버라는 새로운 세팅이 필요한 것
=>Team 객체에 private List memberList에다가 @OneToMany(mappedBy =”team”)을 조져줘야함.

  • 객체는 사실 단방향이 좋고 양방향은 어려움.

2) mappedBy
A. JPA에서 가장 빡치는부분. 객체와 테이블 간에 연관관계를 맺는 차이를 이해해야 함.

B. 객체는 연관관계가 되는 키 포인트가 2개가 있음. (회원->팀, 팀->회원) = 단방향 연관관계가 두개가 있는 거임. 억지로 양방향 연관관계라 하는 것.

C. 테이블에는 연관관계가 멤버<->팀 하나임. (외래키 값, 이걸로 팀이랑 조인하면 어느 소속인지 알 수 있지. 근데 팀 입장에서도 PK에 멤버 FK를 조인하면 내 팀에 누가 있는지 알 수 있음.) 즉, FK 하나로 모든 연관관계가 끝이 나는 거임. 방향이 없다고 보는게 맞음.

D. 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.

3) 여기서 딜레마가 옵니다.. 지금 객체는 멤버에 팀이 있고, 팀에 멤버스가 있는데 그럼 뭘 외래키로 관리해야하나?? 멤버가 다른 팀에 들어가고 싶어, 그러면 멤버의 팀을 수정해야되는거야 아님 팀의 멤버를 수정해야 하는거야??

  • 결국에는 디비가 업데이트 되면 되는건데.. 양방향이 되면서 팀에 있는 멤버스까지도 고려를 해야 하는 거지.
  • 그래서 룰이 생김. 둘 중 하나로 외래키를 관리해야 한다. = 연관관계의 주인

4) 연관관계의 주인(Owner)
A. 객체의 두 관계중 하나를 연관관계의 주인으로 지정
B. 연관관계의 주인만이 외래키를 관리
C. 주인이 아닌 쪽은 읽기만 가능
D. 주인은 mappedBy 속성 사용 X!! : 나는 이거에 매핑되어버렸어..라는 뜻. 주인이 아닌 경우에만 mappedBy 사용
E. 그래서 그러면 누구를 주인으로 해야해??
F. 그러면 이제 @JoinColumn으로 되어 있는 이 친구가 주인이 되는 거임. 얘를 mappedBy로 했으니까!
G. 누가 주인???? => 외래 키가 있는 곳을 주인으로 정해라!!!!!!!!!
H. 이제 팀의 멤버스는 가짜 매핑임. 주인의 반대편.
I. FK가 있는 곳이 어디야. Member란 말이야. 그래야 안헷갈려.
J. Team의 값을 바꿨어. 근데 쿼리가 Member로 나가. 그러면 이상하잖아..? 그럼 너무 헷갈려. 그리고 성능 이슈도 있어.

K. 기준! FK가 있는 곳을 주인으로 딱 정하세요. 그래야 헷갈릴게 없어요.

L. 즉, DB 입장에서 보면 외래키가 있는 곳이 무조건 N이 되게 할 수 있음. 즉, N쪽이 주인이 되는 거임. 요렇게 정해서 설계해야 좀 깔끔하게 나옵니다.

M. 연관관계의 주인이라고 하니까 비즈니스 적 중요한 애 같은데, 사실 그냥 디비에서 N이 되는 애가 주인이 되면 됨. 자동차와 자동차 바퀴로 따지면 비즈니스 적으로는 자동차가 훨씬 중요하겠지만 주인은 바퀴가 되는 거임. 그렇게 해야 성능 이슈도 없고 깔끔하게 들어감.

Chapter 12. 양방향 연관관계와 연관관계의 주인2 – 주의점

1) 연관관계의 주인이 아닌 가짜 주인으로 입력값을 넣음 – team.members.add(member) 하는 경우

2) 양방향 매핑할 때는 사실 답이 있습니다. 양 쪽에 값을 넣어주는게 맞아요. 사실, 그냥 member에 setTeam해가지고 persist 하면 디비에는 나중에 반영이 되기는 합니다만.. 객체의 입장에서 봤을대는..?

  • 이게 디비에 반영을 안하고, 영속성 컨텍스트에 들어가 있는 상태에서만 쓰면 메모리에서는 아무것도 안 들어가 있단 말이야. 순수한 객체 상태란 말이야. 그걸 가져오면 team에서는 당연히 아무것도 없겠지?

  • 그래서 어쨌든 멤버로 setTeam(team) 하고 team.getMembers().add(member)까지 해줘야 하는게 맞아!

** 결론 : 양방향 연관관계 세팅 할 때는 양 쪽에다가 다 세팅하는게 맞다.

  • 거기다가 연관관계 편의 메서드를 작성하자. Members의 setTeam 을 쓸 때 그 안에다가 이제 team.getMembers.add(this)이런 식으로.

  • 아니면 사실 changeTeam 막 이런 식으로 set 말고 다른 이름으로 쓰는게 좋다. 여기에 이제 여러 로직들이 들어갈 수 있겠지? 팀이 바뀌면 기존에 있는거에서 막 빼내고 이런것도. 사실 이런 복잡한 로직이 필요 없으면 그냥 연관관계 매핑에서 잘 쓰세요.

  • 이런 연관관계 편의 메서드는 어디 넣어도 상관 없는데, 상황에 맞게 넣으면 됨. 근데 둘 다 쓰는건 지양.

3) 양방향 연관관계 쓸 때 무한루프를 조심하자
A. toString 이런걸 서로 쓰게 되면 막 서로 계속 무한으로 toString 계속 쓰기도 함.

  • lombok이나 이런거에서도. 혹은 엔티티를 json으로 바꾸는 생성 라이브러리를 쓰거나 하게 되면..

=> 컨트롤러에서 response로 엔티티를 직접 보내버리면 그렇게 될 수도 있음. 어 멤버에는 팀이 있네? 팀에는 멤버스가 있네? 멤버스에는 팀이 있네? 팀에는 멤버스가 있네? X 무한루프.

B. 답이 정해져있죠. toString, lombok에서 이걸 빼고 쓰라. Json 생성 라이브러리 같은 경우네는 컨트롤러에서는 엔티티를 절대 반환하지 마세요.

i. 무한루프 생길 수 있음
ii. 엔티티는 변경이 가능 한데 엔티티를 api 반환을 해버리면 엔티티를 변경하는 순간 api 스펙이 바뀜. 가져다 쓰는 경우 매우 황당하겠지?

DTO로 변경해서 반환해야 합니다. 이렇게만 해도 대다수의 문제들은 해결이 됩니다.

4) 양방향 매핑 정리
A. 단방향 매핑만으로도 이미 연관관계 매핑은 완료 : 처음에는 이제 단방향으로 끝내야 합니다!!! 일단은 JPA를 쓰고 설계를 하잖아? 그러면 단방향으로 설계를 완료해야됨.

실무에서는 정말 완전히 객체만으로 설계가 가능? 불가능! 테이블 설계를 계속 조지면서 객체도 생각을 해야 함. 그런 과정에서 1:N, FK 이런게 이제 다 됨. 처음에는 단방향으로 무조건 끝내야함.

B. 그러면 이제 양방향은 뭐냐? 반대방향에서 조회기능이 추가되는 것. JPA에서 설계라는
것은 단방향으로 이미 완료가 된겁니다. 그러면 이제 나중에 개발을 하다가 테이블 한번 만들면 굳어지니깐..

C. 그럼 양방향은 언제함? 객체 입장에서 양방향으로 해봤자 고민거리만 많아지지.. 테이블이랑 객체를 해서 매핑한다는 그 관점에서 보면 단방향으로만 해도 끝난거임. 그래서 언제들어가냐?????? => 막상 실무에서는 역방향으로 참조를 하는 경우가 왕왕 있을 수 있음.

D. 근데 이제 여기서 중요한거. 단방향만 잘 해주면, 양방향은 필요할 때 추가만 해주면 됨!!!!!!!!!!! 테이블에 영향을 안주니까!! 자바 코드 몇 줄 넣는 건 안어렵자너. 4

E. 결론 : 단방향 매핑으로 다 끝내버리자. 설계를 쫙 끝낸다. => 실제 개발할 때 고민.

F. 연관관계의 주인을 정하는 기준? 비즈니스 로직에 따르면 안되고, 외래키의 위치를 기준으로 정해야 함!!

Chapter 13. 다양한 연관관계 매핑

애매할 때는 반대쪽을 생각해 보자. 다대일, 일대다, 일대일, 다대다는 대칭성이 있기 때문에.

사실 다대다 @ManyToMany는 실무에서 쓰면 안된다.

  1. 단방향 양방향
  • 테이블 : 외래 키 하나로 양쪽 조인 가능, 사실상 방향이라는 개념이 없음.

  • 객체 : 참조용 필드가 있는 쪽으로만 참조 가능, 한쪽만 참조하면 단방향, 양쪽이 서로 참조하면 서로 단방향 두개.

  1. 다대일 N:1
  • 관계형 DB에서는 다(N)쪽에 FK가 들어가는게 맞다. 외래키 있는 곳을 기준으로 참조를 걸어서 매핑을 하면 된당
  • 가장 많이 사용하는 관계
  • N이 관계의 주인, @ManyToOne, @Joincolumn() 써주면 댐
  • 반대쪽이 필요할 경우 @OneToMany(mappedBy = “” ) 해주면 댐
  1. 일대다 단방향

*그다지 추천하는 모델은 아니지만, 일단은

객체 입장에서는 이러한게 필요할 수도. 팀은 멤버가 궁금한데, 멤버는 팀이 궁금하지 않을 경우.

  • 하지만 db에서는 Team에 FK가 들어갈 수는 없다 당연히. 당연히 N에 FK가 들어가야 하는데.
  • @OneToMany , @JoinColumn(name=”TEAM_ID”) 이런 식으로. 어색하지만 동작은 함.

  • Update 쿼리가 추가로 나감. 팀 테이블 인서트 이외에도 멤버 테이블의 업데이트 쿼리가 한번 더 나가야 함 -> 성능상 약간의 단점. 큰 이슈는 아니긴 함.

  • 심각한 것은, JPA를 잘 쓰는 사람이더라도 이게 헷갈릴 가능성이 매우 큼. 나는 Team만 손을 댔는데 왜 멤버 업데이트 쿼리가 나가지…….? 이런 경우 발생 가능 매우 큼. 실무에서는 테이블 수십개가 엮여서 돌아가는데.. 이렇게 되면 운영이 매우 힘듬..

  • 그래서 차라리 다대일 단방향 관계 + 필요하면 양방향 추가 => 이 전략으로 가는게 나음. 약간 객체지향적 손해를 보더라도 (멤버에서 팀을 갈 일이 없는데도 어거지로 만듬) 그냥 다대일 양방향으로 바꿔서 만드는게 나을 수도 있음.. orm 보다는 db의 설계에 맞춰서. 유지보수하기 쉽게.

  • 이게 테이블에는 항상 N쪽에 외래 키가 있기 때문이야!!

  • @JoinColumn을 꼭 사용하세요. 안그러면 Join Table방식을 사용함… => 중간 테이블이 만들어짐. 그러면 장점도 있지만, 단점은 테이블 한 개가 더 들어가니까 성능상 애매하고 운영하기가 쉽지 않음.

  • 단점 : 엔티티가 관리하는 외래 키가 다른 테이블에 있고, 그래서 업데이트 쿼리가 추가로 실행됨

  • 결론 : 쓰지 마세요. 다대일 양방향 매핑을 사용하자. 그냥 안쓰는것도 방법임 ㅋㅋ

  • 하다 보니까 역방향(양방향)도 하고싶어…? : 표준 스펙은 아니지만 야매로 가능하기는 함.

멤버에다가 @ManyToOne , @JoinColumn(name=”TEAM_ID”,insertable=false,updatable = false) 이런 식으로 매핑을 걸어버림. 연관관계의 주인처럼 해놓고 업데이트 인서트를 제약을 걸어버리는 거임. 읽기 전용으로 걸어버리는 거. 이러면 사실상 양방향 매핑이랑 똑같이 되는거임. 멤버 테이블의 FK 관리는 팀이 여전히 하면서 멤버는 읽기 전용만.

  • 가끔가다 읽기 전략으로 사용하는 경우가 필요할 수도 있기 때무네…
  • 쓰지 마세요.
  1. 일대일 관계
    주 테이블이나 대상 테이블 중에 외래 키 선택 가능. 어디다 둬도 됨.
  • 주 테이블에 외래 키 두기
  • 대상 테이블에 외래 키 두기.
    외래 키에 데이터베이스 유니크 제약조건 추가. 꼭 안그래도 되기는 하는데 관리를 빡세개 해야겠지?
  • @OneToOne, @JoinColumn(name=”LOCKER_ID”) 이런 식으로.
  • 락커에서는 @OneToOne(mappedBy=”locker”) 이런 식으로.
  • 다대일 단방향 매핑, 양방향과 유사.
  • 요게 이제 제일 가장 심플한 일대일
  • 다대일 처럼 외래 키가 있는 곳이 연관관계의 주인. 반대편에는 mappedBy 적용
    일대일 : 대상 테이블에 외래키 단 방향? 지원도 안되고 방법도 없습니다. 내 엔티티의 외래키는 내 테이블에 있어야함. 즉, 일대일 대상 테이블 외래키 양방향 = 그냥 위에 처럼 한 것.
  • 그래서 FK는 어디다 둬야 하지…? 그냥 멤버에다 락커 두고 멤버 테이블에서 락커 아이디를 가지고 있는 형식을 선호하신답니다.. 상황에 맞게..
  • 주 테이블에 외래 키 :
    -> 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
    -> 객체지향 개발자 선호
    -> JPA 매핑 편리
    -> 장점 : 주 테이블만 조회해도 대상 테이블에 데이턱가 있는지 없는지 확인 가능
    -> 단점 : 값이 없으면 외래 키에 NULL 허용 (DB 입장에서는 치명적..!)

  • 대상 테이블에 외래 키
    -> 대상 테이블에 외래 키가 존재
    -> 전통적인 데이터베이스 개발자 선호
    -> 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지 가능
    -> 단점 : 주로 멤버에서 랄커를 엑세스를 하는 하기 때문에 이제 양방향으로 만들어야됨
    -> 치명적 단점 : 프록시 기능의 한계로, 지연 로딩으로 설정해도 항상 즉시 로딩이 되어버림… JPA의 한계
    -> 멤버를 로딩 할 때, 만약 멤버에 락커를 조회 하는 경우, 멤버 테이블만 조회해서 안됨. 어차피 락커를 뒤져야 함. 락커를 뒤져서 멤버 이게 있는지 없는지를 봐야 알 수가 있음.. 즉 어차피 쿼리가 나감… 그럼 이걸 프록시로 할 이유가 없음.. 그래서 하이버네이트에서는 지연로딩으로 세팅을 해도, 1대1관계에서 대상 테이블에 FK가 있는 경우 무조건 즉시 로딩이 됨.

  1. 다대다
  • 쓰지 마세요. 근데 왜 쓰면 안되는지에 대한.
  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수가 없음..
    연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함.
    중간 테이블을 만들어가지고, 1:N, N:1 로 풀어야 함.

    	-객체는 삽가능. 컬렉션을 사용해서 객체 2개로 다대다 관계 가능. 멤버도 프로덕트 리스트를 가지고 프로적트는 멤버 리스트를 가지고.
    
    	@ManyToMany 사용, @JoinTable 사용. 
    
    	편리해 보이지만 실무에서 사용하면 안됨!!
    
    	연결 테이블이 단순히 연결만 하고 끝나지 않음. 매핑 정도만 들어가고, 추가 정보를 쓸 수가 없음………
    
    	쿼리도 이상하게 나감. 중간 테이블 들어가고 막 조인되고 나와서 내가 생각도 못한 쿼리가 나감. 

그럼 어떻게?? => 중간 테이블을 엔티티로 승격

+) 보통은 이런 연결 테이블을 멤버아이디(PK,FK), 프로덕트아이디(PK,FK) 이런식으로 해서 저 두개를 PK로 묶어서 사용하는데, 이거 보다는 그냥 ID를 따로 두는게 낫기는 함. ID는 의미 없는 값으로. 그냥 모든 테이블에 GENERATED VALUE로 하는게..낫지 않나..

Chapter14. 상속관계 매핑

1.상속관계 매핑

1) 테이블

  • 관계형 데이터베이스는 상속관계가 없음.

  • db에서는 슈퍼타입과 서브타입 관계라는 모델링 기법이 객체 상속과 유사함

  • 상속관계 매핑 : 객체의 상속과 구조와 db의 슈퍼타임 서브타입 관계를 매핑.

  • 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법?
  • 조인 전략 : 아이템 테이블 – 앨범/무비/북 , 인서트는 아이템이랑 구현체에 한번씩 해서 두번 씩. 아이템 칸에 dtype이라고 구분하는 칸을 둠. 각각의 구현체는 아이템_아이디 를 pk,FK로 둠.

  • 단일 테이블 전략 : 논리 모델을 한 테이블로 다 합쳐버리는거임. 아이템-앨범,무비,북 이렇게 있으면 컬럼을 아이템에다가 다 때려박아서 하나로 관리. 물론 DTYPE도 있긴 하지.

  • 구현 클래스마다 테이블 전략 : 각각의 앨범, 무비, 북이 아이템아이디를 PK로 가지고 있으면서 원래 아이템 테이블이 가지고 잇던 네임, 프라이스를 다 가지고 있는 그런 것.

=> 객체는 그냥 상속관계 지원하기 때문에 그대로 모델링을 할거고, DB 설계를 저렇게 했는데 지금 이거를 객체는 상속하고 있을 수도 있고 뭐 그런 여러 가능성이 존재하기는 함.

=> JPA는 원래 싱글 테이블 전략으로 조짐. 엔티티 달고 상속 시킨 클래스 만들면 테이블 하나로 다 때려박음

2) 조인 전략

만약 이제 아이템 – 앨범(아이템아이디 PK,FK),무비(아이템아이디 PK,FK),북(아이템아이디 PK,FK) 이런 식의 정교한 모델링을 했다?

  • @Entity, @Inheritance(strategy = InheritanceType.JOINED)를 아이템에다가 써주면 조인 전략으로 테이블을 만들어줌

  • 지금 보면 DTYPE은 생략되어 있는데, @DiscriminateColumn을 써주면 이제 생김. 엔티티 명이 들어가게 됨 디폴트로. 이게 있는게 좋아요. 물론 name=”” 해서 dtype이라는 이름 말고 다른 걸로 바꿔도 됩니다. 근데 컨벤션을 따릅시당. dba가 말해주는 대로..

  • 자식 엔티티에서는 @DiscriminatorValue(“”) 하면 이제 dtype에 들어가는 이름을 바꿈.

3) 한 테이블로 가자! 단일 테이블 전략

  • @Inheritance(strategy = InheritanceTyoe.SINGLE_TABLE)

  • 상관 없는 놈들은 이제 다 널로 들어감.

  • 쿼리도 한방에 들어감.

  • 성능상의 이점

  • 조인도 안필요함. 한방에 셀렉트.

  • Discriminator 전략 표시 안해도 dtype 무조건 들어감. 내가 앨범인지 아티스트인지 알 방법이 없기 때문에. 조인 전략은 이제 테이블이라도 떨어져 있으니까.. jpa 스펙에서는 원래 필순데 하이버네이트가 안해놓은듯…

  • Jpa의 장점! 코드를 바꿀 필요가 없음. 걍 전략만 바꿔 버리면 됨. 쿼리 안고쳐도 됨. 개꿀쓰.

4) 구현 클래스마다 테이블 전략

  • 조인 전략이랑 비슷한데 그냥 상위 테이블을 없에버리고 그 속성을 서브 테이블들이 다 공유. InheritanceType.TABLE_PER_CLASS 를 써주면 됨.
  • Item은 abstract class로 써주고.
  • 단순하게 값을 넣고 뺄 때는 좋은데 잘못하면 망함.
  • 조회를 하는데 가령
  • Item item = em.find(Item.class,movie.getid) 이렇게 하면 당연히 부모 클래스로 확인할 수 있어야지 => 엄청 복잡한 쿼리가 나감. union으로. 아이템 아이디만 알아. 그러면 이제 모든 테이블을 다 뒤져봐야 되는거잖아?
    쓰지 마세요

5) 장단점
A. 조인 전략 : 객체랑도 잘 맞고, 깔끔해서 이제 기준이라고 보면 됨

  • 테이블이 정규화 되어 있음, 외래 키 참조 무결성 제약조건 활용 가능 = ITEM 딱 보면 됨, 저장 공간 효율화

  • 조회 시 조인을 많이 사용->성능 저하(큰 문제는 아님), 조회 쿼리가 복잡. 데이터 저장시 인서트 쿼리 2번 나감

  • B. 단일 테이블 전략 :

  • 조인이 필요가 없기 때문에 일반적으로 조회 성능이 빠름, 조회 쿼리가 단순

  • 자식 엔티티가 매핑한 컬럼은 모두 NULL 허용(치명적), 단일 테이블에 모든 것을 저장하기 때문에 테이블이 커질 수도 있고 조회 성능이 오힐 느려질 수도 있음. (근데 이정도 임계점을 넘는 경우는 딱히)

C. 구현 클래스마트 테이블 전략 :

  • 쓰지마세요. 이거는 DB쪽, 객체지향 쪽 둘 다 싫어합니다. 뭔가 묶이는게 없잖아요. 유니온 쿼리 날려야되고 자식 테이블을 통합해서 쿼리 하기가 어려움. 변경할 때도 안좋음. 굉장히 많은 걸 뜯어야 함.
  • 결론 : 조인 전략을 기본으로 깔꼬 / 단일 테이블 전략을 고려. 만약 설계가 너무 단순하고 데이터도 얼마 안되고 딱히 확장할 일도 없을 것 같다..? => 단일 테이블로. 중요하고 복잡하다
    => 조인 전략. 다 트레이드 오프가 있기 때문에 둘 중 고민. 구현 클래스 마다 테이블 전략은 아예 고려 X
  1. @MappedSuperclass

갑자기 어느날 DBA가 와서 모든 테이블에 수정 날짜 이런거 다 넣어야 된다고 하면…? 너무 귀찮고… 속성만 상속받아서 쓰고 싶은데…. = @MappedSuperclass

BaseEntity라는걸 만들고 이제 거기다가 다 써놓은 다음에 extends BaseEntity 하면 됨. BaseEntity에는 @MappedSupercalss 써주면 됨. 어려운게 아니고 “이 속성을 같이 쓰고 싶어” 할 때 쓰면 됨.

createdBy 이런거는 이제 다 자동화 시킬 수 있음! 어드민으로 로그인되어 있는 세션 정보를 읽어와서 -> 이벤트 활용! createdBy, createdDate, modifiedBy, modifiedDate 등등

  • 상속관계 매핑X
  • 엔티티 X, 테이블로 안만들어짐
  • 부모 클래스를 상속받는 자식 클래스에 정보만 넘겨줌
  • 부모타입으로 조회 불가능
  • 추상 클래스로 쓰는 것을 추천.
  • 실무에서 자주 사용하기 좋음 abstract로 baseentity같은거 하나 만들어서

Chapter 15. 프록시

  1. 프록시
    가령, 멤버와 팀을 동시에 출력하는 로직이 있다고 생각해보자.
    String username = member.getUsername();
    System.out.println(username);
    Team userTeam = member.getTeam();
    System.out.println(userTeam.getName());

이런 경우가 있다고 생각을 해보자. 이런 경우는 쿼리가 한번에 날라가서 다 가져오는게 좋겠지?

근데 그냥 유저 네임만 가져오는 경우, 가령

Public static void printMember(Member member) {
System.out.printlmn(“member = “ + member.getUserName());
}

이런 경우도 둘 다 있다. 이런 경우
Member member = em.find(Member.class, 1L);
여기서 연관되어 있는 애들까지 다 (team)까지 sql을 날려서 가져오냐 혹은 멤버만 가져오냐로 최적화를 할 수가 있음.

  1. 프록시 기초
  • em.find() : 실제 쿼리를 날려서 가져오는 경우
  • em.getReference : db를 뒤지지 않고 가짜 객체만 가져옴.
    -> getRefrence를 호출하는 시점에는 쿼리가 나가지 않음

즉, 가짜를 가져온 거임. Id는 이미 있으니까. 근데 getUsername은 없잖아?
이 때 쿼리를 날려서 가져옴.

-> 그럼 대체 findMember는 뭘까? 하이버네이트가 가짜로 만든 가짜 클래스(=프록시)

  • 프록시 : 껍데기는 똑같은데 안에가 텅텅 비어있음. target이란게 있는데 초기에는 null
    	-> 프록시의 특징 : 실제 엔티티를 상속받아서 만들어짐 = 겉 모양이 똑같음 (하이버네이트가 내부적으로 프록시 라이브러리를 사용해서 만들어냄)
    
    	 -> 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨! (킹론상)
  1. 프록시 특징
  • 프록시 객체는 실제 객체의 참조(target)을 보관
//프록시 객체 생성
Member member = em.getReference(Member.class, “id1”);

System.out.println(member.getName()) // 어 멤버는 프록시 객체인데 target이 비어있구나!

/*
영속성 컨텍스트에 target(진짜 객체의 참조)를 가져와 달라고 요청
쿼리문 발생 
select * from Member m where m.id = "id1" 이런게 이제 나감.
*/

Member.getName(); -> 어 target에 값이 없네? -> 영속성 컨텍스트에다가 가져다 달라고 말해줌 -> Member target 이라는 변수에다가 만들어줌. -> target의 진짜 getName, 즉, target.getName()을 호출해서 값을 가져옴.

  • 즉, 초기화 요청을 영속성 컨텍스트에다가 알려달라 해서 알려줌.
  • 사실 프록시 객체에 대한 매커니즘은 jpa 스펙에 없고 라이브러리가 구현하기 나름이긴 함.

프록시의 특징
1) 프록시 객체는 처음 사용할 때 한번만 초기화

2) 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는게 아님. 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것 뿐임. 교체되는게 아니라, 프록시 객체의 target이라는 애만 채워지는 것 뿐

3) 프록시 객체는 원본 엔티티를 상속받음. 따라서, 타입 체크시 주의해야함 (== 비교 쓰면 안되고, instanceof를 사용해서 비교해야함). 즉, 이게 멤버타입인가? 하고서 “==”비교를 하면 안된다는 것.

Ex) System.out.println(“m1 == m2” + (m1.getClass() == m2.getClass()); => True
근데, m2를 get refrence를 가져온다면 ? => False가 나옴. 이게 프록시를 파라미터로 받아서 비교를 할지 아닐지를 알 수가 없음. 즉, 따라서
(m1 instanceof Member) (m2 instanceof Member) 이렇게 해서 타입으로 비교를 해야 함.

5) 영속성 컨텍스트에 이미 있으면 초기화 할 때 실제 객체로 채워줌.

  • 이미 멤버가 영속성 컨텍스트에 있는데 굳이 새로 가져와?
  • jpa는 항상 한 트랜잭션 안에서 같은 애들에 대해서 같은 걸 보장해줌.

6) JPA는 동일성을 보장해줘야하기 때문에, 프록시를 한번 채우면 EM.FIND()를 해도 프록시로 가져옴. => 중요한 거는 프록시든 아니든 실제처럼 쓸 수 있다는 거임. 큰 상관은 없긴 함.

  • 프록시라는게 부모엔티티를 상속받아서 만드는 거기 때문에 마치 실제처럼 다룰 수 있는 것. 중요한 것은 다만 == 비교가 아니라 INSTANCEOF를 쓰는 거.

7) 영속성 컨텍스트의 도움을 받을 수 없는 준 영속상태일 때 프록시를 초기화하면 문제가 생김

  • 만약에 중간에 em.close() 되거나, em.detach()가 되는 경우 jpa 입장에서는 영속성 컨텍스트의 도움을 받아야 하는데 이제 도움을 안받을거야! 하면 Could not initialize peoxy – no Session (영속성 컨텍스트가 없다)
  • 실무에서 진짜 ㄹㅇ 많이 만나는 상태.
  • 보통 트랜잭션이 끝나고 나서 영속성 컨텍스트도 꺼지게 만드는데 그 이후에 이제 조회를 하면 이렇게 됨.
  • 분명 만나게 될겁니다.
  1. 프록시 확인
  • emf.getPersistenceUnitUtil().isLoaded(refMember) : 초기화 여부 확인
  1. 즉시로딩과 지연로딩
  • 멤버 객체 부를 떄 팀까지 가져와 말아..?
  1. 지연로딩
    => @ManyToOne(fetch = FetchType.LAZY) -> 프록시 객체로 조회함
    = 멤버 클래스만 db에서 조회한다는 의미

Member m = em.find(Member.class, member1.getId()
System.out.println(“m = “ , m.getTeam().getClass());

  • 이렇게 하면 team을 안 가져오고, team에 대한 메서드가 호출되는 시점에 이제 프록시 객체가 초기화되면서 값을 가져옴.
  • 즉, 지연로딩 lazy를 한다면 객체를 프록시로 가져온다는 것.

1. 로딩 -> 멤버1 -> 팀1(프록시 팀1 엔티티) : 지연로딩
== 실제 team을 사용하는 시점에 초기화.

  1. 비즈니스 로직 상 팀이 항상 같이 쓰인다.
  • 즉시로딩, @ManyToOne(fetch = FetchType.EAGER)
  • 아예 멤버랑 팀을 조인을 해서 한방에 진짜로 팀까지 다 가져옴. 그래서 프록시 객체를 쓰지 않고 진짜 객체를 가져옴.
  • 초기화를 안함.
  • 대부분의 JPA구현체는 한번에 다 가져ㅑ오려고 한다. 조인 쿼리로.

3. 지금부터 중요한 내용입니다.

1) 가급적 지연로딩만 사용(실무에서). 실무에서 즉시로딩은 쓰지 마세요!

2) 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생하기 때문에.

  • 조인이 너무 많고 성능이 안나오는데요….?
  • 아차 이거구나.. 시바..
  • 한 5개만 더 서로 걸려있으면, 조인 쿼리 5개가 무조건 서로 다 날라간다는 거야. 바로 망합니다.
  • 다 LAZY로 걸어야 한다.

3) 즉시 로딩은 JPQL에서 N+1문제를 일으킴.

  • “select m from Member m” => sql로 번역, select * from Member
  • 다 가져오려고 보니까 반환할 때 team에 값이 들어있어야 함. 즉시로딩이니까.
  • select * from Team where 이런게 다 들어가야 함.
  • 쿼리를 하나 날렸는데 추가 쿼리 (n)개가 나가더라 = n+1 문제.

=> 일단 모든 연관관계를 다 lazy로 깔고
- fetch 조인 (jpql)
- entitygraph를 쓰는 방법.
- batchsize라고 해서 그렇게 푸는 방법이 더 있죠.

  • @ManyToOne, @OneToOne 은 fetch가 디폴트가 eager임. 그래서 직접 다 lazy를 해야함
  • @OneToMany, @ManyToMany(안씀)은 디폴트가 lazy.
  1. 지연로딩 활용
    이론적인건 이론적인거고 실무에서는 다 지연로딩으로 발라야 합니다.

영속성 전이

  • 위의 뭐 지연로딩 이런거랑 아무상관이 없음.
  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을떄

ex) 부모 엔티티를 만들 때 자식 엔티티도 같이 만들고 싶을 때

-컬렉션 안에 있는 애들도 다 persist 날려줄거야 하는 그거임.

@OneToMany(mappedBy = “parent”, casacade = CasacadeType.ALL) 
Private List<Child> childList = new ArrayList<>();
  • 언제써야함? : 소유자가 하나일 때는 이걸 써도 좋아. 하지만, 다른 entity가 child와 연관관계가 있다? 그러면 쓰면 안대. 그러면 운영이 너무 힘들어짐.

Parent와 child의 라이프 사이클이 거의 유사할 때, 단일 소유자일 때,
고아 객체

@OneToMany(mappedBy = “parent”, casacade = CasacadeType.ALL, orpahanRemoval = True)
Private List<Child> childList = new ArrayList<>();
  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳하나일 때 사용해야
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany

Chapter 16. 값 타입

  1. JPA의 타입 분류
    JPA에서는 최상위의 개념으로 보면, 1)엔티티타입 2) 값 타입으로 구분이됨

1) 엔티티타입

  • @Entity로 정의하는 클래스 객체.
  • 내부 이터가 변해도 식별자로 지속해서 추적이 가능
    -> 회원 엔티티의 키나 나이값 속성이 다 변경되어도, 식별자로 인식이 가능. 추적가능
    2) 값타입
  • int, integer, string 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
  • 식별자가 없고, 값만 있으므로 변경시 추척 불가
    -> 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체. 이게 뭐 과거에 100이었다 이런걸 알 수가 없음. 변경 시 추적 불가능
    -> ex) 게시판의 string contents : boarder라는 entity의 값이 바뀌는 것 뿐.
    a. 기본 값 타입
  • 자바 기본타입 ( int, double)
  • 래퍼 클래스 (Integer, Long)
  • String
    b. 임베디드 타입(복합 값 타입)
  • 가령, 좌표 이런거. X,Y를 묶어서 쓰고 싶어. 그럴 때 커스텀 할 수 있는 Position 이런걸 만들 고 싶어
    c. 컬렉션 타입 : 기본 값 타입이나 임베디드 타입을 넣을 수 있는.
    3) 기본 값 타입
  • ex)String name, int age
  • 생명 주기를 엔티티에 의존 : 회원을 삭제하면 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면 안됨 : 회원 이름 변경하는데 다른 회원의 이름도 변경되면 안됨

자바의 기본타입은 절대 공유X : Python이랑 다르게 참조하지 않음. 복사가 되는 별개의 저장 공간을 가지고 있음.

단, Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유가됨. 아마 파이썬이 이 경우일걸?

Integer a = new Integer(10);
Integer b = a;

이러면 10이 복제가 되는게 아니라, a의 레퍼런스가 b로 넘어가는 거라서 같게 나옴. 클래스는 공유 가능한 객체

그래서 변경 자체를 불가능하게 만들어서 SIDE EFFECT가 발생하지 않게 함.

4) 임베디드 타입 : 복합 값 타입. 내장 타입.

새로운 값 타입을 직접 정의할 수 있음.
주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함.
Int, String처럼 값 타입임

Ex) 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.

  • 작일 종료일은 뭔가 클래스 타입을 만들 수 있지 않을까…?

@Embeddable : 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용 하는 곳에 표시

  • 기본 생성자 만들어야됨
  • 재사용 가능 / 높은 응집도 / Period.isWork()처럼 해당 값 타입만 사용하는 의미있는

메소드를 만들 수 있음

  • 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명 주기를 의존함.
  • 임베디드 타입도 값 타입입니다. 엔티티 죽으면 다 죽는거고 생성될 때 다 들어오고
    그런거죠. 너무 당연하죠.
  • 임베디드 타입과 테이블 매핑 : 그냥 그 안에 들어있는 애들이 테이블로 매핑됨.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다
  • 객체와 테이블을 세밀하게 매핑하는 것이 가능함. 메서드를 만들어서 활용할 수 있는게 좋음.
  • 잘 설계한 orm 어플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
  • 어마어마하게 많이 쓰는 것은 아니고, 그냥 아 요런데다 쓰면 좋겠구나 정도. 코드도 공통화되고,, 좋음,,

임베디드 타입과 연관관계

임베디드 타입이 엔티티를 가질 수 있음.

@AttributeOverride : 속성 재정의

  • 한 엔티티에서 같은 값 타입을 사용하려면? -> 컬럼 명이 중복되어버림.
  • 잘 쓰진 않는데…
  • 어차피 임베디드 타입의 필드들이 테이블로 가니까,, 같은거 쓰면 이제 에러나지.
    임베디드 타입과 null
  • 임베디드 타입 값이 null이면 매핑한 컬럼 값은 모두 null

5) 값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

우리가 값을 복사하고 이러는건 별로 신경을 안씀. 엔티티에 대해서는 신경을 많이 쓰는데, 값을 변경하는 것은 고민을 안함. 그 이유가 자바 세상에서 단순하고 안전하게 설계가 되어 있기 때문임.

값 타입과 불변 객체 : 결론 - 값 타입은 불변으로 만들어라. 안그래서 생긴 버그는 잡을 길이 없다.

1) 객체를 공유하는 경우

“값 타입은 복잡한 객체 세상을 조금이라도 단순화 하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.”

값 타입 공유 참조 : 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함. 부작용 발생함.

아니, 나는 공유하고 싶었는데? : 엔티티를 써야지. 이런 사이드 이펙트를 쓰면 안대.

=> 복사를 해서 만들어서 써야됨. 가령

Address address = new Address(“city”, “street”,1000)
Address copyAddress = new Address(address.getCity(), address.getStreet(),) 

이런 식으로 복사를 해서 써야됨..

근데 만약 복사를 안하고 그냥 넣을 수도 있음. 컴파일 레벨에서 막을 수가 있나??? => 없음!!

**문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다!!

**문제는, 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.

객체의 공유 참조는 피할 수가 없다. =으로 넣으면 다 들어간단 말이야! 난리가 날 가능성이 크다.

2) 객체 타입의 한계

Int a = 10; int b = a; b= 4 : 기본 타입은 값을 복사
Address a = new Address(Old);
Address b = a;
b.setCity(New) //  <- a도 바뀌어버림.

3) 불변 객체
객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
불변객체란? 생성 시점 이후 절대 값을 변경할 수 없는 객체.
-> 생성자를 통해 값을 설정하고, setter를 만들지 않으면 됨.
-> Integer, Stringd은 자바가 제공하는 대표적인 불변 객체
-> 아니면 setter를 private으로 만들자.

불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.

그러면, 실제로 값을 바꾸고 싶을 때는 어떻게 하나요?
새로 만들면 됩니다. 완전히 통으로 갈아내야됨.
Address nwqAddress = new Address(“newCity”, address.getStreet(),…)

4) 값 타입의 비교
값 타입 : 인스턴스가 달라도, 그 안에 값이 같으면 같은 것으로 봐야 함.
가령,
Address a = new Address(“Seoul”)
Address b = new Address(“Seoul”)
a==b? <——— False. 참조가 다르잖아 당연히. 레퍼런스를 비교하니까.

값 타입의 비교
동일성 비교 : 인스턴스의 참조 값을 비교, == 사용
동일성 비교 : 인스턴스의 값을 비교, equals()를 만들어야 함.
a.equals(b) <- False. equals의 기본이 == 비교이기 때문에. Override 해야함. 자동으로 만들어주는 걸로 하는게 좋음 + hash도 만들어줘야지?

*현업에서 사실 equals()를 쓰나요..? 하면 꼭 그렇지는 않지만…. 걍 필드 값 꺼내와서 비교 할 수도 있기는 한데 이렇게 해놓는게 좋긴 좋다.

5) 값 타입 컬렉션
값 타입을 컬렉션에 담아서 쓰는걸 의미하죠.
엔티티를 컬렉션에 넣어서 쓰는 (@OneToMany) 것 말고, 값 타입을 이제 넣는 거지.

이런식으로 된다 치면 이제 일대 다의 개념으로 디비에서 관리가 되는 거라고. 값 타입에다가 식별자를 generatedvalue 같은 걸 쓰면 이제 엔티티가 되니까. 멤버를 pk, fk로 쓰는거지.

데이터베이스는 컬렉션을 같은 테이블에 저장할 수가 없으니까. 일대 다로 구현이 되는 거지. 컬렉션을 저장하기 위한 별도의 테이블이 필요함.

@ElementCollection
@CollectionTalbe(name =Favorite_Food, joinColumn = @JoinColumn(name=“member_id”))
Private Set<String> favoriteFoods = new HashSet<>();

@ElementCollection
@CollectionTalbe(name =Favorite_Food, joinColumn = @JoinColumn(name=“member_id”))
Private List<Address> addreses = new ArrayList<>();

보면 지금 컬렉션들도 다 따로 persist 안하고 같이 날라갔죠? 이게 다 멤버에 소속되어 있는거죠. 값 타입 컬렉션들도 다 값 타입이기 때문에 멤버에 의존하는 겁니다. 즉, casacade + 고아객체 제거 기능을 필수로 가지는 것.

컬렉션들은 지연 로딩임.

어떻게 바꾸지 ? 값 타입은 통째로 갈아끼워야 해!!!!

Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address())

//통째로 갈아끼기.
findMemeber.getFavoriteFoods().remove(“치킨”)
findMember.getFavoriteFoods().add(“피자”)

findMember.getAddressHistory().remove(new Address(“old1”,))
=> 보통 컬렉션들의 remove는 equals and hascode 로 조지기 때문에, 이걸 꼭 잘 만들어줘야함!! 여기서 만약 이게 안만들어지면 저게 안지워진다는 것.
findMember.getAddressHistory().add(new Address(~))
이렇게.

=> 근데 이렇게 하면, 기존에 old1-old2 에서 new1-old2 이렇게 되기는 됨. 근데 이게 갈아끼는게 아니라 쿼리를 보면, 기존의 old1 old2 를 싹 날려버리고 new1,old2를 삽입하는 쿼리가 나감… 시발…?

*값 타입 컬렉션의 제약사항
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다 .
즉, 그 테이블에서 그 멤버랑 관련된 애들을 싹 날리고, 최종적으로 객체에 남겨진 애들을 다시 싹 인서트.

=> 쓰면 안된다…. @OrderColumn(name=“address_history_order”) 막 이런 식으로 해가지고 업데이트 쿼리를 날릴 수도 있기는 한데. 값 타입 컬렉션은 식별자가 있는게 아니라서 추적이 안되서. 근데 저것도 이제 위험함.
=> 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함. 쓰고 싶으면, 이제 저 친구들을 다 묶어서 pk로 만들어줘야함….

*결론?
실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
일대다 관계를 위한 엔티티를 만들고 여기서 값 타입을 사용.
영속성 전이 카사케이드 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용.

@Entity
Public class AddressEntity {
	@Id @GeneratedValue
	private Long id;
	
	private Addres address;

	~~~
}
  ``` java

<멤버>
@OneToMany(casacade=casacadetype.all, orpahnRemoval = true)
@JoinColumn(name=“member_id”)
Private List<AddressEntity> addressHistory = new ArrayList<>();

값 타입 컬렉션을 언제 쓰냐..? 지이이이이이이인짜 단순할 때. 
주소 이력? 엔티팁니다. 값을 변경하지 않는다고 하더라도, 디비 쿼리를 그쪽에서부터 가져와야되고 이런거는 다 엔티티라고 보면 됩니다. 
profile
SKKU Humanities & Computer Science

0개의 댓글