Search
Duplicate
πŸ—„οΈ

ν”„λ‘μ‹œ 객체가 μ‹€μ œ 값을 κ°€μ§€λŠ” μ‹œμ 

생성일
2022/08/18
νƒœκ·Έ
Spring
JPA

λ°°κ²½

저희 λͺ¨λ½ ν”„λ‘œμ νŠΈμ˜ μ„œλΉ„μŠ€ 둜직 개발 쀑, equals 둜 비ꡐ해야 ν•  둜직이 ν•„μš”ν–ˆμŠ΅λ‹ˆλ‹€. 쑰금 더 κ΅¬μ²΄μ μœΌλ‘œλŠ” νˆ¬ν‘œλ₯Ό μƒμ„±ν•œ ν˜ΈμŠ€νŠΈμ™€, μˆ˜μ •/μ‚­μ œλ₯Ό μš”μ²­ν•œ 멀버가 같은지 ν™•μΈν•˜λŠ” λ‘œμ§μ΄μ—ˆμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ findById 둜 객체λ₯Ό λΆˆλŸ¬μ„œ 비ꡐλ₯Ό ν•˜λ €κ³  ν•˜λŠ”λ° μ›ν•˜λŠ”λŒ€λ‘œ κ²°κ³Όκ°€ λ‚˜μ˜€μ§€ μ•Šκ³  계속 μΌμΉ˜ν•˜μ§€ μ•ŠλŠ”λ‹€λŠ” 응닡을 λ°›μ•˜μŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ equals 내뢀에 디버그λ₯Ό μ°μ–΄λ³΄λ‹ˆ μ΄μƒν•˜κ²Œ 계속 인자둜 받은 객체가 ν”„λ‘μ‹œ 객체둜 μ €μž₯λ˜μ–΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
JPA λŠ” ν”„λ‘μ‹œ 객체λ₯Ό μ˜μ†μ„± μ»¨ν…μŠ€νŠΈμ— μ €μž₯ν•˜κ³  이λ₯Ό λ‹€μ–‘ν•œ λΆ€λΆ„μ—μ„œ ν™œμš©ν•©λ‹ˆλ‹€. 이 덕뢄에 DB 컀λ„₯μ…˜μ„ ν•  λ•Œ λ°œμƒν•˜λŠ” μ„±λŠ₯ μ €ν•˜λ₯Ό 쀄이고, 이 κΈ°λŠ₯이 JPA 의 κ°€μž₯ κ°•λ ₯ν•œ κΈ°λŠ₯ 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ 쑰회 μ‹œ μ˜μ†μ„± μ»¨ν…μŠ€νŠΈμ— 쑰회λ₯Ό μ›ν•˜λŠ” 객체가 μ—†μœΌλ©΄ λ°˜λ“œμ‹œ DB λ₯Ό μ°”λŸ¬ ν•΄λ‹Ή 객체λ₯Ό 가지고 와야 ν•˜κ³ , 저희가 λ§Œλ“  λ‘œμ§μ—μ„œλŠ” ν•΄λ‹Ή 객체가 μ˜μ†μ„± μ»¨ν…μŠ€νŠΈμ— μ—†λ‹€κ³  νŒλ‹¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€. 이와 λ”λΆˆμ–΄ ν•΄λ‹Ή 객체λ₯Ό μ‘°νšŒν•˜λŠ” SQL ꡬ문이 λ‘œκ·Έμ— μ°ν˜”μŒμ—λ„ λΆˆκ΅¬ν•˜κ³  계속 ν”„λ‘μ‹œ 객체둜 λ‚¨μ•„μžˆμ—ˆκ³ , 이 κ³Όμ •μ—μ„œ equals λ‚΄λΆ€ 객체 비ꡐ λ‘œμ§μ—μ„œ false 둜 λ°˜ν™˜λ˜λŠ” κ²ƒμ΄μ—ˆμŠ΅λ‹ˆλ‹€.
λ„λŒ€μ²΄ λ¬Έμ œκ°€ 뭔지, 디버그λ₯Ό 찍어도 해결이 μ•ˆλ˜μ—ˆκ³  μ½”μΉ˜λΆ„λ“€κ»˜ 도움을 μš”μ²­ν•˜μ—¬ λ‹€ν–‰νžˆ ν•΄λ‹Ή 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이 κ³Όμ •μ—μ„œ μ•Œκ²Œλœ λ‚΄μš©κ³Ό ν•΄κ²° 방법에 λŒ€ν•΄ κ³΅μœ ν•˜κ³ μž ν•©λ‹ˆλ‹€.

κΈ°λ³Έ μ„ΈνŒ…

