Skip to content

[BUG] 히든 이미지 제출 시 회원 상태 DB 반영 오류 수정#377

Merged
sgo722 merged 2 commits intomainfrom
develop
Dec 20, 2025
Merged

[BUG] 히든 이미지 제출 시 회원 상태 DB 반영 오류 수정#377
sgo722 merged 2 commits intomainfrom
develop

Conversation

@sgo722
Copy link
Contributor

@sgo722 sgo722 commented Dec 20, 2025

📌 PR 정보


📋 Summary

구버전 앱(1.2.0 미만) 사용자의 히든 프로필 이미지 제출 시, 회원 상태가 HIDDEN_COMPLETED로 변경되지 않던 트랜잭션 경계 분리로 인한 영속성 컨텍스트 문제를 해결했습니다.


🐛 Problem

발생 상황

  1. 사용자가 히든 프로필 이미지 제출 (POST /v1/signup/hidden-images)
  2. 이미지는 S3에 정상 업로드
  3. member.completeHiddenProfile() 메서드 호출
  4. 회원 상태가 DB에 업데이트되지 않음
  5. 회원이 PERSONALITY_COMPLETED 상태에 머물러 관리자 심사 진행 불가

근본 원인: 트랜잭션 경계 분리 문제

@Transactional
override fun handleHiddenImages(member: Member, images: List<MultipartFile>): ResponseEntity<Any> {
    // 1. SignupService의 @Transactional 메서드 호출
    signupService.registerHiddenImages(member, images)
    //    ↑ 이 메서드 실행 중 member는 영속 상태
    //    ↓ 메서드 종료 후 member는 준영속 상태

    // 2. 준영속 상태의 member에 대한 변경
    member.completeHiddenProfile()  // ⚠️ 변경 감지(Dirty Checking) 작동 안 함!

    // 3. 트랜잭션 커밋 시 변경사항이 DB에 반영되지 않음
}

JPA 영속성 컨텍스트 문제:

  • registerHiddenImages() 메서드 실행 중 member는 영속성 컨텍스트에서 관리됨
  • 메서드 종료 후 member준영속 상태로 전환
  • 준영속 상태의 엔티티는 변경 감지(Dirty Checking)가 작동하지 않음
  • 따라서 completeHiddenProfile()로 변경한 memberStatus 필드가 DB에 반영되지 않음

✅ Solution

변경 사항

파일: src/main/kotlin/codel/member/business/signup/PreVerificationStrategy.kt

Before

@Transactional
override fun handleHiddenImages(member: Member, images: List<MultipartFile>): ResponseEntity<Any> {
    log.info { "본인인증 전 플로우 - userId: ${member.getIdOrThrow()}" }

    // 히든 이미지 등록
    signupService.registerHiddenImages(member, images)

    // ⚠️ 준영속 상태의 member 사용
    member.completeHiddenProfile()

    log.info { "정상 가입 플로우 완료 - userId: ${member.getIdOrThrow()}" }

    // Discord 알림
    asyncNotificationService.sendAsync(
        notification = Notification(
            type = NotificationType.DISCORD,
            targetId = member.getIdOrThrow().toString(),
            title = "${member.getProfileOrThrow().getCodeNameOrThrow()}님이 심사를 요청하였습니다.",
            body = "code:L 프로필 심사 요청이 왔습니다.",
        ),
    )

    return ResponseEntity.ok().build()
}

After

@Transactional
override fun handleHiddenImages(member: Member, images: List<MultipartFile>): ResponseEntity<Any> {
    log.info {
        "본인인증 전 플로우 - userId: ${member.getIdOrThrow()}, " +
        "status: ${member.memberStatus}, appVersion: < 1.2.0"
    }

    // 히든 이미지 등록
    signupService.registerHiddenImages(member, images)

    // ✅ 영속 상태의 member 재조회
    val findMember = memberJpaRepository.findByMemberId(member.getIdOrThrow())
        ?: throw MemberException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다.")

    // ✅ 영속 상태의 엔티티에 대한 변경 → 변경 감지 작동
    findMember.completeHiddenProfile()

    log.info {
        "정상 가입 플로우 완료 - userId: ${findMember.getIdOrThrow()}, " +
        "status: ${findMember.memberStatus}"
    }

    // Discord 알림 (재조회한 findMember 사용)
    asyncNotificationService.sendAsync(
        notification = Notification(
            type = NotificationType.DISCORD,
            targetId = findMember.getIdOrThrow().toString(),
            title = "${findMember.getProfileOrThrow().getCodeNameOrThrow()}님이 심사를 요청하였습니다.",
            body = "code:L 프로필 심사 요청이 왔습니다.",
        ),
    )

    return ResponseEntity.ok().build()
}

핵심 변경 내용

  1. Member 엔티티 재조회

    val findMember = memberJpaRepository.findByMemberId(member.getIdOrThrow())
        ?: throw MemberException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다.")
    • 영속성 컨텍스트에서 관리되는 엔티티를 재조회
    • 재조회된 findMember영속 상태
  2. 영속 상태 엔티티에 대한 변경

    findMember.completeHiddenProfile()
    • 영속 상태의 엔티티이므로 변경 감지(Dirty Checking) 정상 작동
    • 트랜잭션 커밋 시점에 자동으로 UPDATE 쿼리 실행
  3. 로그 개선

    • 상태 변경 전후 로그에 memberStatus 포함
    • 앱 버전 정보 추가 (appVersion: < 1.2.0)

🧪 Testing

테스트 시나리오

1. 정상 플로우 테스트

// Given
val member = createMember(status = MemberStatus.PERSONALITY_COMPLETED)
val images = listOf(createMockImage())

