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

[✨ feat] 그룹 정보 보기 API 구현 #58

Merged
merged 23 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1afc706
chore(QueryDsl) : QueryDSL 의존성 추가
jsoonworld Feb 17, 2025
166dd61
refactor(GroupCreateResponse) : 패키지 구조 변경
jsoonworld Feb 17, 2025
d74b7e3
refactor(GroupCreateInternalResponse) : 패키지 구조 변경
jsoonworld Feb 17, 2025
e4b5ec8
refactor(GroupInternalRetrieveResponse) : 패키지 구조 변경
jsoonworld Feb 17, 2025
2cabe89
refactor(GroupRetrieveResponse) : 패키지 구조 변경
jsoonworld Feb 17, 2025
c4a4575
refactor(GroupsRetrieveResponse) : 패키지 구조 변경
jsoonworld Feb 17, 2025
461ed9e
feat(QueryDslConfig) : QueryDSL 설정 구현
jsoonworld Feb 17, 2025
5070cf4
feat(GroupController) : 그룹 정보 조회 엔드포인트 추가
jsoonworld Feb 17, 2025
57b49ab
refactor(GroupService) : DTO 패키지 변경 반영
jsoonworld Feb 17, 2025
44f1064
feat(GroupInfoService) : 그룹 정보 조회 서비스 인터페이스 추가
jsoonworld Feb 17, 2025
4b325d5
feat(GroupInfoServiceImpl) : 그룹 정보 조회 서비스 구현
jsoonworld Feb 17, 2025
48b59c4
refactor(GroupRetrieveServiceImpl) : 그룹 조회 로직 개선 및 패키지 변경 반영
jsoonworld Feb 17, 2025
32c36f1
feat(GroupService) : 그룹 정보 조회 기능 추가
jsoonworld Feb 17, 2025
184591c
feat(GroupErrorCode) : 멤버 조회 실패 예외 코드 추가
jsoonworld Feb 17, 2025
00ef6a3
feat(GroupSuccessCode) : 그룹 정보 조회 성공 코드 추가
jsoonworld Feb 17, 2025
7801ce9
feat(GroupInfoResponse) : 그룹 정보 응답 DTO 추가
jsoonworld Feb 17, 2025
fd90e9f
feat(GroupMemberInfoResponse) : 그룹 멤버 정보 응답 DTO 추가
jsoonworld Feb 17, 2025
380e6e2
feat(GroupSummaryResponse) : 그룹 요약 정보 응답 DTO 추가
jsoonworld Feb 17, 2025
0504517
refactor(MemberGroupRepository) : 커스텀 리포지토리 인터페이스 추가
jsoonworld Feb 17, 2025
9802aa8
feat(MemberGroupRepositoryCustom) : 커스텀 리포지토리 인터페이스 추가
jsoonworld Feb 17, 2025
9b4f01a
feat(MemberGroupRepositoryImpl) : 커스텀 리포지토리 구현 추가
jsoonworld Feb 17, 2025
1d72ed6
test(GroupInfoServiceImplTest) : 그룹 정보 조회 서비스 테스트 작성
jsoonworld Feb 17, 2025
372a1df
test(MemberGroupRepositoryTest) : 커스텀 리포지토리 메서드 테스트 구현
jsoonworld Feb 17, 2025
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
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ repositories {
mavenCentral()
}

