[TIL 45์ผ ์ฐจ] Sprint Mission6 - Feedback
์ค๋์ ํ์ต
- ๊ฐ๋ฐ ์งํ ์ํฉ
- Feedback : ์ฑ๋ ๋ชฉ๋ก ์กฐํ ์ N+1 ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ
- ์ฑ๋ ๋ชฉ๋ก ์กฐํ ์
ReadStatus๋ฅผ ์กฐํํ๋ ๋ถ๋ถ์ ๊ฐ์ - Feedback ๋ฐ๋ก๊ฐ๊ธฐ
- ์ฑ๋ ๋ชฉ๋ก ์กฐํ ์
- Feedback : ์ฑ๋ ๋ชฉ๋ก ์กฐํ ์ N+1 ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ
- JPQL์ ์์ฑ์ ํํ์(Constructor Expression)
์กฐํ ๊ฒฐ๊ณผ๋ฅผ Entity๊ฐ ์๋ DTO ๊ฐ์ฒด๋ก ๋ฐ๋ก ๋ง๋ค์ด์ ๋ฐํํ๋ ๋ฌธ๋ฒ
- ํํ :
new DTO์ด๋ฆ(์กฐํ๊ฐ1, ์กฐํ๊ฐ2) - ์์ :
SELECT new com.sprint.mission.discodeit.dto.message.ChannelLastMessageAtDto(m.channel.id, max(m.createdAt))
- ํํ :
Map๋ง๋ค๊ธฐ- ํ๋์ ํค์ ์ฌ๋ฌ ๊ฐ์ ๋งคํํด์ ๋น ๋ฅด๊ฒ ์ฐพ๊ณ ์ถ์ ๋,
Map๊ณผStream API,getOrDefault()๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ฉด ๊น๋ํ ์ฝ๋๋ฅผ ๋ง๋ค ์ ์๋ค. - Map์ ์ฌ์ฉํ๋ฉด, ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์กฐํํ ๋ค, ๋ฉ๋ชจ๋ฆฌ์์ ๋น ๋ฅด๊ฒ ๊บผ๋ด ์ฌ์ฉํ ์ ์์ด, ๋งค๋ฒ DB์ ์กฐํํ์ฌ ๋ฐ์ํ๋ N+1 ๋ฌธ์ ๋ฅผ ์๋ฐํ ์ ์๋ค.
Collectors.toMap(keyMapper, valueMapper):Stream์์๋ค์Map์ผ๋ก ๋ณํํ๋ ๋ฉ์๋keyMapper: ์ด๋ค ๊ฐ์ key๋ก ์ฌ์ฉํ ์งvalueMapper: ์ด๋ค ๊ฐ์ value๋ก ์ฌ์ฉํ ์ง
Collectors.groupingBy(classifier, downstream):Stream์์๋ค์ ์ด๋ค ๊ธฐ์ค์ผ๋ก ๋ฌถ์ดMap์ผ๋ก ๋ณํํ๋ ๋ฉ์๋classifier: ๋ฌด์์ ๊ธฐ์ค์ผ๋ก ๊ทธ๋ฃน์ ๋ฐ์ดํฐ๋ฅผ ๋๋์งdownstream: ๊ฐ ๊ทธ๋ฃน ์์ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ๊ฐ๊ณตํ ์ง
Collectors.mapping(mapper, downstream): ๊ทธ๋ฃน์ ๋ค์ด์๋ ์์๋ฅผ ๋ค๋ฅธ ํํ๋ก ๋ณํํ ๋ค ์์งํ ๋ ์ฌ์ฉmapper: ์ด๋ค ๊ฐ์ผ๋ก ๋ณํํ ์งdownstream: ๋ณํ ํ ์ด๋ป๊ฒ ๋ชจ์์ง
-
Collectors.toList(): Stream ๊ฒฐ๊ณผ๋ฅผList๋ก ๋ชจ์์ค๋ค.// ๊ฐ ์ฑ๋์ ๋ง์ง๋ง ๋ฉ์์ง createdAt ์๊ฐ Map<UUID, Instant> lastMessageAtMap = messageRepository.findLastMessageAtDtoByChannelIds(channelIds).stream() .collect(Collectors.toMap( dto -> dto.id(), dto -> dto.lastMessageAt() ) ); // ์ฑ๋๋ณ ์ฐธ๊ฐ์ ๋ชฉ๋ก ์กฐํ Map<UUID, List<UserDto>> participantMap = readStatusRepository.findAllByChannelIdsWithUserAndChannel(privateChannelIds).stream() .collect(Collectors.groupingBy( readStatus -> readStatus.getChannel().getId(), Collectors.mapping( readStatus -> userMapper.toDto(readStatus.getUser()), Collectors.toList() ) ));
- ํ๋์ ํค์ ์ฌ๋ฌ ๊ฐ์ ๋งคํํด์ ๋น ๋ฅด๊ฒ ์ฐพ๊ณ ์ถ์ ๋,
getOrDefault(key, defaultValue)- ์ด๋ค
key๋ก ๊ฐ์ ์ฐพ์ ๋,key๊ฐ ์กด์ฌํ๋ฉด ํด๋น ๊ฐ์ ๋ฐํํ๊ณ ,key๊ฐ ์๋ค๋ฉด ๊ธฐ๋ณธ๊ฐ์ ๋์ ๋ฐํํด์ฃผ๋ ๋ฉ์๋๋ก,Map์ธํฐํ์ด์ค์์ ๋ง์ด ์ฌ์ฉํจ. key: ์ฐพ๊ณ ์ถ์ ํคdefaultValur: ํค๊ฐ ์์ ๋ ๋์ ๋ฐํํ ๊ฐ
protected List<UserDto> assignParticipantInMap(Channel channel, Map<UUID, List<UserDto>> participantMap) { if (!channel.getType().equals(ChannelType.PRIVATE)) { return List.of(); } return participantMap.getOrDefault(channel.getId(), List.of()); } protected Instant assignLastMessageAtInMap(Channel channel, Map<UUID, Instant> lastMessageAtMap) { return lastMessageAtMap.getOrDefault(channel.getId(), null); }- ์ด๋ค
26.03.09 - Feedback
Feedback01
๋ฌธ์ : ์ฑ๋ ๋ชฉ๋ก ์กฐํ ์ N+1 ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ
- ์ฑ๋ ๋ชฉ๋ก ์กฐํ ์
ReadStatus๋ฅผ ์กฐํํ๋ ๋ถ๋ถ์ ๊ฐ์ ํ์ - ์ฑ๋ ํ๋๋ง๋ค
ReadStatus๋ฅผ ์กฐํํ๋ ๊ฒ ์๋๋ผ - ์๋น์ค ๊ณ์ธต์์ ์ฟผ๋ฆฌ๋ฅผ 2~3๋ฒ์ผ๋ก ๋๋ ์ ์กฐํ ํ, Mapper์์ ํฉ์น๊ธฐ
- ์ฑ๋ ๋ชฉ๋ก๋ง ์กฐํ
- ์ฑ๋๋ณ ๋ง์ง๋ง ๋ฉ์์ง ์๊ฐ ํ ๋ฒ์ ์กฐํ
- ์ฑ๋๋ณ ์ฐธ๊ฐ์ ๋ชฉ๋ก ์กฐํ
- ์ 3๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ํฉ์ณ์
ChannelDto๋ฅผ ๋ง๋ค๊ธฐ.
์์ ์ ์ฝ๋
@Service
@RequiredArgsConstructor
@Transactional
public class BasicChannelService implements ChannelService {
private final ChannelRepository channelRepository;
private final UserRepository userRepository;
private final ReadStatusRepository readStatusRepository;
private final MessageRepository messageRepository;
private final ChannelMapper channelMapper;
@Transactional(readOnly = true)
@Override
public List<ChannelDto> findAllByUserId(UUID userId) {
// User ID null ๊ฒ์ฆ
validateUserByUserId(userId);
// ๋ชจ๋ ์ฑ๋์์ PUBLIC์ธ ์ฑ๋ ์ ์ฒด์ ์ ์ ๊ฐ ์ฐธ์ฌํ ๋ชจ๋ ์ฑ๋
return channelRepository.findChannelByUserId(ChannelType.PUBLIC, userId).stream()
.map(channel -> channelMapper.toDto(channel))
.toList();
}
public void validateUserByUserId(UUID userId) {
ValidationMethods.validateId(userId);
userRepository.findById(userId)
.orElseThrow(() -> new NoSuchElementException("User with id " + userId + " not found"));
}
}
public interface ReadStatusRepository extends JpaRepository<ReadStatus, UUID> {
@Query(value = "SELECT r FROM ReadStatus AS r " +
"LEFT JOIN FETCH r.channel " +
"LEFT JOIN FETCH r.user AS u " +
"LEFT JOIN FETCH u.profile " +
"LEFT JOIN FETCH u.status " +
"Where r.channel.id = :channelId")
List<ReadStatus> findAllByChannelIdWithUserAndChannel(@Param("channelId") UUID channelId);
}
@Mapper(componentModel = "spring", uses = {UserMapper.class})
public abstract class ChannelMapper {
@Autowired
private MessageRepository messageRepository;
@Autowired
private ReadStatusRepository readStatusRepository;
@Autowired
private UserMapper userMapper;
@Mapping(target = "participants", expression = "java(assignParticipants(channel))")
@Mapping(target = "lastMessageAt", expression = "java(assignLastMessageAt(channel))")
public abstract ChannelDto toDto(Channel channel);
protected List<UserDto> assignParticipants(Channel channel) {
List<UserDto> participants = new ArrayList<>();
if (channel.getType().equals(ChannelType.PRIVATE)) {
readStatusRepository.findAllByChannelIdWithUserAndChannel(channel.getId()).stream()
.map(readStatus -> userMapper.toDto(readStatus.getUser()))
.forEach(userDto -> participants.add(userDto));
}
return participants;
}
protected Instant assignLastMessageAt(Channel channel) {
return messageRepository.findLastMessageAtByChannelId(channel.getId())
.orElse(null);
}
}
์์ ํ ์ฝ๋
@Service
@RequiredArgsConstructor
@Transactional
public class BasicChannelService implements ChannelService {
private final ChannelRepository channelRepository;
private final UserRepository userRepository;
private final ReadStatusRepository readStatusRepository;
private final MessageRepository messageRepository;
private final ChannelMapper channelMapper;
private final UserMapper userMapper;
@Transactional(readOnly = true)
@Override
public List<ChannelDto> findAllByUserId(UUID userId) {
// User ID null ๊ฒ์ฆ
validateUserByUserId(userId);
// ์ ๊ทผ ๊ฐ๋ฅํ ์ ์ฒด ์ฑ๋ ์กฐํ
List<Channel> channels = channelRepository.findChannelByUserId(ChannelType.PUBLIC, userId);
// ์ ๊ทผ ๊ฐ๋ฅํ ์ ์ฒด ์ฑ๋ ID
List<UUID> channelIds = channels.stream()
.map(channel -> channel.getId())
.toList();
// ์ ๊ทผ ๊ฐ๋ฅํ Private ์ฑ๋ ID
List<UUID> privateChannelIds = channels.stream()
.filter(channel -> channel.getType().equals(ChannelType.PRIVATE))
.map(channel -> channel.getId())
.toList();
// ๊ฐ ์ฑ๋์ ๋ง์ง๋ง ๋ฉ์์ง createdAt ์๊ฐ
Map<UUID, Instant> lastMessageAtMap = messageRepository.findLastMessageAtDtoByChannelIds(channelIds).stream()
.collect(Collectors.toMap(
dto -> dto.id(),
dto -> dto.lastMessageAt()
)
);
// ์ฑ๋๋ณ ์ฐธ๊ฐ์ ๋ชฉ๋ก ์กฐํ
Map<UUID, List<UserDto>> participantMap = readStatusRepository.findAllByChannelIdsWithUserAndChannel(privateChannelIds).stream()
.collect(Collectors.groupingBy(
readStatus -> readStatus.getChannel().getId(),
Collectors.mapping(
readStatus -> userMapper.toDto(readStatus.getUser()),
Collectors.toList()
)
));
return channels.stream()
.map(channel -> channelMapper.toListDto(channel, participantMap, lastMessageAtMap))
.toList();
}
public void validateUserByUserId(UUID userId) {
ValidationMethods.validateId(userId);
userRepository.findById(userId)
.orElseThrow(() -> new NoSuchElementException("User with id " + userId + " not found"));
}
}
public interface MessageRepository extends JpaRepository<Message, UUID> {
@Query(value = "SELECT max(m.createdAt) AS lastMessageAt " +
"FROM Message AS m " +
"WHERE m.channel.id = :channelId")
Optional<Instant> findLastMessageAtByChannelId(@Param("channelId") UUID channelId);
@Query(value = "SELECT new com.sprint.mission.discodeit.dto.message.ChannelLastMessageAtDto(m.channel.id, max(m.createdAt)) " +
"FROM Message AS m " +
"WHERE m.channel.id IN :channelIds " +
"GROUP BY m.channel.id")
List<ChannelLastMessageAtDto> findLastMessageAtDtoByChannelIds(@Param("channelIds") List<UUID> channelIds);
}
package com.sprint.mission.discodeit.dto.message;
import java.time.Instant;
import java.util.UUID;
public record ChannelLastMessageAtDto(
UUID id, // channelId
Instant lastMessageAt
) {
}
public interface ReadStatusRepository extends JpaRepository<ReadStatus, UUID> {
@Query(value = "SELECT r FROM ReadStatus AS r " +
"LEFT JOIN FETCH r.channel " +
"LEFT JOIN FETCH r.user AS u " +
"LEFT JOIN FETCH u.profile " +
"LEFT JOIN FETCH u.status " +
"Where r.channel.id = :channelId")
List<ReadStatus> findAllByChannelIdWithUserAndChannel(@Param("channelId") UUID channelId);
@Query(value = "SELECT r FROM ReadStatus AS r " +
"LEFT JOIN FETCH r.channel " +
"LEFT JOIN FETCH r.user AS u " +
"LEFT JOIN FETCH u.profile " +
"LEFT JOIN FETCH u.status " +
"Where r.channel.id IN :channelIds")
List<ReadStatus> findAllByChannelIdsWithUserAndChannel(@Param("channelIds") List<UUID> channelIds);
}
@Mapper(componentModel = "spring", uses = {UserMapper.class})
public abstract class ChannelMapper {
@Autowired
private MessageRepository messageRepository;
@Autowired
private ReadStatusRepository readStatusRepository;
@Autowired
private UserMapper userMapper;
@Mapping(target = "participants", expression = "java(assignParticipants(channel))")
@Mapping(target = "lastMessageAt", expression = "java(assignLastMessageAt(channel))")
public abstract ChannelDto toDto(Channel channel);
// `@Mapping`์ ์ถ์ ๋ฉ์๋์์๋ง ์ฌ์ฉ ๊ฐ๋ฅ
public ChannelDto toListDto(Channel channel, Map<UUID, List<UserDto>> participantMap, Map<UUID, Instant> laseMessageAtMap) {
return new ChannelDto(
channel.getId(),
channel.getType(),
channel.getName(),
channel.getDescription(),
assignParticipantInMap(channel, participantMap),
assignLastMessageAtInMap(channel, laseMessageAtMap)
);
}
protected List<UserDto> assignParticipants(Channel channel) {
List<UserDto> participants = new ArrayList<>();
if (channel.getType().equals(ChannelType.PRIVATE)) {
readStatusRepository.findAllByChannelIdWithUserAndChannel(channel.getId()).stream()
.map(readStatus -> userMapper.toDto(readStatus.getUser()))
.forEach(userDto -> participants.add(userDto));
}
return participants;
}
protected Instant assignLastMessageAt(Channel channel) {
return messageRepository.findLastMessageAtByChannelId(channel.getId())
.orElse(null);
}
protected List<UserDto> assignParticipantInMap(Channel channel, Map<UUID, List<UserDto>> participantMap) {
if (!channel.getType().equals(ChannelType.PRIVATE)) {
return List.of();
}
return participantMap.getOrDefault(channel.getId(), List.of());
}
protected Instant assignLastMessageAtInMap(Channel channel, Map<UUID, Instant> lastMessageAtMap) {
return lastMessageAtMap.getOrDefault(channel.getId(), null);
}
}
GitHub Repository ์ฃผ์
https://github.com/JungH200000/10-sprint-mission/tree/sprint6
Leave a comment