Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 초대코대 코드 유일성 높이도록 개선 #507

Merged
merged 12 commits into from
Sep 12, 2024

Conversation

hyeon0208
Copy link
Contributor

🚩 연관 이슈

close #506


📝 작업 내용

인코딩 방식 제거

기존에 ID로 초대코드를 인코딩하던 방식이 meeting 저장 시 기본 초대코드 저장 -> 반환된 meeting의 ID로 초대코드 인코딩해 update하는
좋지 않은 흐름들을 만든다고 생각했어요
ID를 사용하나 텍스트를 사용해 조회하던 알아본 결과 텍스트가 너무 길거나 Like 문의 %을 문자열 앞에 붙이는 등이 있지만 않다면
큰 성능 차이가 없는 것 같았습니다.
따라서 기존의 인코딩 방식을 제거하고 다음과 같은 방법으로 유일성이 높은 8자리의 초대 코드를 생성하도록 했어요.

UUID의 앞 글자를 따 SHA-256으로 해쉬화된 Byte 추출
추출된 Byte의 앞 4글자를 16진수로 변환해 2글자씩 추가 -> 4  * 2 = 8 글자 추출

초대코드 타입 및 제약조건 추가

8글자 고정으로 생성되기에 type을 가변인 varchar() -> char(8)로 변경했습니다.
고정형 타입을 사용함으로써 데이터가 항상 일정한 크기를 가져 데이터를 저장하고 검색하는 데에 효율적이라고 하더라구요

유일성이 높아졌고 중복될 수 없기에 unique 제약조건을 추가했습니다
이로인해 논 클러스터링 인덱스가 생성되 조회시에 더 빠르게 조회가 가능할 것 같아요


🏞️ 스크린샷 (선택)


🗣️ 리뷰 요구사항 (선택)

Copy link

github-actions bot commented Sep 10, 2024

Test Results

129 tests  +2   121 ✅ +2   5s ⏱️ -1s
 40 suites ±0     8 💤 ±0 
 40 files   ±0     0 ❌ ±0 

Results for commit 5d8e3a5. ± Comparison against base commit 96e9588.

♻️ This comment has been updated with latest results.

Copy link

github-actions bot commented Sep 10, 2024

📝 Test Coverage Report

Overall Project 77.23% -0.31%
Files changed 85.98% 🍏

File Coverage
Meeting.java 100% 🍏
InviteCodeGenerator.java 88.14% -11.86% 🍏
MeetingService.java 87.01% -1.69% 🍏
MeetingController.java 36.11% -6.94%

Copy link
Contributor

@coli-geonwoo coli-geonwoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카키! 초대코드 로직 작성하느라 고생 많으셨습니다!

카키의 의견처럼 사실 meeting 간 식별될 수 있도록 unique한 값만 가지면 되는데 id로 encoding 하려 했던 이유가 궁금하네요(분명 이유가 있었던 것 같은데..)

우선 RC 남겨놓아요! approve해도 충분하지만 approve하면 카키가 남긴 답변을 확인 못할 수도 있을까봐서요. 의견 남겨주시고 한번더 request 요청주시면 approve 하도록 할게요!

고생 많으셨습니다 🙇

