티스토리 뷰
쿠버네티스 리소스의 status를 보고 Pod가 비정상 상태라면 데이터를 저장하고 비정상 상태가 지속 시간을 만족하면 알람이 발생되는 로직이 있었다.
쿠버네티스 리소스의 status와 Alarm은 일대일 관계이기 때문에, @OneToOne
관계로 설정을 했고 연관 관계 owner가 아닌 테이블에 mappedBy 설정으로 조회가 되게 설정 후 fetch = FetchType.LAZY
로 모두 설정해서 Lazy 로딩이 동작될 것으로 예상했지만, 예상햇던 대로 SQL query가 나가지 않는 이슈가 발생했다.
Entity에 대한 설정은 아래와 같이 설정했다.
Alarm Entity
@ToString(exclude = {"k8sEvent"})
@Entity
public class Alarm {
@Id
@UuidGenerator
@Column(name = "alarm_id")
private String id;
@Enumerated(EnumType.STRING)
private Severity severity;
private LocalDateTime fireTime;
private LocalDateTime clearTime;
private String probableCause;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id")
private K8sEvent k8sEvent;
protected Alarm() {
}
@Builder
public Alarm(Severity severity, LocalDateTime fireTime, LocalDateTime clearTime, String probableCause, K8sEvent k8sEvent) {
this.severity = severity;
this.fireTime = fireTime;
this.clearTime = clearTime;
this.probableCause = probableCause;
this.k8sEvent = k8sEvent;
}
public static Alarm from(K8sEvent k8sEvent, Severity severity) {
return Alarm.builder()
.severity(severity)
.probableCause(k8sEvent.getReason())
.fireTime(LocalDateTime.now())
.k8sEvent(k8sEvent)
.build();
}
}
K8sEvent Entity
@Getter
@ToString(exclude = {"alarm"})
@Entity
public class K8sEvent {
@Id
@UuidGenerator
@Column(name = "event_id")
private String id;
private String sourceId;
private String sourceName;
@Enumerated(EnumType.STRING)
private K8sType k8sType;
private String reason;
private LocalDateTime fireAtDateTime;
@OneToOne(mappedBy = "k8sEvent", fetch = FetchType.LAZY)
private Alarm alarm;
protected K8sEvent() {
}
protected K8sEvent(String sourceId, String sourceName, K8sType k8sType, String reason, LocalDateTime fireAtDateTime) {
this.sourceId = sourceId;
this.sourceName = sourceName;
this.k8sType = k8sType;
this.reason = reason;
this.fireAtDateTime = fireAtDateTime;
}
public static K8sEvent createNewK8sEvent(String sourceId, String sourceName, K8sType k8sType, String reason, LocalDateTime fireAtDateTime) {
return new K8sEvent(sourceId, sourceName, k8sType, reason, fireAtDateTime);
}
}
Alarm은 연관 관계의 주인으로 event_id
FK를 가지고 있고, K8sEvent에서는 mappedBy
로 연관된 Alarm을 조회할 수 있다. K8sEvent와 관련된 Alarm이 발생된 경우, 중복 알람이 발생되지 않도록 조치가 필요해서 mappedBy
로 선언해 함께 조회될 수 있게 했다. Alarm에서는 K8sEvent에 대해서 Lazy 로딩이 되도록 설정하고 마찬가지로 mappedBy
로 선언된 K8sEvent에서도 K8sEvent만 조회하거나 Alarm이 함께 조회가 필요한 경우 Lazy 로딩이 되도록 설정을 해둔 상태였다.
Alarm관련된 데이터만 필요한 경우 Alarm테이블의 데이터를 사용하고, Alarm 해제 시 K8sEvent를 조회해서 삭제하기 위해 사용했고, K8sEvent는 Event를 받아서 상태를 업데이트 하고, Polling을 통해서 K8sEvent에 대한 Alarm이 발생되도록 했다. K8sEvent에 대한 알람 발생 전에 Alarm 연관 엔티티를 조회해서 중복 알람이 있는지 여부를 확인하려고 했다.
하지만, 테스트 도중 K8sEvent를 조회하는 경우에 예상하지 못한 SQL Query들이 실행 되고 있었고, 속도가 저하되는 이슈가 있었다.
그래서 해당 현상을 좀 더 확인해보고자 아래 테스트 코드로 JPQL과 SQL을 조회해 봤다.
테스트 코드
결과를 @ToString
으로 조회하는데 연관 관계는 조회하지 않기 때문에, select query가 나가지 않도록 했다.
@SpringBootTest
@Transactional
class AlarmTest {
@PersistenceContext
private EntityManager em;
@Test
public void alarmRetrieveTest() throws Exception {
saveTestData();
em.flush();
em.clear();
Alarm findAlarm = em.createQuery("select a from Alarm a", Alarm.class)
.getSingleResult();
System.out.println("findAlarm = " + findAlarm);
}
private void saveTestData() {
K8sEvent k8sEvent = K8sEvent.createNewK8sEvent("podId1", "podName1", K8sType.POD, "KubePodNotReady",
LocalDateTime.now().plusMinutes(5));
em.persist(k8sEvent);
Alarm alarm = Alarm.from(k8sEvent, Severity.MINOR);
em.persist(alarm);
}
}
테스트 결과
목적이 query를 보는 것이었기 때문에 Assertions
로 검증하지 않고 간단하게 출력했다.
2024-06-17T23:01:49.674+09:00 DEBUG 6606 --- [ Test worker] org.hibernate.SQL :
/* select
a
from
Alarm a */ select
a1_0.alarm_id,
a1_0.clear_time,
a1_0.fire_time,
a1_0.event_id,
a1_0.probable_cause,
a1_0.severity
from
alarm a1_0
findAlarm = Alarm(id=48b51d0e-e573-4049-b7aa-6fdfcff6d5a2, severity=MINOR, fireTime=2024-06-17T23:01:49.533174, clearTime=null, probableCause=KubePodNotReady)
Alarm Entity를 조회하는 경우에는 Alarm Entity만 조회 되어서 출력되고 K8sEvent는 로딩이 안되는 것을 확인해 볼 수 있었다.
테스트 코드
이번에는 K8sEvent를 조회해봤다. (마찬가지로 연관 관계가 조회되지 않기 위해 출력하지 않도록 했다.)
@Test
public void k8sEventRetrieveTest() throws Exception {
saveTestData();
em.flush();
em.clear();
K8sEvent findK8sEvent = em.createQuery("select k from K8sEvent k", K8sEvent.class)
.getSingleResult();
System.out.println("findK8sEvent = " + findK8sEvent);
}
테스트 결과
2024-06-17T23:04:00.301+09:00 DEBUG 6617 --- [ Test worker] org.hibernate.SQL :
/* select
k
from
K8sEvent k */ select
ke1_0.event_id,
ke1_0.fire_at_date_time,
ke1_0.k8s_type,
ke1_0.reason,
ke1_0.source_id,
ke1_0.source_name
from
k8s_event ke1_0
2024-06-17T23:04:00.307+09:00 DEBUG 6617 --- [ Test worker] org.hibernate.SQL :
select
a1_0.alarm_id,
a1_0.clear_time,
a1_0.fire_time,
a1_0.event_id,
a1_0.probable_cause,
a1_0.severity
from
alarm a1_0
where
a1_0.event_id=?
findK8sEvent = K8sEvent(id=6ce0a619-724c-4fa8-aa3c-cc42d8579b09, sourceId=podId1, sourceName=podName1, k8sType=POD, reason=KubePodNotReady, fireAtDateTime=2024-06-17T23:09:00.151917)
select query가 K8sEvent에 대해서 먼저 나가고 그 다음 Alarm에 대한 select query가 나가고 있다. 분명 fetch = FetchType.LAZY
로 잘 설정을 해두었지만, Lazy 로딩으로 동작되지 않고 있었다.
그 이유를 좀 확인해보고자 리서치를 해봤는데,,,
Lazy fetching for one-to-one associations
Notice that we did not declare the unowned end of the association fetch=LAZY. That’s because:
not every Person has an associated Author, andthe foreign key is held in the table mapped by Author, not in the table mapped by Person.
Therefore, Hibernate can’t tell if the reference from Person to Author is null without fetching the associated Author.
On the other hand, if every Person was an Author, that is, if the association were non-optional, we would not have to consider the possibility of null references, and we would map it like this:
@OneToOne(optional=false, mappedBy = Author_.PERSON, fetch=LAZY) Author author;
An Introduction to Hibernate 6
To interact with the database, that is, to execute queries, or to insert, update, or delete data, we need an instance of one of the following objects: a JPA EntityManager, a Hibernate Session, or a Hibernate StatelessSession. The Session interface extends
docs.jboss.org
Hibernate 에서 하는 설명은 mappedBy
로 설정된 즉, FK owner가 아닌 Entity에서는 연관 관계에 있는 객체가 null인지 아닌지를 알 수 없기 때문에, Lazy로 설정을 해도 Eager 한번에 데이터를 가져올 수 밖에 없다는 의미이다.
해결 방법
1) Hibernate에서 작성된 방법 (Primary key 공유 + FK Not Null)
위 Hibernate Document에서도 알 수 있듯이, FK가 항상 존재하는 경우에는 이러한 이슈를 해결할 수 있는데, 그 방법은, mappedBy
위치 즉, FK owner가 아닌 Entity에 optional = false
설정을 해주고, @MapsId
를 설정해 줘서 이를 해결해 줄 수 있다는 얘기다.
하지만, 이미 테이블의 key는 서로 다른 값을 사용하도록 설정되어 있었고, FK가 not null constriant도 아니었기 때문에 위 방법은 사용할 수 없었다.
적용 방법은 아래와 같다.
@ToString(exclude = {"k8sEvent"})
@Entity
public class Alarm {
@Id
// @UuidGenerator
@Column(name = "alarm_id")
private String id;
@Enumerated(EnumType.STRING)
private Severity severity;
private LocalDateTime fireTime;
private LocalDateTime clearTime;
private String probableCause;
@OneToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "event_id")
@MapsId
private K8sEvent k8sEvent;
}
@ToString(exclude = {"alarm"})
@Entity
public class K8sEvent {
@Id
@UuidGenerator
@Column(name = "event_id")
private String id;
private String sourceId;
private String sourceName;
@Enumerated(EnumType.STRING)
private K8sType k8sType;
private String reason;
private LocalDateTime fireAtDateTime;
@OneToOne(optional = false, mappedBy = "k8sEvent", fetch = FetchType.LAZY)
private Alarm alarm;
}
Alarm의 Id를 할당하던 @UuidGenerator
가 주석처리 되어 있는데, 이는 Alarm이 K8sEvent의 Id를 사용하기 때문에 별도로 Id를 할당하면 안되기 때문이다. (주석을 해제해도 실행은 되지만, 마찬가지로 동일하게 같은 Id를 사용한다.)
테스트 코드
이번에는 K8sEvent를 먼저 출력하고, Alarm도 출력되게 했다.
@Test
public void k8sEventRetrieveTest() throws Exception {
saveTestData();
em.flush();
em.clear();
K8sEvent findK8sEvent = em.createQuery("select k from K8sEvent k", K8sEvent.class)
.getSingleResult();
System.out.println("findK8sEvent = " + findK8sEvent);
System.out.println("findK8sEvent.getAlarm() = " + findK8sEvent.getAlarm());
}
테스트 결과
2024-06-18T01:58:26.957+09:00 DEBUG 6895 --- [ Test worker] org.hibernate.SQL :
/* select
k
from
K8sEvent k */ select
ke1_0.event_id,
ke1_0.fire_at_date_time,
ke1_0.k8s_type,
ke1_0.reason,
ke1_0.source_id,
ke1_0.source_name
from
k8s_event ke1_0
findK8sEvent = K8sEvent(id=c78ecce6-9bcf-4410-8d01-a0c293dc6057, sourceId=podId1, sourceName=podName1, k8sType=POD, reason=KubePodNotReady, fireAtDateTime=2024-06-18T02:03:26.814882)
2024-06-18T01:58:26.965+09:00 DEBUG 6895 --- [ Test worker] org.hibernate.SQL :
select
a1_0.event_id,
a1_0.clear_time,
a1_0.fire_time,
a1_0.probable_cause,
a1_0.severity
from
alarm a1_0
where
a1_0.event_id=?
findK8sEvent.getAlarm() = Alarm(id=c78ecce6-9bcf-4410-8d01-a0c293dc6057, severity=MINOR, fireTime=2024-06-18T01:58:26.820097, clearTime=null, probableCause=KubePodNotReady)
K8sEvent를 먼저 출력하고 Alarm을 나중에 읽어오면서, Lazy 로딩이 잘 동작하고 있는 모습이다.
2) OneToMany나 ManyToOne관계로 변경
두번째 방법으로는 ManyToOne 이나 OneToMany 관계를 활용하는 것이다. List는 null이 아닌 빈 리스트로 표현이 가능하기 때문에, Hibernate가 Proxy객체로 감쌀 수 있다. 따라서 OneToOne에서의 이슈가 발생되지 않는다.
3) fetch join 사용
마지막 방법으로는 Lazy로 query를 2번 날리거나 Eager로 예상치 못한 query가 작동하게 하는 것이 아닌 fetch join을 사용하는 방법이다. K8sEvent를 조회할 때, Alarm도 같이 조회 되도록 fetch join을 사용해서 데이터를 가져오면 select query를 1번만 사용해서 조회를 할 수 있다. 하지만, 성능이 중요한 경우에는 Batch 사이즈 설정 혹은 수동으로 in 절을 작성해서 이를 최적화를 함께 해주는 것이 중요하다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] SpringBoot 3.X 버전 Querydsl 설정 (0) | 2024.06.18 |
---|
- Total
- Today
- Yesterday
- producer
- springboot3.x
- Prometheus Operator
- ServiceMonitor
- MySQL 외부 IP
- minikube node add
- Java 장단점
- DD파일
- kubernetes
- minikube
- 서버 클라이언트
- Java 란
- node add
- OneToOne
- 데스크톱 애플리케이션
- cpus
- WEB-INF
- 웹 애플리케이션
- 애노테이션 프로세서
- Servlet
- 특정 ip
- Servlet Container
- ExpectedException
- 애플리케이션 변화 과정
- Spring Cloud Stream
- consumer
- Java 특징
- docker-compose
- Kafka
- StreamBridge
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |