티스토리 뷰

문제 상황


사용자 회원가입하는 API 의 쿼리를 찍어보면 아래와 같이 Select 문장이 두 개 + Insert 문 하나가 찍히는 것을 확인할 수 있다. 

2024-08-23T11:22:30.710+09:00 DEBUG 148 --- [moongge] [nio-8080-exec-2] org.hibernate.SQL                        : 
    /* <criteria> */ select
        ue1_0.userId 
    from
        UserEntity ue1_0 
    where
        ue1_0.userId=? 
    limit
        ?

2024-08-23T11:22:41.611+09:00 DEBUG 148 --- [moongge] [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        ue1_0.userId,
        ue1_0.badgeList,
        ue1_0.birth,
        ue1_0.fcmToken,
        ue1_0.group_code,
        ue1_0.intro,
        ue1_0.nickname,
        ue1_0.password,
        ue1_0.profileImage,
        ue1_0.userName,
        ue1_0.userType 
    from
        UserEntity ue1_0 
    where
        ue1_0.userId=?
        
2024-08-23T11:27:16.468+09:00 DEBUG 148 --- [moongge] [nio-8080-exec-2] org.hibernate.SQL                        : 
    /* insert for
        com.narsha.moongge.entity.UserEntity */insert 
    into
        UserEntity (badgeList, birth, fcmToken, group_code, intro, nickname, password, profileImage, userName, userType, userId) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

 

회원가입 등록하는 로직을 확인해보면

@Override
public UserDTO register(UserRegisterDTO userRegisterDTO) {

    // 중복된 유저 있을 때
    Optional<UserEntity> existingUser = userRepository.findByUserId(userRegisterDTO.getUserId());
    if (existingUser.isPresent()) {
        throw new RegisterException(ErrorCode.DUPLICATE_ID_REQUEST);
    }

    UserEntity user = UserEntity.builder()
            .userId(userRegisterDTO.getUserId())
            .userType(userRegisterDTO.getUserType())
            .password(userRegisterDTO.getPassword())
            .userName(userRegisterDTO.getName())
            .build();

    UserEntity savedUser = userRepository.save(user);

    return UserDTO.mapToUserDTO(savedUser);
}

첫 번째 select 문은 중복된 유저를 확인하기 위해 날아간 select임을 알 수 있지만 

두 번째 select 문은?

 

디버깅 결과 userRepository.save() 에서 날아가는 것을 확인할 수 있다. 

 

문제 원인


 

지금 유저를 저장할 때 UserRepository는 스프링 데이터 JPA의  save를 활용하고 있다. 

 

보통 id값을 auto increament로 지정하게 되면 insert할 때 id값이 생성이 된다. 

하지만 지금의 경우는, insert하기 전에 id값이 생성이 된다.

UserEntity를 빌드를 할 때 유저 아이디를 할당하는 것을 볼 수 있다. 

 

문제는 Spring Data JPA를 통해 save를 할 때는, @Id 필드에 값이 설정된 것을 보고,

이 엔티티가 이미 데이터베이스에 존재하는지 확인하기 위해 'SELECT' 쿼리를 실행합니다. 

 

이 경우는 새로운 유저를 저장하는 경우이므로 당연히 데이터베이스에 저장된 것이 없기 때문에 'INSERT' 문이 날아가는 것이다. 

 

문제  해결


우선 Spring Data JPA를 통해 save가 어떻게 처리하는지 확인해 보자

@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);
	}
}

새로운 엔티티가 save()를 호출할 경우 =>  isNew(entity)가 true => persist 가 동작,

아니라면 merge 가 동작한다. 

 

우리는 새로운 유저를 저장할 때 isNew 조건을 타게 하면 되는데,

isNew이 true가 되는 기준을 @Id 필드를 기준으로 판단하는 것이 아닌, 다른 기준에 따라 판단하고자 합니다. 

  • 영속성 컨텍스트에서 조회되지 않았을 때
  • 영속 상태가 되기 전

그러기 위해서는 'Persistable' 인터페이스를 활용해 엔티티의 'isNew'조건을 커스텀마이징 할 수 있습니다. 

 

