최근에 특정 서비스를 새로 런칭하기 위해서 부하테스트를 진행할 일이 있었습니다. 현재 팀에서 운영하는 서비스는 대부분 하루에 1천~1만건 사이의 호출이 들어오기 때문에 부하로 인한 이슈는 거의 없었습니다. 대부분 잘못된 쿼리로 인한 문제였습니다. 그러나 새로 런칭해야할 서비스는 쿼리는 간단한 대신에 1일간 10만건에서 100만건까지의 호출을 처리를 해야한다는 것이었습니다.
흔히말하는 네카라쿠배 같은 기업이나 특별한 스타트업, 혹은 매출/이익이 많은 서비스를 운영하는 기업들은 이 정도의 트래픽은 아무것도 아니라고 생각할 수도 있겠지만 저희 회사의 기준에서는 여러 제약이 있는 상황이기에 일반적인 인프라 환경에서 해당 호출은 상당히 부담될 수 있는 요소라고 생각되었습니다.
그래서 내부적으로 1차 목표로 잡은 5만건/시간을 달성하기 위해서 부하테스트를 진행하다가 제목과 같은 이슈를 발견했습니다. JPA를 처음 적용해보고 있는 상태였기 때문에 JPA의 특성을 잘 모르고 있어서 생긴 문제였습니다.
일반적으로 마이바티스를 쓰는 느낌으로 사용을 했기 때문에 테이블에 데이터를 insert를 하면, insert만 되어야했는데 이상하게 select를 한 번 하고 insert를 하고 있었습니다. 사용하고 있는 테이블에서는 JPA의 save 함수만 호출하고 있고, find 함수를 호출하지 않았는데도 말이죠.
소스 내부에서의 원인은 매우 단순했습니다.
JPA는 save()라는 함수를 통해서 데이터베이스에 데이터를 저장할 때, 기본적으로 isNew()라는 함수를 통해서 단순히 insert 만 할 것인가, merge를 할 것인가를 판단하고 있습니다. 이 때 merge로 넘어가는 경우에 데이터베이스에서 select를 한 번 더 조회를 하고 있었습니다.
즉, 계속 isNew가 False 였기 때문에 merge 함수를 타고 있었던 것이죠.
문제는 이 테이블에서 저희는 항상 새로운 데이터를 넣고 있었는데 왜 isNew 가 False 였을까요?
JPA의 isNew 라는 함수는 일반적으로 getId 라는 함수를 통해서 특정 데이터베이스와 매핑되는 클래스의 값이 새로운 값인지를 판단합니다. 그런데, 우리 테이블은 복합키 테이블이었기 때문에 id 컬럼을 사용하지 않습니다.
이 경우에는 isNew 가 항상 false를 리턴했습니다. 이 경우에는 무조건 select + insert가 일어나게 됩니다. 따라서 JPA 가 isNew를 실제로 새로 넣는 데이터에 대해 true 라고 리턴할 수 있도록 처리해야합니다.
첫 번째 방식은 무조건 적으로 isNew 를 상속받은 후에 true로 처리하는 방식을 고민해 볼 수 있었는데요. 이런 방식은 프로그램적으로 delete를 호출할 일이 절대 없는 테이블에서만 사용할 수 있습니다. 저렇게 사용했다가 delete 함수를 호출시에 내부적으로는 잘 처리되었다고 응답을 주는데 실제 데이터베이스를 까보면 데이터가 삭제되지 않습니다. delete 함수 호출시에도 isNew 를 확인하기 때문입니다. isNew = true이면 삭제를 하지 않고 그냥 돌려보냅니다.
두 번째 방식은 제대로 된 방식으로 isNew를 판단할 수 있도록 함수를 만들어주는 것입니다. 복합키를 사용할 때의 샘플 코드를 적어보겠습니다.
(1) 새로운 데이터임을 판별할 수 있는 키를 별도 클래스로 생성합니다. 여기에서는 testEntityId 로 하겠습니다.
(2) testEntity 클래스에서 Persistable을 상속해줍니다. 단 이 때, 제네릭 안에는 위에서 정의한 키 클래스를 적어줍니다.
(3) testEntity 클래스에서 getId 함수와 isNew 함수를 오버라이딩합니다.
(4) testEntityId 클래스에서 롬복으로 Builder 어노테이션 추가해줍니다.
(5) isNew 판별을 위해서 regDtms 를 자동으로 생성하는 방식 추가
<TestEntity >
@IdClass(testEntityId.class)
@EntityListeners(AuditingEntityListener.class)
public class testEntity implements Persistable<testEntityId> {
@Column
@Id
private String A;
@Column
@Id
private String B;
@CreatedDate
@Column
private LocalDateTime regDtms;
@Override
public testEntityId getId(){
return testEntityId.builder()
.A(A)
.B(B)
.build();
}
@Override
public boolean isNew() {
return getRegDtms() == null;
}
}
<TestEntityId>
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class testEntityId {
public String A;
public String B;
}
<JpaConfig>
@Configuration
@EnableJpaAuditing
public class JpaConfig {}
이렇게 처리한 후에 다시 insert를 실행해보면 정상적으로 isNew를 판단한 후에 insert만 하는 것을 확인할 수 있습니다.
'Java > Spring' 카테고리의 다른 글
WebClient Request, Response 통신데이터 확인하기 (1) | 2023.11.27 |
---|---|
Spring Boot, JPA로 만든 API 서비스의 성능 개선하기 (0) | 2023.05.08 |
JPA로 mssql(sql server)의 문자열 데이터 조회할 때 인덱스 적용 안됨 이슈 (0) | 2023.04.20 |
댓글