[TIL 45์ผ ์ฐจ] Sprint Mission6 - Feedback

์˜ค๋Š˜์˜ ํ•™์Šต

  1. ๊ฐœ๋ฐœ ์ง„ํ–‰ ์ƒํ™ฉ
    • Feedback : ์ฑ„๋„ ๋ชฉ๋ก ์กฐํšŒ ์‹œ N+1 ๋ฌธ์ œ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
  2. JPQL์˜ ์ƒ์„ฑ์ž ํ‘œํ˜„์‹(Constructor Expression) ์กฐํšŒ ๊ฒฐ๊ณผ๋ฅผ Entity๊ฐ€ ์•„๋‹Œ DTO ๊ฐ์ฒด๋กœ ๋ฐ”๋กœ ๋งŒ๋“ค์–ด์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฌธ๋ฒ•
    • ํ˜•ํƒœ : new DTO์ด๋ฆ„(์กฐํšŒ๊ฐ’1, ์กฐํšŒ๊ฐ’2)
    • ์˜ˆ์‹œ : SELECT new com.sprint.mission.discodeit.dto.message.ChannelLastMessageAtDto(m.channel.id, max(m.createdAt))
  3. 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()
                      )
              ));
      
  4. 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์—์„œ ํ•ฉ์น˜๊ธฐ
    1. ์ฑ„๋„ ๋ชฉ๋ก๋งŒ ์กฐํšŒ
    2. ์ฑ„๋„๋ณ„ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ ์‹œ๊ฐ„ ํ•œ ๋ฒˆ์— ์กฐํšŒ
    3. ์ฑ„๋„๋ณ„ ์ฐธ๊ฐ€์ž ๋ชฉ๋ก ์กฐํšŒ
    4. ์œ„ 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