본문 바로가기

개발

[스프링부트] 필수 약관 - Native Query

요구사항
1) 회원가입 시 User는 필수 약관 동의를 동의하지 않으면 가입할 수 없다.
2) 약관의 최초 생성 일자만 남기면 된다. (약관의 수정 일자는 저장하지 않아도 된다.)
3) 필수 약관 생성 또는 수정 시, 약관에 따라 User에게 동의를 구하지 않고 동의로 체크한다 .
4) 이 때, 공지사항은 약관 생성 또는 수정이 반영되기 이전에 관리자가 직접 게시한다.

 

1) 필수 약관 생성 Entity 생성

[module-common & module-admin] → TermsCondition

@Entity
@NoArgsConstructor
@Table(name = "terms_condition")
public class TermsCondition {
    @Id
    @Column(name = "terms_condition_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title; // 약관 제목
    private String content; // 약관 내용
    private LocalDate createdAt; // 최초 생성 일자
}

 

 

2) 필수 약관 멤버 동의 여부를 저장하는 Entity 생성

[module-common & module-admin] → MemberTermsAgreement

: Member의 필수 동의 여부 저장 Entity. 무조건 필수 동의라 isAgreed 필드는 필요없을 것 같은데 명시성을 위해 작성했음

@Entity
@NoArgsConstructor
@Table(name = "member_terms_agreement")
public class MemberTermsAgreement {
    @Id
    @Column(name = "member_terms_agreement_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "terms_condition_id")
    private TermsCondition termsCondition;

    @Column(name = "is_agreed", nullable = false)
    private Boolean isAgreed; // 동의 여부를 나타내는 필드

    @Column(name = "agreed_at")
    private LocalDate agreedAt; // 동의 일자


    @Builder
    public MemberTermsAgreement(Member member, TermsCondition termsCondition, Boolean isAgreed, LocalDate agreedAt) {
        this.member = member;
        this.termsCondition = termsCondition;
        this.isAgreed = isAgreed;
        this.agreedAt = agreedAt;
    }
}

 

 

3) 필수 약관을 저장하는 로직 생성

→ 필수 약관 생성 시 무조건 필수 약관을 동의하는 MemberTermsAgreement 가 생성되어야 함

[module-admin] → TermsConditionServiceImpl

아래의 방식은 모든 Member에 대한 업데이트인데 하나씩 insert하는 방식은 성능상 좋지 않다

@Override
@Transactional
    public void createTermsCondition(CreateTermsConditionRequest request) {

        TermsCondition termsCondition = TermsCondition.builder()
                .title(request.getTitle())
                .content(request.getContent())
                .createdAt(LocalDate.now())
                .build();

        termsConditionRepository.save(termsCondition);

        // 필수 동의 체크를 위한 MemberTermsCondition -> 자동 생성/ User의 동의를 구하지 않음
        // 모든 멤버에 대해 동의 사항 생성
        updateAllMembersToAgreeToTerms(termsCondition);
    }
    
@Transactional
private void updateAllMembersToAgreeToTerms(TermsCondition termsCondition){
    List<Member> members = memberService.findAllMember();
    for (Member member : members) {
        memberTermsAgreementRepository.save(MemberTermsAgreement.builder()
                .member(member)
                .termsCondition(termsCondition)
                .isAgreed(true)
                .agreedAt(LocalDate.now())
                .build()
        );
    };
}

 

 

성능 최적화 방법
가정: 3천명

1) Batch 처리

  • 장점: JPA의 편리함을 유지하면서도 데이터를 일정 크기(batch size)로 나누어 처리할 수 있습니다. 메모리 사용량을 제한하고 성능을 최적화할 수 있습니다.
  • 예시: 50개씩 처리하는 배치 처리 방식을 사용하면, 3천 명의 유저를 60개의 배치로 나누어 처리하게 됩니다. flush() 및 clear()를 통해 메모리 사용을 줄일 수 있습니다.