// When
preVerificationStrategy.handleHiddenImages(member, images)

// Then
val updatedMember = memberRepository.findById(member.id).get()
assertThat(updatedMember.memberStatus).isEqualTo(MemberStatus.HIDDEN_COMPLETED)
assertThat(updatedMember.profile.hiddenCompletedAt).isNotNull()

2. 재현 테스트 (수정 전 동작 확인)

  • ✅ 히든 이미지 S3 업로드 확인
  • member_statusHIDDEN_COMPLETED로 변경 확인
  • hidden_completed_at 타임스탬프 확인
  • ✅ Discord 알림 발송 확인

수동 테스트 결과

  • Postman으로 API 호출 테스트
  • DB에서 상태 변경 확인
  • 관리자 페이지에서 심사 대기 목록 확인

📊 Impact

영향받는 사용자

  • 앱 버전: 1.2.0 미만 사용자
  • 영향 범위: 히든 프로필 이미지를 제출하는 신규 가입자

영향받는 기능

  • ✅ 회원가입 플로우 (히든 프로필 완료)
  • ✅ 관리자 프로필 심사 대기 목록
  • ✅ Discord 알림 발송

Breaking Changes

  • ❌ 없음 (내부 로직 수정만)

🔍 Technical Details

JPA 영속성 컨텍스트와 트랜잭션

영속성 컨텍스트 생명주기

[요청] → @Transactional 시작
    ↓
[영속성 컨텍스트 생성]
    ↓
registerHiddenImages() 호출
    ↓ (member는 영속 상태)
registerHiddenImages() 종료
    ↓ (member는 준영속 상태로 전환될 수 있음)
member.completeHiddenProfile() ⚠️ 변경 감지 안 됨
    ↓
[트랜잭션 커밋] → 변경사항 DB 반영 안 됨 ❌

수정 후 플로우

[요청] → @Transactional 시작
    ↓
[영속성 컨텍스트 생성]
    ↓
registerHiddenImages() 호출 및 종료
    ↓
findMember = memberRepository.findById() ✅ 영속 상태로 재조회
    ↓
findMember.completeHiddenProfile() ✅ 변경 감지 작동
    ↓
[트랜잭션 커밋] → UPDATE 쿼리 실행 ✅

대안 방법들

방법 1: EntityManager.merge() 사용

val mergedMember = entityManager.merge(member)
mergedMember.completeHiddenProfile()
  • 장점: 재조회 없이 준영속 엔티티를 영속 상태로 전환
  • 단점: 모든 필드를 복사하므로 성능 오버헤드

방법 2: Repository에서 직접 UPDATE (현재 선택)

val findMember = memberRepository.findById(member.id).get()
findMember.completeHiddenProfile()
  • 장점: 명확한 의도 표현, 최신 상태 보장
  • 단점: 추가 SELECT 쿼리 발생

방법 3: 트랜잭션 전파 설정 변경

@Transactional(propagation = Propagation.REQUIRES_NEW)
fun registerHiddenImages(...)
  • 장점: 트랜잭션 분리로 부작용 방지
  • 단점: 복잡도 증가, 롤백 처리 어려움

선택 이유: 방법 2가 가장 명확하고 안전


📝 Checklist

개발

  • 코드 수정 완료
  • 로그 추가 (상태 변경 확인용)
  • 예외 처리 추가 (회원 조회 실패 시)
  • 단위 테스트 작성
  • 통합 테스트 작성

문서

배포 전

  • 코드 리뷰 승인
  • QA 테스트 완료
  • 관리자 페이지 동작 확인
  • Discord 알림 테스트

🔗 Related

Issues

Commits

  • 5fff556 [bug] 히든이미지 저장 시 필드값 저장이 DB반영이 안되던 문제 수정

References


📸 Screenshots

Before (버그 재현)

-- 히든 이미지 제출 전
SELECT id, member_status FROM member WHERE id = 1;
-- | id | member_status          |
-- | 1  | PERSONALITY_COMPLETED  |

-- 히든 이미지 제출 후 (버그 상태)
SELECT id, member_status FROM member WHERE id = 1;
-- | id | member_status          |
-- | 1  | PERSONALITY_COMPLETED  | ⚠️ 변경 안 됨!

After (수정 완료)

-- 히든 이미지 제출 전
SELECT id, member_status, hidden_completed_at FROM member WHERE id = 1;
-- | id | member_status          | hidden_completed_at |
-- | 1  | PERSONALITY_COMPLETED  | NULL                |

-- 히든 이미지 제출 후 (수정 완료)
SELECT id, member_status, hidden_completed_at FROM member WHERE id = 1;
-- | id | member_status     | hidden_completed_at      |
-- | 1  | HIDDEN_COMPLETED  | 2025-12-20 15:30:45     | ✅ 정상 반영!

👨‍💻 Reviewers

@[팀원1] @[팀원2]

💬 Notes

  • 이 버그는 앱 버전 1.2.0 미만에서만 발생하는 레거시 플로우 문제입니다.
  • 1.2.0 이상 버전에서는 PostVerificationStrategy를 사용하므로 영향 없습니다.
  • 향후 유사한 트랜잭션 경계 문제를 방지하기 위해 팀 내 JPA 영속성 컨텍스트 교육이 필요할 수 있습니다.

sgo722 and others added 2 commits December 20, 2025 23:41
[bug] 히든이미지 저장 시 필드값 저장이 DB반영이 안되던 문제 수정
@sgo722 sgo722 self-assigned this Dec 20, 2025
@sgo722 sgo722 merged commit 15cb629 into main Dec 20, 2025
1 check passed
@sgo722 sgo722 added the Bug 버그 수정입니다. label Dec 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug 버그 수정입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant