diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..03640e68 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,53 @@ +# μ„±λŠ₯ κ°œμ„  + +## πŸš€ κ°œμ„  λ‚΄μš© + +- κ°œμ„  λŒ€μƒ: +- λ³€κ²½ λ‚΄μš©: + +## πŸ” 문제 및 원인 + +- ν™•μΈλœ 문제: +- 원인: + +## πŸ“Š κ°œμ„  κ²°κ³Ό + +| μ§€ν‘œ | κ°œμ„  μ „ | κ°œμ„  ν›„ | +| ---------- | ------: | ------: | +| TPS / RPS | | | +| Avg | | | +| P95 | | | +| P99 | | | +| Error Rate | | | + +## βœ… μΆ”κ°€ 확인 + +- μΆ”κ°€ ν…ŒμŠ€νŠΈ λ˜λŠ” 확인이 ν•„μš”ν•œ λΆ€λΆ„: + +--- + +# ν…ŒμŠ€νŠΈ + +## πŸ§ͺ ν…ŒμŠ€νŠΈ λ‚΄μš© + +- ν…ŒμŠ€νŠΈ λŒ€μƒ: +- ν…ŒμŠ€νŠΈ λͺ©μ : + +## βš™οΈ ν…ŒμŠ€νŠΈ 쑰건 + +- VU: +- ν…ŒμŠ€νŠΈ μ‹œκ°„: +- μš”μ²­ 수 / μš”μ²­ λΉˆλ„: + +## πŸ“Š ν…ŒμŠ€νŠΈ κ²°κ³Ό + +- TPS / RPS: +- Avg: +- P95: +- P99: +- Error Rate: + +## πŸ” ν™•μΈλœ 문제 + +- νŠΉμ΄μ‚¬ν•­ λ˜λŠ” ν™•μΈλœ 병λͺ©: +- μΆ”κ°€ 확인이 ν•„μš”ν•œ λΆ€λΆ„: diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a4570e15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +**/node_modules/ + +**/.next/ + +# ν™˜κ²½ λ³€μˆ˜ +.env +.env.* +!.env.example + +# Java / Maven build +**/target/ \ No newline at end of file diff --git a/apps/backend/.mvn/wrapper/maven-wrapper.jar b/apps/backend/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..bf82ff01 Binary files /dev/null and b/apps/backend/.mvn/wrapper/maven-wrapper.jar differ diff --git a/apps/backend/src/main/java/com/ktb/chatapp/repository/MessageRepository.java b/apps/backend/src/main/java/com/ktb/chatapp/repository/MessageRepository.java index 932ce8e8..0080a53c 100644 --- a/apps/backend/src/main/java/com/ktb/chatapp/repository/MessageRepository.java +++ b/apps/backend/src/main/java/com/ktb/chatapp/repository/MessageRepository.java @@ -2,9 +2,12 @@ import com.ktb.chatapp.model.Message; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; @@ -19,6 +22,15 @@ public interface MessageRepository extends MongoRepository { @Query(value = "{ 'room': ?0, 'timestamp': { $gte: ?1 } }", count = true) long countRecentMessagesByRoomId(String roomId, LocalDateTime since); + @Aggregation(pipeline = { + "{ '$match': { 'room': { '$in': ?0 }, 'timestamp': { '$gte': ?1 } } }", + "{ '$group': { '_id': '$room', 'count': { '$sum': 1 } } }", + "{ '$project': { '_id': 0, 'roomKey': '$_id', 'count': 1 } }" + }) + List countRecentMessagesByRoomIds( + Set roomIds, + LocalDateTime since); + /** * fileId둜 λ©”μ‹œμ§€ 쑰회 (파일 κΆŒν•œ κ²€μ¦μš©) */ diff --git a/apps/backend/src/main/java/com/ktb/chatapp/repository/RecentMessageCount.java b/apps/backend/src/main/java/com/ktb/chatapp/repository/RecentMessageCount.java new file mode 100644 index 00000000..5b60eaf4 --- /dev/null +++ b/apps/backend/src/main/java/com/ktb/chatapp/repository/RecentMessageCount.java @@ -0,0 +1,4 @@ +package com.ktb.chatapp.repository; + +public record RecentMessageCount(String roomKey, long count) { +} diff --git a/apps/backend/src/main/java/com/ktb/chatapp/service/RecentMessageCounter.java b/apps/backend/src/main/java/com/ktb/chatapp/service/RecentMessageCounter.java index 39a1fd00..e54daf56 100644 --- a/apps/backend/src/main/java/com/ktb/chatapp/service/RecentMessageCounter.java +++ b/apps/backend/src/main/java/com/ktb/chatapp/service/RecentMessageCounter.java @@ -1,8 +1,12 @@ package com.ktb.chatapp.service; import com.ktb.chatapp.repository.MessageRepository; +import com.ktb.chatapp.repository.RecentMessageCount; import java.time.Duration; import java.time.LocalDateTime; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -21,4 +25,16 @@ public int countRecentMessages(String roomId) { LocalDateTime since = LocalDateTime.now().minus(RECENT_WINDOW); return (int) messageRepository.countRecentMessagesByRoomId(roomId, since); } + + public Map countRecentMessagesByRoomIds(Set roomIds) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + LocalDateTime since = LocalDateTime.now().minus(RECENT_WINDOW); + return messageRepository.countRecentMessagesByRoomIds(roomIds, since).stream() + .collect(Collectors.toMap( + RecentMessageCount::roomKey, + count -> Math.toIntExact(count.count()))); + } } diff --git a/apps/backend/src/main/java/com/ktb/chatapp/service/RoomService.java b/apps/backend/src/main/java/com/ktb/chatapp/service/RoomService.java index 5f5ecf40..9323ab2a 100644 --- a/apps/backend/src/main/java/com/ktb/chatapp/service/RoomService.java +++ b/apps/backend/src/main/java/com/ktb/chatapp/service/RoomService.java @@ -10,9 +10,11 @@ import java.time.LocalDateTime; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,27 +37,39 @@ public RoomsResponse getAllRooms(String name) { try { // 전체 방을 μ‘°νšŒν•΄ μ΅œμ‹ μˆœμœΌλ‘œ μ •λ ¬ν•œλ‹€ - List roomResponses = roomRepository.findAll().stream() - .map(room -> mapToRoomResponse(room, name)) + List rooms = roomRepository.findAll(); + if (rooms.isEmpty()) { + return successfulRoomsResponse(List.of()); + } + + Set userIds = new HashSet<>(); + Set roomIds = new HashSet<>(); + for (Room room : rooms) { + if (room.getId() != null) { + roomIds.add(room.getId()); + } + if (room.getCreator() != null) { + userIds.add(room.getCreator()); + } + if (room.getParticipantIds() != null) { + userIds.addAll(room.getParticipantIds()); + } + } + + Map usersById = new HashMap<>(); + userRepository.findAllById(userIds) + .forEach(user -> usersById.put(user.getId(), user)); + Map recentMessageCounts = + recentMessageCounter.countRecentMessagesByRoomIds(roomIds); + + List roomResponses = rooms.stream() + .map(room -> mapToRoomResponse(room, name, usersById, recentMessageCounts)) .sorted(Comparator.comparing( RoomResponse::getCreatedAtDateTime, Comparator.nullsLast(Comparator.reverseOrder()))) .collect(Collectors.toList()); - PageMetadata metadata = PageMetadata.builder() - .total(roomResponses.size()) - .page(0) - .pageSize(roomResponses.size()) - .totalPages(1) - .hasMore(false) - .currentCount(roomResponses.size()) - .build(); - - return RoomsResponse.builder() - .success(true) - .data(roomResponses) - .metadata(metadata) - .build(); + return successfulRoomsResponse(roomResponses); } catch (Exception e) { log.error("λ°© λͺ©λ‘ 쑰회 μ—λŸ¬", e); @@ -194,6 +208,33 @@ private RoomResponse mapToRoomResponse(Room room, String name) { int recentMessageCount = recentMessageCounter.countRecentMessages(room.getId()); + return buildRoomResponse(room, name, creator, participants, recentMessageCount); + } + + private RoomResponse mapToRoomResponse( + Room room, + String name, + Map usersById, + Map recentMessageCounts) { + User creator = usersById.get(room.getCreator()); + List participants = room.getParticipantIds() == null + ? List.of() + : room.getParticipantIds().stream() + .map(usersById::get) + .filter(java.util.Objects::nonNull) + .toList(); + int recentMessageCount = recentMessageCounts.getOrDefault(room.getId(), 0); + + return buildRoomResponse(room, name, creator, participants, recentMessageCount); + } + + private RoomResponse buildRoomResponse( + Room room, + String name, + User creator, + List participants, + int recentMessageCount) { + return RoomResponse.builder() .id(room.getId()) .name(room.getName() != null ? room.getName() : "제λͺ© μ—†μŒ") @@ -216,4 +257,21 @@ private RoomResponse mapToRoomResponse(Room room, String name) { .recentMessageCount(recentMessageCount) .build(); } + + private RoomsResponse successfulRoomsResponse(List roomResponses) { + PageMetadata metadata = PageMetadata.builder() + .total(roomResponses.size()) + .page(0) + .pageSize(roomResponses.size()) + .totalPages(1) + .hasMore(false) + .currentCount(roomResponses.size()) + .build(); + + return RoomsResponse.builder() + .success(true) + .data(roomResponses) + .metadata(metadata) + .build(); + } } diff --git a/apps/backend/src/test/java/com/ktb/chatapp/repository/MessageRepositoryIntegrationTest.java b/apps/backend/src/test/java/com/ktb/chatapp/repository/MessageRepositoryIntegrationTest.java new file mode 100644 index 00000000..1f645fbb --- /dev/null +++ b/apps/backend/src/test/java/com/ktb/chatapp/repository/MessageRepositoryIntegrationTest.java @@ -0,0 +1,65 @@ +package com.ktb.chatapp.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ktb.chatapp.config.MongoTestContainer; +import com.ktb.chatapp.config.RedisTestContainer; +import com.ktb.chatapp.model.Message; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest +@Import({MongoTestContainer.class, RedisTestContainer.class}) +@TestPropertySource(properties = { + "spring.data.mongodb.auto-index-creation=true", + "socketio.enabled=false" +}) +@DisplayName("MessageRepository 톡합 ν…ŒμŠ€νŠΈ") +class MessageRepositoryIntegrationTest { + + @Autowired private MessageRepository messageRepository; + + @AfterEach + void tearDown() { + messageRepository.deleteAll(); + } + + @Test + @DisplayName("μ—¬λŸ¬ μ±„νŒ…λ°©μ˜ 졜근 λ©”μ‹œμ§€ 수λ₯Ό ν•œ 번의 μ§‘κ³„λ‘œ λ°˜ν™˜ν•œλ‹€") + void countRecentMessagesByRoomIds_GroupsRecentMessagesByRequestedRoom() { + LocalDateTime now = LocalDateTime.now(); + saveMessage("room-1", now.minusMinutes(10)); + saveMessage("room-1", now.minusMinutes(20)); + saveMessage("room-1", now.minusMinutes(40)); + saveMessage("room-2", now.minusMinutes(5)); + saveMessage("room-3", now.minusMinutes(5)); + + List result = + messageRepository.countRecentMessagesByRoomIds( + Set.of("room-1", "room-2"), + now.minusMinutes(30)); + + assertThat(result) + .extracting(RecentMessageCount::roomKey, RecentMessageCount::count) + .containsExactlyInAnyOrder( + org.assertj.core.groups.Tuple.tuple("room-1", 2L), + org.assertj.core.groups.Tuple.tuple("room-2", 1L)); + } + + private void saveMessage(String roomId, LocalDateTime timestamp) { + Message saved = messageRepository.save(Message.builder() + .roomId(roomId) + .content("λ©”μ‹œμ§€") + .build()); + saved.setTimestamp(timestamp); + messageRepository.save(saved); + } +} diff --git a/apps/backend/src/test/java/com/ktb/chatapp/service/RecentMessageCounterTest.java b/apps/backend/src/test/java/com/ktb/chatapp/service/RecentMessageCounterTest.java new file mode 100644 index 00000000..2968d334 --- /dev/null +++ b/apps/backend/src/test/java/com/ktb/chatapp/service/RecentMessageCounterTest.java @@ -0,0 +1,78 @@ +package com.ktb.chatapp.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.ktb.chatapp.repository.MessageRepository; +import com.ktb.chatapp.repository.RecentMessageCount; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@DisplayName("RecentMessageCounter λ‹¨μœ„ ν…ŒμŠ€νŠΈ") +class RecentMessageCounterTest { + + @Mock private MessageRepository messageRepository; + + private RecentMessageCounter recentMessageCounter; + + @BeforeEach + void setUp() { + recentMessageCounter = new RecentMessageCounter(messageRepository); + } + + @Test + @DisplayName("μ—¬λŸ¬ μ±„νŒ…λ°©μ˜ 졜근 λ©”μ‹œμ§€ 수λ₯Ό ν•œ λ²ˆμ— μ‘°νšŒν•΄ Map으둜 λ³€ν™˜ν•œλ‹€") + void countRecentMessagesByRoomIds_LoadsCountsOnceAndMapsByRoomId() { + Set roomIds = Set.of("room-1", "room-2"); + when(messageRepository.countRecentMessagesByRoomIds( + eq(roomIds), any(LocalDateTime.class))) + .thenReturn(List.of( + new RecentMessageCount("room-1", 3), + new RecentMessageCount("room-2", 5))); + + Map result = + recentMessageCounter.countRecentMessagesByRoomIds(roomIds); + + assertThat(result).containsExactlyInAnyOrderEntriesOf( + Map.of("room-1", 3, "room-2", 5)); + verify(messageRepository).countRecentMessagesByRoomIds( + eq(roomIds), any(LocalDateTime.class)); + } + + @Test + @DisplayName("μ±„νŒ…λ°© IDκ°€ μ—†μœΌλ©΄ λ©”μ‹œμ§€λ₯Ό μ‘°νšŒν•˜μ§€ μ•ŠλŠ”λ‹€") + void countRecentMessagesByRoomIds_EmptyRoomIdsSkipsRepository() { + Map result = + recentMessageCounter.countRecentMessagesByRoomIds(Set.of()); + + assertThat(result).isEmpty(); + verifyNoInteractions(messageRepository); + } + + @Test + @DisplayName("단건 졜근 λ©”μ‹œμ§€ 쑰회 λ™μž‘μ„ μœ μ§€ν•œλ‹€") + void countRecentMessages_KeepsSingleRoomQuery() { + when(messageRepository.countRecentMessagesByRoomId( + eq("room-1"), any(LocalDateTime.class))) + .thenReturn(7L); + + int result = recentMessageCounter.countRecentMessages("room-1"); + + assertThat(result).isEqualTo(7); + verify(messageRepository).countRecentMessagesByRoomId( + eq("room-1"), any(LocalDateTime.class)); + } +} diff --git a/apps/backend/src/test/java/com/ktb/chatapp/service/RoomServiceTest.java b/apps/backend/src/test/java/com/ktb/chatapp/service/RoomServiceTest.java new file mode 100644 index 00000000..fce0c1bc --- /dev/null +++ b/apps/backend/src/test/java/com/ktb/chatapp/service/RoomServiceTest.java @@ -0,0 +1,154 @@ +package com.ktb.chatapp.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.ktb.chatapp.dto.RoomResponse; +import com.ktb.chatapp.dto.RoomsResponse; +import com.ktb.chatapp.model.Room; +import com.ktb.chatapp.model.User; +import com.ktb.chatapp.repository.RoomRepository; +import com.ktb.chatapp.repository.UserRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.StreamSupport; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +@DisplayName("RoomService λ‹¨μœ„ ν…ŒμŠ€νŠΈ") +class RoomServiceTest { + + @Mock private RoomRepository roomRepository; + @Mock private UserRepository userRepository; + @Mock private RecentMessageCounter recentMessageCounter; + @Mock private PasswordEncoder passwordEncoder; + @Mock private ApplicationEventPublisher eventPublisher; + + private RoomService roomService; + + @BeforeEach + void setUp() { + roomService = new RoomService( + roomRepository, + userRepository, + recentMessageCounter, + passwordEncoder, + eventPublisher); + } + + @Test + @DisplayName("μ±„νŒ…λ°© λͺ©λ‘μ˜ μƒμ„±μžμ™€ μ°Έμ—¬μžλ₯Ό ν•œ λ²ˆμ— μ‘°νšŒν•΄ 응닡을 μ‘°λ¦½ν•œλ‹€") + void getAllRooms_LoadsDistinctUsersOnceAndMapsResponses() { + LocalDateTime older = LocalDateTime.of(2026, 8, 9, 10, 0); + LocalDateTime newer = older.plusHours(1); + Room olderRoom = room("room-1", "user-1", Set.of("user-1", "user-2"), older); + Room newerRoom = room("room-2", "user-2", Set.of("user-2", "user-3"), newer); + User user1 = user("user-1", "μ‚¬μš©μž 1"); + User user2 = user("user-2", "μ‚¬μš©μž 2"); + User user3 = user("user-3", "μ‚¬μš©μž 3"); + + when(roomRepository.findAll()).thenReturn(List.of(olderRoom, newerRoom)); + when(userRepository.findAllById(anySet())).thenReturn(List.of(user1, user2, user3)); + when(recentMessageCounter.countRecentMessagesByRoomIds(anySet())) + .thenReturn(Map.of("room-1", 3, "room-2", 5)); + + RoomsResponse response = roomService.getAllRooms("user@example.com"); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getData()).extracting(RoomResponse::getId) + .containsExactly("room-2", "room-1"); + assertThat(response.getData().get(0).getCreator().getId()).isEqualTo("user-2"); + assertThat(response.getData().get(0).getParticipants()) + .extracting(participant -> participant.getId()) + .containsExactlyInAnyOrder("user-2", "user-3"); + assertThat(response.getData().get(0).getRecentMessageCount()).isEqualTo(5); + assertThat(response.getMetadata().getTotal()).isEqualTo(2); + assertThat(response.getMetadata().getCurrentCount()).isEqualTo(2); + + @SuppressWarnings("unchecked") + ArgumentCaptor> userIdsCaptor = ArgumentCaptor.forClass(Iterable.class); + verify(userRepository).findAllById(userIdsCaptor.capture()); + assertThat(StreamSupport.stream(userIdsCaptor.getValue().spliterator(), false)) + .containsExactlyInAnyOrder("user-1", "user-2", "user-3"); + verify(userRepository, never()).findById(anyString()); + verify(recentMessageCounter, times(1)) + .countRecentMessagesByRoomIds(Set.of("room-1", "room-2")); + verify(recentMessageCounter, never()).countRecentMessages(anyString()); + } + + @Test + @DisplayName("μ‘°νšŒλ˜μ§€ μ•Šμ€ μƒμ„±μžλŠ” null둜 두고 μ‘°νšŒλ˜μ§€ μ•Šμ€ μ°Έμ—¬μžλŠ” μ œμ™Έν•œλ‹€") + void getAllRooms_OmitsMissingParticipantsAndKeepsMissingCreatorNull() { + Room room = room("room-1", "missing-creator", Set.of("user-1", "missing-user"), + LocalDateTime.of(2026, 8, 10, 10, 0)); + when(roomRepository.findAll()).thenReturn(List.of(room)); + when(userRepository.findAllById(anySet())).thenReturn(List.of(user("user-1", "μ‚¬μš©μž 1"))); + when(recentMessageCounter.countRecentMessagesByRoomIds(anySet())).thenReturn(Map.of()); + + RoomsResponse response = roomService.getAllRooms("user@example.com"); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getData()).singleElement().satisfies(roomResponse -> { + assertThat(roomResponse.getCreator()).isNull(); + assertThat(roomResponse.getParticipants()) + .extracting(participant -> participant.getId()) + .containsExactly("user-1"); + assertThat(roomResponse.getRecentMessageCount()).isZero(); + }); + verify(userRepository).findAllById(anySet()); + verify(userRepository, never()).findById(anyString()); + verify(recentMessageCounter).countRecentMessagesByRoomIds(Set.of("room-1")); + verify(recentMessageCounter, never()).countRecentMessages(anyString()); + } + + @Test + @DisplayName("μ±„νŒ…λ°©μ΄ μ—†μœΌλ©΄ μ‚¬μš©μžμ™€ 졜근 λ©”μ‹œμ§€λ₯Ό μ‘°νšŒν•˜μ§€ μ•ŠλŠ”λ‹€") + void getAllRooms_EmptyRoomsSkipsUserAndMessageQueries() { + when(roomRepository.findAll()).thenReturn(List.of()); + + RoomsResponse response = roomService.getAllRooms("user@example.com"); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getData()).isEmpty(); + assertThat(response.getMetadata().getTotal()).isZero(); + verifyNoInteractions(userRepository, recentMessageCounter); + } + + private Room room( + String id, + String creatorId, + Set participantIds, + LocalDateTime createdAt) { + return Room.builder() + .id(id) + .name("μ±„νŒ…λ°© " + id) + .creator(creatorId) + .participantIds(participantIds) + .createdAt(createdAt) + .build(); + } + + private User user(String id, String name) { + return User.builder() + .id(id) + .name(name) + .email(id + "@example.com") + .build(); + } +}