Java의 Class는 Equals 또는 HashCode로 서로 동일한지 판단할 수 있다. 만약 단순히 new 생성자로 Class를 생성 후 ==비교하게되면 서로 다른 주소값을 가지기때문에 두 Class는 다르다고 판단한다. 또한 Class 객체는Equals로 비교할 경우에도 주소값을 사용하여 비교한다. 그리고 hashcode는 객체의 주소값을 해싱한 값(해시 코드)이므로 아래처럼 새로운 Class를 생성하게되면 서로 다른 hashcode를 가진다.
public class Dummy {
private final int a;
private final int b;
public Dummy(int a, int b) {
this.a = a;
this.b = b;
}
}
public class Main {
public static void main(String[] args) {
Dummy dummy1 = new Dummy(1, 2);
Dummy dummy2 = new Dummy(1, 2);
System.out.println(dummy1 dummy2); //false
System.out.println(dummy1.equals(dummy2)); //false
System.out.println(dummy1.hashCode()); //1149319664
System.out.println(dummy2.hashCode()); //2093631819
}
}
그렇기 때문에 Class들이 서로 동일한지 비교하기위해서 Equals와 HashCode를 Override(재정의)한다.
public class Dummy {
private final int a;
private final int b;
public Dummy(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public boolean equals(Object o) {
if (this o) return true;
if (!(o instanceof Dummy that)) return false;
return this.a that.a && this.b that.b;
}
@Override
public int hashCode() {
return Objects.hash(a, b);
}
}
public class Main {
public static void main(String[] args) {
Dummy dummy1 = new Dummy(1, 2);
Dummy dummy2 = new Dummy(1, 2);
System.out.println(dummy1 dummy2); //false
System.out.println(dummy1.equals(dummy2)); //true
System.out.println(dummy1.hashCode()); //994
System.out.println(dummy2.hashCode()); //994
}
}
==은 여전히 주소값을 사용하여 비교하기때문에 생성된 Class들을 서로 다르다고 판단하지만 Equals, HashCode는 재정의함으로써 Class들이 서로 같다고 판단하게된다.
IMPORTANT Equals와 HashCode는 같이 재정의(Override)되어야한다.
GC에도 영향을 줄까?
그렇다면 Garbage Collector(GC)도 Equals, HashCode를 재정의한 것에 따라 동작이 달라질까?
테스트를 위해 먼저 간단한 Spring Boot Application을 사용했고 JMeter, jstat을 사용하여 부하 테스트 및 모니터링을 수행했다.
테스트 환경
JDK 17(corretto) / G1 GC
Mac M1
Spring Boot 2.7.18
Heap Size: 1024MB
Application
먼저 테스트에 사용된 Application은 사용자와 은행 계좌 Entity가 있으며 사용자의 계좌를 조회하는 request를 모니터링헀다. 이때 DTO 객체 2개와 Entity 객체 2개가 사용되며 조회한 사용자의 계좌는 총 3개가 존재한다.
//사용자
@Entity
public class AccountUser {
@Id
@GeneratedValue
private Long id;
private String name;
}
//계좌
@Entity
public class Account {
@Id
@GeneratedValue
private Long id;
@ManyToOne
private AccountUser accountUser;
...
}
//계좌 DTO
public class AccountDto {
private final Long userId;
private final String accountNumber;
...
}
//계좌 정보(계좌 번호, 잔고) DTO
public class AccountInfo {
private final String accountNumber;
private final Long balance;
}
부하테스트를 위해 JMeter를 사용하였고 아래처럼 설정했다. 즉, 500명의 사용자가 1초간 2번씩 요청하도록 설정했다.
jstat
jstat을 사용하여 GC 모니터링을 수행했고 -gc command를 사용했다. 또한 jcmd로 GC.run, GC.run_finalization을 실행시킨 후 테스트를 수행했다.
jstat -gc {PID} 500
S0C: Survivor 0의 용량
S1C: Survivor 1의 용량
S0U: Survivor 0의 사용량
S1U: Survivor 1의 사용량
EC: 현재 Eden의 용량
EU: Eden의 사용량
OC: 현재 Old의 용량
OU: Old의 사용량
MC: Metaspace의 용량
MU: Metaspace의 사용량
CCSC: Compressed class의 용량
CCSU: Compressed class의 사용량
YGC: young generation GC 횟수
YGCT: young generation의 GC 시간
FGC: full GC 횟수
FGCT: full GC 시간
GCT: 총 GC 시간
equals, hashcode를 재정의하지 않은 경우
JMeter 첫 실행
Eden 영역: +139.264 KB
첫 Young GC
Survivor1: +6548.9 KB
Metaspace: +4931.4 KB
Compressed Class: +741.2 KB
equals, hashcode를 재정의한 경우
JMeter 첫 실행
Eden 영역: +132,096 KB
첫 Young GC
Survivor1: +6546.5 KB
Metaspace: +4850.5 KB
Compressed Class: +710.8 KB
결과
결론적으로 GC와 Equals, HashCode는 관련이 없었다. 위 jstat 결과처럼 각 영역에 증가하는 크기도 큰 차이가 없었다. 단지 Eden 영역에는 매번 새로운 객체가 쌓이게되고 EC만큼 다차게되면 YGC로 인해 살아남은 객체는 S1으로 옮겨진다. 왜 매번 새로운 객체를 생성하여 메모리에 쌓을까라는 질문을 던져보았을 때 상수를 생각해보면 될 것같다. 상수처럼 immutable하며 여러 곳에 공유하여 사용할 경우 Native Memory Heap 저장하여 GC 대상에서 벗어날 수 있지만 mutable하며 공유하면 안되는 객체는 어쩔 수 없이 Java Heap에 저장될 수 밖에 없다. (Java 8 이후 기준)
그렇다면 static class로 정의한다면 어떻게 될까?
public class AccountOuterDto {
public static class AccountDto {
private final Long userId;
private final String accountNumber;
...
}
public static class AccountInfo {
private final String accountNumber;
private final Long balance;
...
}
}
바로 결과를 확인해보면 다음과 같이 메모리를 좀 더 소비하는 모습을 볼 수 있었다. 결국 메모리에 쌓이는 것은 새로운 객체이며 static class로 정의한다고 메모리를 더 절약하지는 못했다. class의 meta data는 여전히 metaspace에 저장될 뿐...
후기
의미가 없는 테스트라고 생각이 들긴하지만 이 테스트로 인해서 GC와 메모리, Object와의 관계에 대해서 더 깊이 생각할 수 있었던 것 같다. 재밌는 결과를 얻지 못해서 아쉽네...