우테코/요즘카페

ID가 있는 엔티티를 save() 할때 발생하는 SELECT 쿼리 없애기

dev_kong 2023. 9. 27. 10:47
728x90
728x90

INSERT 하려면 SELECT 2번 나감

최근에 인덱스를 한번 정리하고 싶어서, 서버의 API로 요청이 들어올 때 발생하는 모든 쿼리를 리스트업 했었다.


그리고 다음은 새로운 유저가 OAuth를 통해 로그인 할 때 발생하는 쿼리들이다.

 

Member를 SELECT 하는 쿼리가 두번 발생한 이후에 INSERT 쿼리가 발생한다.

 

우리 서비스는 로컬 회원가입이 없다.
구글과 카카오를 통한 소셜 로그인만 가능한데,
사용자가 로그인 버튼을 누르고 사용자가 선택한 provider로 부터 사용자의 정보를 받는다.


위 정보를 통해 우리 서비스에 등록된 사용자라면 로그인을,
등록되지 않은 사용자라면 등록 후 로그인을 진행한다.

 

@Transactional  
public TokenResponse createAccessToken(final String code, final OAuthProvider provider) {  
    final MemberInfo memberInfo = provider.getUserInfo(code);  

    final Member member = memberRepository.findById(memberInfo.openId())  
            .orElseGet(() -> memberRepository.save(memberInfo.toMember()));  

    return new TokenResponse(jwtTokenProvider.createAccessFrom(member.getId()));  
}

우리 서버에 등록된 사용자인지 확인하기 위해 findBy() 를 한번 호출한다.
그렇기 때문에 첫번째 SELECT 는 이해가 되는데 두번째 SELECT는 왜 발생하는 걸까.

원인

repository의 save method를 뜯어보자.
CrudRepository의 구현체인 SimpleJpaRepositorysave()method를 확인해봤다.

 

@Transactional  
@Override  
public <S extends T> S save(S entity) {  

   Assert.notNull(entity, "Entity must not be null");  

   if (entityInformation.isNew(entity)) {  
      em.persist(entity);  
      return entity;  
   } else {  
      return em.merge(entity);  
   }
}

 

인자로 들어온 entityInformationisNew() 의 인자로 다시 entity를 넘겨서 결과(boolean)에 따라,
persist() 또는 merge 를 호출한다.

 

merge의 동작 순서

  • 1차 캐시에 해당 식별자가 있는지 확인하고 없으면 database 를 조회한다.(SELECT)
  • database 에 값이 있으면 해당 값을 받아오고,
    입력받은 test 객체의 값을 복사해서 database 에서 가져온 객체에 copy 를 한다.
  • 그런 뒤 db에 저장.(INSERT)

 

아무래도 INSERT 직전의 SELECT 두번은 persist가 아닌 merge가 호출 되기 때문인듯하다.


JPA가 save하려는 entity가 새로운 entity인지 아닌지 판단하는 기준이 무엇일까?(isNew())

디버깅을 찍고 하나씩 내려가봤다.


AbstractEntityInformation라는 추상 클래스에 정의된 isNew()메서드를 타는 것을 확인했다.

 

 

대충 entityidPrimitieve Type이 아닌 경우에는 idnull인 경우 새로운 entity라 판단을 하고,
idPrimitive Type인 경우에는 Number 의 인스턴스가 아닌경우에는 예외가 터진다.
그리고 Number의 인스턴스인 경우는 long type으로 변환한 뒤, 그 값이 0L인 경우에만 새로운 entity라고 판단한다.

 

위에서 말했든 우리 서비스는 로컬 회원 가입이 없다.
그리고 member enttiy의 경우 pk의 값을 auto_increament로 하는 것은 보안상 좋지 않다고 한다.
그렇기 때문에 우리는 member entitypk값을 각각의 provider 에서 받아온 OIDC를 그대로 이용하고 있다.

 

@Entity  
public class Member {  

    @Id  
    private String id;

    @Column(nullable = false)  
    private String name;  

    @Column(nullable = false)  
    private String image;
}

 

그렇기 때문에, 새로운 회원일지라도 id 값을 갖고 있고,
그렇기 때문에 항상 merge가 호출되어 불필요한 select문이 발생하는 것이다.

 

해결책

아까 위에서 본 AbstractEntityInformation 의 두개의 구현체와 하나의 확장 추상클래스가 있더라.

 

그중에 JpaMetamodelEntityInformationsuper 를 타고 Abstract~ 에 있는 isNew() 가 실행되었다.

 

 

그리고 남은 하나의 구현체는 JpaPersistableEntityInformation 이다.
이 구현체의 isNew() method를 확인해보면, 단순하게 entity의 isNew() method를 호출한다.

 

 

그리고 마지막으로 하나 남은 JpaEntityInformationSupport 를 확인 해보면,

 


domainClass(entity.class)가 Persistable 의 구현체이면 PersistableEntityInformation 이 반환되고,
아니라면, 현재 사용되고 있는 JpaMetamodelEntityInformation이 반환된다.
그리고 이 정적메서드(getEnitityInformation)는
SimpleJpaRepository의 생성자에서 사용된다.

 

 

그렇다면 우리가 사용하고 있는 Member Entity 클래스가 Persistable의 구현체가 된다면,
SimpleJpaRepository 필드의 EntityInformationPersistableEntityInformation이 할당 될거고,
save가 호출 될때, entityisNew 메서드가 호출되며, 그 결과값이 true를 반환한다면,
SELECT 없이 바로 INSERT 쿼리가 발생할 것으로 예상된다.

적용

이제 적용을 해보자.


Member Entity 에 이게 새로운 Entity인지 기존에 있던 건지 구분을 할 컬럼이 하나 필요하다.
boolean으로 하나 박을 수도 있긴한데,

너무 의미 없는 컬럼이 될것 같아서 CreatedAt을 사용하기로 했다.

 

기존에 다른 entity에서 사용하던 Base Entity가 있다.

@MappedSuperclass  
@EntityListeners(AuditingEntityListener.class)  
public abstract class BaseEntity {  

    @CreatedDate  
    @Column(nullable = false)  
    protected LocalDateTime createdAt;  

    public LocalDateTime getCreatedAt() {  
        return createdAt;  
    }
}

Member에 얘랑 Persistable 두개를 심어주자.

@Entity  
public class Member extends BaseEntity implements Persistable<String> {
// ...
    @Override  
    public boolean isNew() {  
        return Objects.isNull(createdAt);  
    }  

    @Override  
    public String getId() {  
        return id;  
    }
}

그리고 나서 다시 test를 돌려보면

한번의 SELECT 이후 바로 INSERT가 발생하는 것을 확인할 수 있다.

728x90
728x90