μ €ν¬λŠ” νˆ¬ν‘œμ™€ 멀버 도메인 μ‚¬μ΄μ—μ„œ λ°œμƒν•œ λ¬Έμ œμ˜€λŠ”λ°, ν•΄λ‹Ή λ¬Έμ œλŠ” 1:N μ—°κ΄€κ΄€κ³„μ—μ„œ λ°œμƒν•˜λŠ” 문제이기 λ•Œλ¬Έμ— 쑰금 더 보편적인 μ˜ˆμ‹œλ₯Ό λ“€κ³ μž ν•©λ‹ˆλ‹€. νˆ¬ν‘œλ₯Ό λ©€λ²„λ‘œ λ°”κΎΈκ³ , 멀버λ₯Ό νŒ€μœΌλ‘œ λ°”κΎΈμ–΄ ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜λ €κ³  ν•©λ‹ˆλ‹€. 즉 멀버:νˆ¬ν‘œ=1:N 의 관계λ₯Ό νŒ€:멀버=1:N 으둜 λ°”κΎΌ μ˜ˆμ‹œμž…λ‹ˆλ‹€.
@Entity @NoArgsConstructor @Getter public class Team { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Team team = (Team)o; return Objects.equals(getId(), team.getId()) && Objects.equals(getName(), team.getName()); } @Override public int hashCode() { return Objects.hash(id, name); } }
Java
볡사
비ꡐλ₯Ό μœ„ν•΄ equals λ₯Ό μž¬μ •μ˜ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
@Entity @NoArgsConstructor @Getter public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToOne(fetch = FetchType.LAZY) private Team team; public boolean isTeam(Team team) { return this.team.equals(team); } }
Java
볡사
저희가 λ°œμƒν–ˆλ˜ ν™˜κ²½κ³Ό λ™μΌν•˜κ²Œ ν•˜κΈ° μœ„ν•΄ 1:N @ManyToOne μ—°κ΄€ 관계λ₯Ό μ„€μ •ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
@Service @RequiredArgsConstructor public class MorakService { private final MemberRepository memberRepository; private final TeamRepository teamRepository; public boolean test(Long memberId, Long teamId) { Member member = memberRepository.findById(teamId).orElseThrow(); Team team = teamRepository.findById(memberId).orElseThrow(); return member.isTeam(team); } }
Java
볡사
member 와 team 을 μ‘°νšŒν•˜κ³  비ꡐ λ‘œμ§μ„ μˆ˜ν–‰ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

문제 지점 확인