ext {
set('queryDslVersion', "5.0.0")
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -38,6 +42,12 @@ dependencies {

// Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// QueryDSL
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

tasks.named('test') {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/noostak/global/config/QueryDslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.noostak.global.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
18 changes: 14 additions & 4 deletions src/main/java/org/noostak/group/api/GroupController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import org.noostak.global.success.SuccessResponse;
import org.noostak.group.application.GroupService;
import org.noostak.group.dto.request.GroupCreateRequest;
import org.noostak.group.dto.response.GroupCreateResponse;
import org.noostak.group.dto.response.GroupsRetrieveResponse;
import org.noostak.group.dto.response.create.GroupCreateResponse;
import org.noostak.group.dto.response.info.GroupInfoResponse;
import org.noostak.group.dto.response.retrieve.GroupsRetrieveResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;

import static org.noostak.group.common.success.GroupSuccessCode.GROUP_CREATED;
import static org.noostak.group.common.success.GroupSuccessCode.GROUP_RETRIEVED;
import static org.noostak.group.common.success.GroupSuccessCode.*;

@RestController
@RequestMapping("/api/v1/groups")
Expand Down Expand Up @@ -41,4 +41,14 @@ public ResponseEntity<SuccessResponse<GroupsRetrieveResponse>> getGroups(
GroupsRetrieveResponse groups = groupService.findGroups(memberId);
return ResponseEntity.ok(SuccessResponse.of(GROUP_RETRIEVED, groups));
}

@GetMapping("/{groupId}/members")
public ResponseEntity<SuccessResponse<GroupInfoResponse>> getGroupInfo(
// @AuthenticationPrincipal Long memberId,
@PathVariable Long groupId
) {
Long memberId = 1L;
GroupInfoResponse groupInfo = groupService.getGroupInfo(memberId, groupId);
return ResponseEntity.ok(SuccessResponse.of(GROUP_INFO, groupInfo));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.noostak.group.application;

import org.noostak.group.dto.request.GroupCreateRequest;
import org.noostak.group.dto.response.GroupCreateInternalResponse;
import org.noostak.group.dto.response.create.GroupCreateInternalResponse;

import java.io.IOException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
import org.noostak.group.domain.vo.GroupName;
import org.noostak.group.domain.vo.GroupProfileImageKey;
import org.noostak.group.dto.request.GroupCreateRequest;
import org.noostak.group.dto.response.GroupCreateInternalResponse;
import org.noostak.group.dto.response.create.GroupCreateInternalResponse;
import org.noostak.infra.KeyAndUrl;
import org.noostak.infra.S3DirectoryPath;
import org.noostak.infra.S3Service;
import org.noostak.member.domain.Member;
import org.noostak.member.domain.MemberRepository;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.noostak.group.application;

import org.noostak.group.dto.response.info.GroupInfoResponse;

public interface GroupInfoService {
GroupInfoResponse getGroupInfo(Long memberId, Long groupId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.noostak.group.application;

import lombok.RequiredArgsConstructor;
import org.noostak.group.common.exception.GroupErrorCode;
import org.noostak.group.common.exception.GroupException;
import org.noostak.group.domain.Group;
import org.noostak.group.domain.GroupRepository;
import org.noostak.group.dto.response.info.GroupInfoResponse;
import org.noostak.group.dto.response.info.GroupMemberInfoResponse;
import org.noostak.group.dto.response.info.GroupSummaryResponse;
import org.noostak.infra.S3Service;
import org.noostak.member.domain.Member;
import org.noostak.membergroup.domain.MemberGroupRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class GroupInfoServiceImpl implements GroupInfoService {

private final GroupRepository groupRepository;
private final MemberGroupRepository memberGroupRepository;
private final S3Service s3Service;

@Override
public GroupInfoResponse getGroupInfo(final Long memberId, final Long groupId) {
Group group = findGroupById(groupId);

Member member = findMemberInGroup(memberId, groupId);
GroupMemberInfoResponse myInfoResponse = convertToGroupMemberInfo(member);


GroupMemberInfoResponse groupHostInfo = findGroupHost(groupId);

List<GroupMemberInfoResponse> groupMembersInfo = findGroupMembers(groupId);

GroupSummaryResponse groupSummaryResponse = convertToGroupSummaryResponse(group, groupHostInfo, groupMembersInfo);

return GroupInfoResponse.of(myInfoResponse, groupSummaryResponse);
}

private Member findMemberInGroup(Long memberId, Long groupId) {
return memberGroupRepository.findMembersByGroupId(groupId).stream()
.filter(m -> m.getMemberId().equals(memberId))
.findFirst()
.orElseThrow(() -> new GroupException(GroupErrorCode.MEMBER_NOT_FOUND));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

findMembersByGroupId -> findMemberByGroupIdAndMemberId처럼 WHERE 조건 절에 MemberId와 GroupId가 일치하는 확인하는 방법으로 해결할 수 없을까요?

조건부 연산을 DBMS에서 수행할 수 있으면, DBMS에서 넘어오는 데이터에 대한 Bandwidth의 부담을 줄일 수 있습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

피드백 감사합니다! 😊

현재 findMembersByGroupId(groupId)를 호출한 후 Java 스트림을 이용해 memberId를 필터링하고 있는데, QueryDSL으로 구현된 로직을 개선하면 해당 필터링을 DB에서 직접 수행할 수 있을 것 같습니다.

제안해주신 대로 findMemberByGroupIdAndMemberId(groupId, memberId)와 같은 형태로 WHERE 조건을 적용하면, 불필요한 데이터를 애플리케이션으로 가져오지 않고 DBMS가 직접 필터링을 수행하여 네트워크 및 메모리 사용량을 줄일 수 있을 것 같습니다.

예를 들어, 현재 findMembersByGroupId(groupId)는 다음과 같이 모든 멤버를 가져옵니다:

public List<Member> findMembersByGroupId(Long groupId) {
    return queryFactory
        .select(member)
        .from(memberGroup)
        .join(memberGroup.member, member)
        .where(memberGroup.group.groupId.eq(groupId))
        .fetch();
}

이후 Java 스트림을 사용하여 필터링하고 있는데, QueryDSL에서 직접 처리하는 방식으로 변경하면 더 효율적일 것입니다:

public Optional<Member> findMemberByGroupIdAndMemberId(Long groupId, Long memberId) {
    return Optional.ofNullable(
        queryFactory
            .select(member)
            .from(memberGroup)
            .join(memberGroup.member, member)
            .where(memberGroup.group.groupId.eq(groupId)
                .and(member.memberId.eq(memberId)))
            .fetchOne()
    );
}

이렇게 하면 DB에서 직접 groupIdmemberId 조건을 적용해 검색하므로 불필요한 데이터 전송을 방지할 수 있을 것 같아요!

따라서 findMemberInGroup 메서드를 다음과 같이 변경하는 것이 더 적절할 것 같습니다:

private Member findMemberInGroup(Long memberId, Long groupId) {
    return memberGroupRepository.findMemberByGroupIdAndMemberId(groupId, memberId)
            .orElseThrow(() -> new GroupException(GroupErrorCode.MEMBER_NOT_FOUND));
}

이렇게 하면 전체 데이터를 불러온 후 필터링하는 것이 아니라, DB에서 직접 원하는 데이터를 가져오기 때문에 성능상 더 유리겠어요!

이 부분은 이슈로 등록하고 개선하겠습니다!

좋은 피드백 감사합니다! 🙌 🚀


private Group findGroupById(Long groupId) {
return groupRepository.findById(groupId)
.orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND));
}

private GroupMemberInfoResponse findGroupHost(Long groupId) {
Member host = memberGroupRepository.findGroupHostByGroupId(groupId);
if (host == null) throw new GroupException(GroupErrorCode.GROUP_MEMBER_NOT_FOUND);

return convertToGroupMemberInfo(host);
}

private List<GroupMemberInfoResponse> findGroupMembers(Long groupId) {
return memberGroupRepository.findMembersByGroupId(groupId).stream()
.map(this::convertToGroupMemberInfo)
.toList();
}

private GroupMemberInfoResponse convertToGroupMemberInfo(Member member) {
return GroupMemberInfoResponse.of(
member.getName().value(),
s3Service.getImageUrl(member.getKey().value())
);
}

private GroupSummaryResponse convertToGroupSummaryResponse(Group group, GroupMemberInfoResponse hostInfo, List<GroupMemberInfoResponse> membersInfo) {
String groupProfileImageUrl = getGroupProfileImageUrl(group);

return GroupSummaryResponse.of(
hostInfo,
group.getName().value(),
groupProfileImageUrl,
group.getCount().value(),
group.getCode().value(),
membersInfo
);
}

private String getGroupProfileImageUrl(Group group) {
return s3Service.getImageUrl(group.getKey().value());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.noostak.group.application;

import org.noostak.group.dto.response.GroupsRetrieveResponse;
import org.noostak.group.dto.response.retrieve.GroupsRetrieveResponse;

public interface GroupRetrieveService {
GroupsRetrieveResponse findGroups(Long memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
import lombok.RequiredArgsConstructor;
import org.noostak.group.common.exception.GroupErrorCode;
import org.noostak.group.common.exception.GroupException;
import org.noostak.group.domain.Groups;
import org.noostak.group.dto.response.GroupInternalRetrieveResponse;
import org.noostak.group.dto.response.GroupRetrieveResponse;
import org.noostak.group.dto.response.GroupsRetrieveResponse;
import org.noostak.group.domain.Group;
import org.noostak.group.dto.response.retrieve.GroupRetrieveResponse;
import org.noostak.group.dto.response.retrieve.GroupsRetrieveResponse;
import org.noostak.infra.S3Service;
import org.noostak.membergroup.domain.MemberGroup;
import org.noostak.membergroup.domain.MemberGroupRepository;
import org.noostak.membergroup.domain.MemberGroups;
import org.springframework.stereotype.Service;

import java.util.List;
Expand All @@ -24,35 +21,28 @@ public class GroupRetrieveServiceImpl implements GroupRetrieveService {

@Override
public GroupsRetrieveResponse findGroups(Long memberId) {
MemberGroups memberGroups = getMemberGroups(memberId);
Groups groups = convertToGroups(memberGroups);
List<Group> groups = getGroupsByMemberId(memberId);
validateGroups(groups);
return convertToResponse(groups);
}

private MemberGroups getMemberGroups(Long memberId) {
List<MemberGroup> foundMemberGroups = memberGroupRepository.findByMemberId(memberId);
MemberGroups memberGroups = MemberGroups.of(foundMemberGroups);
private List<Group> getGroupsByMemberId(Long memberId) {
return memberGroupRepository.findGroupsByMemberId(memberId);
}

if (memberGroups.isEmpty()) {
private void validateGroups(List<Group> groups) {
if (groups.isEmpty()) {
throw new GroupException(GroupErrorCode.GROUP_NOT_FOUND);
}

return memberGroups;
}

private Groups convertToGroups(MemberGroups memberGroups) {
return Groups.of(memberGroups.toGroups());
}

private GroupsRetrieveResponse convertToResponse(Groups groups) {
List<GroupInternalRetrieveResponse> internalResponses = groups.getGroups().stream()
.map(group -> GroupInternalRetrieveResponse.of(group, s3Service.getImageUrl(group.getKey().value())))
private GroupsRetrieveResponse convertToResponse(List<Group> groups) {
List<GroupRetrieveResponse> groupResponses = groups.stream()
.map(group -> GroupRetrieveResponse.of(
group,
s3Service.getImageUrl(group.getKey().value())
))
.toList();

List<GroupRetrieveResponse> groupResponses = internalResponses.stream()
.map(internal -> GroupRetrieveResponse.of(internal.group(), internal.groupProfileImageUrl()))
.toList();

return GroupsRetrieveResponse.of(groupResponses);
}
}
12 changes: 9 additions & 3 deletions src/main/java/org/noostak/group/application/GroupService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import lombok.RequiredArgsConstructor;
import org.noostak.group.dto.request.GroupCreateRequest;
import org.noostak.group.dto.response.GroupCreateInternalResponse;
import org.noostak.group.dto.response.GroupCreateResponse;
import org.noostak.group.dto.response.GroupsRetrieveResponse;
import org.noostak.group.dto.response.create.GroupCreateInternalResponse;
import org.noostak.group.dto.response.create.GroupCreateResponse;
import org.noostak.group.dto.response.info.GroupInfoResponse;
import org.noostak.group.dto.response.retrieve.GroupsRetrieveResponse;
import org.springframework.stereotype.Service;

import java.io.IOException;
Expand All @@ -15,6 +16,7 @@ public class GroupService {

private final GroupCreateService groupCreateService;
private final GroupRetrieveService groupRetrieveService;
private final GroupInfoService groupInfoService;

public GroupCreateResponse createGroup(Long memberId, GroupCreateRequest request) throws IOException {
GroupCreateInternalResponse response = groupCreateService.createGroup(memberId, request);
Expand All @@ -24,4 +26,8 @@ public GroupCreateResponse createGroup(Long memberId, GroupCreateRequest request
public GroupsRetrieveResponse findGroups(Long memberId) {
return groupRetrieveService.findGroups(memberId);
}

public GroupInfoResponse getGroupInfo(Long memberId, Long groupId) {
return groupInfoService.getGroupInfo(memberId, groupId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public enum GroupErrorCode implements ErrorCode {

GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "그룹을 찾을 수 없습니다."),
GROUP_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "그룹 멤버를 찾을 수 없습니다."),

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "멤버를 찾을 수 없습니다.")
;

public static final String PREFIX = "[GROUP ERROR] ";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public enum GroupSuccessCode implements SuccessCode {
GROUP_CREATED(HttpStatus.CREATED, "그룹이 성공적으로 생성되었습니다."),

GROUP_RETRIEVED(HttpStatus.OK, "그룹이 성공적으로 조회되었습니다."),

GROUP_INFO(HttpStatus.OK, "그룹 정보가 성공적으로 조회되었습니다.")
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.noostak.group.dto.response;
package org.noostak.group.dto.response.create;


import org.noostak.group.domain.Group;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.noostak.group.dto.response;
package org.noostak.group.dto.response.create;

import org.noostak.group.domain.Group;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.noostak.group.dto.response.info;

public record GroupInfoResponse(
GroupMemberInfoResponse myInfo,
GroupSummaryResponse groupInfo
) {
public static GroupInfoResponse of(final GroupMemberInfoResponse myInfo, final GroupSummaryResponse groupInfo) {
return new GroupInfoResponse(myInfo, groupInfo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.noostak.group.dto.response.info;

public record GroupMemberInfoResponse(
String memberName,
String memberProfileImageUrl
) {
public static GroupMemberInfoResponse of(final String memberName, final String memberProfileImageUrl) {
return new GroupMemberInfoResponse(memberName, memberProfileImageUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.noostak.group.dto.response.info;

import java.util.List;

public record GroupSummaryResponse(
GroupMemberInfoResponse groupHostInfo,
String groupName,
String groupProfileImageUrl,
Long groupMemberCount,
String groupInvitationCode,
List<GroupMemberInfoResponse> groupMemberInfo
) {
public static GroupSummaryResponse of(final GroupMemberInfoResponse groupHostInfo, final String groupName, final String groupProfileImageUrl, final Long groupMemberCount, final String groupInvitationCode, final List<GroupMemberInfoResponse> groupMemberInfo) {
return new GroupSummaryResponse(groupHostInfo, groupName, groupProfileImageUrl, groupMemberCount, groupInvitationCode, groupMemberInfo);}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.noostak.group.dto.response;
package org.noostak.group.dto.response.retrieve;

import org.noostak.group.domain.Group;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.noostak.group.dto.response;
package org.noostak.group.dto.response.retrieve;

import org.noostak.group.domain.Group;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.noostak.group.dto.response;
package org.noostak.group.dto.response.retrieve;

import java.util.List;

Expand Down
Loading