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();
}
'Side Projects > 프로젝트 사이' 카테고리의 다른 글
7. 오류 해결 스토리 - constraint ["PUBLIC.UK_MBMCQELTY0"] (0) | 2022.07.30 |
---|---|
6. 오류 해결 스토리 - Update/delete queries cannot be typed (0) | 2022.07.30 |
4. DB 구조 설계 - ERD (0) | 2022.06.29 |
3. 요구사항 정의 및 개발 명세서 작성 (0) | 2022.06.11 |
1. 로고 제작기 (0) | 2022.06.09 |
댓글