isNew 라는 상태 값을 하나 만들고 default 값을 true로 줍니다. 

=> 처음 객체를 만들 때는 isNew 조건을 타게 될 것입니다. 

 

그다음 우리가 원하는 기준들을 위해 어노테이션을 활용해 구현해 줄 건데,

영속성 컨텍스트에서 조회되지 않았을 때를 판단하기 위해서는 영속성 컨텍스트에서 조회가 될 경우 isNew 를 false로 바꿔줍니다. 

=> 이를 위해 @PostLoad 어노테이션을 활용할 수 있습니다. 

 

영속 상태가 되기 전을 판단하기 위해서는 영속 상태가 되기 직전에 isNew를 false로 바꿔줍니다. 

=> 이를 위해서는 @PrePersist 어노테이션을 활용할 수 있습니다. 

 

마지막으로 Persistable 인터페이스를 구현을 마무리하기 위해 isNew(), getId() 메서드를 오버라이드해주면 끝입니다

아래 실제 코드로 확인해 보겠습니다.

@Getter
@Entity
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity implements Persistable<String> {

    @Id
    @Column(nullable = false, length=100)
    @JoinColumn(name = "user_id")
    private String userId; // 유저 id
	...
    
    @Transient 
    @Builder.Default 
    private boolean isNew = true;
    
    @PostLoad  
    @PrePersist 
    private void markNotNew() {
        this.isNew = false;
    }
    
    @Override 
    public String getId() {
        return this.userId;
    }

    @Override 
    public boolean isNew() {
        return this.isNew;
    }
}

++ 현재 예제에서는 Builder 패턴을 이용해 객체를 생성하고 있습니다. => 빌더를 통해서도 isNew = true를 적용하기 위해 @Builder.Default를 활용했습니다. 

 

확인


다시 회원가입 API를 통해 쿼리를 확인해 보면 새로운 유저를 생성할 때는 INSERT만 나가는 것을 확인할 수 있습니다. 

++ 첫 번째 쿼리는 유저 아이디 중복 확인용 쿼리입니다. (따로 추가한 것임)

2024-08-23T15:00:24.694+09:00 DEBUG 6040 --- [moongge] [nio-8080-exec-1] org.hibernate.SQL                        : 
    /* <criteria> */ select
        ue1_0.userId 
    from
        UserEntity ue1_0 
    where
        ue1_0.userId=? 
    limit
        ?
        
2024-08-23T15:00:25.092+09:00 DEBUG 6040 --- [moongge] [nio-8080-exec-1] org.hibernate.SQL                        : 
    /* insert for
        com.narsha.moongge.entity.UserEntity */insert 
    into
        UserEntity (badgeList, birth, fcmToken, group_code, intro, nickname, password, profileImage, userName, userType, userId) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

 

 

 

 

Reference

https://kapentaz.github.io/jpa/Spring-Data-JPA%EC%97%90%EC%84%9C-insert-%EC%A0%84%EC%97%90-select-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0/#

 

Spring Data JPA에서 Insert 전에 Select 하는 이유

이전 글에서 bulk insert 처리를 할 수 있는 방법 중에 하나가 @Id 값 알고 있는 경우라고 했었는데요. 실제로 잘 되는지 확인하는 과정에 발생한 문제 내용을 공유합니다.

kapentaz.github.io

https://ttl-blog.tistory.com/191

 

[JPA] 리스너 - 엔티티의 생명주기에 따른 이벤트 처리

리스너 모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 하는 요구사항이 있다고 가정하자. 이때 애플리케이션 삭제 로직을 하나 하나 찾아가며 로그를 남기

ttl-blog.tistory.com

 

'TIL' 카테고리의 다른 글

OneToOne 관계에서의 N + 1 문제  (0) 2024.09.07
내가 원하는 컬럼으로만 업데이트  (2) 2024.08.24
Docker란?  (0) 2024.08.19
쿠키, 세션, 토큰 어떤 차이일까?  (0) 2024.04.23