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/#163/participant service #168

Merged
merged 17 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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 @@ -16,6 +16,7 @@
import space.space_spring.exception.CustomException;
import space.space_spring.response.BaseResponse;
import space.space_spring.service.LiveKitService;
import space.space_spring.service.VoiceRoomParticipantService;
import space.space_spring.service.VoiceRoomService;
import space.space_spring.util.space.SpaceUtils;
import space.space_spring.util.user.UserUtils;
Expand All @@ -40,6 +41,7 @@ public class VoiceRoomController {
private final UserSpaceDao userSpaceDao;
private final UserUtils userUtils;
private final SpaceUtils spaceUtils;
private final VoiceRoomParticipantService voiceRoomParticipantService;

//VoiceRoom 생성/수정
@PostMapping("")
Expand Down Expand Up @@ -133,10 +135,11 @@ public BaseResponse<GetParticipantList.Response> getParticipants(
//해당 voiceRoom이 해당 space에 속한것이 맞는지 확인
validateVoiceRoomInSpace(spaceId,roomId);

List<GetParticipantList.ParticipantInfo> participantInfoList = voiceRoomService.getParticipantInfoListById(roomId);
//List<GetParticipantList.ParticipantInfo> participantInfoList = voiceRoomService.getParticipantInfoListById(roomId);
List<GetParticipantList.ParticipantInfo> participantInfoList = voiceRoomParticipantService.getParticipantInfoListById(roomId);

return new BaseResponse<GetParticipantList.Response>(new GetParticipantList.Response(participantInfoList));
}

@PatchMapping("")
public BaseResponse<String> updateVoiceRoom(
@PathVariable("spaceId") @NotNull long spaceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static class Response{
@Setter
public static class ParticipantInfo{
private String name;
private Long userId;
private Long id;
private Long userSpaceId;
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 필드 네이밍을 userId 에서 id 로 변경하신 특별한 이유가 있을까요??
프로젝트 전체 코드들에서 userId 로 통일된 느낌이 있는데, 이 필드의 네이밍을 수정하신 이유가 궁금합니다!

Copy link
Member

Choose a reason for hiding this comment

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

의도와 맞는진 모르겠지만 클린코드를 실천하려면 불필요한 중복은 피하는 게 좋다고 하니 변경한 네이밍이 더 적절할 것 같아요!
https://dining-developer.tistory.com/68

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

participant의 id는 LiveKit 에서 각 참가자를 구분하는 id입니다. userId와 userSpaceId는 저희 서비스에서 사용하는 Id입니다.
원래는 LiveKit에서 Id값을 userId 로 사용했습니다. 하지만 개발 과정에서 userId가 아닌 userSpaceId로 변경이 되었고, 이 과정에서 코드를 수정하는데 꽤 번거로움?헷갈림?이 발생했었습니다.

따라서 LiveKit id와 서비스 로직의 userSpaceId를 구분하여 코드를 작성해야겠다고 생각해 위와 같이 수정하였습니다.
현재 participant 내의 userSpaceId와 id는 값은 같고, 의미적으로 분리해놓은 상태입니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

아하 ParticipantInfo 자체의 고유한 id 라는 말씀이시군요! 이해했습니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

의도와 맞는진 모르겠지만 클린코드를 실천하려면 불필요한 중복은 피하는 게 좋다고 하니 변경한 네이밍이 더 적절할 것 같아요! https://dining-developer.tistory.com/68

저도 제가 예전에 작성한 코드보면서 느낀건데, 굳이 Entity 내부의 id 필드명이 userId, spaceId 와 같이 엔티티의 이름이 한번 더 중복해서 들어가 있는게 불필요해 보이더라고요. 레퍼런스 참고해서 이 부분도 수정해보겠습니다!

private boolean isMute;
private String profileImage;
Expand All @@ -39,7 +39,7 @@ public static ParticipantInfo convertParticipantDto(ParticipantDto participantDt
.name(participantDto.getName())
.isMute(participantDto.isMicMute())
.profileImage(participantDto.getProfileImage())
.userId(participantDto.getId())
.id(participantDto.getId())
.userSpaceId(participantDto.getUserSpaceId())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static VoiceRoomInfo convertRoomDto(RoomDto roomDto){
.order(roomDto.getOrder())
.metadate(roomDto.getMetadata())
.participantInfoList(
GetParticipantList.ParticipantInfo.convertParticipantDtoList(roomDto.getParticipantDTOList())
GetParticipantList.ParticipantInfo.convertParticipantDtoList(roomDto.getParticipantListDto().getParticipantDtoList())
)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ public void setProfileImage(String imageUrl){

public static ParticipantDto convertParticipant(LivekitModels.ParticipantInfo participantInfo){
if(participantInfo==null){return null;}
Long id = Long.valueOf(participantInfo.getIdentity());
Long userSpaceId = id;
return ParticipantDto.builder()
.id(Long.valueOf(participantInfo.getIdentity()))
.id(id)
.userSpaceId(userSpaceId)
.name(participantInfo.getName())
.isMicMute(checkMicMute(participantInfo.getTracksList()))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package space.space_spring.dto.VoiceRoom;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ParticipantListDto {


private List<ParticipantDto> participantDtoList;
Copy link
Member

Choose a reason for hiding this comment

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

final을 적용하시지 않은 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

불변성이 없어서 final을 붙이지 않았습니다.
하지만, 재할당을 막기 위해서 final을 추가하는 것이 안정성 면에서 좋아보이네요!
필요한 클래스에 추가하겠습니다!

Copy link
Member

Choose a reason for hiding this comment

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

넵 만약 불변성까지 보장하려면 생성할 때 unmodifiable로 넣어주면 될 것 같습니다


private ParticipantListDto(List<ParticipantDto> participantDtoList){
this.participantDtoList=participantDtoList;
}

public static ParticipantListDto from(List<ParticipantDto> participantDtoList ){
return new ParticipantListDto(participantDtoList);
}
public static ParticipantListDto empty(){
return new ParticipantListDto(Collections.emptyList());
}

public static ParticipantListDto nullList(){
return new ParticipantListDto(null);
}

public void setProfileImage(Function<Long, String> profileFinder){
this.participantDtoList.forEach(participantDto -> {
String profileImage = profileFinder.apply(participantDto.getUserSpaceId());
participantDto.setProfileImage(profileImage);
});
}



//Todo 생성/변환 책임분리 필요
public List<GetParticipantList.ParticipantInfo> convertParticipantDtoList(){
if(this.participantDtoList==null){System.out.print("\n[DEBUG] participant List is NULL\n"); return null;}
if(this.participantDtoList.isEmpty()){System.out.print("\n[DEBUG] participant List is Empty\n"); return Collections.emptyList();}
return this.participantDtoList.stream()
.map(GetParticipantList.ParticipantInfo::convertParticipantDto)
.collect(Collectors.toList());
}

public List<ParticipantDto> getParticipantDtoList() {
return participantDtoList;
}
}
35 changes: 23 additions & 12 deletions src/main/java/space/space_spring/dto/VoiceRoom/RoomDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,34 @@ public class RoomDto {
private int numParticipants;
private long startTime; //time Stamp
private long createdAt;
private List<ParticipantDto> participantDTOList;
//private List<ParticipantDto> participantDTOList;
private ParticipantListDto participantListDto;
private int order;
private String sid;
private String metadata;


public void setParticipantDTOListByInfo(List<LivekitModels.ParticipantInfo> participantInfoList){
if(participantInfoList==null||participantInfoList.isEmpty()){return;}
this.participantDTOList = participantInfoList.stream()
.map(ParticipantDto::convertParticipant)
.collect(Collectors.toList());
}
// public void setParticipantDTOListByInfo(List<LivekitModels.ParticipantInfo> participantInfoList){
// if(participantInfoList==null||participantInfoList.isEmpty()){return;}
// this.participantDTOList = participantInfoList.stream()
// .map(ParticipantDto::convertParticipant)
// .collect(Collectors.toList());
// }
public RoomDto setParticipantDTOList(List<ParticipantDto> participantDtoList){
this.participantDTOList = participantDtoList;
if(participantDtoList==null){
this.participantListDto = ParticipantListDto.empty();
}
this.participantListDto = ParticipantListDto.from(participantDtoList);
return this;
}

public RoomDto setParticipantDTOList(ParticipantListDto participants){
if(participants==null){
this.participantListDto = ParticipantListDto.empty();
}
this.participantListDto = participants;
return this;
}
public static RoomDto convertRoom(LivekitModels.Room room){
if(room==null){return null;}
return RoomDto.builder()
Expand All @@ -45,7 +56,7 @@ public static RoomDto convertRoom(LivekitModels.Room room){
.startTime(room.getCreationTime())
.sid(room.getSid())
.metadata(room.getMetadata())
.participantDTOList(null)
.participantListDto(ParticipantListDto.nullList())
.build();
}

Expand All @@ -67,7 +78,7 @@ public static RoomDto convertRoom(VoiceRoom voiceRoom){
.sid(null)
.metadata(null)
//.startTime()
.participantDTOList(null)
.participantListDto(ParticipantListDto.nullList())
.build();
}

Expand Down Expand Up @@ -128,6 +139,6 @@ public String toString() {
}
}



public ParticipantListDto getParticipantListDto(){return participantListDto;}
//public List<ParticipantDto> getParticipantDtoList(){return participantListDto.getParticipantDtoList();}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,11 @@ public List<GetVoiceRoomList.VoiceRoomInfo> convertVoicRoomInfoList() {
return convertVoicRoomInfoList( null);
}

public void setParticipantListDto(Map<Long,ParticipantListDto> participantListDtoMap){

for(RoomDto roomDto : this.roomDtoList){
roomDto.setParticipantDTOList(participantListDtoMap.get(roomDto.getId()));
}
}

}
2 changes: 1 addition & 1 deletion src/main/java/space/space_spring/entity/VoiceRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public RoomDto convertRoomDto(){
.sid(null)
.metadata(null)
//.startTime()
.participantDTOList(null)
.participantListDto(null)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package space.space_spring.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Service;
import space.space_spring.dao.UserSpaceDao;
import space.space_spring.dao.VoiceRoomRepository;
import space.space_spring.domain.user.repository.UserDao;
import space.space_spring.dto.VoiceRoom.GetParticipantList;
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

import space.space_spring.dto.VoiceRoom.ParticipantDto;
import space.space_spring.dto.VoiceRoom.ParticipantListDto;
import space.space_spring.dto.VoiceRoom.RoomDto;
import space.space_spring.entity.Space;
import space.space_spring.entity.User;
import space.space_spring.entity.UserSpace;
import space.space_spring.util.LiveKitUtils;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class VoiceRoomParticipantService {
final private UserSpaceDao userSpaceDao;
final private UserDao userDao;
final private VoiceRoomRepository voiceRoomRepository;
final private LiveKitUtils liveKitUtils;
private final TaskExecutor taskExecutor;
public List<GetParticipantList.ParticipantInfo> getParticipantInfoListById(long voiceRoomId){
return getParticipantDtoListById(voiceRoomId).convertParticipantDtoList();
}
private ParticipantListDto getParticipantDtoListById(long voiceRoomId){
Space space = voiceRoomRepository.findById(voiceRoomId).getSpace();
Copy link
Collaborator

Choose a reason for hiding this comment

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

voiceRoomRepository.findById() 메서드가 Optional 객체를 return 하고, 이 결과값이 null 인지 검증하는 로직이 추가되면 좋을거 같습니다!

Copy link
Member

Choose a reason for hiding this comment

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

Space space = voiceRoomRepository.findById(voiceRoomId).orElseThrow(IllegalArgumentException::new);

이런 식으로 한 줄에 null 처리도 가능합니당

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

해당 내용 수정해서 push 했습니다.

Copy link
Member

Choose a reason for hiding this comment

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

앗 추가로 ((이미 알고 계실 수도 있겠지만))
jpa에서 find 반환값이 리스트인 경우 조회 결과가 없다면 null이 아닌 빈 리스트를 반환해서
이 경우엔 Optional 대신 리스트가 empty인지로 체크하면 된다고 합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

저번에 Optional로 통일하기로 정했어서 Optional 사용하겠습니다.

Copy link
Member

Choose a reason for hiding this comment

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

그럼 null인 경우 자체가 없어도 Optional로 통일하는 건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오류나 예외적인 상황 (예를 들어 글을 읽는 도중 글이 삭제됨. Post Entity 검색 불가) 발생하면, null-pointer error가 발생할 수 있다고 생각합니다

Copy link
Member

@hyunn522 hyunn522 Nov 5, 2024

Choose a reason for hiding this comment

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

앗 추가로 ((이미 알고 계실 수도 있겠지만)) jpa에서 find 반환값이 리스트인 경우 조회 결과가 없다면 null이 아닌 빈 리스트를 반환해서 이 경우엔 Optional 대신 리스트가 empty인지로 체크하면 된다고 합니다!

아 제가 말씀드린 건 이 케이스였습니다! 단일 엔티티를 반환하는 케이스는 Optional로 통일하는 것이 좋은데
여러 엔티티를 리스트 형태로 반환하는 케이스에선 반환값이 null인 경우가 아예 발생하지 않아서요

Copy link
Collaborator

Choose a reason for hiding this comment

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

앗 추가로 ((이미 알고 계실 수도 있겠지만)) jpa에서 find 반환값이 리스트인 경우 조회 결과가 없다면 null이 아닌 빈 리스트를 반환해서 이 경우엔 Optional 대신 리스트가 empty인지로 체크하면 된다고 합니다!

아 제가 말씀드린 건 이 케이스였습니다! 단일 엔티티를 반환하는 케이스는 Optional로 통일하는 것이 좋은데 여러 엔티티를 리스트 형태로 반환하는 케이스에선 반환값이 null인 경우가 아예 발생하지 않아서요

오 이부분은 공통적으로 생각해봐야할거같은데요? 레포지토리의 반환값이 list 인 메서드를 서비스단이 사용할 때는, 해당 이슈 생각해서 검증하면 좋을 듯 합니다

//Todo 다른 네이밍 고려
List<ParticipantDto> participantDtos = liveKitUtils.getParticipantInfo(String.valueOf(voiceRoomId));
if(participantDtos==null || participantDtos.isEmpty()){
participantDtos = Collections.emptyList();
}

ParticipantListDto participantListDto = ParticipantListDto.from(participantDtos);
participantListDto.setProfileImage(this::findProfileImageByUserSpaceId);
return participantListDto;
}
private String findProfileImageByUserSpaceId(Long userSpaceId){
return userSpaceDao.findProfileImageById(userSpaceId).orElse("");
}




//1. 이 함수를 VoiceRoomListDto의 parameter로 넘긴다
//장점. 동기 처리의 책임을 service가 질 수 있음
//단점. 굳이 이렇게까지 코드를 꼬야야하나 싶을 수 있음. 그냥 getRoomList해서 RoomList를 밖으로 빼는게 나을지도

//2. 이 함수를 VoiceRoomListDto 내부로 이전
//장점. findProfileImageByUserSpaceId만 function parameter로 넘기면 됨 -> 책임과 구조가 더 명확하게 보인다고
//단점. 비동기 병렬처리의 책임을 VoiceRoomListDto가 가져야함.
public void setParticipant(List<RoomDto> roomDtoList){
List<CompletableFuture<Void>> roomDtoFutureList = roomDtoList.stream()
.map(r->CompletableFuture.runAsync(()->r.setParticipantDTOList(getParticipantDtoListById(r.getId())),taskExecutor)
//.exceptionally(ex->{throws ex;})
)
.collect(Collectors.toList());


// 모든 Future의 완료를 기다림
CompletableFuture<Void> allOf = CompletableFuture.allOf(
roomDtoFutureList.toArray(new CompletableFuture[0]));

// 결과 수집 및 출력
allOf.join();
}
public Map<Long,ParticipantListDto> getParticipantList(List<Long> roomIdList){

Map<Long,CompletableFuture<ParticipantListDto>> futureMap = roomIdList.stream()
.collect(Collectors.toMap(
roomId->roomId,
roomId->CompletableFuture.supplyAsync(
()-> getParticipantDtoListById(roomId),
taskExecutor

).exceptionally(throwable -> {
log.error("failed to fetch and get participantList",throwable);
return null;//empty ParticipantListDto
})
));
try {
// 모든 Future의 완료를 기다림
CompletableFuture.allOf(
futureMap.values().toArray(new CompletableFuture[0]))
.exceptionally(throwable -> {
log.error("Error while waiting for all participant fetches to complete",throwable);
return null;
})
.join();

return futureMap.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey(),
entry->entry.getValue().getNow(ParticipantListDto.empty())
));

}catch (Exception e){
log.error("Critical error while processing participant fetches", e);
return Collections.emptyMap(); // 심각한 오류 발생 시 빈 Map 반환
}
}
}
Loading
Loading