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

골 참여자 방출 기능 구현 #99

Merged
merged 32 commits into from
May 20, 2024
Merged

골 참여자 방출 기능 구현 #99

merged 32 commits into from
May 20, 2024

Conversation

jhsseonn
Copy link
Member

3/30
골 참여자 방출 기능 구현 완료했습니다.
두가지 논의사항이 있어 질문 드립니다. 논의가 필요한 코드 윗줄에 TO DO로 주석 남겨놓아서 쉽게 찾아보실 수 있을 것 같습니다.

[논의사항]

  1. 참여자 방출시 해당 참여자의 모든 스탬프를 삭제하는 로직을 stampService에서 구현하고 해당 메서드를 goalService에서 참여자를 방출할 때 호출하는 방식으로 구현했는데, goalService의 골 참여자 방출 메서드에서 한꺼번에 구현하는 것이 나을까요? stmapService로 로직을 분리한 이유는 스탬프 삭제 로직은 스탬프 서비스에 존재하는 것이 역할상 더 적절하다고 생각했습니다.
  2. 골 참여자 방출 api 형식을 /goals/{goalId}/user/{userId} 로 지정했는데, /user 부분이 꼭 필요없을까요? 프론트에서 api를 호출할 때 /user를 넣는 것이 더 구분하기 쉬울 수도 있겠다는 생각에 우선 넣었습니다.

[작업 내용]

  • 골 참여자 방출 서비스 및 컨트롤러 기능 구현
  • 골 참여자 방출 시 참여자의 모든 스탬프 삭제
  • Exception 클래스명 변경(클래스명 통일을 위해 리팩터링 했습니다.)
  • closed [Feat] 기존 골 참여자 방출 기능 구현 #90

p.s. 이슈번호 잘못 보고 브랜치 파는 바람에 브랜치를 다시 팠습니다.. 다음엔 확실히 확인하도록 하겠습니다..

@jhsseonn jhsseonn added the feature 기능 추가 label Mar 29, 2024
@jhsseonn jhsseonn self-assigned this Mar 29, 2024
@jhsseonn jhsseonn requested a review from JJ503 March 30, 2024 07:23
Copy link
Member

@JJ503 JJ503 left a comment

Choose a reason for hiding this comment

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

기능 구현하시느라 고생 많으셨습니다!
몇 가지 질문들이 있어 일단 rc로 처리해 두었습니다.
논의 사항에 대한 답변은 각 TODO 코드 쪽에 코멘트 달아두었습니다.
의견을 정리하자면 현재 상태가 좋다입니다!

Comment on lines +155 to +159
private void validUserToDeleteUser(final Long userId) {
if (!isManager(userId)) {
throw new ForbiddenGoalToDeleteException.ForbiddenUserToDeleteUser();
}
}
Copy link
Member

Choose a reason for hiding this comment

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

해당 부분은 생각하지 못했는데 좋네요! 👍

Comment on lines 90 to 98
final List<GoalTeam> updatedGoalTeams = this.goalTeams.stream()
.filter(GoalTeam::isDeletedFalse)
.toList();
updateGoalTeams(updatedGoalTeams);
}

private void updateGoalTeams(final List<GoalTeam> goalTeams) {
this.goalTeams = goalTeams;
}
Copy link
Member

Choose a reason for hiding this comment

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

이렇게 하더라도 goalTeam 테이블에서 삭제된 사용자가 제거되거나 goalId를 null로 만드는 등의 행위는 하지 않을 것으로 보이는데, 삭제 후 다시 조회했을 때 삭제된 사용자는 불러와지지 않나요? 어떤 의미가 있는 것인지 궁금합니다.
또한, 팀원을 softDelete하는 것과 hardDelete하는 것 중 뭐가 더 적절할지도 고민이네요!

Copy link
Member Author

Choose a reason for hiding this comment

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

이렇게 하면 삭제된 사용자는 불러와지지 않습니다. updateGoalTeams를 하지 않을 경우, 삭제된 사용자가 그대로 goalTeams에 남아있기 때문에 삭제를 하지 않은 사용자들만 다시 goalTeams에 할당했습니다. 그런데 너무 어렵게 생각한 것 같네요.. 정수님이 밑에서 피드백 주신대로 for문 돌면서 if문 걸리면 delete하고 goalTeams에서도 remove 하는 방식으로 수정했습니다!