2) Native Query 사용

  • 장점: SQL을 직접 사용하므로, 성능이 뛰어나고 데이터베이스에 대한 추가적인 부하가 적습니다. 특히, 대량의 데이터를 한 번에 처리할 수 있습니다.
  • 예시: 모든 유저를 한 번의 쿼리로 업데이트하는 방식입니다. 이 방식은 간단하고, 3천 명의 유저를 한 번의 트랜잭션으로 처리할 수 있습니다.

 

 

 

updateAlMemberToAgreeToTerms 메소드 수정

: Native Query 사용

@Transactional
private void updateAllMembersToAgreeToTerms(TermsCondition termsCondition){
        // Native SQL 쿼리 작성
        String query = "INSERT INTO member_terms_agreement (member_id, terms_condition_id, is_agreed, agreed_at) " +
                "SELECT m.id, :termsConditionId, true, CURRENT_DATE " +
                "FROM member m.member_id " +
                "ON CONFLICT (member_id, terms_condition_id) " +
                "DO UPDATE SET is_agreed = true, agreed_at = CURRENT_DATE";

        entityManager.createNativeQuery(query)
                .setParameter("termsConditionId", termsCondition.getId())
                .executeUpdate();
    }

 

 

해당 오류 발생

원인: ON CONFLICT에 고유 제약 조건을 추가했지만 Entity를 생성할 때 고유 제약 조건으로 설정하지 않았기 때문

해결: MemberTermsAgreement 테이블에 고유 제약 조건 추가

"trace": "org.hibernate.exception.SQLGrammarException: JDBC exception executing SQL [INSERT INTO member_terms_agreement (member_id, terms_condition_id, is_agreed, agreed_at) SELECT m.member_id, ?, true, CURRENT_DATE FROM member m ON CONFLICT (member_id, terms_condition_id) DO UPDATE SET is_agreed = true, agreed_at = CURRENT_DATE] [ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification]

 

@Table(name = "member_terms_agreement",
        uniqueConstraints = {
            @UniqueConstraint(columnNames = {"member_id", "terms_condition_id"})
        }
)

 

[테스트 성공]

 

id값이 8인 동의약관이 생성됨

 

모든 멤버에 대한 약관동의가 생성됨


4) 필수 약관 수정 로직 생성

약관 수정

[module-admin] → TermsConditionServiceImpl

@Override
    @Transactional
    public void updateTermsCondition(UpdateTermsConditionRequest request) {
        TermsCondition termsCondition = findTermsConditionById(request.getId());
        termsCondition.setTitle(request.getTitle());
        termsCondition.setContent(request.getContent());

        termsConditionRepository.save(termsCondition);
    }

 

 

테스트 

: id 8의 제목과 내용 수정 완료

 

 

5) 회원가입 시 필수 약관을 동의했는지 체크하는 로직 추가

[module-common] → AuthServiceImpl 수정

: 회원가입 시 필수 동의 여부 체크와 동의한 것에 대한 MemberTermsAgreement 생성 로직 추가

{
 (기존 코드)
 .
 .
//회원가입 시 필수 동의 여부 체크
        // 필수 약관 ID 목록
        List<Long> requiredTermsIds = List.of(1L, 2L, 3L);

        // 사용자가 동의한 약관 ID 리스트
        List<Long> agreedTermsIds = request.getAgreedTermsIds();

        // 필수 약관에 동의했는지 확인
        if (!agreedTermsIds.containsAll(requiredTermsIds)) {
            throw new IllegalArgumentException("모든 필수 약관에 동의해야 합니다.");
        }
.
.
// MemberTermsAgreement 생성 로직
	for (Long termsId : agreedTermsIds) {
           TermsCondition termsCondition = termsConditionService.findTermsConditionById(termsId);

           memberTermsAgreementService.saveMemberTermsAgreement(
                   MemberTermsAgreement.builder()
                        .member(member)
                        .termsCondition(termsCondition)
                        .isAgreed(true)
                        .agreedAt(LocalDate.now())
                        .build());
        }
   .
   .
  }