SpringBoot

JPA에서 ID 값이 NULL일 때와 존재할 때, INSERT vs UPDATE의 차이점

Terror123 2025. 3. 18. 23:41

개요

  • User Entity, UserWallet Entity는 현재 OneToOne 관계를 가지고 있습니다.
  • User Entity가 생성되는, 생성자에 UserWallet을 넣어 저장하고있습니다.
  • 일반 회원가입 시도시 잘되지만, 테스트코드에서의 오류에 대해 짚어보고자 합니다.

시나리오 (Success)

회원가입

    public UserResponseDto signUp(final SignupRequestDto dto) {
        Boolean flag = userQuery.existsByEmail(dto.getEmail());
        // 중복된 이메일이 없을 경우
        if (!flag) {
            String encodedPassword = pe.encode(dto.getPassword());
            User user = User.of(dto,encodedPassword);
            User saveUser = userCommand.create(user);
            return UserResponseDto.of(saveUser);
        }
        else {
            throw new UserException(EMAIL_ALREADY_EXISTS);
        }
    }

User.of(...)

    // 일반 회원가입 - of
    public static User of(SignupRequestDto dto, String encodedPassword){
        return new User(
            dto.getNickname(),
            dto.getEmail(),
            encodedPassword,
            dto.getPhoneNumber(),
            dto.getUserRole(),
            dto.getResidentRegistrationNumber(),
            dto.getBank(),
            dto.getAccountNumber()
        );
    }

Constructor

    // 일반 회원가입
    public User(String nickname, String email, String password, String phoneNumber, UserRole userRole, String residentRegistrationNumber, BankType bank, String accountNumber) {
        this.nickname = nickname;
        this.email = email;
        this.password = password;
        this.phoneNumber = phoneNumber;
        this.userRole = userRole != null ? userRole : UserRole.ROLE_USER; // 기본값 적용
        this.authProvider = AuthProvider.GENERAL;
        this.residentRegistrationNumber = residentRegistrationNumber;
        this.userWallet = UserWallet.of(this,bank,accountNumber);
    }

회원가입 성공

  • User Entity

  • UserWallet Entity

  • 잘 저장이 되는 모습이다

  • 그 다음은 문제의 테스트코드를 봐보자

시나리오 (Fail)

전체코드

@SpringBootTest // ✅ 실제 빈을 로드하여 통합 테스트
@ActiveProfiles(profiles = "dev") // ✅ application-dev.yml을 사용
public class KakaoPaymentServiceIntegrationTest {

    @Autowired
    KakaoPaymentService kakaoPaymentService; // ✅ 실제 서비스 빈 주입

    @Autowired
    UserCommand userCommand;

    @Test
    @DisplayName("카카오 페이먼트 결제 준비 성공 테스트")
    public void test1() {
        // given
        User user1 = MockUserTestInfo.getUser1();
        User user = userCommand.create(user1);
        KakaoPaymentReadyRequestDto dto = MockPaymentInfo.getKakaoPaymentReadyRequestDto1();
        AuthUser authUser = MockUserTestInfo.getAuthUser1();

        // when
        KakaoPaymentReadyResponseDto response = kakaoPaymentService.ready(authUser,dto);

        // then
        assertThat(response).isNotNull();
        assertThat(response.getTid()).isNotEmpty(); // ✅ TID 값이 비어있지 않은지 확인
        assertThat(response.getNextRedirectPcUrl()).isNotEmpty(); // ✅ PC Redirect URL 확인
        assertThat(response.getNextRedirectMobileUrl()).isNotEmpty(); // ✅ Mobile Redirect URL 확인
        assertThat(response.getNextRedirectAppUrl()).isNotEmpty(); // ✅ App Redirect URL 확인
    }

MockUserTestInfo.getUser1()

    // 1. User를 먼저 생성 (Patient와 Caregiver는 나중에 설정)
    public static User getUser1() {
        return new User(ID_1, NICKNAME_1, EMAIL_1, PHONE_1, PASSWORD_1, ROLE, SSN_1, PROVIDER, NOW, NOW, null, null, BANK, ACCOUNT_NUMBER1);
    }

new User(...)

