소개 배경
현재 개발중인 프로젝트 모각코에서 간단한 매핑 이슈가 발생하여 소개하고자 포스팅하게 되었다.
기본적인 DB 매핑개념인 1:1, 1:N, N:N에 대한 지식이 동반되면 이해가 빠를 것이다.
Trouble(문제 원인)
기본적으로 JPA를 사용할 때 Entity Own(사용하고자 하는 엔티티)와 Entity Geu(Mapping을 해야되는 대상)의 Mapping을 맺을 때 해당 관계의 주인을 지정해주어야한다.
현재 문제가 된 부분은 Ranking 시스템을 구축하던 중 Ranking과 MemberSocial Entity간의 이슈였다.
현재 모각코 중 Timer 서비스에서는 공부 시간을 기록하여 해당 시간(이하 Score)을 전부 초(Sec)로 환산하여 DB에 저장시킨다.
Ranking 서비스에서는 Timer Entity의 필드 값을 전부 가져와 MAP에 <MemberSocial,Long>형태로 회원 Entity를 Key값으로 Score를 합산한후 이를 Ranking Table에 저장시킨다.
또한 여기서 중복저장을 피하기 위해 Data JPA를 이용하여 회원 Entity로 객체를 찾아 해당 컬럼이 존재한다면 변경감지를 통한 update 로직을 구현했었으나, 해당 부분에서 NullPointerException를 일으키며 오류가 발생하게되었다.
원인 코드
NPE가 발생하게 된 이유는 MemberSocial이 Ranking Table에 존재하지 않기 때문이다. 하지만 나는 분명 매핑관계를 맺었고 그렇다면, 이는 논리적으로 말이 되지 않는 것이었다.
그렇게 생각하며 천천히 코드를 디버깅해보았다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Ranking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rankingSeq;
private int rank;
private long score;
@OneToOne(fetch = FetchType.LAZY,mappedBy = "ranking")//회원 EntityMapping
private MemberSocial memberSocial;
@OneToMany(fetch = FetchType.LAZY,mappedBy = "ranking")
private List<Timer> timers;
public void changeScoreInfo(Long changeInfoScore){
this.score=changeInfoScore;
}
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Builder
public class MemberSocial extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long member_seq;
... //중략
@OneToOne(fetch = FetchType.LAZY) // 랭킹 Entity 매핑
public Ranking ranking;
}
위는 현재 사용되는 Ranking과 MemberSocial Entity의 일부이다.
@Override
public void recodeTimeOfMemberRankingInit() {
Map<MemberSocial,Long> rankScore=new HashMap<>();
for (Timer t:timerService.getTimerAllInfo()){
rankScore.put(t.getMemberSocial(),rankScore.getOrDefault(t.getMemberSocial(),0L)+t.getDay_of_totalTime());
System.out.println("member="+t.getMemberSocial().getNickname());
System.out.println("RankScore="+rankScore.get(t.getMemberSocial()));
}
for (MemberSocial m:rankScore.keySet()){
System.out.println(m.getNickname());
Optional<Ranking> findR = rankingRepository.findByMemberSocial(m); //NPE 발생지점
if (findR.isEmpty()) {
rankingRepository.save(
Ranking.builder()
.memberSocial(m)
.score(rankScore.get(m))
.build()
);
}
else {
findR.get().changeScoreInfo(rankScore.get(m));
}
}
rankInitialize();
}
위는 에러를 발생시키던 Service Layer의 비즈니스 로직이다.
주석처리를 한 부분이 NPE가 발생하는 지점이었다. Error Message를 살펴보면, MemberSocial을 찾을 수가 없다고 뜬다.
그렇게 로직에서 문제점을 찾던 와중 문득 Entity 매핑부분에서 잘못된 점을 발견하였다.
원래 Ranking의 요구사항을 이해하고 최초 코드를 작성하였을 때 1:N 관계로 Entity를 Mapping하였었는데, 이후 요구사항을 수정하며 관계를 수정할 때, 이전 작성하였던 mappedBy가 문제가 되었다.
맨 처음 말할때 Entity를 매핑할 때에는 주인관계가 있어야한다고 말했고 이는 1:1 관계에서 양방향 참조를 사용할 경우에도 마찬가지이다.
현재 마이페이지와 랭킹 시스템에서 자신의 순위를 볼 수 있도록 설계하기 위해 양방향 참조를 사용하였는데, Ranking Entity에서만 mappedBy를 사용하여 Ranking Entity는 종속 엔티티, MemberSocial이 주인 엔티티로 설정되었다.
이 경우 MemberSocial에서는 참조가 가능하지만, Ranking에서는 참조가 불가능하여 컴파일 단계에서 Error를 발생시키지는 않지만, 실제 DB에서 해당 값을 찾을 때 참조가 불가하여 NPE를 발생시키게 되었다.
Trouble Shooting(문제 해결)
우선 회원이 주가 되어야 생각하여 연관관계의 주인을 Member Entity로 선정하였다.
💡 MemberSocial Entity가 주가 되어야 하는 이유
회원이 탈퇴 혹은 서버상의 장애로 삭제될 경우(무조건적으로 DB에서 없어진다는 가정)에 랭킹은 그 빈자리를 채워야한다. 반대로 생각하여 랭킹이 주인관계를 가지고 있을 경우 랭킹시스템을 폐지한다고 해서 회원이 사라지지는 않기 때문에 회원이 주인 엔티티가 되어야함
그에 따라 Entity의 매핑 관계를 간단히 수정하여주었다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Ranking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rankingSeq;
...
@OneToOne(fetch = FetchType.LAZY,mappedBy = "ranking",cascade = CascadeType.ALL)
private MemberSocial memberSocial;
}
우선 Ranking의 경우 mappedBy를 유지함으로서 종속 엔티티임을 선언하고, cascade를 사용해 Member Entity가 지워질 경우 매핑되는 Ranking Entity 또한 삭제한다.
또한 MemberEntity에서는 아래와 같이 @JoinColumn 어노테이션을 이용하여 양방향 참조를 사용할 수 있도록 구현하였다.
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rankingSeq")
public Ranking ranking;
결론
JPA에 관한 트러블 슈팅, 심지어 매핑과 같은 아주 간단한 이슈를 작성할 때마다 매번 느끼는 거지만 익숙할수록 잊혀지기 쉬운 것같다.
기초를 다시 천천히 다지는 시간이 필요하다고 느낀다
'Trouble-Shooting' 카테고리의 다른 글
Jackson InvalidDefinitionException (0) | 2023.05.17 |
---|---|
NoClassDefFoundError, Command is too long (0) | 2023.04.23 |
[JPA] Service Layer 간의 순환 참조 (1) | 2023.04.14 |
Refresh Token Rotation 이슈 해결 (0) | 2023.03.17 |
406 Error - Not Acceptable (0) | 2023.03.13 |