@@ -40,6 +41,7 @@ public class Meeting extends BaseEntity {
private Location target;

@NotNull
@Column(columnDefinition = "CHAR(8)", unique = true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금도 좋고 잘 읽혀요!

다만 다른 class에서는 uniqueness를 UniqueConstraints를 활용해서 지정해주었던 것 같아요! 물론 복합키여서 그런 것도 있지만 일관성을 위해 맞추어주는 것도 좋다고 생각되어 남겨봅니다!

@Table(uniqueConstraints = {
   @UniqueConstraint(name = "UniqueNumberAndStatus", columnNames = {"phoneNumber", "isActive"}),
   @UniqueConstraint(name = "UniqueSecurityAndDepartment", columnNames = {"securityNumber", "departmentCode"})})

그런데, 지금도 좋은 구조라 생각해서, 복합 unique 조건이 필요할 때 반영해도 될 것 같다면 추후 반영해도 좋을 것 같습니다!

Copy link
Contributor Author

@hyeon0208 hyeon0208 Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

복합키 특성상 테이블 어노테이션 내에서 제약조건을 걸어줬는데
일관성도 좋지만 특성이 조금 다른 것 같아
개인적으로 단일 필드는 @Column()의 내부 옵션들로 필드에 대한 설정을 할 수 있어 위 처럼 명시해주는 것이 잘 들어오는 것 같아요 !
스키마에서도 단일 unique 제약 조건은 컬럼의 끝에 붙이지만 복합 unique는 필드 선언 마지막에 unique(컬럼, 컬럼)으로 거는 것 처럼요 !

@@ -49,10 +51,6 @@ public Meeting(String name, LocalDate date, LocalTime time, Location target, Str
this(null, name, date, TimeUtil.trimSecondsAndNanos(time), target, inviteCode, false);
}

public void updateInviteCode(String inviteCode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러네요. inviteCode는 식별자이기만 하면 되는데 왜 id 값으로 인코딩해야한다고 생각했던 걸까요?

update문이 없어져서 좋고, 무엇보다 save되기 전에 부자연스러운 로직이 없어져서 더욱 깔끔해진 것 같아요.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inviteCode는 식별자이기만 하면 되는데 왜 id 값으로 인코딩해야한다고 생각했던 걸까요?

id값으로 인코딩하면 약속 id 조회 시에 DB를 찌르지 않아도 되기 때문에 그랬던 것 같아요!
그러나, 지금 방식이 더 좋다고 생각합니다👍 update문 없는거 너무 좋네요

return MeetingSaveResponseV1.from(meeting);
}

private String generateUniqueInviteCode() {
String inviteCode = InviteCodeGenerator.generate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]

do while 문으로 생성 로직을 하나 아껴보는 건 어떤가요?

Suggested change
String inviteCode = InviteCodeGenerator.generate();
do{
String inviteCode = InviteCodeGenerator.generate()
}while(meetingRepository.exsitsByInviteCode(inviteCode))
return inviteCode;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        String inviteCode;
        do {
            inviteCode = InviteCodeGenerator.generate();
        } while (meetingRepository.existsByInviteCode(inviteCode));
        return inviteCode;

콜리가 제시한 방법도 좋지만 do while문을 사용하면 위와 같이 초기화되지 않은 변수를 할당한 채로 반복문을 실행해야해서
while 문을 선택했습니다 !
생성된 초대코드가 이미 존재하면 while문을 안타서 생성 로직수는 똑같아요 !

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초기화되지 않은 변수를 할당 이 포인트를 잊었네요! 답변 감사해요 카키!

@@ -86,7 +87,7 @@ void updateAllByOverdueMeetings() {
twoDayBefore.toLocalDate(),
twoDayBefore.toLocalTime(),
TARGET_LOCATION,
"초대코드"
InviteCodeGenerator.generate()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[의견 - 선택]

비슷한 로직이 반복되는 것 같은데 Fixture로 초대코드 만들어두는 건 어떨까요?
INVITE_CODE로 표기되어 있으면 더 잘 읽힐 것 같아서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초대코드가 유니크 제약조건이 걸려있어서 같은 초대코드를 사용하면
테스트가 터지는 문제가 발생해서 각 모임마다 다른 초대코드가 생성되도록 했어요 !


Meeting meeting = meetingRepository.findByInviteCode(odyMeeting.getInviteCode()).get();

assertThat(meeting).usingRecursiveComparison().isEqualTo(odyMeeting);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usingRecursiveComparison을 처음 보아서 검색해보았는데 안에 있는 필드들의 동등성을 재귀적으로 비교하는 메서드네요! 정말 편한 것 같아요 😮

다만, meeting이 아닌 연관관계가 있는 객체의 경우 무한재귀가 발생할 우려가 있으니 조심해야 할 것 같아요 🤔

+) 더 찾아보니 ignoreFields로 무시할 필드명 지정 방식도 있어 공유합니다.

assertThat(cartItem).usingRecursiveComparison()
        .ignoringFields("id")
        .isEqualTo(expectedCartItem);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동등성을 간편하게 비교하고 싶어 알아봤는데 이런 메서드가 있더라구요
콜리말대로 연관관계가 설정된 상황에선 Lazy Loading으로 인한 오류 상황을 조심하고
양방향 관계에서도 무한재귀를 조심해야할 것 같아요 👍
공유 감사합니다 😊

Copy link
Contributor

@eun-byeol eun-byeol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 시원하다 못해 춥네요👍 update 문까지 없애주신거 좋아요!

추가적으로, 스키마 변경이 생겼을텐데(Nullable, Unique) sql 수정도 필요할 것 같아요. 로컬에서 실행했을때는 H2라서 아마도 깨지지 않았을텐데, 배포된 서버에서 돌렸을땐 깨질것 같아요.

@@ -16,11 +15,9 @@
public class BaseEntity {

@Column(updatable = false)
@NotNull
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문]
NotNull은 왜 삭제했나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데이터가 생성된 시점에 JPA가 감지해 시간 필드를 추가해준다는 점과
NotNull로 설정함으로써 해당 클래스를 상속받는 엔티티들을 저장할 때 해당 필드가 Not null 제약조건을 위배해
테스트가 꺠져서 제거해줬습니다 ~!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 테스트가 깨지던가요?? createdAt과 updatedAt은 nullalble로 설정하면 안 된다고 생각해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어라 이상하네요 null 제약조건 위반으로 테스트가 꺠져서 제거하고 해결했는데
다시 하니 되네요 허허 😅
반영완 !

Comment on lines 31 to 33
boolean existsByInviteCode(String inviteCode);

Optional<Meeting> findByInviteCode(String inviteCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]
소소하지만 순서 바꿔주는건 어떤가요? 기부니 조크등요ㅎㅎ

Suggested change
boolean existsByInviteCode(String inviteCode);
Optional<Meeting> findByInviteCode(String inviteCode);
Optional<Meeting> findByInviteCode(String inviteCode);
boolean existsByInviteCode(String inviteCode);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영 완 👍

Long meetingId = InviteCodeGenerator.decode(inviteCode);
return findById(meetingId);
return meetingRepository.findByInviteCode(inviteCode)
.orElseThrow(() -> new OdyNotFoundException("존재하지 않는 초대코드 입니다."));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]
소소 +1

Suggested change
.orElseThrow(() -> new OdyNotFoundException("존재하지 않는 초대코드 입니다."));
.orElseThrow(() -> new OdyNotFoundException("존재하지 않는 초대코드입니다."));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영 완 + 1👍

Comment on lines 56 to 60
try {
findByInviteCode(inviteCode);
} catch (OdyNotFoundException exception) {
throw new OdyNotFoundException("존재하지 않는 초대 코드 입니다.");
throw new OdyNotFoundException("유효하지 않은 초대코드입니다.");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문]
여기 try catch문이 있어야 하는 이유가 있나요?🤔 findByInviteCode()에서 존재하지 않은 초대코드에 대한 NotFound 예외로 다시 던져주고 있어서 질문드려요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 보니 try catch 구문이 의미가 없어보이네요
그렇다면 validateInviteCode() 내부에서는 findByInviteCode() 메서드만 사용하면 될 것 같은데
그러기엔 그냥 초대코드 검증 API에서 findByInviteCode() 를 그냥 사용하면 어떨까 싶기도하네요
메서드 네이밍으로 의도를 드러내기 위한 목적이라 하더라도 조회에서 예외가 터지면 검증 실패로 이어지는 것도 자연스러운 것 같아
해당 API를 매핑하는 컨트롤러에서 validateInviteCode() 를 사용하지 않고 findByInviteCode() 를 사용하는 것에 대해선 어떻게 생각하시나요 ??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 API를 매핑하는 컨트롤러에서 validateInviteCode() 를 사용하지 않고 findByInviteCode() 를 사용하는 것에 대해선 어떻게 생각하시나요 ??

findByInviteCode() 사용에 동의해요! 처음봤을때는 이상했는데 두번 보니 괜찮네요ㅎㅎ

    @Override
    @GetMapping("/invite-codes/{inviteCode}/validate")
    public ResponseEntity<Void> validateInviteCode(
            @AuthMember Member member,
            @PathVariable String inviteCode
    ) {
        meetingService.findByInviteCode(inviteCode);
        return ResponseEntity.ok()
                .build();
    }

}

