일단 이유는 JPA의 엔티티 객체 그래프는 DB와 데이터 일관성을 유지해야 하기 때문입니다.
뭔 소리냐고요?
(저도 처음 듣고 엥스바리 했습니다.)
예시를 들어 보겠습니다.
(저는 이번에 공부하면서 영속성 컨텍스트라는 개념이 얼마나 중요한지 ㅋ
싸악 알게 되었습니다)
먼저 예시 데이터를 보겠습니다.
Team1 - 유저1, 유저2
Team2 - 유저3
인 상황입니다.
JPQL
// 1. 팀을 페치조회하는데 유저이름으로 필터링
public List<Team> findByMemberName(String memberNm){
return em.createQuery("select t from Team t join fetch t.members m"+
" where m.name = memberNm", Team.class)
.setParameter("memberNm",memberNm)
.getResultList();
}
// 2. teamId로 team을 가져옴
public List<Team> findByTeamId(Long teamId){
return em.createQuery("select t from Team t where t.id =teamId", Team.class)
.setParameter("teamId", teamId)
.getResultList();
}
첫 번째 JPQL을 보면 Team을 조회하면서 fetch조인으로 연관된 Member 엔티티를 모두 가져오는데 별칭을 사용해 memberNm으로 필터링해서 가져옵니다. (Team1 {members=['유저1']} )
두 번째 JPQL은 단순히 teamId로 Team을 가져옵니다.
그럼 이 테스트 코드를 보겠습니다.
테스트 코드
@Test
public void 페치조인_데이터_일관성_확인() throws Exception{
List<Team> teamsWithUser1 = memberRepository.findByMemberName("유저1");
List<Team> fullTeamById = memberRepository.findByTeamId(1L);
for (Team team:teamsWithUser1) {
for (Member member:team.getMembers()) {
System.out.println("teamsWithUser1.member = " + member.getName());
}
}
for (Team team2:fullTeamById) {
for (Member member:team2.getMembers()) {
System.out.println("fullTeamById.member = " + member.getName());
}
}
}
단순히 위에 JPQL을 호출해서 해당 팀에 멤버를 조회하는 테스트 코드입니다.
결과가 예상 가능하신가요?
첫 번째 teamWithUser1에서는 유저1 / 두 번째 fullTeamById 에서는 teamId 1인 팀에 멤버를 가져오니까 유저1, 유저2 이렇게 조회될까요?
테스트 결과
select
team0_.team_id as team_id1_7_0_,
members1_.member_id as member_i1_4_1_,
team0_.name as name2_7_0_,
members1_.city as city2_4_1_,
members1_.street as street3_4_1_,
members1_.zipcode as zipcode4_4_1_,
members1_.name as name5_4_1_,
members1_.team_team_id as team_tea6_4_1_,
members1_.team_team_id as team_tea6_4_0__,
members1_.member_id as member_i1_4_0__
from
team team0_
inner join
member members1_
on team0_.team_id=members1_.team_team_id
where
members1_.name='유저1'
2025-05-13 15:30:15.556 DEBUG 34196 --- [ Test worker] org.hibernate.SQL :
select
team0_.team_id as team_id1_7_,
team0_.name as name2_7_
from
team team0_
where
team0_.team_id='1'
team.member = 유저1
team2.member = 유저1
놀랍게도 teamId 1인 Team을 조회한 Team1의 멤버는 유저1만 나옵니다.
왜일까요????
영속성 컨텍스트를 기억하시나요?
조회된 엔티티는 영속성 컨텍스트 1차 캐시에 들어가게 되고 식별자는 id값입니다.
그래서 처음 조회한 필터링된 Team1이 1차 캐시에
id = 1 , Entity = Team {Memebers=['유저1']} 이러한 형태로 들어가게 됩니다.
그다음 teamId 값으로 Team을 조회했을 때 db에 접근하기 전에 1차 캐시를 먼저 들려 id를 확인해서 있으니
이건 다음에 다른 글로 포스팅하고 먼저 이해해야 하는 일반 조인과 fatch 조인의 차이를 알아보겠다!
Fetch Join
일단 fetch 조인이란 sql과 비슷해 보이지만 jpql에서만 제공하는 기능으로 엔티티를 조회해 올 때 연관된 엔티티를
한 번에 같이 조회 할수 있는 기능이다.
연관된 엔티티를 한번에 같이 조회할 수 있는 게 어떤 의미인가 하면! 함께 조회한 엔티티가 영속성 컨텍스트에 올라가게
되는데 밑에 예제를 보면서 자세히 보겠습니다 ㅋ
모든 멤버와 해당 멤버의 팀까지 조회해보겠습니다.
현재 데이터는 아래와 같습니다. 유저 1,2가 있고 모두 팀이 1인 상황입니다.
멤버, 팀 데이터
JPQL
// 1.일반조인
public List<Member> findAll(){
return em.createQuery("select m from Member m join m.team t",Member.class)
.getResultList();
}
// 2.fetch조인
public List<Member> findAllWithFetchJoin(){
return em.createQuery("select m from Member m join fetch m.team ",Member.class)
.getResultList();
}
일반 조인 테스트 코드
@Test
public void 일반조인() throws Exception{
List<Member> members = memberRepository.findAll(); // 1
for (Member member:members) {
System.out.println("member.class = " + member.getTeam().getClass());
System.out.println("member.class init= " + Hibernate.isInitialized(member.getTeam()));
System.out.println("member name = " + member.getTeam().getName());
member.getTeam().getName();
}
}
혹시 쿼리가 몇 번 나올 것인지 예상되시나요?
처음 모든 Member를 가져오는 쿼리 한번 , 해당 멤버의 팀을 가져오는 쿼리 1번(멤버의 팀이 달랐다면 2번 쿼리가 나갔을 텐데 첫 번째 유저의 팀을 조회했을 때 영속성 컨텍스트에 저장되어 있기 때문에 1차 캐시에서 가져오고 DB조회 안 함)
도합 2번 나갑니다.
조회된 일반 조인 SQL
select
member0_.member_id as member_i1_4_,
member0_.city as city2_4_,
member0_.street as street3_4_,
member0_.zipcode as zipcode4_4_,
member0_.name as name5_4_,
member0_.team_team_id as team_tea6_4_
from
member member0_
inner join
team team1_
on member0_.team_team_id=team1_.team_id
member.class = class jpabook.jpashop.domain.Team$HibernateProxy$cRvKZ9yd -- 프록시객체
member.class init= false
2025-05-12 15:02:34.096 DEBUG 2092 --- [ Test worker] org.hibernate.SQL :
select
team0_.team_id as team_id1_7_0_,
team0_.name as name2_7_0_
from
team team0_
where
team0_.team_id=?
member name = Team1
member.class = class jpabook.jpashop.domain.Team$HibernateProxy$cRvKZ9yd-- 프록시객체
member.class init= true
member name = Team1
로그를 보면 member에서 team 객체에서 프록시 객체를 넣어 지연로딩 합니다.
그리고 나는 프록시 객체에서 초기화하면 실제 객체로 바뀌지 않을까? 했는데 그냥 초기화 해주고 로그를 보면 프록시 객체 그대로 쓰는 것 같다.
fetch조인은 연관된 엔티티도 다 가져와 영속성 컨텍스트에 저장하기 때문에 연관객체를 프록시 객체로 가져오지 않고 실제 객체로 가져옵니다.
그래서 밑에서 team을 조회해도 다시 db에서 조회하지 않고 영속성 컨텍스트에 있는 값을 가져옵니다.
조회된 Fetch 조인 SQL
select
member0_.member_id as member_i1_4_0_,
team1_.team_id as team_id1_7_1_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.name as name5_4_0_,
member0_.team_team_id as team_tea6_4_0_,
team1_.name as name2_7_1_
from
member member0_
inner join
team team1_
on member0_.team_team_id=team1_.team_id
member.class = class jpabook.jpashop.domain.Team
membersFetch = Team1
member.class = class jpabook.jpashop.domain.Team
membersFetch = Team1
status - 파일 상태 확인: Untracked(관리대상이 아님) git add 하지 않은 상태 Tracked(관리대상임) : git이 관리하는 파일임을 의미 unmodified : 최근 커밋과 비교했을 때 바뀐 내용이 없는 상태 modified : 최근 커밋과 비교했을 때 바뀐 내용이 있는 상태 staged: 파일이 수정되고 나서 스테이지 공간에 올아와 있는 상태 git add 후 상태
git ignore 파일clone 원격 저장소의 코드를 컴퓨터에 받아올 수 잇습니다.
branch
checkout
- 브랜치 이동
- checkout기능이 많아져서 switch , restore가 도입
- switch : 브랜치 이동
- restore : 파일 수정 내용 복원과 add를 통해 스케이지에 올린 파일 빼낼 때 사용합니다.
브랜치 삭제
브랜치 복구
Toongather
오늘 본격적으로 프로젝트 시작 전에 필요한 것들을 정했다.
1. 깃 마일스톤, 프로젝트 생성 2. 이슈 규칙 / 템플릿 3. PR 규칙 / 템플릿 4. Branch 생성 규칙 5. commit 메세지 규칙
서로의 의견을 취합해서 결론은
프로젝트 생성, 마일스톤은 매주 이슈제목은 [도메인] + 제목 내용은 만들고 pr은 작은리뷰로 그리고 이슈 닫을 때 close 문구 쓴다 브랜치 - [상태]/이슈번호 로하구 커밋 - [상태] 제목 , 상세내용 추가할거면 엔터치고 줄 글 로쓰기