안녕하세요

저번에 Fetch조인과 일반 조인 차이점에 대해 포스팅했었는데

그 궁금증의 시작

"왜 Fetch Join 대상에 alias를 사용하면 안될까?"

대해 포스팅 해보겠습니다.

 

 

일단 이유는 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를 확인해서 있으니

기대값과는 다른 멤버를 반환하게 됩니다.

 

이렇게 데이터 일관성이 깨지게 됩니다.

 

그래서 페치 조인은 연관 엔티티를 모두를 가져오는 기능에 맞춰서 사용해야 됩니다.

 

 

 

 

정말 이거 공부하면서 좀 개념 틀이 잡힌 느낌이 들었습니다 ㅋ

여하튼 틀린 부분이나 궁금한 부분 있으시면 댓글 주시길 바랍니다~!

감사합니다~!

 

 

 

반응형

 

jpa활용2 강의를 듣고 있는데 김영한 강사님이 강의 중에 이런 말을 하셨음.

"fetch조인에서는 엘리어스를 쓰면 안된다.. .. .. "

그래서 왜지? 하고 생각만 했다가 완강하고 다시 검색해보는데.

 

https://www.inflearn.com/community/questions/15876/fetch-join-%EC%8B%9C-%EB%B3%84%EC%B9%AD%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4

 

역시 나와 같은 사람이 있었음 ㅋ

그래서 답변을 쭉쭉 읽는데도 이해가 안 되어서 왜 안되나 했는데

ㅋㅋ..

 

내가 fetch 조인을 잘 모르고 있었음 ㅋ

그냥 단순히 fetch 조인 쓰면 연관관계에 있는 거 다 가져오는 거구만 개꿀 ㅋ

이 정도로만 생각해서 이해를 못 했던 것임

 

 

"JPA의 엔티티 객체 그래프는 DB와 데이터 일관성을 유지해야 하기 때문입니다.."

왜 사용하면 안되는지에 대한 영한님의 답변인데

<- 왜? 이것 또한 이해 못 했음..

그냥 가져오는 건 상관없는 거 아닌가 쩝 ..

이 생각했는데 fetch 조인은 엔티티를 가져오는 거 기 때문에.. 안됨. 큰일 남;;

이건 다음에 다른 글로 포스팅하고 먼저 이해해야 하는 일반 조인과 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 조인은 어떻게 될까요?

 

Fetch조인 테스트 코드

    @Test
    public void 페치조인() throws Exception{
        List<Member> membersFetch  = memberRepository.findAllWithFetchJoin(); // 1

        for (Member member:membersFetch) {
            System.out.println("membersFetch = " + member.getTeam().getName());
            member.getTeam().getName();
        }
    }

또 예상해 보시죠 ㅋ

쿼리가 몇 번 나갈까요?

한번 나갑니다.

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

로그를 보면 실제객체를 참조하고 있는 게 보인다 ㅋ

 

정리해 보면 일반 조인은 값을 가져오긴 하지만 연관된 객체는 프록시 객체로 가져오고

fetch조인은 연관된 객체 모두 실제 객체로 초기화하여 영속성 컨텍스트에 저장한다!

 

 

여기까지만 알아보자 ㅋ

틀린 내용이 있다면 댓글 부탁드립니다 ㅋ

반응형

배열

- 메모리의 연속 공간에 값이 채워져 있는 형태의 자료구조

- 인덱스를 통해 참조

- 선언한 자료형의 값만 저장할 수 있음

 

특징

- 인덱스를 사용하여 값에 바로 접근 할 수 있음

- 새로운 값 삽입 삭제 하기 어렵다 . 값을 삽입하거나 삭제하려면 해다 인덱스 주변에 있는 값을 이동 시키는 과정이 필요함

- 배열의 크기는 선언할 때 지정할수 있고 변경할수 없음

 

리스트

- 값과 포인터를 묶은 노드(값,포인터를 쌍으로 갖는 기초 단위)라는 것을 포인터로 연결한 자료구조

 

특징

- 인덱스가 없으므로 값에 접근하려면 head 포인터부터 순서대로 접근해야한다. 고로 접근하는데 속도가 오래걸림

- 포인터로 연결되어 있으므로 데이터를 삽입하거나 삭제하는 연산속도가 빠름

- 선언할 때 크기는 지정 않아도 됨

- 포인터를 저장할 공간이 필요하므로 배열보다 구조가 복잡함.

 

자바에서 Array, ArrayList, LinkedList

 

참고

https://velog.io/@letskuku/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Array-vs-ArrayList-vs-LinkedList

반응형

문제

https://www.acmicpc.net/problem/1546

슈도코드

String으로 받아옴 split 배열로 만들고 계산

가장 높은 점수를 찾아야함

모든 값을 /가장높은점수*100으로 계산하고

다 더해서 평균을 구한다.

나의 풀이

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main{    
    
  public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    int numCnt = Integer.parseInt(br.readLine());
    String scoreStr = String.valueOf(br.readLine());
    String[] scoreArr = scoreStr.split(" ");
    int maxScore = 0;

    for (int i = 0; i < numCnt; i++) {
      int score = Integer.parseInt(scoreArr[i]);
      if(maxScore < score ){
        maxScore = score;
      }
    }
    float newSum = 0;
    float avl = 0L;

    for (int i = 0; i < numCnt; i++) {      
      newSum += (Float.parseFloat(scoreArr[i])/maxScore * 100);
    }
    avl = newSum / numCnt;
    System.out.println(avl);
  } 
}

만난 에러

read로 했을 때 char로 읽어오기 때문에 3 -> 51

다른사람 풀이

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        int n = readInt();
        int maxScore = 0;
        int totalScore = 0;
        for (int i = 0; i < n; ++i) {
            int score = readInt();
            totalScore += score;
            maxScore = maxScore > score ? maxScore : score;
        }
        System.out.println(100.0d * totalScore / maxScore / n);
    }
    
    static int readInt() throws IOException {
        int retValue = 0;
        boolean negative = false;
        while (true) {
            int i = System.in.read();
            if (i == '\r') continue;
            if (i == ' ' || i == '\n') {
                break;
            }
            else {
                if (i == '-') negative = true;
                else {
                    retValue = retValue * 10 + i - '0';
                }
            }
        }
        return negative ? -1 * retValue : retValue;
    }      
}

배운 것

read 

readLine

https://docs.oracle.com/javase/8/docs/api/java/io/BufferedReader.html

반응형

'코딩 테스트' 카테고리의 다른 글

배열 vs 리스트  (0) 2024.05.07
[백준] 11720 숫자의 합  (0) 2024.05.05

+ Recent posts