본문 바로가기
Side Projects/프로젝트 사이

5. 오류 해결 스토리 - Join Table 접근과 영속 상태

by devraphy 2022. 7. 27.

0. 개요

- 프로젝트 사이의 백엔드를 개발하면서 겪은 다양한 문제의 발생 과정과

   해결책을 찾아가는 사고 과정을 기록합니다.

 

 

1. 문제 설명

a) Entity 관계

- 프로젝트 사이는 3개의 Entity(Member, Event, Friend)로 구성된다.

- Event와 Friend는 모두 Member_id를 외래 키로 사용한다.

- 즉, Member를 기준으로 Event와 Friend 객체가 DB에 등록된다.

 

- Event는 다수의 Friend 객체를 보유할 수 있고, Friend는 다수의 Event에 소속될 수 있다.

- Event와 Friend는 ManyToMany의 관계를 가지며, 이는 Join Table 전략으로 관리한다.

 

@Entity @Getter
public class Event {

    @Id @GeneratedValue
    @Column(name = "event_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member owner;

    @NotNull
    private LocalDate date;

    @Enumerated(EnumType.STRING) @NotNull
    private EventPurpose purpose;

    @Column(name = "event_name") @Nullable
    private String name;

    @Enumerated(EnumType.STRING) @NotNull
    private EventEvaluation evaluation;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "event_participants",
            joinColumns = @JoinColumn(name = "event_id"),
            inverseJoinColumns = @JoinColumn(name = "friend_id"))
    private List<Friend> participants = new ArrayList<>();
}

 

 

b) 문제가 발생한 메서드

- Event Repository에는 Event의 참가자(= List<Friend>)를 이용하여 Event를 검색하는 메서드가 있다.

- 참가자(= Friend)는 다수의 Event에 소속되기 때문에, 이 메서드는 다수의 Event(= List<Event>)를 반환한다.

- 그러므로 다음과 같은 형태를 가진다.

 

public List<Event> findByParticipants(Member owner, List<Friend> friendList) {}

 

 

c) 문제의 원인 - 할 줄 모르는게 문제다.

- 처음에는 이 기능을 다음과 같이 구현하였다.

 

public List<Event> findByParticipants(Member owner, List<Friend> friendList) {
     return em.createQuery("select e from Event e where e.owner = :owner and e.participants in :friendList", Event.class)
                .setParameter("owner", owner)
                .setParameter("friendList", friendList)
                .getResultList();
}

 

- 그러나 Parameter value type not match 오류가 발생했다.

 

org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [projectsai.saibackend.domain.Friend@3e79bdf3] did not match expected type [java.util.Collection (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [projectsai.saibackend.domain.Friend@3e79bdf3] did not match expected type [java.util.Collection (n/a)]

 

- 오류 메세지를 읽어보니 Friend 객체와 Collection 객체를 비교하므로 데이터 타입이 맞지 않는다는 의미로 해석된다.

- 이는 JPA가 List<Friend> friendList 객체를 단순 Collection 객체로 인식한다는 것이다.

- 무엇을 놓치고 있을까? 

 

 

d) 문제의 원인 - 기본 개념의 중요성

- 문제의 해결책은 JPA의 기본 개념에 있었다. 

- JPA의 관점에서 e.participants는 영속 상태의 객체이므로 이는 다수의 Friend 객체로 처리된다. 

- 그러나 friendList는 비영속 상태의 객체이므로 일반적인 Java의 Collection 객체로 처리된다.  

- 즉, 이 둘을 비교하는 것 자체가 잘못된 시작점이다.

 

 

e) 사실은...

- 사실 이 포스팅을 작성하는 도중에 해당 오류(= type not match)가 발생한 이유를 뒤늦게 깨달았다.

- 아래에 작성한 문제 해결 과정은 이를 깨닫지 못한 채, 추론으로 해결책을 찾아가는 답답한 프로세스가 이어진다.

- 물 없이 고구마를 먹는 느낌일 것이므로 미리 양해를 구합니다. 

 

 

2. 문제 해결 과정

a) 문제 해결 과정(1) - For 반복문 사용

- 오류를 기반으로 추론한 해결책은 friendList에서 Friend 객체를 하나씩 꺼내오는 방법이다.

- 그렇다면 for 반복문을 사용하는데, 이는 반복 회수만큼 SELECT 쿼리가 발생하는 문제가 생긴다.

 

- 그래도 일단 아이디어를 시도해본다.

 

public List<Event> findByParticipants(Member owner, List<Friend> friendList) {
    List<Event> result = new ArrayList<>();
        for(Friend participant : friendList) { 
            result.add(em.createQuery("select e from Event e where e.owner = :owner and e.participants = :participant", Event.class)
                .setParameter("participant", participant)
                .setParameter("owner", owner)
                .getSingleResult());
    }
    return result;
}

 