문제 지점 확인을 μœ„ν•΄ ν…ŒμŠ€νŠΈλ₯Ό λŒλ ΈμŠ΅λ‹ˆλ‹€. 더미 λ°μ΄ν„°λŠ” λ‹€μŒκ³Ό 같이 λ„£μ—ˆμŠ΅λ‹ˆλ‹€.
Team
id
name
1
morak
Member
id
name
team_id
1
eden
1
@Test @DisplayName("같은 νŒ€μΈμ§€ ν™•μΈν•œλ‹€.") void test() { //given Long memberId = 1L; Long teamId = 1L; //when boolean isSameTeam = morakService.test(memberId, teamId); //then assertThat(isSameTeam).isTrue(); }
Java
볡사
ν…ŒμŠ€νŠΈλ₯Ό 돌리면 μ‘°νšŒν•œ νŒ€(id=1)κ³Ό 멀버에 μ €μž₯된 νŒ€(id=1)이 같은데 μ‹€νŒ¨ν•©λ‹ˆλ‹€.

κ³Όμ •

μ°¨κ·Όμ°¨κ·Ό μ„œλΉ„μŠ€ λ‘œμ§λΆ€ν„° 디버그λ₯Ό 돌렀 λ³΄μ•˜μŠ΅λ‹ˆλ‹€.
쑰회 이후 찍은 λ””λ²„κ·Έμ—μ„œ member 의 team κ³Ό team λ‘˜ λ‹€ ν”„λ‘μ‹œ 객체둜 μ„€μ •λ˜μ–΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€. member 의 team 은 member 쑰회 μ‹œ fetch type 을 lazy 둜 ν•΄μ„œ 이해가 λ˜μ—ˆμ§€λ§Œ, team 쑰회 μ‹œμ—λ„ team 이 ν”„λ‘μ‹œ 객체둜 μžˆλŠ” 것이 μ΄μƒν–ˆμŠ΅λ‹ˆλ‹€. 이 ν˜„μƒμ€ 일단 λ‚˜μ€‘μ— ν™•μΈν•œλ‹€ 해도, λ©€λ²„μ˜ νŒ€κ³Ό μ‘°νšŒν•œ νŒ€μ€ λ‘˜ λ‹€ ν”„λ‘μ‹œ 객체이고 같은 μ£Όμ†Œκ°’μ„ κ°€μ§€λŠ”λ° μ™œ false κ°€ λ‚˜νƒ€λ‚¬λŠ”μ§€ 이해가 λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.
λ‚΄λΆ€ 둜직으둜 디버그λ₯Ό 더 λ“€μ–΄κ°€ λ³΄μ•˜μŠ΅λ‹ˆλ‹€.
β€’
member.isTeam(team) λ‚΄λΆ€
ν˜„μž¬κΉŒμ§€λ„ member 의 team κ³Ό νŒŒλΌλ―Έν„°λ‘œ λ“€μ–΄μ˜¨ team λͺ¨λ‘ 같은 μ£Όμ†Œκ°’μ„ κ°€μ§€λŠ” λ™μΌν•œ ν”„λ‘μ‹œ κ°μ²΄μž…λ‹ˆλ‹€.
β€’
Override ν•œ team.equals(team) λ‚΄λΆ€
this 인 β€˜member 의 team’ 이 Team 객체둜 λ°”λ€Œμ—ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ νŒŒλΌλ―Έν„°λ‘œ λ“€μ–΄μ˜¨ team(λ³€μˆ˜λͺ… o)은 μ—¬μ „νžˆ ν”„λ‘μ‹œ κ°μ²΄μ˜€κ³ ,Β getClass() != o.getClass()Β μ—μ„œ false 둜 κ±ΈλŸ¬μ§„ κ²ƒμ΄μ—ˆμŠ΅λ‹ˆλ‹€.
μ–΄λ–»κ²Œ λ©”μ„œλ“œ 호좜이 ν”„λ‘μ‹œλ₯Ό μ‹€μ œ 데이터 객체둜 λ§Œλ“€κ²Œ λ˜λŠ” κ²ƒμΌκΉŒ?
ν”„λ‘μ‹œΒ κ°μ²΄μ˜Β λ©”μ„œλ“œκ°€Β ν˜ΈμΆœλ˜λ©΄Β ProxyConfigurationΒ λ‚΄λΆ€μ˜Β InterceptorDispatcher 클래슀의 interceptΒ λ©”μ„œλ“œκ°€Β λ¨Όμ € μ‹€ν–‰λ©λ‹ˆλ‹€.
public interface ProxyConfiguration { // ... interface Interceptor { @RuntimeType Object intercept(@This Object instance, @Origin Method method, @AllArguments Object[] arguments) throws Throwable; } class InterceptorDispatcher { @RuntimeType public static Object intercept( @This final Object instance, @Origin final Method method, @AllArguments final Object[] arguments, @StubValue final Object stubValue, @FieldValue(INTERCEPTOR_FIELD_NAME) Interceptor interceptor ) throws Throwable { if ( interceptor == null ) { // 1 if ( method.getName().equals( "getHibernateLazyInitializer" ) ) { return instance; } else { return stubValue; } } else { return interceptor.intercept( instance, method, arguments );// 2 } } } }
Java
볡사
1 μ—μ„œ interceptor κ°€ null 이 μ•„λ‹ˆκΈ°λ•Œλ¬Έμ— 2κ°€ μ‹€ν–‰λ©λ‹ˆλ‹€. ν•΄λ‹Ή interceptorλŠ” ProxyConfiguration.InterceptorΒ μΈν„°νŽ˜μ΄μŠ€μ—μ„œΒ μ„ μ–Έλœ intercept(instance,Β method,Β arguments)Β λ©”μ„œλ“œλ₯ΌΒ ν˜ΈμΆœλ©λ‹ˆλ‹€. ν•΄λ‹Ή interceptor μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•œ ν΄λž˜μŠ€λŠ” μ•„λž˜μ˜ ByteBuddyInterceptor μž…λ‹ˆλ‹€.
public class ByteBuddyInterceptor extends BasicLazyInitializer implements ProxyConfiguration.Interceptor { // ... public Object intercept(Object proxy, Method thisMethod, Object[] args) throws Throwable { Object result = this.invoke( thisMethod, args, proxy ); if ( result == INVOKE_IMPLEMENTATION ) { Object target = getImplementation(); final Object returnValue; try { if ( ReflectHelper.isPublic( persistentClass, thisMethod ) ) { if ( !thisMethod.getDeclaringClass().isInstance(target) ) { // ... } returnValue = thisMethod.invoke( target, args );// - 2 } // ... return returnValue; } catch (InvocationTargetException ite) { throw ite.getTargetException(); } } else { return result; } } protected final Object invoke(Method method, Object[] args, Object proxy) throws Throwable { String methodName = method.getName(); int params = args.length; if (params == 0) { // ... } else if (params == 1) { if (!overridesEquals && "equals".equals(methodName)) {// - 1 return args[0] == proxy; } else if (method.equals(setIdentifierMethod)) { initialize(); setIdentifier((Serializable) args[0]); return INVOKE_IMPLEMENTATION; } } } }
Java
볡사
interceptor() κ°€ 호좜되면 λ¨Όμ € invoke() λ©”μ„œλ“œλ‘œ equals λ₯Ό μ˜€λ²„λΌμ΄λ“œ ν–ˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. μ €ν¬λŠ” equals λ₯Ό μ˜€λ²„λΌμ΄λ“œ ν–ˆκΈ°λ•Œλ¬Έμ— 1번의 !overridesEquals() μ—μ„œ false κ°€ 되고 INVOKE_IMPLEMENTATION 을 λ¦¬ν„΄ν•©λ‹ˆλ‹€. 이후 getImplementation() 으둜 μ˜μ†μ„± μ»¨ν…μŠ€νŠΈμ—μ„œ μ‹€μ œ 데이터λ₯Ό 가진 객체λ₯Ό κ°€μ Έμ˜€κ³ , μ•žμ„œ ν˜ΈμΆœν•œΒ λ©”μ„œλ“œ(equals)κ°€ 2번의 thisMethod.invoke( target, args ); 둜 μ‹€ν–‰λ©λ‹ˆλ‹€.

ν•΄κ²° 방법

1. fetch type EAGER
지연 λ‘œλ”©μ„ ν•˜μ§€ μ•Šκ³  λ°”λ‘œ λΆˆλŸ¬μ˜€κΈ°λ•Œλ¬Έμ— 이후에 λΆˆλŸ¬μ˜€λŠ” team 도 ν”„λ‘μ‹œ 객체가 μ•„λ‹Œ μ‹€μ œ 객체가 λ©λ‹ˆλ‹€.
ν•˜μ§€λ§Œ νŒ€ λ‚΄λΆ€ 회의 κ²°κ³Ό, ꡳ이 맀번 EAGER 둜 뢈러올 ν•„μš”μ„±μ„ λŠλΌμ§€ λͺ»ν•˜μ—¬ λ‹€λ₯Έ λ°©μ•ˆμ„ μƒκ°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
2. find ν•˜λŠ” μˆœμ„œλ₯Ό λ°”κΎΌλ‹€.
Team team = teamRepository.findById(memberId).orElseThrow(); Member member = memberRepository.findById(teamId).orElseThrow();
Plain Text
볡사
team 을 λ¨Όμ € 뢈러 ν•΄λ‹Ή team 이 λ°”λ‘œ μ‹€μ œ 객체가 λ˜λ„λ‘ ν•©λ‹ˆλ‹€.
ν•˜μ§€λ§Œ 근본적인 해결책이 μ•„λ‹ˆκΈ°λ•Œλ¬Έμ— 맀번 μˆœμ„œλ₯Ό κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€. κ·Έλž˜μ„œ λ‹€λ₯Έ 방법을 또 μ°Ύμ•„λ³΄μ•˜μŠ΅λ‹ˆλ‹€.
3. equals λ₯Ό override ν•˜μ§€ μ•ŠκΈ°
ByteBuddyInterceptor 의 invoke() λ©”μ„œλ“œμ—μ„œ !overrideEquals κ°€ true κ°€ λ˜μ–΄ 두 객체 λͺ¨λ‘ ν”„λ‘μ‹œ 객체둜 남아 ν”„λ‘μ‹œ 객체끼리의 비ꡐ가 κ°€λŠ₯ν•©λ‹ˆλ‹€.
아무리 JPA κ°€ μ˜μ†μ„± μ»¨ν…μŠ€νŠΈλ₯Ό 톡해 동일성을 보μž₯ν•  수 μžˆλ‹€μ§€λ§Œ μ œμ–΄κΆŒμ΄ JPA 에 λ„˜μ–΄κ°€λŠ” 것 κ°™μ•˜μŠ΅λ‹ˆλ‹€.
4. equals override μ»€μŠ€ν…€ν•˜κΈ°
override ν•œ equals μ—μ„œ getClass() != o.getClass() 뢀뢄을 μ§€μ›Œμ£Όκ³  !(o instanceof Team) 을 λ„£μ—ˆμŠ΅λ‹ˆλ‹€. ν”„λ‘μ‹œ κ°μ²΄λŠ” 상속을 μ΄μš©ν•˜μ—¬ μƒμ„±λ˜κΈ° λ•Œλ¬Έμ— instanceof ν‚€μ›Œλ“œλ‘œ μΆ©λΆ„νžˆ λ§Œμ‘±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
public boolean equals(Object o) { if (this == o) { return true; } if (o == null !(o instanceof Team)) { return false; } Team team = (Team) o; return Objects.equals(id, team.getId()) && Objects.equals(name, team.getName()); }
Kotlin
볡사