softDelete vs hardDelete 관련해서는 골 팀은 hardDelete를 해도 문제가 없을 것 같아요. 정말 단지 골 참여자 목록에 추가하기 위해 만들어진 테이블이기 때문에 소속 되어있던 골에서 방출되면 더 이상 테이블의 존재 의미가 없어지긴 합니다. 추후 분석용 데이터로서는 어느정도 의미가 있을까요? 딱히 생각나는 활용방안이 없어서 저는 hardDelete를 해도 좋을 것 같습니다.

Copy link
Member

Choose a reason for hiding this comment

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

hardDelete를 해도 괜찮을 것 같습니다.
원래 해당 리뷰를 작성할 때 팀원 정보가 스탬프에 영향을 주지 않을까 했었는데, user를 fk로 사용하고 있어 큰 문제가 없을 것 같습니다.
그래서 결론적으로는 hardDelete로 하면 좋을 것 같네요!

Copy link
Member

@JJ503 JJ503 Apr 7, 2024

Choose a reason for hiding this comment

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

그런데 TeamsdeleteUser에서는 현재는 soft delete로 처리하고 있는 것 같은데 맞을까요?
코멘트와 로직이 서로 달라 여쭤봅니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

hardDelete로 구현하기로 결정된 게 아니라고 판단해 로직을 추가하지 않았습니다. 로직 추가해서 리뷰 재요청하도록 하겠습니다.

Comment on lines 120 to 121
// TO DO: api 형식에서 /user는 불필요할까요?
@DeleteMapping(value = "/{goalId}/user/{userId}", headers = "X-API-VERSION=1")
Copy link
Member

Choose a reason for hiding this comment

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

저는 필요하다고 생각합니다!

Copy link
Member

Choose a reason for hiding this comment

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

그리고 이건 그냥 TODO 팁 아닌 팁인데, template에 todo가 있어 todo라고만 적어도 todo 주석이 자동생성됩니다.
또한, TODO는 붙여 적어줘야 아래 이미지처럼 Intellij가 todo임을 인지할 수 있고, 커밋 시에도 경고를 통해 todo가 있음을 계속 상기시켜 줍니다.
현재 방식처럼 사용하시는 게 편하시다면 상관없지만, 그냥 혹시 모르실까 해서 말씀드립니다!
image

Comment on lines 117 to 123
TO DO: 골 아이디와 사용자 아이디를 받아서 모든 스탬프를 삭제하는 로직을 스탬프 서비스에 구현했는데,
골 서비스에서 한꺼번에 구현하는 것이 나을까요?
*/
public void deleteAllByGoalIdAndUserId(final Long goalId, final Long userId) {
final List<Stamp> stamps = stampRepository.findAllByGoalIdAndUserId(goalId, userId);
stamps.forEach(Stamp::delete);
}
Copy link
Member

Choose a reason for hiding this comment

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

해당 부분은 좀 고민이 되네요....
일당 당장의 생각은 Goal 패키지에서 stamp가 어떻게 삭제되는지는 알 필요가 없다고 생각하긴 합니다. 그리고 StampService와 GoalService에서 순환참조가 일어나지 않고 있으므로 문제는 없다고 생각합니다. 그래서 일단은 현재처럼 진행하고 나중에 전체적으로 의존성과 관련해 리팩터링에 대한 논의를 하면 좋을 것 같긴 합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

좋습니다! 그러면 지금은 일단 이렇게 두고, 의존성 관련 리팩터링에 대한 논의 후에 정해진 대로 리팩터링 하는 것으로 해요!

Comment on lines 370 to 373
final List<GoalTeam> goalTeam = goal.getTeams()
.stream()
.filter(GoalTeam::isDeletedFalse)
.toList();
Copy link
Member

Choose a reason for hiding this comment

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

방출에 대한 테스트를 할 때 해당 부분은 적절하지 않다고 생각합니다.
이렇게 되면 골 조회 등과 같은 상황에 항상 해당 로직을 서비스에서 수행해야 한다는 생각이 드네요.
그래서 goal의 teams를 가져와 진행하는 것이 의도하신 바에 더 적합한 테스트가 아닌가 하는 생각이 들어서요.
방출과 관련해 의도하신 것이 무엇일까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