public Meeting findById(Long meetingId) {
return meetingRepository.findByIdAndOverdueFalse(meetingId)
.orElseThrow(() -> new OdyNotFoundException("존재하지 않는 모임입니다."));
.orElseThrow(() -> new OdyNotFoundException("존재하지 않는 약속입니다."));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꼼꼼👍

MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
return messageDigest.digest(input);
} catch (NoSuchAlgorithmException exception) {
log.error("알고리즘 지원 불가 에러 발생 : {}", exception.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]
로그찍을 때 exception 객체 자체를 찍는건 어떤가요? 예외 핸들러에서 message는 중복으로 찍고 있어요!

Suggested change
log.error("알고리즘 지원 불가 에러 발생 : {}", exception.getMessage());
log.error("알고리즘 지원 불가 에러 발생 : {}", exception);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 핸들러에서는 예외가 발생했을 때 해당 예외를 잡아 메세지를 뽑아내 응답으로 주지만
로그는 그런 과정이 없어 예외 객체만 로그로 찍힐 것 같아요 🤔
로그 필터에서 따로 처리하는 부분이 있나요 ??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 로그 필터에서는 요청/응답 값에 대한 것만 찍고 있어요.

에러객체를 로그로 찍자고 제안한 이유는, 디버깅할 때 stacktrace까지 있어야 로그만 보고도 쉽게 디버깅할 수 있어서입니다! 에러 메시지만으로는 정보가 적어서 시뮬레이션 하는게 번거롭기도 했어요.

OdyServerErrorException 발생시, 아래 메서드로 핸들링되는데 여기에서도 에러메시지는 중복으로 찍고 있습니다!

    @ExceptionHandler(OdyException.class)
    public ProblemDetail handleCustomException(OdyException exception) {
        log.warn("message: {}", exception.getMessage());
        return ProblemDetail.forStatusAndDetail(exception.getHttpStatus(), exception.getMessage());
    }

제가 다르게 이해한 부분이 있으면 말해주세요!😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 제거 잘못이해했어요 ! 😂
에러들은 이미 핸들러 내에서 로그가 찍히고 있어 중복방지로 위 처럼 로그를 남기지 않아도 되겠네요

초대코드 생성 오류의 경우에는 약속 자체가 생성안되서 치명적인 오류라 생각해서 로깅 레벨 error로 처리를 의도해
OdyServerErrorException를 던졌지만 현재 모든 Ody 커스텀 예외들이 � OdyException을 상속받아 위 핸들러로 처리되어
warn 레벨을 던지는 부분이 있었는데

조조가 처리하고 있군요 ! 👍

@@ -49,10 +51,6 @@ public Meeting(String name, LocalDate date, LocalTime time, Location target, Str
this(null, name, date, TimeUtil.trimSecondsAndNanos(time), target, inviteCode, false);
}

public void updateInviteCode(String inviteCode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inviteCode는 식별자이기만 하면 되는데 왜 id 값으로 인코딩해야한다고 생각했던 걸까요?

id값으로 인코딩하면 약속 id 조회 시에 DB를 찌르지 않아도 되기 때문에 그랬던 것 같아요!
그러나, 지금 방식이 더 좋다고 생각합니다👍 update문 없는거 너무 좋네요

@hyeon0208
Copy link
Contributor Author

@eun-byeol
리뷰 반영 후 다시 리뷰 요청드립니다 조조 ~ 🐥

추가적으로, 스키마 변경이 생겼을텐데(Nullable, Unique) sql 수정도 필요할 것 같아요. 로컬에서 실행했을때는 H2라서 아마도 깨지지 않았을텐데, 배포된 서버에서 돌렸을땐 깨질것 같아요.

현재 구조에서 로컬에서도 엔티티와 스키마를 비교해 일치하지 않으면 ddl-auto validate 옵션 검증 예외가 발생해 확인 가능해요 !
Auditing을 사용한 필드에 NotNull 어노테이션을 제거해도 검증 오류가 안터지던데 이 이유는 모르겠네요 ??
그래도 동일하게 해당 필드가 Nullable 하도록 스키마 수정했습니다 !

모든 PR들이 머지 되고 나면 스키마와 일치하도록 prod랑 dev 서버에 테이블 추가 작업 달려야할 것 같아요 🫣

Copy link
Contributor

@eun-byeol eun-byeol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문에 대한 답변 남겼어요! 반영은 카키의 판단대로 하심 될것 같습니다~ 고생하셨어요!👍

에러 로그는 제가 이번에 맡은 이슈에 포함되는 내용이라,
OdyServerErrorException 관련해서 전반적으로 수정사항이 생길 것같아요! 제 이슈 해치우기 전에 에러 로깅하는 전략에 대해 BE 팀원들과 짧게 이야기해보면 어떨까요?

모든 PR들이 머지 되고 나면 스키마와 일치하도록 prod랑 dev 서버에 테이블 추가 작업 달려야할 것 같아요 🫣

추석 전에 다같이 해치워봅시다👍

Comment on lines 56 to 60
try {
findByInviteCode(inviteCode);
} catch (OdyNotFoundException exception) {
throw new OdyNotFoundException("존재하지 않는 초대 코드 입니다.");
throw new OdyNotFoundException("유효하지 않은 초대코드입니다.");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 API를 매핑하는 컨트롤러에서 validateInviteCode() 를 사용하지 않고 findByInviteCode() 를 사용하는 것에 대해선 어떻게 생각하시나요 ??

findByInviteCode() 사용에 동의해요! 처음봤을때는 이상했는데 두번 보니 괜찮네요ㅎㅎ

    @Override
    @GetMapping("/invite-codes/{inviteCode}/validate")
    public ResponseEntity<Void> validateInviteCode(
            @AuthMember Member member,
            @PathVariable String inviteCode
    ) {
        meetingService.findByInviteCode(inviteCode);
        return ResponseEntity.ok()
                .build();
    }

MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
return messageDigest.digest(input);
} catch (NoSuchAlgorithmException exception) {
log.error("알고리즘 지원 불가 에러 발생 : {}", exception.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 로그 필터에서는 요청/응답 값에 대한 것만 찍고 있어요.

에러객체를 로그로 찍자고 제안한 이유는, 디버깅할 때 stacktrace까지 있어야 로그만 보고도 쉽게 디버깅할 수 있어서입니다! 에러 메시지만으로는 정보가 적어서 시뮬레이션 하는게 번거롭기도 했어요.

OdyServerErrorException 발생시, 아래 메서드로 핸들링되는데 여기에서도 에러메시지는 중복으로 찍고 있습니다!

    @ExceptionHandler(OdyException.class)
    public ProblemDetail handleCustomException(OdyException exception) {
        log.warn("message: {}", exception.getMessage());
        return ProblemDetail.forStatusAndDetail(exception.getHttpStatus(), exception.getMessage());
    }

제가 다르게 이해한 부분이 있으면 말해주세요!😄

@@ -16,11 +15,9 @@
public class BaseEntity {

@Column(updatable = false)
@NotNull
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 테스트가 깨지던가요?? createdAt과 updatedAt은 nullalble로 설정하면 안 된다고 생각해요!

@mzeong mzeong merged commit 89d76ad into develop Sep 12, 2024
3 checks passed
@mzeong mzeong deleted the feature/506 branch September 12, 2024 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

refactor: 초대코드 유일성 높아지도록 개선
4 participants