티스토리 뷰
문제 상황
사용자 회원가입하는 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
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 |
- 변경감지
- onetoone
- @Id
- N + 1
- 영속성 컨텍스트
- 1차 캐시
- 메일
- @joincolumn
- @ManyToOne
- @OneToMany
- 즉시 로딩
- 인메모리 db
- 엔티티 매니저
- JPA
- 조인 전략
- 최적화
- @MappedSuperclass
- 비동기
- @GeneratedValue
- @Table
- @Entity
- 단일 테이블 전략
- 연관관계
- 비영속
- @TransactionalEventListener
- Redis
- @Cacheable
- 준영속
- 스키마 자동 생성
- mappedBy