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

[BE] Topic 패키지 관련 코드 리팩토링 #238

Merged
merged 69 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
8646b1a
[Docs] GitHub Issue 및 PR Template 설정 (#37)
junpakPark Jul 18, 2023
5b153cc
[Docs] GitHub Issue Template 파일명 오류 수정 (#39)
junpakPark Jul 18, 2023
c54a142
Merge branch 'develop' of https://github.com/woowacourse-teams/2023-m…
kpeel5839 Jul 25, 2023
1397964
Merge branch 'develop' of https://github.com/woowacourse-teams/2023-m…
kpeel5839 Jul 25, 2023
4efef6e
Merge branch 'develop' of https://github.com/woowacourse-teams/2023-m…
kpeel5839 Jul 26, 2023
29997b1
feat: member 구현 중
junpakPark Jul 27, 2023
82233e0
Merge branch 'feature/member' of https://github.com/woowacourse-teams…
kpeel5839 Jul 27, 2023
70f351f
feat: 패키지 분리, AuthMember 구현
junpakPark Jul 28, 2023
b98ce02
feat: MemberArgumentResolver 구현
kpeel5839 Jul 28, 2023
bd32df0
Merge branch 'feature/member' of https://github.com/woowacourse-teams…
kpeel5839 Jul 28, 2023
69268ee
feat: AuthTopic 구현
kpeel5839 Jul 28, 2023
f6faad0
Merge branch 'feature/member' of https://github.com/woowacourse-teams…
kpeel5839 Jul 28, 2023
960b9b3
Merge branch 'feature/junepark-member' into feature/junjun
kpeel5839 Jul 28, 2023
68838dc
feat: MemberArgumentResolver 구현
kpeel5839 Jul 28, 2023
5bda12f
refactor: API 명세 수정을 위한 임의 커밋
junpakPark Jul 28, 2023
83f79ff
feat: Publicity, Permission 정적 팩토리 및 Converter 추가
kpeel5839 Jul 28, 2023
27a0738
refactor: Topic 에 Publicity, Permission 추가로 인한 테스트 수정
kpeel5839 Jul 28, 2023
41ee4ef
refactor: 모든 테스트가 통과하도록 수정
kpeel5839 Jul 28, 2023
b0850af
refactor: 모든 테스트가 통과하도록 수정
kpeel5839 Jul 28, 2023
9ffcefd
feat: Topic 에 관한 CRUD 에 권한 적용 완료
kpeel5839 Jul 29, 2023
471a826
fix: Converter 반환값 이상으로 인한 오류 해결
kpeel5839 Jul 29, 2023
5075d4b
feat: Pin 기능에 권한 설정 추가
kpeel5839 Jul 29, 2023
30a97f7
style: 사용하지 않는 import 문 제거 및 접근 제어자 조정
kpeel5839 Jul 29, 2023
470fcb5
refactor: .. 지송;;^^
junpakPark Jul 31, 2023
9b58830
refactor: TopicStatus 로직 오류 수정 LocationController 오타 수정
junpakPark Jul 31, 2023
80d7e9f
refactor: 불필요 상수 제거
junpakPark Jul 31, 2023
939964c
refactor: PinResponse 자료형 변경
junpakPark Jul 31, 2023
12e83b4
feat: Image 클래스 구현
junpakPark Jul 31, 2023
8c869a1
feat: LocationRepository에 하버사인을 이용한 find 메서드 구현
junpakPark Jul 31, 2023
7bdea8a
refactor: LocationController의 메서드명 변경
junpakPark Jul 31, 2023
5cf4318
refactor: getTopicsWithPermission의 반환값 변경
junpakPark Jul 31, 2023
cc4c0c4
style: 불필요 공백 제거
junpakPark Jul 31, 2023
b45f3b1
refactor: 로직 오류 수정 및 테스트 추가
junpakPark Jul 31, 2023
0dec766
test: Topic 패키지 테스트 추가
junpakPark Jul 31, 2023
c451958
refactor: Auth 관련 리팩터링 및 TopicIntegrationTest
junpakPark Aug 1, 2023
2433935
refactor: auth 어노테이션 제거
junpakPark Aug 1, 2023
4383f25
test: AddressTest 추가
junpakPark Aug 1, 2023
209a4e2
test: Pin 관련 테스트 추가
junpakPark Aug 1, 2023
d6e4e7a
refactor: Coordinate BigDecimal -> Double 로 수정
kpeel5839 Aug 1, 2023
712a289
refactor: 모든 테스트가 성공하도록 수정
kpeel5839 Aug 1, 2023
846490a
refactor: LocationRepositoryTest 수정
kpeel5839 Aug 1, 2023
cd57810
refactor: Git Conflict Merge 해결
junpakPark Aug 1, 2023
426ad35
refactor: AuthTopic 객체 제거
junpakPark Aug 2, 2023
f8370c3
refactor: RestDocs를 위한 Interceptor 조건문 추가
junpakPark Aug 2, 2023
f43c20a
refactor: DTO 변수 타입 수정 (primitive -> wrapper)
cpot5620 Aug 2, 2023
1e36545
refactor: Image 정적 팩토리 메서드 명 변경 (of -> from)
cpot5620 Aug 2, 2023
f4b7dd6
refactor: Image 검증 메서드 부정 조건문 수정
cpot5620 Aug 2, 2023
40d230b
refactor: enum 클래스 변수명 변경 (title -> value)
cpot5620 Aug 2, 2023
20d9d75
refactor: Topic 메서드 내 변수 분리
cpot5620 Aug 2, 2023
e7bb218
refactor: TopicInfo 검증 메서드 수정
cpot5620 Aug 2, 2023
a838a12
refactor: TopicQueryService 메서드 명 수정
cpot5620 Aug 2, 2023
feb58e9
refactor: 토픽 생성 및 병합 메서드 분리
cpot5620 Aug 3, 2023
b65374e
refactor: Topic 조회시 검증 로직 추가
cpot5620 Aug 7, 2023
cfe84c0
test: TopicRepository soft-deleting 테스트 구현
cpot5620 Aug 7, 2023
cd9169c
test: Topic 권한에 따른 조회 테스트 구현
cpot5620 Aug 7, 2023
422d067
test: Topic 권한에 따른 생성 및 병합 수정 삭제 테스트 구현
cpot5620 Aug 7, 2023
0458224
test: TopicController 다중 조회 및 핀 병합 테스트 구현
cpot5620 Aug 7, 2023
e04e896
refactor: 토픽 다중 조회 검증 로직 추가
cpot5620 Aug 8, 2023
1ad610f
refactor: 예외 메세지 수정 및 null 검증 방식 수정
cpot5620 Aug 8, 2023
8f3be11
refactor: 요청에 따른 토픽 생성 분기 시점 변경
cpot5620 Aug 8, 2023
8581453
refactor: 토픽 권한 수정 메서드 추가
cpot5620 Aug 8, 2023
853bf26
refactor: TopicController 변수 명 통일
cpot5620 Aug 8, 2023
4bd3877
style: 예외 메세지 오탈자 수정
cpot5620 Aug 8, 2023
eb2bcd3
refactor: TopicInfo 불변 객체로 수정
cpot5620 Aug 8, 2023
0af5c36
refactor: TopicStatus 검증 로직 추가
cpot5620 Aug 8, 2023
29806b9
style: 주석 제거
cpot5620 Aug 8, 2023
521e8e2
refactor: 핀 복사 메서드 수정
cpot5620 Aug 9, 2023
fa309a5
refactor: 핀 조회 메서드 분리
cpot5620 Aug 9, 2023
5699a7d
chore: conflict 해결
cpot5620 Aug 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,28 @@
@Getter
public class Image {

private static final String VALID_IMAGE_URL_REGEX = "(http(s?):)([/|.|\\w|\\s|-])*\\.(?:jpg|gif|png)";
private static final String IMAGE_URL_REGEX = "(http(s?):)([/|.|\\w|\\s|-])*\\.(?:jpg|gif|png)";

@Pattern(regexp = VALID_IMAGE_URL_REGEX)
@Pattern(regexp = IMAGE_URL_REGEX)
@Column(nullable = false, length = 2048)
private String imageUrl;

private Image(String imageUrl) {
this.imageUrl = imageUrl;
}

public static Image of(String imageUrl) {
public static Image from(String imageUrl) {
validateUrl(imageUrl);

return new Image(imageUrl);
}

private static void validateUrl(String imageUrl) {
if (!RegexUtil.matches(VALID_IMAGE_URL_REGEX, imageUrl)) {
throw new IllegalArgumentException("잘못된 형식의 URL입니다.");
if (RegexUtil.matches(IMAGE_URL_REGEX, imageUrl)) {
return;
}

throw new IllegalArgumentException("잘못된 형식의 URL입니다.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

저랑 취향이 비슷하시군요 ㅋㅋ 좋습니다

}

}
21 changes: 13 additions & 8 deletions backend/src/main/java/com/mapbefine/mapbefine/pin/Domain/Pin.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
Expand Down Expand Up @@ -81,13 +80,19 @@ public void updatePinInfo(String name, String description) {
pinInfo.update(name, description);
}

public Pin copy(Topic topic) {
return Pin.createPinAssociatedWithLocationAndTopic(
pinInfo.getName(),
pinInfo.getDescription(),
location,
topic
);
/**
* createPinAssociatedWithLocationAndTopic을 재사용하지 않은 이유
* 내부적으로 copy를 처리하면, 반환값이 필요 없음 -> 외부에서 반환값을 무시함
* 반환값이 있을 경우, topic과의 연관관계를 맺어주지 않은 상태로 반환하고,
* 외부에서 해당 연관관계를 맺어주어도 됨 -> 이 방법은 객체가 불안정한 상태라고 판단 됨.
Copy link
Collaborator

Choose a reason for hiding this comment

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

copyToTopic 메서드에서 반환값이 필요 없다는 생각은 동의합니다.
근데 반환 값이 있을 경우, topic과의 연관관계를 맺어주지 않은 상태로 반환한다는 말이
무슨 의미인지 잘 이해가 되지 않네요.
createPinAssociatedWithLocationAndTopic내의 topic.addPin(pin)이 연관관계를 맺어주고 있지 않나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

간략하게 쓰려다보니 생략된 부분이 다소 존재해서, 혼란의 여지가 있었던 것 같아요.

반환값이 있을 경우, topic과의 연관관계를 맺어주지 않은 상태로 반환하고,
외부에서 해당 연관관계를 맺어주어도 됨 -> 이 방법은 객체가 불안정한 상태라고 판단 됨.

의 의미는 다음과 같습니다.

(1) copyToTopic 메서드를 사용하지 않고 createPinAssociatedWithLocationAndTopic를 재사용할 경우 반환 값을 외부에서 사용해야 한다. (반환 값을 무시하지 말아야 한다고 생각하기 때문에)

(2) (억지로라도 ?) 외부에서 사용할 수 있도록 하는 방법은 외부에서 연관관계를 맺어주는 방법이 있다.

(3) (2)의 방법을 위해서는 기존의 createPinAssociatedWithLocationAndTopic에서 연관관계를 맺어주는 로직을 제거하고, 해당 메서드를 호출하는 쪽에서 연관관계를 맺어주도록 수정한다.

(4) 이 방법은 객체를 불안정한 상태로 반환하는 것과 같다.

(5) 따라서, 별도의 메서드로 분리했다.

글로 표현하려다보니 전달이 잘 안될 수도 있을 것 같네요.
혹시 이해가 안 되신다면 다시 한 번 말씀해주세요 !

Copy link
Collaborator

@yoondgu yoondgu Aug 8, 2023

Choose a reason for hiding this comment

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

이 부분을 놓쳐서 추가 코멘트 남깁니다!
(1)에서 반환값을 무시하지 말아야 한다고 생각하는 이유는, 반환값을 사용하지 않으면 불필요한 값을 반환하는 메소드 호출이 되기 때문일까요?
그렇다면 update 후 변경된 행의 개수를 반환하는 메서드 같은 경우는 어떻게 생각하시나요?
저는 해당 메서드를 사용할 때 절대 반환값을 쓸 일이 없다면 비효율적인 행위겠지만, 그게 아니라면 반환값의 사용은 꼭 필수는 아니라는 생각이 들어서 여쭤봅니다!

또, Pin 리팩터링 PR에 반영되어있는 Pin.copy 메서드를 보시면,
Pin 객체는 Pin.createXXX 메서드로만 생성할 것이라 기대하여 해당 메서드에서 PinImage의 복사를 함께 하도록 작성했는데요.
이 부분에 대한 고려도 같이 해야 할 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(1)에서 반환값을 무시하지 말아야 한다고 생각하는 이유는, 반환값을 사용하지 않으면 불필요한 값을 반환하는 메소드 호출이 되기 때문일까요?

제가 아는 선에서는 "프로그래머가 예상치 못한 동작을 할 수 있다"라는 이유에서의 권장사항으로 알고 있습니다.
관련 링크

그렇다면 update 후 변경된 행의 개수를 반환하는 메서드 같은 경우는 어떻게 생각하시나요?

저는 변경 된 행의 개수를 검증하는 것도 필요하다고 생각해요.
예를 들어, 내가 id값을 통해 update하여 변경하려던 행의 개수가 1개라고 예상을 했었는데, 실제로 2개의 행이 변경될 수 있죠.
(테이블의 제약조건을 잘못 설정했거나 하는 이유로)

위 상황에서 이를 무시하게 되면 프로그래머가 예상치 못한 동작을 할 수도 있다고 생각해요.
그래서 반환 값을 무시하지 않고 검증을 수행해야 한다고 생각하는 편이에요.

물론, List의 add 메서드에서도 성공/실패 여부를 반환해주지만, 이는 대부분 무시하잖아요 ?
이는 위의 데이터베이스 예제와는 다르게, 프로그래머의 실수로 인해 add가 잘못 동작하는 경우가 없어서 무시하는 것이라고 생각해요.
(예외가 아닌, 예기치 못한 동작을 말하는 겁니다 !!)

저는 해당 메서드를 사용할 때 절대 반환값을 쓸 일이 없다면 비효율적인 행위겠지만, 그게 아니라면 반환값의 사용은 꼭 필수는 아니라는 생각이 들어서 여쭤봅니다!

사실, 권장사항일 뿐 항상 지켜야 하는 것은 아니라고 생각해요.
재사용하지 않은 이유에 대해 추가적인 의견을 덧붙이자면, 재사용했을 때의 코드를 확인하면 조금은 납득이 되실까 싶어요.

   // Pin 클래스의 일부

   public void copyToTopic(Topic otherTopic) {
        createPinAssociatedWithLocationAndTopic(
                pinInfo.getName(),
                pinInfo.getDescription(),
                location,
                otherTopic
        );
    }

저는 위 코드에서 메서드 이름과는 다르게, 핀을 복사한다는 로직이 안 보이는 것 같아요.

또한, createXXX 메서드를 재사용 하게 됐을 경우, 나중에 정책이 변경되면 copyToTopic 메서드에도 영향이 가지 않을까요?
예를 들어, 핀 사진에 대한 저작권 문제로 인해, 복사시에는 핀 사진을 제외하고 복사해야하는 경우에요 !
파라미터에 null을 넣어주는 방법도 있겠지만, Pin과 PinImage 사이의 연관관계 매핑을 해주는 작업에서 문제가 발생하지 않을까 싶네요.
(적절하지 않은 예시일수도 있습니다 ㅎㅎ)

추가적인 질문이나, 의견 환영입니다 !!
글 쓰는 솜씨가 없어서, 의사 전달이 제대로 이루어졌을지 모르겠네요.
이해가 안 되는 부분이 있다면 내일 추가적으로 이야기해봅시다 !

Copy link
Collaborator

Choose a reason for hiding this comment

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

자세히 설명해주셔서 이해가 갔습니다 !!
저는 반대로 copy도 사실상 객체 생성이기 때문에 정책 변경 시 copyToTopic에 영향이 가는 것이 당연하다고 생각했고,
객체를 생성하는 방식이 여러 가지로 분화된다는 점이 걸려요!
하지만 반환값을 쓰지 않는 것을 지양하고자 하는 이유도 이해가 가서 다른 분들 의견까지 마저 듣고 정리하면 좋겠네요!

*/

public void copyToTopic(Topic otherTopic) {
PinInfo copiedPinInfo = PinInfo.of(pinInfo.getName(), pinInfo.getDescription());
Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 PinInfo를 새로 생성해야할 필요가 있을까요?
PinInfo는 Id값이 없는 단순한 VO라서 기존의 PinInfo를 바로 넣어줘도 될 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

VO라고 하기에는 내부에 값을 변경할 수 있는 update가 존재하고, 불변을 보장할 수 없지 않나요 ?

기존의 pinInfo를 그대로 사용한다고 가정했을 때, 하나의 트랜잭션에서
(1) 기존의 핀들을 복사한다.
(2) 복사된 핀의 값을 변경(update)한다.
라는 로직이 수행된다면, 기존의 핀의 데이터에도 변화가 생길 것 같아요.

현재 존재하는 기능은 아니지만, 특정 핀을 복사해서 나의 토픽으로 넣을 때 핀의 정보를 수정해서 넣을 수 있게끔 하는 기능이 추가될 수 있지 않을까요 ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

제가 (2) 사례를 고려하지 못했군요 ㅋㅋ

Pin copiedPin = new Pin(copiedPinInfo, location, otherTopic);
location.addPin(copiedPin);

otherTopic.addPin(copiedPin);
}

public void addPinImage(PinImage pinImage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private PinImage(Image image, Pin pin) {
}

public static PinImage createPinImageAssociatedWithPin(String imageUrl, Pin pin) {
PinImage pinImage = new PinImage(Image.of(imageUrl), pin);
PinImage pinImage = new PinImage(Image.from(imageUrl), pin);
pin.addPinImage(pinImage);

return pinImage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Collection;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -34,112 +35,166 @@ public TopicCommandService(
this.memberRepository = memberRepository;
}

public long createNew(AuthMember member, TopicCreateRequest request) {
Topic topic = createNewTopic(member, request);
public Long saveEmptyTopic(AuthMember member, TopicCreateRequest request) {
Topic topic = convertToTopic(member, request);

List<Long> pinIds = request.pins();
List<Pin> original = pinRepository.findAllById(pinIds);
Topic savedTopic = topicRepository.save(topic);

validateExist(pinIds.size(), original.size());
pinRepository.saveAll(copyPins(original, topic));

return topic.getId();
return savedTopic.getId();
}

public long createMerge(AuthMember member, TopicMergeRequest request) {
List<Long> topicIds = request.topics();
List<Topic> topics = findTopicsByIds(member, topicIds);
private Topic convertToTopic(AuthMember member, TopicCreateRequest request) {
Member creator = findCreatorByAuthMember(member);

validateExist(topicIds.size(), topics.size());
Topic topic = createMergeTopic(member, request);
return Topic.createTopicAssociatedWithCreator(
request.name(),
request.description(),
request.image(),
request.publicity(),
request.permission(),
creator
);
}

List<Pin> original = getPinFromTopics(topics);
pinRepository.saveAll(copyPins(original, topic));
private Member findCreatorByAuthMember(AuthMember member) {
if (Objects.isNull(member.getMemberId())) {
throw new IllegalArgumentException("Guest는 토픽을 생성할 수 없습니다.");
}
Comment on lines +65 to +67
Copy link
Collaborator

Choose a reason for hiding this comment

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

validation 좋네요 👍


return topic.getId();
return memberRepository.findById(member.getMemberId())
.orElseThrow(NoSuchElementException::new);
}

public void updateTopicInfo(
AuthMember member,
Long id,
TopicUpdateRequest request
) {
Topic topic = topicRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Topic입니다."));
member.canTopicUpdate(topic);
public Long saveTopicWithPins(AuthMember member, TopicCreateRequest request) {
Topic topic = convertToTopic(member, request);

topic.updateTopicInfo(
request.name(),
request.description(),
request.image()
);
List<Pin> originalPins = findAllPins(request.pins());
validateCopyablePins(member, originalPins);
copyPinsToTopic(originalPins, topic);
Copy link
Collaborator

Choose a reason for hiding this comment

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

SaveEmptyTopicsaveTopicWithPins을 굳이 나눠줄 필요 있을까요?

해당 부분만

        pinIds = request.pins();
        if (pinIds.size() > 0) {
            List<Pin> originalPins = findAllPins(pinIds);
            validateCopyablePins(member, originalPins);
            copyPinsToTopic(originalPins, topic);
        }

이렇게 처리하면 한 메서드로 처리 가능할 것 같습니다.

Copy link
Collaborator Author

@cpot5620 cpot5620 Aug 8, 2023

Choose a reason for hiding this comment

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

(메서드 명만 보고 해당 로직이 무엇인지 유추할 수 있게 하기 위해) 조건문에 따라 다른 로직을 수행한다면, 이는 메서드로 분리해야 한다고 생각해요 !

이럴 경우, 해당 코멘트에도 남겨놓았지만 해당 구조가 조금 어색하게 느껴지네요.. 메서드 분리를 하지 말아야 할까요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ㅎㅎㅎㅎㅎㅎㅎㅎ 생각을 잘못 했네요 수정했습니다


Topic savedTopic = topicRepository.save(topic);

return savedTopic.getId();
}

public void delete(AuthMember member, Long id) {
Topic topic = topicRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Topic입니다."));
member.canDelete(topic);
private List<Pin> findAllPins(List<Long> pinIds) {
List<Pin> findPins = pinRepository.findAllById(pinIds);

if (pinIds.size() != findPins.size()) {
throw new IllegalArgumentException("존재하지 않는 핀 Id가 존재합니다.");
}

pinRepository.deleteAllByTopicId(id);
topicRepository.deleteById(id);
return findPins;
}

private List<Topic> findTopicsByIds(AuthMember member, List<Long> topicIds) {
return topicRepository.findAllById(topicIds)
.stream()
.filter(member::canRead)
.toList();
private void validateCopyablePins(AuthMember member, List<Pin> originalPins) {
int copyablePinCount = (int) originalPins.stream()
.filter(pin -> member.canRead(pin.getTopic()))
.count();

if (copyablePinCount != originalPins.size()) {
throw new IllegalArgumentException("복사할 수 없는 pin이 존재합니다.");
}
Comment on lines +101 to +103
Copy link
Collaborator

Choose a reason for hiding this comment

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

validation 훌륭한데요?

}

private List<Pin> getPinFromTopics(List<Topic> topics) {
return topics.stream()
.map(Topic::getPins)
.flatMap(Collection::stream)
.toList();
private void copyPinsToTopic(List<Pin> pins, Topic topic) {
pins.forEach(pin -> pin.copyToTopic(topic));
}

private Topic createMergeTopic(AuthMember member, TopicMergeRequest request) {
Member creator = findCreatorByAuthMember(member);
Topic topic = Topic.of(
request.name(),
request.description(),
request.image(),
request.publicity(),
request.permission(),
creator
);
return topicRepository.save(topic);
public Long merge(AuthMember member, TopicMergeRequest request) {
Topic topic = convertToTopic(member, request);

List<Topic> originalTopics = findAllTopics(request.topics());
validateCopyableTopics(member, originalTopics);
List<Pin> originalPins = getAllPinsFromTopics(originalTopics);

copyPinsToTopic(originalPins, topic);

Topic savedTopic = topicRepository.save(topic);

return savedTopic.getId();
}

private Topic createNewTopic(AuthMember member, TopicCreateRequest request) {
private Topic convertToTopic(AuthMember member, TopicMergeRequest request) {
Member creator = findCreatorByAuthMember(member);
Topic topic = Topic.of(

return Topic.createTopicAssociatedWithCreator(
request.name(),
request.description(),
request.image(),
request.publicity(),
request.permission(),
creator
);
return topicRepository.save(topic);
}

private Member findCreatorByAuthMember(AuthMember member) {
return memberRepository.findById(member.getMemberId())
.orElseThrow(NoSuchElementException::new);
private List<Topic> findAllTopics(List<Long> topicIds) {
List<Topic> findTopics = topicRepository.findAllById(topicIds);

if (topicIds.size() != findTopics.size()) {
throw new IllegalArgumentException("존재하지 않는 토픽 Id가 존재합니다.");
}

return findTopics;
}

private void validateExist(int idCount, int existCount) {
if (idCount != existCount) {
throw new IllegalArgumentException("찾을 수 없는 ID가 포함되어 있습니다.");
private void validateCopyableTopics(AuthMember member, List<Topic> originalTopics) {
int copyablePinCount = (int) originalTopics.stream()
.filter(member::canRead)
.count();

if (copyablePinCount != originalTopics.size()) {
throw new IllegalArgumentException("복사할 수 없는 토픽이 존재합니다.");
}
}

private List<Pin> copyPins(List<Pin> pins, Topic topic) {
return pins.stream()
.map(original -> original.copy(topic))
private List<Pin> getAllPinsFromTopics(List<Topic> topics) {
return topics.stream()
.map(Topic::getPins)
.flatMap(Collection::stream)
.toList();
}

public void updateTopicInfo(
AuthMember member,
Long topicId,
TopicUpdateRequest request
) {
Topic topic = findTopic(topicId);

validateUpdateAuth(member, topic);

topic.updateTopicInfo(request.name(), request.description(), request.image());
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

updateTopicStatus도 수정하는 메서드가 필요할 것 같습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

해당 사항 반영했습니다 !

private Topic findTopic(Long topicId) {
return topicRepository.findById(topicId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Topic입니다."));
}

private void validateUpdateAuth(AuthMember member, Topic topic) {
if (member.canTopicUpdate(topic)) {
return;
}

throw new IllegalArgumentException("업데이트 권한이 없습니다.");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

validate을 이렇게 빼니까 훨씬 깔끔하네요!!


public void delete(AuthMember member, Long topicId) {
Topic topic = findTopic(topicId);

validateDeleteAuth(member, topic);

pinRepository.deleteAllByTopicId(topicId);
topicRepository.deleteById(topicId);
}

private void validateDeleteAuth(AuthMember member, Topic topic) {
if (member.canDelete(topic)) {
return;
}

throw new IllegalArgumentException("삭제 권한이 없습니다.");
}

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.mapbefine.mapbefine.topic.application;

import com.mapbefine.mapbefine.auth.domain.AuthMember;
import com.mapbefine.mapbefine.location.domain.LocationRepository;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.domain.TopicRepository;
import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse;
import com.mapbefine.mapbefine.topic.dto.response.TopicResponse;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -17,28 +15,47 @@ public class TopicQueryService {

private final TopicRepository topicRepository;

public TopicQueryService(final TopicRepository topicRepository, LocationRepository locationRepository) {
public TopicQueryService(final TopicRepository topicRepository) {
this.topicRepository = topicRepository;
}

public List<TopicResponse> findAll(AuthMember member) {
public List<TopicResponse> findAllReadable(AuthMember member) {
return topicRepository.findAll().stream()
.filter(topic -> member.canRead(topic))
.filter(member::canRead)
.map(TopicResponse::from)
.toList();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

findAllReadable이라는 네이밍이 아주 이해하기 좋네요!!
PinQueryService의 findAll 메서드 네이밍도 동일하게 통일하겠습니다 😀

Copy link
Collaborator

Choose a reason for hiding this comment

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

근데 저희 모든 조회 쿼리에서 isDeleted = true 인 건 걸러줘야겠네요....?

Copy link
Collaborator Author

@cpot5620 cpot5620 Aug 8, 2023

Choose a reason for hiding this comment

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

맞아요.. 걸러줘야해요..
근데, 아직 유저가 삭제하는 경우가 없다고 해서, 따로 구현하지 않았는데 Repository에서 @Query 어노테이션을 활용하면 될 것 같아요.

Copy link
Collaborator

Choose a reason for hiding this comment

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

유저 삭제가 없으니 저도 아직은 구현 안하겠습니닿ㅎㅎ
@where 를 쓰면 @query 처럼 매 쿼리 마다 직접 저희가 설정해줄 필요 없을 것 같은데,
그러면 대신 테스트에서 쓰던 조회 메서드들 따로 처리해줘야 해서
나중에 한번에 수정하는 게 좋을것같아요

https://www.baeldung.com/spring-jpa-soft-delete


public List<TopicDetailResponse> findAllByIds(List<Long> ids) {
return topicRepository.findByIdIn(ids).stream()
public TopicDetailResponse findDetailById(AuthMember member, Long id) {
Topic topic = topicRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당하는 Topic이 존재하지 않습니다."));

if (member.canRead(topic)) {
return TopicDetailResponse.from(topic);
}

throw new IllegalArgumentException("조회할 수 없는 Topic 입니다.");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3. TopicCommandService와 예외처리 메서드 분리 방식을 통일해도 괜찮을 것 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

어떤 메서드에서는 조건문 안에서 예외를 던지고, 어떤 메서드에서는 조건문 안에서 early return을 해서 이를 통일해달라는 말씀이신거죠 ?

사실 이 부분을 적용하고자 했었는데, 부정 조건문 사용을 지양하려다보니까 통일이 잘 안 되더라구요 ㅠ_ㅠ
혹시 좋은 아이디어 있으실까요 !?

Copy link
Collaborator

Choose a reason for hiding this comment

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

이미 TopicCommandService에서 쓴 방식이 좋아서, 같은 방식으로 하면 좋겠다 싶었어요!!
이렇게요

    public TopicDetailResponse findDetailById(AuthMember member, Long topicId) {
        Topic topic = findTopic(topicId);
        validateReadAuth(member, topic);

        return TopicDetailResponse.from(topic);
    }
    
    private void validateReadAuth(AuthMember member, Topic topic) {
        if (member.canRead(topic)) {
            return;
        }

        throw new IllegalArgumentException("조회 권한이 없습니다.");
    }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

반영했어요 !!


// TODO: 2023/08/07 토픽의 id가 존재하지 않는 경우도 검증해야 하는가 ?
Copy link
Collaborator

Choose a reason for hiding this comment

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

제가 생각했을 때는 검증하는 것이 좋을 것 같다는 생각이 들어요!

사용자가 웹을 통해 접근한 경우를 고려해봤을 때, 해당 메서드를 통해 전달된 토픽의 id 가 존재하지 않는 경우라면, 사용자가 해당 토픽을 선택할 때에는 존재했지만, 같이 보기를 누르기 이전에 사라진 경우일 것 같아요.

저희가 이전에 delete 할 때 id가 존재하지 않는 경우를 검증해야 하는가에 대해서 토의할 때, 쥬니 말을 빌리면, 사용자가 삭제하려던 토픽이었으니 삭제하기 이전에 이미 삭제되었어도 사용자의 의도대로 된 것이니까 상관이 없지만, 이번에는 사용자가 해당 토픽의 내용을 보려고하는데 사라진 경우이니 사용자의 의도와 다른 방향으로 흘러갔다고 생각이 들기 때문에 예외를 터트리는 것이 맞을 것 같다는 생각이 드네요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

해당 사항 검증 로직 추가 및 테스트 추가 구현 완료하였습니다 !

public List<TopicDetailResponse> findDetailsByIds(AuthMember member, List<Long> ids) {
List<Topic> topics = topicRepository.findByIdIn(ids);

validateReadableTopics(member, topics);

return topics.stream()
.map(TopicDetailResponse::from)
.collect(Collectors.toList());
.toList();
}

public TopicDetailResponse findById(AuthMember member, Long id) {
Topic topic = topicRepository.findById(id)
private void validateReadableTopics(AuthMember member, List<Topic> topics) {
int readableCount = (int) topics.stream()
.filter(member::canRead)
.orElseThrow(() -> new IllegalArgumentException("해당하는 Topic이 존재하지 않습니다."));
.count();

return TopicDetailResponse.from(topic);
if (topics.size() != readableCount) {
throw new IllegalArgumentException("읽을 수 없는 토픽이 존재합니다.");
}
}

}
Loading