개발/Spring

OneToMany, ManyToOne 테스트

jis1218 2024. 9. 16. 22:31
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity
public class Nationality {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;

    @OneToMany
    private List<Player> players;

    @Enumerated(EnumType.STRING)
    private Continent continent;

}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Player {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    private Nationality nationality;

    @ManyToOne
    private Club club;

    @Enumerated(EnumType.STRING)
    private PlayerStatus status;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity
public class Nationality {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;

    @OneToMany
    private List<Player> players;

    @Enumerated(EnumType.STRING)
    private Continent continent;

}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Club {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    
    @OneToMany
    private List<Player> players;

}

클래스가 다음과 같이 주어졌을 때 nationalityRepository.findAll(); 을 하게 되면 먼저 nationality를 모두 가져오고 nationality에 연관관계가 있는 Player를 join한다. Club과 nationality는 Player에 연관관계가 있으므로 left join 해서 가져온다. 쿼리를 확인하면 nationality의 개수만큼 player, nationality, club을 조회함을 알 수 있다. (1+N 번 쿼리)

select
        n1_0.id,
        n1_0.continent,
        n1_0.name 
    from
        nationality n1_0
        
 select
        p1_0.nationality_id,
        p1_1.id,
        c1_0.id,
        c1_0.name,
        p1_1.name,
        n1_0.id,
        n1_0.continent,
        n1_0.name,
        p1_1.status 
    from
        nationality_players p1_0 
    join
        player p1_1 
            on p1_1.id=p1_0.players_id 
    left join
        club c1_0 
            on c1_0.id=p1_1.club_id 
    left join
        nationality n1_0 
            on n1_0.id=p1_1.nationality_id 
    where
        p1_0.nationality_id=1
        
  select
        p1_0.nationality_id,
        p1_1.id,
        c1_0.id,
        c1_0.name,
        p1_1.name,
        n1_0.id,
        n1_0.continent,
        n1_0.name,
        p1_1.status 
    from
        nationality_players p1_0 
    join
        player p1_1 
            on p1_1.id=p1_0.players_id 
    left join
        club c1_0 
            on c1_0.id=p1_1.club_id 
    left join
        nationality n1_0 
            on n1_0.id=p1_1.nationality_id 
    where
        p1_0.nationality_id=2
        

하지만 결과는 다음과 같다.

[
    {
        "id": 1,
        "name": "Korea",
        "players": [],
        "continent": "ASIA"
    },
    {
        "id": 2,
        "name": "Japan",
        "players": [],
        "continent": "ASIA"
    },
    {
        "id": 3,
        "name": "China",
        "players": [],
        "continent": "ASIA"
    },
    {
        "id": 4,
        "name": "USA",
        "players": [],
        "continent": "NORTH_AMERICA"
    },
    {
        "id": 5,
        "name": "Brazil",
        "players": [],
        "continent": "SOUTH_AMERICA"
    },
    {
        "id": 6,
        "name": "Germany",
        "players": [],
        "continent": "EUROPE"
    },
    {
        "id": 7,
        "name": "France",
        "players": [],
        "continent": "EUROPE"
    },
    {
        "id": 8,
        "name": "England",
        "players": [],
        "continent": "EUROPE"
    },
    {
        "id": 9,
        "name": "Spain",
        "players": [],
        "continent": "EUROPE"
    },
    {
        "id": 10,
        "name": "Italy",
        "players": [],
        "continent": "EUROPE"
    }
]

Players에 값이 하나도 없다. 왜 그럴까?

그건 바로 Nationality와 Players에 foreign key가 무엇이다를 알려줘야 하는데 그것이 없었기 때문이다. 따라서 Nationality 의 List<Player> players를 아래와 같이 바꿔주면 조회가 된다.

@OneToMany(mappedBy = "nationality")
private List<Player> players; 

하지만 이렇게 하고 nationalityRepository.findAll(); 조회를 하면 아래와 같이 무한으로 json값이 생기므로 response 객체를 내려줄 땐 dto를 이용하자.

[
    {
        "id": 1,
        "name": "Korea",
        "players": [
            {
                "id": 1,
                "name": "Son",
                "nationality": {
                    "id": 1,
                    "name": "Korea",
                    "players": [
                        {
                            "id": 1,
                            "name": "Son",
                            "nationality": {

그럼 N+1 문제는 어떻게 해결할까?

우선 Player 의 ManyToOne에 fetchType을 Lazy로 줘 보자. 기본값은 Eager이다.

@ManyToOne(fetch = FetchType.LAZY)
private Nationality nationality;

@ManyToOne(fetch = FetchType.LAZY)
private Club club;

그리고 나서 조회를 하면 아래와 같은 오류가 발생하는데 이는 Lazy Loading으로 인해 아직 초기화되지 않은 Club 의 Entity 프록시(ByteBuddy를 이용함)를 Jackson이 직렬화 하려고 하기 때문에 Type definition error 가 발생하는 것이다.

"org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]\\n\\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:489)\\n\\tat org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:114)\\n\\tat org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.

따라서 일단 Club 에 @JsonIgnore 를 붙여주고 실행하면 에러 없이 N+1번 쿼리가 발생하는 것을 알 수 있다.

하지만 아직도 N+1 문제는 해결되지 않았다.

Fetch Join

NationalityRepository 에 다음과 같은 메서드를 추가하자.

@Query("select n from Nationality n join fetch n.players")
List<Nationality> findAllByFetchJoin();

이 방법을 통해 조회하면 쿼리가 한번만 나가는 것을 알 수 있다.

select
        n1_0.id,
        n1_0.continent,
        n1_0.name,
        p1_0.nationality_id,
        p1_0.id,
        p1_0.club_id,
        p1_0.name,
        p1_0.status 
    from
        nationality n1_0 
    join
        player p1_0 
            on n1_0.id=p1_0.nationality_id

players에 연관관계를 가진 club까지 불러오려면 아래와 같이 작성하면 된다.

@Query("select n from Nationality n join fetch n.players p join fetch p.club")
List<Nationality> findAllByFetchJoin();
select
        n1_0.id,
        n1_0.continent,
        n1_0.name,
        p1_0.nationality_id,
        p1_0.id,
        c1_0.id,
        c1_0.name,
        p1_0.name,
        p1_0.status 
    from
        nationality n1_0 
    join
        player p1_0 
            on n1_0.id=p1_0.nationality_id 
    join
        club c1_0 
            on c1_0.id=p1_0.club_id

Left Join을 하고 싶으면 left를 추가하면 된다,.

@Query("select n from Nationality n join fetch n.players p left join fetch p.club")
List<Nationality> findAllByFetchJoin();
select
        n1_0.id,
        n1_0.continent,
        n1_0.name,
        p1_0.nationality_id,
        p1_0.id,
        c1_0.id,
        c1_0.name,
        p1_0.name,
        p1_0.status 
    from
        nationality n1_0 
    join
        player p1_0 
            on n1_0.id=p1_0.nationality_id 
    left join
        club c1_0 
            on c1_0.id=p1_0.club_id