- Test를 돌려보니 동일한 오류가 발생한다.

- 무엇이 문제일까, 다시 한번 고민했다.

 

 

b) 문제 해결 과정(2) -  Join Table에 접근하는 방법

- Join Table을 사용한다는 부분을 간과하고 있었다. 

- Event와 Friend는 Join Table에 의해 관리되는데, Event 테이블만 조회하고 있었다.

- Join을 사용하여 Join Table에 접근할 수 있다는 것을 찾아서, 바로 시도하였다.

 

- 이 방식으로 다음과 같은 쿼리가 도출된다. 

 

public List<Event> findByParticipants(Member owner, List<Friend> friendList) {
    List<Event> result = new ArrayList<>();
        for(Friend participant : friendList) {
            result.add(em.createQuery("select e from Event e " +
                            "join e.participants p " +
                            "where p.id = :friend_id and e.owner = :owner", Event.class)
                    .setParameter("friend_id", participant.getId())
                    .setParameter("owner", owner)
                    .getSingleResult());
        }
    return result;
}

 

 

- 처음으로 테스트가 성공하였다.

- 여기까지 데이터를 꺼내오는 과정은 올바르게 구성되었다는 것을 확신하였다. 

 

 

- 이제 For 반복문을 제거하면 쿼리가 완성된다. 

 

 

c) 문제 해결 과정(3) - For 반복문 제거

- 반복문을 제거하기 위해서는 결국 Collection 객체 간의 비교가 가능해야 한다.

- 위에서 작성한 쿼리에서 Friend 객체를 하나씩 추출하여 p.id와 비교하는 방법을 사용했다.

 

- 그렇다면 p.id가 아니라 p를 그대로 사용하여 비교하면 되지 않을까?라는 아이디어를 떠올렸다.

- 이를 구현하기 위해서는 IN 키워드 또는 이와 비슷한 기능을 하는 키워드를 사용해야 한다. 

- 주저하지 않고 바로 시도했다. 

 

public List<Event> findByParticipants(Member owner, List<Friend> friendList) {
    return em.createQuery("select e from Event e " +
                    "join e.participants p " +
                    "where e.owner = :owner " +
                    "and p in :friendList", Event.class)
            .setParameter("owner", owner)
            .setParameter("friendList", friendList)
            .getResultList();
}

 

- 또 한 번 테스트가 성공한 것을 확인하였다.

- 추론한 부분이 맞아떨어졌다. 이로써 또 하나를 배웠다. 

 

 

 

3. 요약 정리

a) Join Table에 접근하는 방법 - Join 사용

- Join Table에 접근하려면 Join Table을 구성하는 두 Entity를 Join 해야 한다. 

- 또는 Join Table을 구성하는 두 Entity 타입의 객체를 Join 한다.

  (Event 객체와 Event.participants를 Join 한 방법, Event.participants는 Friend 객체 타입이다)

 

 

b) Collection 객체 비교 - 영속 상태

- Entity의 속성으로 존재하는 Collection 객체 A와 조회 결과를 저장한 Collection 객체 B가 있다고 해보자.

- JPA는 Collection 객체 A와 B를 전혀 다르게 취급한다.

- 영속 상태가 다르기 때문이다.

 

- Collection 객체 A가 List<Friend> 타입의 객체다. 

- Collection 객체 A는 영속성 Context에 의해 관리되는 Entity 내부에 존재하는 객체로, 영속 상태의 객체이다.

- 그러므로 JPA는 이를 다수의 Friend 객체로 인식한다. 

 

- Collection 객체 B는 조회 메서드의 결과를 저장한 List<Friend> 타입의 객체다.

- Collection 객체 B는 일반적인 Java의 Collection 객체이므로, 비영속 상태의 객체다.  

- 그러므로 JPA는 이를 그냥 Collection 객체로 인식한다. 

 

 

c) 영속 상태의 객체와 비영속 상태의 객체를 비교하는 방법

- Collection 객체 A와 B를 비교하는 경우, 반드시 IN 연산자 또는 이와 비슷한 기능을 하는 연산자를 사용한다.

- 결국 객체 A는 JPA에 의해 하나씩 추출된다. 그러나 객체 B는 직접 추출해야 한다.  

- 아래의 예시를 참고하자. 

 

public List<Event> findByParticipants(Member owner, List<Friend> friendList) {
    return em.createQuery("select e from Event e " +
                    "join e.participants p " +
                    "where e.owner = :owner " +
                    "and p in :friendList", Event.class)
            .setParameter("owner", owner)
            .setParameter("friendList", friendList)
            .getResultList();
}

 

댓글