인프런 김영한님의 Java ORM 표준 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로 조인해서 찾아야됨. 테이블의 슈퍼타입과 서브타입이 그나마 유사함.
ORM? : Object-relational mapping(객체 관계 매핑), 객체는 객체대로, db는 db대로 설계를 하고, orm 프레임워크가 알아서 매핑을 해주겠다.
패러다임의 불일치를 해결해준다.
1) Jpa 표준 명세 :
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 멤버 가져왔다가 나중에 필요할 때 팀만 가져와! => 같이 쓰는게 많은 경우에는 즉시로딩, 따로 쓰는 경우가 많을 경우에는 지연로딩.
jpa에서 가장 중요한 2가지?
1) 엔티티 매니저 팩토리와 엔티티 매니저
웹 어플리케이션이 시작하면 엔티티 매니저 팩토리가 생성 -> 요청에 따라서 엔티티 매니저를 팩토리가 만들어줌. -> 각각의 매니저가 db 커넥션을 이용해 처리함.
2) 영속성 컨텍스트? Jpa를 이해하는데 가장 중요한 용어. “엔티티를 영구 저장하는 환경” 이라는 뜻.
3) 엔티티의 생명주기
4) 비영속
6) 영속성 컨텍스트의 이점
(1) 1차 캐시. 약간 중간에 있는 느낌. 버퍼링도 가능하고, 캐싱도 가능함.
-> 사실 큰 도움은 안됨. 사실 em은 트랜잭션 단위로 만들고 끝나면 지워버리기 때문. 굉장히 찰나의 순간에만 이득이 됨. 여러 명의 고객이 사용하는 캐시가 아님. 어플리케이션에서 공유하는 캐시는 2차 캐시라고 함.
-> 비즈니스 로직이 굉장히 복잡하면 도움이 되겠징?
-> 디비에서 한번 가져오고, 계속해서 영속성 컨텍스트에다가 저장.
-> 성능적인 이점 보다는 컨셉이 주는 이점. 객체지향적인 느낌.
7) 영속 엔티티의 동일성 보장.
”== “ 비교하면 어떻게 되니? 같은 객체로 취급됨. 마치 자바 컬렉션에서 꺼낸 것 처럼. 1차 캐시가 있기 때문에 가능.
8)엔티티 등록
9) 엔티티 수정 : 변경감지(더티 체킹)
jpa의 목적은 마치 자바 컬렉션에 넣은 것처럼 다루는 거임. 만약에 컬렉션 값을 수정 할 때 그 값을 꺼내서 수정하고 다시 그걸 저장함? 안함. 딱 찾아온 다음에 변경만 함. 즉, em.find() 해서 가져온 값을 member.set() 해서 수정 하고, 다시 em.persist 하면 안됨. 마치 자바 컬렉션에서 값 바꾸듯이 바꼈는데 sql이 날라감.
10) 엔티티 삭제
Em.remove() 하면 딜리트 쿼리 생성. 커밋 시점에 날라감.
*결론 : jpa는 값을 바꾸면 트랜잭션 커밋 시점에 알아서 업데이트 쿼리가 날아가는 구나. 라고 생각하고, 따로 persist를 안하는게 정답임.
플러시? 영속성 컨텍스트의 변경내용을 데이터베이스에 반영. 보통 db 트랜잭션이 커밋 될 때 플러시가 일어남. 쌓아놓은 sql문이 날라가는 것. 영속성 컨텍스트의 쿼리들을 db에 쭉 날려주는 것.
영속성 컨텍스트를 플러시 하는 방법 : em.flush() – 직접 호출.. 진짜로 이렇게 쓰는 경우는 없음
플러시를 하면 1차 캐시가 지워지나요? 아니요. 오직 쓰기 지연 sql 저장소에 있는 쿼리들만 db로 날려버리는 거임. 뭔가 바뀐거 이런것만 데이터베이스에 반영이 되는 것.
jpql에서 플러시가 자동으로 호출되는 경우? 만약 persist만 한 상태에서 중간에 jpql로 조회하는 쿼리를 날린다면, 아무것도 안날라올 수도 있음. 따라서, jpql은 무조건 플러시를 한번 날리고 시작함. 아 그래서 날라가는 구나 라고 생각하면 됨.
플러시 모드 옵션? 딱히 쓸 일은 없음. FlushModeType.COMMIT – 커밋 할 때만 플러시, 쿼리에는 플러시를 안한다. 위 같은 경우에 만약 JPQL을 하는데 뭐 아예 다른 테이블을 가져오고 그런 경우에는 굳이 플러시를 할 필요가 없겠지? 근데 굳이 플러시를 하고 싶지 않을 수도 있음. 정 원하면 플러시 모드를 커밋으로 바꾸라 -> 큰 도움이 되지는 않습니다. 걍 AUTO로 쓰세요. 손대지 말고.
플러시는 영속성 컨텍스트를 비우는게 아니라, 영속성 컨텍스트를 DB에 반영 하는 것.
트랜잭션이라는 작업 단위가 중요함. 커밋 직전에만 동기화 하면 됨. 어쨌든 트랜잭션 커밋 직전에만 날려주면 되니까!! JPA는 결국 이런 동시성 관련 이슈는 다 DB 트랜잭션에 위임하기 때문에.
크게 이해하기가 쉽지는 않다. 나중에 실전에서 웹 어플리케이션을 만들 때 자세하게 설명할 거구요, 지금은 약간 이런게 있다, 정도만 알면 됩니다.
영속 -> 준영속
만약, FIND를 했는데 영속성 컨텍스트에 없으면 DB에서 가져와서 1차 캐시에 올려놓음. 즉, 영속 상태가 됨. 근데 만약에 이거 영속성 컨텍스트에서 관리하고 싶지가 않아, 하면 detach()를 씀.
사실 쓸 일이 많지는 않고, 웹 어플리케이션 실제 개발 할 때.
Em.detach() 특정 컨텍스트만 준영속으로 만들 떄.
Em.clear() em이라는 영속성 컨텍스트 안에 있는 걸 전부 날려버림. 따라서 쿼리가 안날라감.
테스트 케이스 작성하고 이럴 때 도움이 될 수도 있음.
Em.close() 하면 em을 닫아버림. Jpa가 관리가 안되겠지?
근데 뭐… 네… 이거에 대해서 깊이있게 이해는 할 필요 없고.. 실제 개발하는 단계에서 응용하는 것.
*jpa에서 제일 중요하게 볼거는 1)메커니즘, 2) 매핑
1) 엔티티 매핑 소개
2) @Entity
3) @Table
jpa에서는 어플리케이션 로딩 시점에 db 테이블 생성하는 기능을 지원해줌. 로컬 pc에서 개발하고 이럴 때 도움. 운영 때 쓰면 안됨!!
** 이렇게 생성된 ddl은 개발 장비에서만 사용!! 운영 서버에서는 사용하지 말고, 적절히 다듬은 후에 사용하는 것을 권장
1) Create-drop : 종료 시점에 table drop
2) Update : 컬럼을 하나 추가를 하고 싶은데, 드랍 테이블 하고 싶지 않고 alter 테이블 하고 싶을 떄. 추가만 가능. 지우는건 안됨., 테이블 컬럼 날라가면 큰일..
3) Validate : 엔티티와 테이블이 정상 매핑되었는지 확인해줌. 만약 이상한 컬럼이 추가되고 실행하면 에러가 남. 스키마에 이런 컬럼 없는데? 이런. 엔티티와 테이블이 정상 매핑 되었는지 확인
4) None : 사실 none은 없어요. 걍 sdh fkjsndfjkdsn이렇게 아무렇게나 쓰는거랑 같음. 매칭되는게 없어서 실행이 안됨.
주의 :
DDL 생성 기능
1) jpa에서 재밌는 기능. @Column(name=”username”, unique=true, length=10) 이런 식으로 추가 가능.
2)어플리케이션에 영향을 주는게 아닌, db에 직접 영향을 주는 것. 그냥 ddl 생성만 딱 도와주는 것.
*db에 기본 키 매핑을 어떻게 하는지 알고 있어야 함.
권장하는 식별자 전략
어려운 내용)
1) @GeneratedValue(startegy = GenerateType.IDENTITY)의 특징
2)시퀀스 전략일 경우에는, 디비에서 시퀀스 값을 가져온 다음에 영속성 컨텍스트에다가 넣고, 그걸로 영속성 컨텍스트에다 넣어놓음. (call next value query), 물론 이런 경우에 인서트 쿼리는 날아가지 않고 커밋에서 날림.
*객체의 참조와 테이블의 외래키를 매핑하는 방법!
”객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.”
사실 orm이 어려운게 아니라, ‘객체지향스럽게 설계하는 것이 무엇인지’ 부터 아는 것이 더 중요함.
*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이 되는 애가 주인이 되면 됨. 자동차와 자동차 바퀴로 따지면 비즈니스 적으로는 자동차가 훨씬 중요하겠지만 주인은 바퀴가 되는 거임. 그렇게 해야 성능 이슈도 없고 깔끔하게 들어감.
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 계속 쓰기도 함.
=> 컨트롤러에서 response로 엔티티를 직접 보내버리면 그렇게 될 수도 있음. 어 멤버에는 팀이 있네? 팀에는 멤버스가 있네? 멤버스에는 팀이 있네? 팀에는 멤버스가 있네? X 무한루프.
B. 답이 정해져있죠. toString, lombok에서 이걸 빼고 쓰라. Json 생성 라이브러리 같은 경우네는 컨트롤러에서는 엔티티를 절대 반환하지 마세요.
i. 무한루프 생길 수 있음
ii. 엔티티는 변경이 가능 한데 엔티티를 api 반환을 해버리면 엔티티를 변경하는 순간 api 스펙이 바뀜. 가져다 쓰는 경우 매우 황당하겠지?
DTO로 변경해서 반환해야 합니다. 이렇게만 해도 대다수의 문제들은 해결이 됩니다.
4) 양방향 매핑 정리
A. 단방향 매핑만으로도 이미 연관관계 매핑은 완료 : 처음에는 이제 단방향으로 끝내야 합니다!!! 일단은 JPA를 쓰고 설계를 하잖아? 그러면 단방향으로 설계를 완료해야됨.
실무에서는 정말 완전히 객체만으로 설계가 가능? 불가능! 테이블 설계를 계속 조지면서 객체도 생각을 해야 함. 그런 과정에서 1:N, FK 이런게 이제 다 됨. 처음에는 단방향으로 무조건 끝내야함.
B. 그러면 이제 양방향은 뭐냐? 반대방향에서 조회기능이 추가되는 것. JPA에서 설계라는
것은 단방향으로 이미 완료가 된겁니다. 그러면 이제 나중에 개발을 하다가 테이블 한번 만들면 굳어지니깐..
C. 그럼 양방향은 언제함? 객체 입장에서 양방향으로 해봤자 고민거리만 많아지지.. 테이블이랑 객체를 해서 매핑한다는 그 관점에서 보면 단방향으로만 해도 끝난거임. 그래서 언제들어가냐?????? => 막상 실무에서는 역방향으로 참조를 하는 경우가 왕왕 있을 수 있음.
D. 근데 이제 여기서 중요한거. 단방향만 잘 해주면, 양방향은 필요할 때 추가만 해주면 됨!!!!!!!!!!! 테이블에 영향을 안주니까!! 자바 코드 몇 줄 넣는 건 안어렵자너. 4
E. 결론 : 단방향 매핑으로 다 끝내버리자. 설계를 쫙 끝낸다. => 실제 개발할 때 고민.
F. 연관관계의 주인을 정하는 기준? 비즈니스 로직에 따르면 안되고, 외래키의 위치를 기준으로 정해야 함!!
애매할 때는 반대쪽을 생각해 보자. 다대일, 일대다, 일대일, 다대다는 대칭성이 있기 때문에.
사실 다대다 @ManyToMany는 실무에서 쓰면 안된다.
테이블 : 외래 키 하나로 양쪽 조인 가능, 사실상 방향이라는 개념이 없음.
객체 : 참조용 필드가 있는 쪽으로만 참조 가능, 한쪽만 참조하면 단방향, 양쪽이 서로 참조하면 서로 단방향 두개.
*그다지 추천하는 모델은 아니지만, 일단은
객체 입장에서는 이러한게 필요할 수도. 팀은 멤버가 궁금한데, 멤버는 팀이 궁금하지 않을 경우.
@OneToMany , @JoinColumn(name=”TEAM_ID”) 이런 식으로. 어색하지만 동작은 함.
Update 쿼리가 추가로 나감. 팀 테이블 인서트 이외에도 멤버 테이블의 업데이트 쿼리가 한번 더 나가야 함 -> 성능상 약간의 단점. 큰 이슈는 아니긴 함.
심각한 것은, JPA를 잘 쓰는 사람이더라도 이게 헷갈릴 가능성이 매우 큼. 나는 Team만 손을 댔는데 왜 멤버 업데이트 쿼리가 나가지…….? 이런 경우 발생 가능 매우 큼. 실무에서는 테이블 수십개가 엮여서 돌아가는데.. 이렇게 되면 운영이 매우 힘듬..
그래서 차라리 다대일 단방향 관계 + 필요하면 양방향 추가 => 이 전략으로 가는게 나음. 약간 객체지향적 손해를 보더라도 (멤버에서 팀을 갈 일이 없는데도 어거지로 만듬) 그냥 다대일 양방향으로 바꿔서 만드는게 나을 수도 있음.. orm 보다는 db의 설계에 맞춰서. 유지보수하기 쉽게.
이게 테이블에는 항상 N쪽에 외래 키가 있기 때문이야!!
@JoinColumn을 꼭 사용하세요. 안그러면 Join Table방식을 사용함… => 중간 테이블이 만들어짐. 그러면 장점도 있지만, 단점은 테이블 한 개가 더 들어가니까 성능상 애매하고 운영하기가 쉽지 않음.
단점 : 엔티티가 관리하는 외래 키가 다른 테이블에 있고, 그래서 업데이트 쿼리가 추가로 실행됨
결론 : 쓰지 마세요. 다대일 양방향 매핑을 사용하자. 그냥 안쓰는것도 방법임 ㅋㅋ
하다 보니까 역방향(양방향)도 하고싶어…? : 표준 스펙은 아니지만 야매로 가능하기는 함.
멤버에다가 @ManyToOne , @JoinColumn(name=”TEAM_ID”,insertable=false,updatable = false) 이런 식으로 매핑을 걸어버림. 연관관계의 주인처럼 해놓고 업데이트 인서트를 제약을 걸어버리는 거임. 읽기 전용으로 걸어버리는 거. 이러면 사실상 양방향 매핑이랑 똑같이 되는거임. 멤버 테이블의 FK 관리는 팀이 여전히 하면서 멤버는 읽기 전용만.
주 테이블에 외래 키 :
-> 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
-> 객체지향 개발자 선호
-> JPA 매핑 편리
-> 장점 : 주 테이블만 조회해도 대상 테이블에 데이턱가 있는지 없는지 확인 가능
-> 단점 : 값이 없으면 외래 키에 NULL 허용 (DB 입장에서는 치명적..!)
대상 테이블에 외래 키
-> 대상 테이블에 외래 키가 존재
-> 전통적인 데이터베이스 개발자 선호
-> 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지 가능
-> 단점 : 주로 멤버에서 랄커를 엑세스를 하는 하기 때문에 이제 양방향으로 만들어야됨
-> 치명적 단점 : 프록시 기능의 한계로, 지연 로딩으로 설정해도 항상 즉시 로딩이 되어버림… JPA의 한계
-> 멤버를 로딩 할 때, 만약 멤버에 락커를 조회 하는 경우, 멤버 테이블만 조회해서 안됨. 어차피 락커를 뒤져야 함. 락커를 뒤져서 멤버 이게 있는지 없는지를 봐야 알 수가 있음.. 즉 어차피 쿼리가 나감… 그럼 이걸 프록시로 할 이유가 없음.. 그래서 하이버네이트에서는 지연로딩으로 세팅을 해도, 1대1관계에서 대상 테이블에 FK가 있는 경우 무조건 즉시 로딩이 됨.
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수가 없음..
연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함.
중간 테이블을 만들어가지고, 1:N, N:1 로 풀어야 함.
-객체는 삽가능. 컬렉션을 사용해서 객체 2개로 다대다 관계 가능. 멤버도 프로덕트 리스트를 가지고 프로적트는 멤버 리스트를 가지고.
@ManyToMany 사용, @JoinTable 사용.
편리해 보이지만 실무에서 사용하면 안됨!!
연결 테이블이 단순히 연결만 하고 끝나지 않음. 매핑 정도만 들어가고, 추가 정보를 쓸 수가 없음………
쿼리도 이상하게 나감. 중간 테이블 들어가고 막 조인되고 나와서 내가 생각도 못한 쿼리가 나감.
그럼 어떻게?? => 중간 테이블을 엔티티로 승격
+) 보통은 이런 연결 테이블을 멤버아이디(PK,FK), 프로덕트아이디(PK,FK) 이런식으로 해서 저 두개를 PK로 묶어서 사용하는데, 이거 보다는 그냥 ID를 따로 두는게 낫기는 함. ID는 의미 없는 값으로. 그냥 모든 테이블에 GENERATED VALUE로 하는게..낫지 않나..
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) 구현 클래스마다 테이블 전략
5) 장단점
A. 조인 전략 : 객체랑도 잘 맞고, 깔끔해서 이제 기준이라고 보면 됨
테이블이 정규화 되어 있음, 외래 키 참조 무결성 제약조건 활용 가능 = ITEM 딱 보면 됨, 저장 공간 효율화
조회 시 조인을 많이 사용->성능 저하(큰 문제는 아님), 조회 쿼리가 복잡. 데이터 저장시 인서트 쿼리 2번 나감
B. 단일 테이블 전략 :
조인이 필요가 없기 때문에 일반적으로 조회 성능이 빠름, 조회 쿼리가 단순
자식 엔티티가 매핑한 컬럼은 모두 NULL 허용(치명적), 단일 테이블에 모든 것을 저장하기 때문에 테이블이 커질 수도 있고 조회 성능이 오힐 느려질 수도 있음. (근데 이정도 임계점을 넘는 경우는 딱히)
C. 구현 클래스마트 테이블 전략 :
갑자기 어느날 DBA가 와서 모든 테이블에 수정 날짜 이런거 다 넣어야 된다고 하면…? 너무 귀찮고… 속성만 상속받아서 쓰고 싶은데…. = @MappedSuperclass
BaseEntity라는걸 만들고 이제 거기다가 다 써놓은 다음에 extends BaseEntity 하면 됨. BaseEntity에는 @MappedSupercalss 써주면 됨. 어려운게 아니고 “이 속성을 같이 쓰고 싶어” 할 때 쓰면 됨.
createdBy 이런거는 이제 다 자동화 시킬 수 있음! 어드민으로 로그인되어 있는 세션 정보를 읽어와서 -> 이벤트 활용! createdBy, createdDate, modifiedBy, modifiedDate 등등
이런 경우가 있다고 생각을 해보자. 이런 경우는 쿼리가 한번에 날라가서 다 가져오는게 좋겠지?
근데 그냥 유저 네임만 가져오는 경우, 가령
Public static void printMember(Member member) {
System.out.printlmn(“member = “ + member.getUserName());
}
이런 경우도 둘 다 있다. 이런 경우
Member member = em.find(Member.class, 1L);
여기서 연관되어 있는 애들까지 다 (team)까지 sql을 날려서 가져오냐 혹은 멤버만 가져오냐로 최적화를 할 수가 있음.
즉, 가짜를 가져온 거임. Id는 이미 있으니까. 근데 getUsername은 없잖아?
이 때 쿼리를 날려서 가져옴.
-> 그럼 대체 findMember는 뭘까? 하이버네이트가 가짜로 만든 가짜 클래스(=프록시)
-> 프록시의 특징 : 실제 엔티티를 상속받아서 만들어짐 = 겉 모양이 똑같음 (하이버네이트가 내부적으로 프록시 라이브러리를 사용해서 만들어냄)
-> 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨! (킹론상)
//프록시 객체 생성
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) 영속성 컨텍스트에 이미 있으면 초기화 할 때 실제 객체로 채워줌.
6) JPA는 동일성을 보장해줘야하기 때문에, 프록시를 한번 채우면 EM.FIND()를 해도 프록시로 가져옴. => 중요한 거는 프록시든 아니든 실제처럼 쓸 수 있다는 거임. 큰 상관은 없긴 함.
7) 영속성 컨텍스트의 도움을 받을 수 없는 준 영속상태일 때 프록시를 초기화하면 문제가 생김
Member m = em.find(Member.class, member1.getId()
System.out.println(“m = “ , m.getTeam().getClass());
1. 로딩 -> 멤버1 -> 팀1(프록시 팀1 엔티티) : 지연로딩
== 실제 team을 사용하는 시점에 초기화.
3. 지금부터 중요한 내용입니다.
1) 가급적 지연로딩만 사용(실무에서). 실무에서 즉시로딩은 쓰지 마세요!
2) 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생하기 때문에.
3) 즉시 로딩은 JPQL에서 N+1문제를 일으킴.
=> 일단 모든 연관관계를 다 lazy로 깔고
- fetch 조인 (jpql)
- entitygraph를 쓰는 방법.
- batchsize라고 해서 그렇게 푸는 방법이 더 있죠.
영속성 전이
ex) 부모 엔티티를 만들 때 자식 엔티티도 같이 만들고 싶을 때
-컬렉션 안에 있는 애들도 다 persist 날려줄거야 하는 그거임.
@OneToMany(mappedBy = “parent”, casacade = CasacadeType.ALL)
Private List<Child> childList = new ArrayList<>();
Parent와 child의 라이프 사이클이 거의 유사할 때, 단일 소유자일 때,
고아 객체
@OneToMany(mappedBy = “parent”, casacade = CasacadeType.ALL, orpahanRemoval = True)
Private List<Child> childList = new ArrayList<>();
1) 엔티티타입
자바의 기본타입은 절대 공유X : Python이랑 다르게 참조하지 않음. 복사가 되는 별개의 저장 공간을 가지고 있음.
단, Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유가됨. 아마 파이썬이 이 경우일걸?
Integer a = new Integer(10);
Integer b = a;
이러면 10이 복제가 되는게 아니라, a의 레퍼런스가 b로 넘어가는 거라서 같게 나옴. 클래스는 공유 가능한 객체
그래서 변경 자체를 불가능하게 만들어서 SIDE EFFECT가 발생하지 않게 함.
4) 임베디드 타입 : 복합 값 타입. 내장 타입.
새로운 값 타입을 직접 정의할 수 있음.
주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함.
Int, String처럼 값 타입임
Ex) 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.
@Embeddable : 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용 하는 곳에 표시
메소드를 만들 수 있음
임베디드 타입과 연관관계
임베디드 타입이 엔티티를 가질 수 있음.
@AttributeOverride : 속성 재정의
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<>();
값 타입 컬렉션을 언제 쓰냐..? 지이이이이이이인짜 단순할 때.
주소 이력? 엔티팁니다. 값을 변경하지 않는다고 하더라도, 디비 쿼리를 그쪽에서부터 가져와야되고 이런거는 다 엔티티라고 보면 됩니다.