Lombok의 @EqualsAndHashCode, 당신의 JPA 성능을 망칠 수도 있똬
분명 Lombok의 애너테이션을 사용하는 것은 편리한 코드 자동 생성을 제공하지만, 성능 및 메모리 사용 측면에서 예상치 못한 문제가 발생할 수 있다는 것을 알고 계신가요???? (아니면 아직 경험을 못 해봤거나..)
처음 Lombok을 도입했을 때, 코드의 양을 줄여주고, 수동으로 일일히 보일러 플레이트 코드를 작성할 필요 없이 간단하게 해결되는 점이 너무 편리하고 좋았습니다. 물론 지금도 좋아하구요 👍👍
하.지.만 실제 프로젝트에서 여러 연관 관계를 가진 엔티티들을 다루다 보니, 예상치 못한 성능 저하나 무한 재귀 호출 등의 문제가 발생하기 시작했습니다.
특히 Lazy 로딩 필드에서 의도치 않게 데이터베이스 조회가 일어나는 경험을 하면서 무조건적으로 롬복 어노테이션을 사용하는 것이 위험할 수 있음을 깨닫게 되었죠..
이 글에서는 롬복의 여러 어노테이션 중 특히 @EqualsAndHashCode
사용 시 발생할 수 있는 주요 문제점들과 이를 해결하기 위한 대안에 대해 알아보겠습니다
1. @EqualsAndHashCode의 문제점
@EqualsAndHashCode
는 분명 편리한 자동 코드 생성을 가능하게 하지만, JPA와 결합할 때 몇 가지 주요 문제점들이 발생합니다. 이를 아래에서 자세히 살펴보겠습니다.
1.1 Lazy 로딩 및 연관 관계 문제
@EqualsAndHashCode
는 모든 필드를 기반으로 equals
와 hashCode
메서드를 자동으로 생성합니다.
하지만 JPA 엔티티에서 연관 관계가 있는 필드를 사용할 경우, 예상치 못한 Lazy 로딩이 발생하여 성능에 큰 영향을 줄 수 있습니다.
문제 설명
JPA 엔티티에서 연관 관계를 가지는 필드를 @ManyToOne
이나 @OneToMany
로 선언할 때, 이를 Lazy 로딩 전략으로 설정하는 것이 일반적입니다
그러나 @EqualsAndHashCode
애너테이션이 연관된 필드를 참조할 경우, 해당 필드가 아직 초기화되지 않았다면 데이터베이스 조회가 발생합니다.
이는 의도치 않은 SQL 쿼리를 유발하고, 성능 저하의 원인이 될 수 있습니다.
예시 코드
@EqualsAndHashCode
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
}
위 예시에서 Order
클래스의 customer
필드는 Lazy 로딩됩니다.
그러나 @EqualsAndHashCode
가 적용되면 equals
나 hashCode
메서드 호출 시 customer
필드를 참조하게 되어 의도치 않은 데이터베이스 조회가 발생할 수 있습니다.
1.2 StackOverflowError 발생 위험
양방향 연관 관계에 있는 엔티티들에 @EqualsAndHashCode
를 사용하면, 무한 재귀 호출로 인해 StackOverflowError
가 발생할 수 있습니다.
문제 설명
양방향 연관 관계가 설정된 엔티티들은 서로를 참조하게 됩니다. 이때 @EqualsAndHashCode
가 적용된 필드에서 서로를 계속 참조하는 구조가 되어, 무한 재귀 호출이 발생합니다.
이로 인해 애플리케이션은 StackOverflowError
로 인해 비정상 종료될 수 있습니다.
예시 코드
@EqualsAndHashCode
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> children;
}
@EqualsAndHashCode
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
}
위 코드에서 Parent
와 Child
는 서로 양방향 연관 관계를 가지고 있으며, 양쪽에 @EqualsAndHashCode
가 적용되어 있다면 equals
및 hashCode
메서드에서 무한 재귀가 발생합니다.
이는 결과적으로 StackOverflowError
를 유발하게 됩니다.
1.3 메모리 과다 사용 문제
모든 필드를 사용하여 hashCode
를 생성하는 경우, 엔티티의 크기가 크다면 메모리 사용량이 증가하게 됩니다. 이는 특히 대규모 엔티티나 복잡한 객체 구조를 가진 경우 문제를 일으킬 수 있습니다.
문제 설명
JPA에서는 엔티티의 hashCode
와 equals
메서드가 영속성 컨텍스트에서 객체를 비교하고 식별하는 데 중요한 역할을 합니다.
모든 필드를 hashCode
생성에 사용하면 필드의 데이터 크기에 비례하여 메모리 사용량이 급격히 증가하게 됩니다.
예시 코드
@EqualsAndHashCode
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private String description;
private BigDecimal price;
private LocalDateTime createdAt;
...
}
위 코드에서 모든 필드를 사용하여 hashCode를 생성하는 경우, Product
객체가 크다면 메모리 과다 사용 및 성능 저하를 초래할 수 있습니다.
이는 특히 해당 객체들이 컬렉션에 포함되어 있을 때 문제를 더욱 심화시킵니다.
2. 대안 및 해결 방안
@EqualsAndHashCode
사용 시 발생할 수 있는 위와 같은 문제를 해결하기 위해, 다음과 같은 대안을 고려할 수 있습니다.
2.1 식별자 필드만 사용
@EqualsAndHashCode
대신, 엔티티의 고유 식별자 필드(예: id
)만을 기준으로 equals
와 hashCode
메서드를 작성하는 것이 좋습니다.
이를 통해 불필요한 필드 참조로 인한 성능 저하를 방지할 수 있습니다.
예시 코드
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Entity
public class Product {
@Id
@GeneratedValue
@EqualsAndHashCode.Include
private Long id;
private String name;
private String description;
}
2.2 Lombok의 @EqualsAndHashCode 옵션 활용
Lombok의 @EqualsAndHashCode
애너테이션에 onlyExplicitlyIncluded = true
옵션을 사용하고, 필요한 필드에 @EqualsAndHashCode.Include
를 적용하는 방식도 추천됩니다.
이를 통해 특정 필드만 포함하여 equals
와 hashCode
를 정의할 수 있습니다.
예시 코드
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Entity
public class Order {
@Id
@GeneratedValue
@EqualsAndHashCode.Include
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
}
위 코드에서는 onlyExplicitlyIncluded = true
를 사용하여 id
필드만 equals
와 hashCode
에 포함시킵니다.
이를 통해 Lazy 로딩 필드를 참조하는 문제나 불필요한 메모리 사용 문제를 피할 수 있습니다.
3. 결론
JPA 엔티티에서 Lombok의 @EqualsAndHashCode
애너테이션을 사용할 때는 Lazy 로딩 필드 참조, 양방향 연관 관계로 인한 StackOverflowError
, 메모리 과다 사용 등의 문제에 주의해야 합니다.
이러한 문제를 방지하기 위해 식별자 필드만 사용하거나, Lombok의 옵션을 적절히 활용하여 불필요한 필드를 제외하는 것이 바람직합니다.
애플리케이션의 성능과 안정성을 보장하려면, @EqualsAndHashCode
의 사용을 신중하게 검토하고 필요한 경우 커스텀 메서드를 작성하는 것을 고려하는 것이 좋습니다.