    // User Test
    public User(Long id, String nickname, String email, String phoneNumber, String password, UserRole userRole, String residentRegistrationNumber, AuthProvider authProvider, LocalDateTime createdAt, LocalDateTime updatedAt, Patient patient, Caregiver caregiver, BankType bank, String accountNumber) {
        this.id = id;
        this.nickname = nickname;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.password = password;
        this.userRole = userRole;
        this.residentRegistrationNumber = residentRegistrationNumber;
        this.authProvider = authProvider;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.patient = patient;
        this.caregiver = caregiver;
        this.userWallet = UserWallet.of(this,bank,accountNumber);
    }

회원가입 실패

  • Error Message

  • UserWallet 생성할떄의 User Entity가 Null이라 Fk로서 참조 할 수 없다는 내용이다.

도대체 뭐가 문제지?

  • 가짜 User 객체를 생성할떄, ID를 넣은것이 문제였다 !
    // 1. User를 먼저 생성 (Patient와 Caregiver는 나중에 설정)
    public static User getUser1() {
        return new User(ID_1, NICKNAME_1, EMAIL_1, PHONE_1, PASSWORD_1, ROLE, SSN_1, PROVIDER, NOW, NOW, null, null, BANK, ACCOUNT_NUMBER1);
    }
  • Jpa는 id값으로 Insert를 할지, update를 할지 구분 짓는다.
  • 가짜 객체를 만들어줄떄 id값을 넣어줬으니 update 쿼리가 나갈것이고
  • update 쿼리는 id를 먼저 select해서 찾고, 만약 존재하지않는다면 새로 생성하게된다.
  • 엥? 그럼 문제 없단거 아닌가요?
  • 아니다 밑에 케이스들을 보며 정리해보자

Merge (update) - 문제발생

  • id자리에 1을 넣었을떄 테스트코드를 돌리면 아래의 쿼리가 실행된다.
Hibernate: 
    select
        u1_0.id,
        ... 중략
        kpr1_0.vat_amount 
    from
        user u1_0 
    left join
        caregiver c1_0 
            on c1_0.id=u1_0.caregiver_id 
    left join
        user u2_0 
            on u2_0.id=c1_0.user_id 
    left join
        patient p2_0 
            on p2_0.id=u1_0.patient_id 
    left join
        user u3_0 
            on u3_0.id=p2_0.user_id 
    left join
        user_wallet uw3_0 
            on uw3_0.id=u1_0.user_wallet_id 
    left join
        user u4_0 
            on u4_0.id=uw3_0.user_id 
    left join
        kakao_payment_ready kpr1_0 
            on u1_0.id=kpr1_0.user_id 
    where
        u1_0.id=?
Hibernate: 
    insert 
    into
        user_wallet
        (account_number, balance, bank, user_id) 
    values
        (?, ?, ?, ?)
  • id에 값이 존재하기때문에 select 쿼리로 엔티티를 가져온후
  • insert를 하려고하는데, user_id는 우리가 가상으로 넣어준 Id 1인 값이기때문에 FK로 설정 할 수 없어 오류가 발생하는것이다.

Insert - 문제해결

  • id자리에 1을 빼고 테스트코드를 돌리면 아래의 쿼리가 실행된다.
Hibernate: 
    insert 
    into
        user_wallet
        (account_number, balance, bank, user_id) 
    values
        (?, ?, ?, ?)
Hibernate: 
    insert 
    into
        user
        (auth_provider, caregiver_id, created_at, email, nickname, password, patient_id, phone_number, resident_registration_number, updated_at, user_role, user_wallet_id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    update
        user_wallet 
    set
        account_number=?,
        balance=?,
        bank=?,
        user_id=? 
    where
        id=?
  • UserWallet을 저장합니다 (User = null)
  • User를 저장합니다 (UserWallet = null)
  • User에 save가 실행되며 영속화되는 과정에서 UserWallet을 업데이트하여 user_id를 채운모습이다.

결론

  • ID값에 데이터를 넣은것과, 안넣은것으로 JPA가 Insert,Update를 구분하여서 처리하고있었기 때문에 오류가 발생했다!