delete 메서드의 반환값이 없어서 트랜잭션 전이가 안되어서 goal의 teams를 가져와도 delete를 수행한 값대로 받아와지지 않더라구요... 이 부분은 아예 잘못된 부분인 것 같습니다.
맨 밑에 피드백 주신대로 원래 골을 수정했을 때 ReadGoalDetaildto로 수정된 내용을 클라이언트로 넘겨주었는데 골 참여자 방출을 delete로 생각하다보니 반환값이 없어도 될 것이라 생각했던 것 같습니다. 그런데 이렇게 되면 수정된 내용을 넘겨주지 못하니 테스트 코드도 부자연스럽게 억지로 끼워맞출 수 밖에 없었던 것 같네요😅 ReadGoalDetailDto로 수정된 내용을 클라이언트로 넘겨주도록 메서드 수정해서 테스트 코드도 그에 맞게 수정했습니다.

Copy link
Member

Choose a reason for hiding this comment

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

테스트 클래스에는 @Transactional을 붙이지 않기에 전이가 되지 않는 것이 맞습니다.
그래서 만약 해당 테스트를 수행했을 것이라면, deleteUser 수행 후 goalRepository를 통해 다시 조회해 가져오는 것이 적절헀을 것이라 생각합니다!
이와 관련해 해당 클래스 157번째 라인인 요청받은_골의_아이디에_해당하는_골을_삭제한다에 대해 예외가 일어나지 않는다가 아니라, 해당 서비스 메서드 수행 후 goalRepository에서 해당 골을 조회할 때 optionalisEmpty()로 테스트를 수행하는 것은 어떨까요?
테스트하고자 하는 바를 명확히 하는 것이 좋을 것 같아 제안드립니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

말씀주신 방식이 테스트 목적을 더 잘 반영할 것 같네요! 이렇게 수정하도록 하겠습니다!

@@ -171,4 +173,11 @@ public void acceptGoalRequest(final Long userId, final Long goalId) {
final Goal goal = getGoal(goalId);
goal.updateAccepted(user);
}

public void deleteUser(final Long userId, final Long goalId, final Long managerId) {
Copy link
Member

Choose a reason for hiding this comment

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

managerId에 들어오는 값은 항상 매니저가 아니라고 생각합니다.
그래서 해당 변수를 userId로 하고, userId deleteUserId라고 하는 건 어떨까요?
이건 그냥 제안사항이기에 효선님께서 판단해 주시고 더 적절하다고 생각되는 것으로 수정해 주시면 됩니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

좋습니다! managerId를 userId로 바꾸고 현 userId를 deleteUserId로 바꾸는 게 좋은 것 같네요!

public void deleteUser(final User user, final Long managerId) {
validUserToDelete(user);
validUserToDeleteUser(managerId);
teams.deleteUser(user.getId());
Copy link
Member

Choose a reason for hiding this comment

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

굳이 user가 아닌 user.getId()를 전달하는 이유가 있을까요?
user에 id는 어차피 들어있고 user.equals(user)라고 하면 더 편할 것 같은데 해당 로직을 user.getId.equals(userId)로 되어 있어 여쭤봅니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

여기까진 미처 생각을 못한 것 같습니다..! user 전체 넘겨주는 것으로 수정했습니다!

@@ -57,4 +57,12 @@ private void processDefaultManager(final User user, final Goal goal) {
public void updateAccepted() {
this.accepted = true;
}

public void updateDeleted() {
Copy link
Member

Choose a reason for hiding this comment

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

그냥 delete라고 하는 것이 더 적절하다고 생각합니다.
왜냐하면 해당 메서드는 deleted의 값을 매개변수로 주입해 바꾸는 것이 아니라 무조건 삭제의 의미인 true로 바꾸는 것이기에 그냥 delete()라는 메서드명으로 삭제한다는 의미를 명확하게 전달하는 것이 더 좋다고 생각합니다.

this.deleted = true;
}

public boolean isDeletedFalse() {
Copy link
Member

Choose a reason for hiding this comment

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

해당 메서드를 만들어 사용하는 이유는 무엇일까요?
lombok을 통해 만들어지는 !isDeleted()를 사용하지 않은 이유가 있을까요?

Copy link
Member Author

@jhsseonn jhsseonn Apr 3, 2024

Choose a reason for hiding this comment

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

final List<User> usersBeforeUpdate = this.goalTeams.stream()
                                                           .filter(GoalTeam::isDeletedFalse)
                                                           .map(GoalTeam::getUser)
                                                           .toList();

위 같은 로직에서 filter를 걸 때 !isDeleted()를 사용하게 되면 goalTeam -> !goalTeam.isDeleted()로 사용하게 됩니다. 이것이 반복되니 아예 따로 deleted가 false인지를 true로 반환하는 메서드를 만들어서 간결하게 표현하는 것이 낫지 않나 생각해 따로 메서드를 만들었습니다.

Copy link
Member

Choose a reason for hiding this comment

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

저는 오히려 동일한 로직이 추가되어 코드의 중복이 발생하는 것이 아닌가 하는 의문이 있습니다.

또한, 해당 메서드를 사용할 것이라면 isNotDeleted()는 어떨까요?
GoalTeams 객체 외부에서는 deleted가 boolean 타입인지 다른 타입인지 알 필요가 없다고 생각합니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

고민해봤는데 isDeletedFalse 자체가 굳이 필요하지 않은 메서드를 만든 것 같아 삭제하고 isDeleted의 True/False 여부 확인으로 수정했습니다!

Comment on lines 85 to 89
this.goalTeams.forEach(goalTeam -> {
if (goalTeam.getUser().getId().equals(userId)) {
goalTeam.updateDeleted();
}
});
Copy link
Member

Choose a reason for hiding this comment

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

개인적으로 아래와 같은 for문을 통해 break를 추가하면 어떨까 하는 생각이 있습니다.
현재는 물론 최대 5명이기에 상관없긴 하지만, 팀원이 많아질 수 있다면 결국 성능상 아주 조금은 낮아지지 않을까 해서요!
물론 정말 완벽한 성능 개선을 생각한다면 좀 더 고민해 봐야 하긴 하지만요...ㅎㅎ

Suggested change
this.goalTeams.forEach(goalTeam -> {
if (goalTeam.getUser().getId().equals(userId)) {
goalTeam.updateDeleted();
}
});
for (GoalTeam goalTeam : goalTeams) {
if (goalTeam.getUser().getId().equals(userId)) {
goalTeam.updateDeleted();
break;
}
}

Copy link
Member Author

Choose a reason for hiding this comment

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

위에서 코멘트 남긴대로 for문 돌다가 if문에서 걸리면 goalTeam delete 해주고 Teams에서는 remove해주는 식으로 구현했습니다. 감사합니다!

@JJ503
Copy link
Member

JJ503 commented Mar 30, 2024

갑자기 생각이 나 코멘트 추가합니다.
현재 방출에 대한 api 응답 값은 no-content입니다.
그런데, 팀원 삭제에 따라 팀 목록과 스탬프 정보들이 달라지는 데 이 부분은 어떻게 다시 받아오나요?
수정 파트에서 골, 팀원 정보를 위해 추가 api 요청은 없는 것으로 알고, 현재 상태라면 수정 탭을 닫으면 fragment를 닫는 것으로 이해해 골 조회 api를 요청하지 않을 것 같아 여쭤봅니다!
제가 현재 골과 관련해 안드로이드와 어떻게 요청이 오가는지 완벽히 이해하지 못해 드리는 질문 같기도 합니다..!

jhsseonn and others added 12 commits April 3, 2024 22:06
* refactor: goalTeam goal, user fk명 수정

* docs: api 문서 업데이트
* chore: flyway 의존성 추가 및 설정

* docs: flyway를 위한 테이블 초기 스크립트 추가

* ci: 서브모듈 최신화

* docs: 스탬프 컬럼 최신화 적용

* docs: flyway 스크립트 실패 시 자동 제거 스크립트 추가

* docs: 실패 자동 제거 테스트를 위한 스크립트 추가

* ci: flyway 스크립트 검증 workflow 추가

* fix: 테스트에서 flyway는 수행하지 않도록 수정

* fix: flyway 검증이 안 되는 문제 해결

* ci: 서브모듈 최신화

* ci: 서브모듈 최신화

* ci: 서브모듈 최신화

* docs: 콜백 스크립트 명 수정

* docs: 콜백 스크립트 명 수정

* docs: 콜백 스크립트 명 수정

* docs: 테스트를 위한 flyway 스크립트 제거

* chore: flyway 잘못된 버전 수정

* refactor: 메시지의 컬럼 타입 변경

text -> varchar

* docs: fk 변경사항 수정

fk_goal_team_user <-> fk_goal_team_goal
@jhsseonn
Copy link
Member Author

jhsseonn commented Apr 3, 2024

브랜치 최신화 관련해서 질문이 있습니다...!
저번에 브랜치 꼬였을 때 develop 브랜치 update project해서 해당 브랜치를 그대로 머지하면 develop 브랜치 커밋 내역이 그대로 딸려와서 rebase로 최신화를 해주었는데 똑같이 커밋 내역이 그대로 따라오더라구요...
혹시 커밋내역 안 따라오게 rebase 하는 법을 아실까요?

@JJ503
Copy link
Member

JJ503 commented Apr 7, 2024

rebase가 아닌 merge를 해야 합니다!
rebase 하게 되는 경우 머지를 하는 대상에 대한 커밋 해시가 새로 생성되어 기존 브랜치에 있는 커밋 내역과 다른 커밋으로 인지하는 것으로 알고 있습니다.
그래서 merge를 해줘야 하는데, 꼬였다는 게 정확히 어떤 의미인지 알 수 없어 좀 더 자세한 설명 혹은 게더를 통해 확인하는 것이 좋을 것 같습니다.

Copy link
Member

@JJ503 JJ503 left a comment

Choose a reason for hiding this comment

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

수정하신 사항들 모두 확인 완료했습니다!
수정하시느라 고생 많으셨습니다.
질문 사항 및 기록용 코멘트 남겨두었습니다. 머지 전에 한 번 확인해 주시면 감사하겠습니다!

@@ -171,4 +176,17 @@ public void acceptGoalRequest(final Long userId, final Long goalId) {
final Goal goal = getGoal(goalId);
goal.updateAccepted(user);
}

public ReadGoalDetailDto deleteUser(final Long deleteUserId, final Long goalId, final Long userId) {
Copy link
Member

Choose a reason for hiding this comment

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

당장 좋은 아이디어가 떠오르지 않지만, 추후 리팩터링 진행 시 어떻게 하면 쿼리를 줄일 수 있을지 함께 고민해 보면 좋을 것 같습니다.
(정말 줄일 수 없는 것이라면 그러한 결론도 괜찮고요!)
기록 겸 코멘트 달아둡니다.

@@ -78,4 +78,8 @@ public boolean isTeamAndAccepted(final User user) {
return goalTeams.stream()
.anyMatch(goalTeam -> goalTeam.getUser().equals(user) && goalTeam.isAccepted());
}

public void deleteUser(final User user) {
goalTeams.removeIf(goalTeam -> goalTeam.getUser().equals(user));
Copy link
Member

Choose a reason for hiding this comment

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

👍👍👍

Comment on lines +47 to +52
@Query("""
SELECT s
FROM Stamp s
WHERE s.goal.id = :goalId AND s.user.id = :userId
""")
List<Stamp> findAllByGoalIdAndUserId(final Long goalId, final Long userId);
Copy link
Member

Choose a reason for hiding this comment

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

jpa 네이밍만으로 안 되기에 쿼리문을 사용한 것이 맞을까요?
궁금해서 여쭤봅니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

네 맞습니다! 네이밍만으로 조회가 불가하다고 판단해 쿼리문 사용했습니다!

Comment on lines 244 to 247
final List<User> goalUsers = goal.getTeams()
.stream()
.map(GoalTeam::getUser)
.toList();
Copy link
Member

Choose a reason for hiding this comment

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

해당 부분은 assertSofly 밖에 작성해줘도 좋을 것 같네요!
오히려 테스트하고자 하는 부분이 잘 안 보여서요.

@Test
void 골_참여자를_방출한다() {
// when
final ReadGoalDetailDto result = goalService.deleteUser(방출할_사용자_아이디, 유효한_골_아이디, 유효한_사용자_아이디);
Copy link
Member

Choose a reason for hiding this comment

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

저희 테스트하고자 하는 객체에 대한 필드명은 acutal로 하기로 했던 것으로 기억합니다!

Copy link

sonarcloud bot commented May 20, 2024

@jhsseonn jhsseonn merged commit e5102cb into develop May 20, 2024
2 checks passed
@jhsseonn jhsseonn deleted the feature/90 branch May 20, 2024 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature 기능 추가
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

[Feat] 기존 골 참여자 방출 기능 구현
2 participants