diff --git a/backend/src/portfolio/FULL_TEXT_INDEX.md b/backend/src/portfolio/FULL_TEXT_INDEX.md deleted file mode 100644 index c31e4d05..00000000 --- a/backend/src/portfolio/FULL_TEXT_INDEX.md +++ /dev/null @@ -1,183 +0,0 @@ -# Full Text Index 적용기 - -태그: PORTFOLIO -날짜: 2024년 3월 13일 -참여자: 김승진, 진주원 - -## 문제 상황 - ---- - -> ‘이길저길’ 서비스 내에서 친구 검색 시 Member의 **닉네임(문자열)으로 검색을 수행하는 API**가 있고 기존 Like 검색은 데이터의 개수가 많아질수록 **성능에서 부족함**이 있었다. -> - -## Like 연산시 - ---- - -Member 데이터를 500만 row 저장하고 특정 닉네임을 검색하는 쿼리를 Like 연산을 이용하여 수행하였다. - -### 준비 작업 (프로시저) - -1. sql로 프로시저 작성 (그대로 실행하면 500만개의 member를 insert한다) - - 기존 자바 코드로 500만개를 넣으려 시도하니 heap 영역의 공간이 없어 error 발생 ! - -```sql -DELIMITER $$ -DROP PROCEDURE IF EXISTS twtw.insertLoop$$ - -CREATE PROCEDURE twtw.insertLoop() -BEGIN - DECLARE i INT DEFAULT 1; - WHILE i <= 5000000 DO - INSERT INTO twtw.MEMBER(id, created_at, updated_at, auth_type, nickname, profile_image, role, client_id) - VALUES (UNHEX(REPLACE(uuid(), '-', '')), now(), now(), 'APPLE', concat('n', i), concat('profile_image_', i), 'ROLE_USER', concat('client_id', i)); - SET i = i + 1; - END WHILE; -END$$ -DELIMITER $$ - -CALL twtw.insertLoop; -$$ -``` - -- 500만 row가 insert 됐다. - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/a4cc5c7b-39db-4aa3-89cd-91c97faee2cc) - -### Like 연산 결과 - -```sql -SELECT * FROM member WHERE nickname like '%123456%'; -``` - -### like 연산 쿼리 결과 [2.63s] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/57bf2918-b2fe-4614-bd08-8499168b1d9a) - -# Full Text Index 적용 - ---- - -> Like 쿼리의 성능이 좋지 않아 효과적인 문자열 탐색에 있어 **Full Text Index를 적용**하기로 했다. -> - -- FULL TEXT INDEX 인덱스 적용 - -```sql -CREATE FULLTEXT INDEX idx_member_nickname ON member (nickname) with parser ngram; -``` - -- FULL TEXT INDEX 쿼리 - -```sql -SELECT * FROM member WHERE MATCH(nickname) AGAINST('123456' IN BOOLEAN MODE); -``` - -## 첫번째 시도 - -### match 쿼리 결과 [error] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/a6f0f6e8-f723-481d-abd9-7874d2cdc0ad) - -- **ngram** - -> 문자열을 모두 [디폴트 최소 길이: 2] 만큼 다 분할해 저장했기 때문에 500만 데이터를 쿼리하는데 드는 비용이 커져 **쿼리 캐시 공간이 부족**했다. -> -- **캐시 사이즈 2배 늘려봤지만** 에러는 해결이 되지 않음 - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/0e62fe27-b3ae-4450-95fb-e347efa8a713) - -> 캐시 사이즈를 늘린다면 메모리 부담이 발생하므로 **최적의 방법이라 판단하지 않았다 !** -> -- 다른 해결 방법인 [인덱스 분할], [ElasticSearch 도입], [하드웨어 리소스 확장]은 오버 엔지니어링이라 판단하고 다른 해결방법 고민해보았다 ! - -### 분석 - -- 테스트시 최대 길이 20의 문자열을 더미데이터로 삽입한 부분이 문제라 판단 - -> 현재 서비스 요구사항에서 닉네임의 길이 제한이 없다는 것에 의문을 두고 팀원들과 회의를 거쳐 닉네임 길이의 최대치를 정하기로 결정 -> -- 최대치 8로 선정 ( 선정 과정에 있어 다른 서비스 모델을 참고 ) - -## 문자열 길이 최대 8인 경우 - -### like 연산 결과 [2.37s] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/e300d6d2-ab41-41b9-ba2a-2292b757d6c3) - -### match against boolean mode는 다음과 같은 결과 [29.22s] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/5045258d-cc73-43e0-84f3-accf6bc7e014) - -- 쿼리가 상당히 느리다. - -> 쿼리 Profiling으로 실행 계획 확인 -> - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/43da9c5b-8b90-4a95-b056-00a943890123) - -- FULLTEXT initialization 이라는 전체 텍스트 인덱스를 초기화하고, 매번 검색을 위해 데이터를 메모리로 로딩하는 과정 에서 약 29s 가 걸림 -- Optimize table member 커맨드를 사용하여 FULLTEXT initialization의 성능을 높이려는 시도 - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/ef5cbc78-e8b6-4ac5-99af-24e330ce2126) - -- 하지만 이후에도 성능에 큰 변화가 없었다. - -## 쿼리를 분석/수정 해보자 - -### 기존 쿼리의 문제점 분석 - -> 검색을 하는 문자열의 길이가 기존 N-gram의 길이 2보다 크기 때문에 더 세세한 검색 과정을 거치기 때문이다. -> -- **2씩 나눠진 토큰을 포함**하면서 동시에 **해당 문자열의 순서까지 일치**하는 데이터를 찾아야 한다. -- N-gram의 길이로 나뉘어진 문자열은 즉 자체로 인덱스를 의미하고 해당 인덱스로만의 탐색이 어렵다는 것이다. - -### 쿼리 수정 - -> ngram parser의 최소 단위인 2만큼 문자열을 나눈 후 이 나눈 문자열이 모두 포함된 부분을 검색하면 더 빠른 검색이 가능할거라 예상 -> - -```sql -SHOW GLOBAL VARIABLES LIKE "ngram_token_size"; ( 2인걸 확인 ) -``` - -```sql -SELECT * FROM member WHERE MATCH(nickname) AGAINST('+12 +23 +34 +45 +56' IN BOOLEAN MODE); -``` - -### 길이 2씩 나누어 검색 [0.53s] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/7e9b0951-ad90-41de-8ec3-0f9000b91c79) - -- 몇개의 결과가 더 나왔으며 이전과 비교해 누락된 row는 없었다. - - 더 나온 이유는 순서를 보장하지 않고 12, 23, 34, 45, 56 을 포함하는 문자열을 검색하기 때문 -- LIKE 연산 결과보다 4.96배 개선 - -### 혹시 몰라 길이 3씩 나누어 검색 [1m 4.19s] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/af50d00f-c1ae-40b4-ade6-1b8ebd9f5dff) - -- 이건 예상대로 훨씬 성능이 좋지 않다. -- 길이 3만큼 나누어 검색하면 기존의 문자열 하나만 조건으로 넣었을 때보다 더 느림 -- 길이가 2가 아닌 문자열인데 심지어 여러개를 검사했기 때문이라 파악 - -### FULL TEXT INDEX 사용 시 - ---- - -- N-gram parser를 이용할 경우 우리가 지정한 길이만큼의 토큰으로 검사를 진행해야 좋은 성능을 발휘할 수 있다. -- 길이가 더 긴 문자열로 탐색을 진행할 경우 해당 문자열의 토큰들의 배치(순서)까지 고려하여 탐색을 진행하기 때문에 시간이 더 오래 걸린다. - -## 결과: [2.63s → 0.53s 로 4.96배 (496%) 개선] - -### before - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/78841f74-4dd0-48f6-89e8-7cb6ddb96812) - -### after - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/be4722b3-7eab-45ae-8160-4138a6e862d8) - -- 추후 데이터가 훨씬 많이 쌓이고 서비스가 확장되면 Elastic Search와 같은 다른 기술의 도입도 가능하지만, - 현재의 주어진 인프라 내에서 서비스 요구사항 수정으로 오류를 해결하고 쿼리를 수정하여 성능도 향상할 수 있었음 diff --git a/backend/src/portfolio/RABBITMQ.md b/backend/src/portfolio/RABBITMQ.md deleted file mode 100644 index b0d92295..00000000 --- a/backend/src/portfolio/RABBITMQ.md +++ /dev/null @@ -1,44 +0,0 @@ -# RabbitMQ 비동기 & 데드레터 처리 - -태그: PORTFOLIO -날짜: 2024년 3월 15일 -참여자: 진주원, 김승진 - -## 문제 상황 & 접근 - ---- - -1. FCM 오류 처리 - -> 그룹 초대 , 친구 요청 서비스를 수행하는데 있어 FCM을 사용했다. FCM 자체의 성능 개선을 위해 RabbitMQ을 사용함. FCM 요청에 있어 오류가 발생할 경우 처리를 위해 데드레터 처리 -> -1. 비동기 적용 - -> FCM 자체는 동기방식이기 때문에 짧은 시간에 많은 요청이 수행되는 경우 성능상 개선 필요, 메시지 큐를 사용하여 비동기 방식으로 처리 -> - -### 적용 - ---- - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/0652aba3-f931-4fab-be8b-72eba57c76de) - -- 알림을 위한 RabbitMQ queue에서 발생한 오류는 데드레터로 메시지가 전달되도록 연결하였다. - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/5940ff1d-a834-4a6a-aef4-9d7e28f4b027) - -- 데드레터가 발생한 경우 상단의 RabbitListener로 전송된다. -- RabbitMQ는 데드레터를 데드레터 큐에 추가한다. -- 이후 로깅을 수행한 이후 개발자의 Slack으로 알림을 전송한다. - -### 결론 - ---- - -## 데드레터 발생시 슬랙 알림 - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/8fe48b9c-c74f-4bdf-8203-eef0e9315fc0) - -## RabbitMQ Management에서 확인한 데드레터 현황 - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/e748c37c-2b2a-475a-9727-4c98005471dc) diff --git a/backend/src/portfolio/REDIS_CACHE.md b/backend/src/portfolio/REDIS_CACHE.md deleted file mode 100644 index 4e5e9eb0..00000000 --- a/backend/src/portfolio/REDIS_CACHE.md +++ /dev/null @@ -1,80 +0,0 @@ -# Redis Cache 성능 비교 - -태그: PORTFOLIO -날짜: 2024년 3월 15일 -참여자: 진주원, 김승진 - -## 문제상황 - ---- - -> 사용자에게 경로를 제공하기 위해 Kakao, Naver, Tmap Open API를 사용한다. 위 API 제공에 있어 호출까지의 **시간이 소요**되고 **비용 낭비**가 발생 -> - -- 이미 반환된 경로를 사용자가 재사용하는 경우가 많기 때문에 경로를 임시 메모리에 저장하는 로직 필요 -- ‘경로’라는 데이터 특성 상 한번 참조된 값에 대해 변경이 자주 일어나지 않음 - -### 접근 - -- **Redis Cache를 이용**하여 Open API의 반환값을 일부 저장하여 사용자에게 제공 - -### 적용 - -- @CachePut과 @Cacheable Annotation을 사용하여 API에 레디스 캐시 적용 - -### 시퀀스 다이어그램 - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/6cb63e15-767d-4a21-89cc-91724323f0b0) - -```Java - -@CachePut( - value = "surroundPlace", - key = "'searchSurroundPlace'.concat(#surroundPlaceRequest.toString())", - cacheManager = "cacheManager") -public PlaceResponse searchSurroundPlace(final SurroundPlaceRequest surroundPlaceRequest) { - return getPlaceResponse(surroundPlaceRequest); -} - -@Cacheable( - value = "surroundPlace", - key = "'searchSurroundPlace'.concat(#surroundPlaceRequest.toString())", - cacheManager = "cacheManager", - unless = "#result.results.size() <= 0") -public PlaceResponse searchSurroundPlaceWithCache( - final SurroundPlaceRequest surroundPlaceRequest) { - return getPlaceResponse(surroundPlaceRequest); -} -``` - -### 시나리오 - -> Redis Cache를 적용하였을 때 성능 분석을 위해 다음과 같은 시나리오를 세우고 테스트 진행 -> - -1. 회원가입 수행 -2. 보행자 경로 API를 요청하여 수행 - -## 결론 - ---- - -## 평균 Latency 4.12배 향상 - -- 추가로, 매번 OpenAPI를 호출하지 않기 때문에 네트워크 비용을 아끼는 효과도 있음 - -## 동기 처리 [평균 Latency 91.88ms] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/cf1f5fe9-4aa9-48e2-96e5-0e89a53cbbc7) - -## 비동기 처리 [평균 Latency 22.26ms] - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/e3d1fb30-0f25-4c79-a4b2-1e1a9a9b45d4) - -### 분석 - ---- - -1. Redis Cache를 사용할 경우 사용하지 않은 경우에 비해 4.12배의 속도 성능 향상 -2. Open API를 중복 요청하지 않아 네트워크 비용 절감 -3. CachePut를 통해 사용자의 요청 경로가 변경된 경우 기존 Cache값을 삭제하고 변경된 경로를 저장하고 제공하여 서비스상 유연성 향상 diff --git a/backend/src/portfolio/RESILIENCE4J.md b/backend/src/portfolio/RESILIENCE4J.md deleted file mode 100644 index 5ffa4a2a..00000000 --- a/backend/src/portfolio/RESILIENCE4J.md +++ /dev/null @@ -1,49 +0,0 @@ -# Resilience4j & 모니터링 - -태그: PORTFOLIO -날짜: 2024년 3월 15일 -참여자: 김승진, 진주원 - -## 문제상황 - ---- - -> 사용자 경로 제공에 있어 Kakao, Naver, Tmap Open API를 사용하였고 Open API에 장애가 발생한 경우 처리에 있어 유연한 처리가 필요함을 느낌 -> - -### 접근 - ---- - -- Resilience4j를 통해 Open API에 장애가 발생한 경우 close - open - half open 단계에 걸쳐 유연한 처리 - -### 설명 - ---- - -- Close - - Open API에 장애가 발생하지 않은 경우 → 기존과 동일하게 로직 수행 -- Open - - Open API에 장애가 발생 ( Open API의 실패율이 30%에 도달한 경우) - - 이후 10초간 Open API 요청을 수행하지 않고 Default Failure 처리 -- Half Open - - Open API 처리가 정상화된 경우 → Close 상태 변경 - - 정상화되지 않은 경우 → Open 상태 변경 - -## 분석 - ---- - -## Resilience4j 모니터링 - -- 서킷브레이커 1개가 OpenAPI 장애로 Open 돼있고 3개가 Close 돼있는 것을 볼 수 있다. - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/b6ab3b74-f99a-4b14-8b51-74311484b91d) - -## SpringBoot 서버 모니터링 (JVM 관련 지표) - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/eddf8eca-c91f-428d-aed1-e4af9a7d30cf) - -## SpringBoot 서버 모니터링 (요청, 로그 관련 지표) - -![image](https://github.com/HongDam-org/TWTW/assets/84346055/8f225ef4-bf0a-4986-a1c9-fc79e32768b8) diff --git a/backend/src/portfolio/TEST.md b/backend/src/portfolio/TEST.md deleted file mode 100644 index 645368b0..00000000 --- a/backend/src/portfolio/TEST.md +++ /dev/null @@ -1,375 +0,0 @@ -# Stub을 이용한 단위 테스트 적용기 - -태그: PORTFOLIO -날짜: 2024년 3월 14일 -참여자: 진주원, 김승진 - -## 문제상황 - ---- - -> ‘TWTW’의 테스트는 크게 Controller, Service, Repository Layer에서 진행되었다. 우리의 목표는 단위 테스트 적용이었다. 하지만 Service 테스트 코드 내에서 Repository를 통한 실제 DB 접근이 이루어져 완벽한 단위 테스트를 수행할 수 없었다. -> - -## 접근 방식 - ---- - -- **Repository Test** - - DB를 통한 접근이 수행되는가에 초점을 맞추어 테스트 코드 작성 -- **Service Test** - - Stub을 활용하여 DB 접근을 하지 않고 서비스 로직에만 초점을 맞추어 테스트 코드 작성 -- **Controller Test** - - mock을 활용하여 Service 로직을 타지 않고 테스트 수행 - - 테스트를 수행하면서 자동으로 rest docs 생성 - -### 테스트용 Repository 분리 - ---- - -**전략 패턴을 사용한 전체 구조** -![image](https://github.com/HongDam-org/TWTW/assets/89020004/35c3d380-0223-4756-804e-afb353c5ecbc) - -**Repository의 추상화** - -```java -@Repository -public interface MemberRepository { - List findAllByNickname(final String nickname); - - List findAllByNicknameContainingIgnoreCase(final String nickname); - - Optional findByOAuthIdAndAuthType(final String oAuthId, final AuthType authType); - - boolean existsByNickname(final String nickname); - - Member save(final Member member); - - Optional findById(final UUID id); - - List findAllByIds(final List friendMemberIds); - - void deleteById(final UUID memberId); -} - -``` - -**실제 서비스용 JpaRepository** - -```java -@Repository -public interface JpaMemberRepository extends JpaRepository, MemberRepository { - - @Query( - value = - "SELECT * FROM member m WHERE MATCH (m.nickname) AGAINST(:nickname IN BOOLEAN" - + " MODE)", - nativeQuery = true) - List findAllByNickname(@Param("nickname") String nickname); - - @Query( - "SELECT m FROM Member m WHERE m.oauthInfo.clientId = :oAuthId AND" - + " m.oauthInfo.authType = :authType") - Optional findByOAuthIdAndAuthType( - @Param("oAuthId") String oAuthId, @Param("authType") AuthType authType); - - @Query("SELECT m FROM Member m WHERE m.id in :friendMemberIds") - List findAllByIds(@Param("friendMemberIds") final List friendMemberIds); -} - -``` - -**테스트용 StubRepository** - -```java -public class StubMemberRepository implements MemberRepository { - - private final Map map = new HashMap<>(); - - @Override - public List findAllByNickname(final String nickname) { - return map.values().stream() - .filter( - member -> - member.getNickname().toUpperCase().contains(nickname.toUpperCase())) - .toList(); - } - - @Override - public List findAllByNicknameContainingIgnoreCase(final String nickname) { - return map.values().stream() - .filter( - member -> - member.getNickname().toUpperCase().contains(nickname.toUpperCase())) - .toList(); - } - - @Override - public Optional findByOAuthIdAndAuthType( - final String oAuthId, final AuthType authType) { - return map.values().stream() - .filter( - member -> { - final OAuth2Info oauthInfo = member.getOauthInfo(); - return oauthInfo.getClientId().equals(oAuthId) - && oauthInfo.getAuthType().equals(authType); - }) - .findFirst(); - } - - @Override - public boolean existsByNickname(final String nickname) { - return map.values().stream().anyMatch(member -> member.getNickname().equals(nickname)); - } - - @Override - public Member save(final Member member) { - map.put(member.getId(), member); - return member; - } - - @Override - public Optional findById(final UUID id) { - return Optional.ofNullable(map.get(id)); - } - - @Override - public List findAllByIds(final List friendMemberIds) { - return map.values().stream() - .filter(member -> friendMemberIds.contains(member.getId())) - .toList(); - } - - @Override - public void deleteById(final UUID memberId) { - map.remove(memberId); - } -} -``` - -> 각 기능별 StubRepository를 만든 후 StubConfig를 통해 테스트 시 빈으로 주입되도록 설정 -> - -```java -@TestConfiguration -public class StubConfig { - - private final Map map = new HashMap<>(); - - @Bean - @Primary - public FriendQueryRepository stubFriendQueryRepository() { - return new StubFriendQueryRepository(map); - } - - @Bean - @Primary - public FriendCommandRepository stubFriendCommandRepository() { - return new StubFriendCommandRepository(map); - } - - @Bean - @Primary - public RefreshTokenRepository refreshTokenRepository() { - return new StubRefreshTokenRepository(); - } - - @Bean - @Primary - public GroupRepository groupRepository() { - return new StubGroupRepository(); - } - - @Bean - @Primary - public MemberRepository memberRepository() { - return new StubMemberRepository(); - } - - @Bean - @Primary - public PlanRepository planRepository() { - return new StubPlanRepository(); - } -} -``` - -### Repository Test - ---- - -> Repository 테스트 시 실제 DB와의 상호작용을 테스트하도록 코드 작성 -> - -```java -@DisplayName("MemberRepository의") -class MemberRepositoryTest extends RepositoryTest { - - @Autowired private MemberRepository memberRepository; - - @Test - @DisplayName("PK를 통한 저장/조회가 성공하는가?") - void saveAndFindId() { - // given - final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity()); - - // when - final UUID expected = member.getId(); - final Member result = memberRepository.findById(expected).orElseThrow(); - - // then - assertThat(result.getId()).isEqualTo(member.getId()); - } - - @Test - @DisplayName("soft delete가 수행되는가?") - void softDelete() { - // given - final Member member = MemberEntityFixture.FIRST_MEMBER.toEntity(); - final UUID memberId = memberRepository.save(member).getId(); - - // when - memberRepository.deleteById(memberId); - - // then - assertThat(memberRepository.findById(memberId)).isEmpty(); - } - - @Test - @DisplayName("DeviceToken이 정상적으로 저장되는가") - void saveDeivceToken() { - // given - final Member member = memberRepository.save(MemberEntityFixture.LOGIN_MEMBER.toEntity()); - final DeviceTokenRequest deviceTokenRequest = new DeviceTokenRequest("THIS_IS_TEST_TOKEN"); - - // when - DeviceToken deviceToken = new DeviceToken(deviceTokenRequest.getDeviceToken()); - member.updateDeviceToken(deviceToken); - - // then - assertThat(member.getDeviceToken().getDeviceToken().equals("THIS_IS_TEST_TOKEN")); - } -} - -``` - -### Service Test - ---- - -> 서비스 로직 테스트를 위해 StubRepository를 이용하여 테스트 작성 -> -- MemberServiceTest의 경우 주입받는 memberRepository는 StubRepository - -```java -@DisplayName("MemberService의") -class MemberServiceTest extends LoginTest { - @Autowired private MemberService memberService; - @Autowired private MemberRepository memberRepository; - - @Test - @DisplayName("닉네임 중복 체크가 제대로 동작하는가") - void checkNickname() { - // given - final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity()); - // when - DuplicateNicknameResponse response = memberService.duplicateNickname(member.getNickname()); - // then - assertTrue(response.getIsPresent()); - } - - @Test - @DisplayName("UUID를 통해 Member 조회가 되는가") - void getMemberById() { - // given - final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity()); - - // when - Member response = memberService.getMemberById(member.getId()); - - // then - assertThat(response.getId()).isEqualTo(member.getId()); - } - - @Test - @DisplayName("Member가 MemberResponse로 변환이 되는가") - void getResponseByMember() { - // given - final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity()); - - // when - MemberResponse memberResponse = memberService.getResponseByMember(member); - - // then - assertThat(memberResponse.getMemberId()).isEqualTo(member.getId()); - } - - @Test - @DisplayName("Nickname을 통한 Member 검색이 수행되는가") - void searchMemberByNickname() { - // given - final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity()); - - // when - final List responses = - memberService.getMemberByNickname(member.getNickname().substring(0, 1)); - - // then - assertThat(responses).isNotEmpty(); - } -} -``` - -### Controller Test - ---- - -> 컨트롤러 Layer에서의 Request & Response 테스트를 위해 Service를 mock으로 만들어 테스트 작성 -> - -```Java - @Test - @DisplayName("닉네임이 중복되었는가") - void duplicate() throws Exception { - final DuplicateNicknameResponse expected = new DuplicateNicknameResponse(false); - given(memberService.duplicateNickname(any())).willReturn(expected); - - final ResultActions perform = - mockMvc.perform( - get("/member/duplicate/{name}", "JinJooOne") - .contentType(MediaType.APPLICATION_JSON)); - - // then - perform.andExpect(status().isOk()).andExpect(jsonPath("$.isPresent").exists()); - // docs - - perform.andDo(print()) - .andDo( - document( - "get duplicate nickname", - getDocumentRequest(), - getDocumentResponse())); - } -``` - -### 분석 - ---- - -- **Stub을 사용하여 유연한 처리** - - Repository Layer가 **JPA에 종속적이지 않고** 테스트에 용이한 **유연한 구조** 가져감 - - Controller 테스트의 경우 하나의 메서드만 mocking하면 되었지만, Service 테스트에서는 많은 의존성 때문에 모두 mock으로 처리하기에 부담, 같은 메서드도 매번 mock 처리하기에도 어려움 - -- **TestContainer 도입** - - MySQL에서 제공하는 기능을 기존에 사용하던 테스트용 H2 DB에서 지원하지 않음(FULL TEXT INDEX) - - Redis, RabbitMQ와 같은 외부 시스템과 연동되는 부분을 원활히 테스트 - -### 테스트 커버리지 - ---- - -- Jacoco 도입으로 테스트시와 PR시 커버리지 확인 가능 -![](https://github.com/HongDam-org/TWTW/assets/89020004/3d90c15c-9020-4df8-82d0-3669c1d58250) - -- Jacoco 커버리지 -![](https://github.com/HongDam-org/TWTW/assets/89020004/c670bcd9-c719-428a-932e-bc017148c148) \ No newline at end of file diff --git a/backend/src/test/java/com/twtw/backend/domain/friend/service/FriendServiceTest.java b/backend/src/test/java/com/twtw/backend/domain/friend/service/FriendServiceTest.java index 0ecd36f7..12ea9c17 100644 --- a/backend/src/test/java/com/twtw/backend/domain/friend/service/FriendServiceTest.java +++ b/backend/src/test/java/com/twtw/backend/domain/friend/service/FriendServiceTest.java @@ -29,11 +29,11 @@ class FriendServiceTest extends LoginTest { @Autowired private FriendService friendService; @Autowired - @Qualifier("stubFriendQueryRepository") + @Qualifier("fakeFriendQueryRepository") private FriendQueryRepository friendQueryRepository; @Autowired - @Qualifier("stubFriendCommandRepository") + @Qualifier("fakeFriendCommandRepository") private FriendCommandRepository friendCommandRepository; @Autowired private MemberRepository memberRepository; diff --git a/backend/src/test/java/com/twtw/backend/domain/member/service/AuthServiceTest.java b/backend/src/test/java/com/twtw/backend/domain/member/service/AuthServiceTest.java index 3550a411..d17edb4b 100644 --- a/backend/src/test/java/com/twtw/backend/domain/member/service/AuthServiceTest.java +++ b/backend/src/test/java/com/twtw/backend/domain/member/service/AuthServiceTest.java @@ -10,8 +10,8 @@ import com.twtw.backend.domain.member.entity.Member; import com.twtw.backend.domain.member.repository.MemberRepository; import com.twtw.backend.fixture.member.MemberEntityFixture; +import com.twtw.backend.support.fake.FakeConfig; import com.twtw.backend.support.service.ServiceTest; -import com.twtw.backend.support.stub.StubConfig; import com.twtw.backend.support.testcontainer.ContainerTestConfig; import org.junit.jupiter.api.DisplayName; @@ -22,7 +22,7 @@ @ServiceTest @ContextConfiguration( initializers = {ContainerTestConfig.class}, - classes = StubConfig.class) + classes = FakeConfig.class) @DisplayName("AuthService의") class AuthServiceTest { diff --git a/backend/src/test/java/com/twtw/backend/support/stub/StubConfig.java b/backend/src/test/java/com/twtw/backend/support/fake/FakeConfig.java similarity index 58% rename from backend/src/test/java/com/twtw/backend/support/stub/StubConfig.java rename to backend/src/test/java/com/twtw/backend/support/fake/FakeConfig.java index 001d9ae8..c8a806c0 100644 --- a/backend/src/test/java/com/twtw/backend/support/stub/StubConfig.java +++ b/backend/src/test/java/com/twtw/backend/support/fake/FakeConfig.java @@ -1,4 +1,4 @@ -package com.twtw.backend.support.stub; +package com.twtw.backend.support.fake; import com.twtw.backend.domain.friend.entity.Friend; import com.twtw.backend.domain.friend.repository.FriendCommandRepository; @@ -17,43 +17,43 @@ import java.util.UUID; @TestConfiguration -public class StubConfig { +public class FakeConfig { private final Map map = new HashMap<>(); @Bean @Primary - public FriendQueryRepository stubFriendQueryRepository() { - return new StubFriendQueryRepository(map); + public FriendQueryRepository fakeFriendQueryRepository() { + return new FakeFriendQueryRepository(map); } @Bean @Primary - public FriendCommandRepository stubFriendCommandRepository() { - return new StubFriendCommandRepository(map); + public FriendCommandRepository fakeFriendCommandRepository() { + return new FakeFriendCommandRepository(map); } @Bean @Primary - public RefreshTokenRepository refreshTokenRepository() { - return new StubRefreshTokenRepository(); + public RefreshTokenRepository fakeRefreshTokenRepository() { + return new FakeRefreshTokenRepository(); } @Bean @Primary - public GroupRepository groupRepository() { - return new StubGroupRepository(); + public GroupRepository fakeGroupRepository() { + return new FakeGroupRepository(); } @Bean @Primary - public MemberRepository memberRepository() { - return new StubMemberRepository(); + public MemberRepository fakeMemberRepository() { + return new FakeMemberRepository(); } @Bean @Primary - public PlanRepository planRepository() { - return new StubPlanRepository(); + public PlanRepository fakePlanRepository() { + return new FakePlanRepository(); } } diff --git a/backend/src/test/java/com/twtw/backend/support/stub/StubFriendCommandRepository.java b/backend/src/test/java/com/twtw/backend/support/fake/FakeFriendCommandRepository.java similarity index 91% rename from backend/src/test/java/com/twtw/backend/support/stub/StubFriendCommandRepository.java rename to backend/src/test/java/com/twtw/backend/support/fake/FakeFriendCommandRepository.java index 1f57261c..25500eab 100644 --- a/backend/src/test/java/com/twtw/backend/support/stub/StubFriendCommandRepository.java +++ b/backend/src/test/java/com/twtw/backend/support/fake/FakeFriendCommandRepository.java @@ -1,4 +1,4 @@ -package com.twtw.backend.support.stub; +package com.twtw.backend.support.fake; import com.twtw.backend.domain.friend.entity.Friend; import com.twtw.backend.domain.friend.repository.FriendCommandRepository; @@ -10,7 +10,7 @@ import java.util.UUID; @RequiredArgsConstructor -public class StubFriendCommandRepository implements FriendCommandRepository { +public class FakeFriendCommandRepository implements FriendCommandRepository { private final Map map; diff --git a/backend/src/test/java/com/twtw/backend/support/stub/StubFriendQueryRepository.java b/backend/src/test/java/com/twtw/backend/support/fake/FakeFriendQueryRepository.java similarity index 96% rename from backend/src/test/java/com/twtw/backend/support/stub/StubFriendQueryRepository.java rename to backend/src/test/java/com/twtw/backend/support/fake/FakeFriendQueryRepository.java index f814c0e0..35020236 100644 --- a/backend/src/test/java/com/twtw/backend/support/stub/StubFriendQueryRepository.java +++ b/backend/src/test/java/com/twtw/backend/support/fake/FakeFriendQueryRepository.java @@ -1,4 +1,4 @@ -package com.twtw.backend.support.stub; +package com.twtw.backend.support.fake; import com.twtw.backend.domain.friend.entity.Friend; import com.twtw.backend.domain.friend.entity.FriendStatus; @@ -10,7 +10,7 @@ import java.util.*; @RequiredArgsConstructor -public class StubFriendQueryRepository implements FriendQueryRepository { +public class FakeFriendQueryRepository implements FriendQueryRepository { private final Map map; diff --git a/backend/src/test/java/com/twtw/backend/support/stub/StubGroupRepository.java b/backend/src/test/java/com/twtw/backend/support/fake/FakeGroupRepository.java similarity index 84% rename from backend/src/test/java/com/twtw/backend/support/stub/StubGroupRepository.java rename to backend/src/test/java/com/twtw/backend/support/fake/FakeGroupRepository.java index 56ad71b4..6a88abf4 100644 --- a/backend/src/test/java/com/twtw/backend/support/stub/StubGroupRepository.java +++ b/backend/src/test/java/com/twtw/backend/support/fake/FakeGroupRepository.java @@ -1,4 +1,4 @@ -package com.twtw.backend.support.stub; +package com.twtw.backend.support.fake; import com.twtw.backend.domain.group.entity.Group; import com.twtw.backend.domain.group.repository.GroupRepository; @@ -8,7 +8,7 @@ import java.util.Optional; import java.util.UUID; -public class StubGroupRepository implements GroupRepository { +public class FakeGroupRepository implements GroupRepository { private final Map map = new HashMap<>(); diff --git a/backend/src/test/java/com/twtw/backend/support/stub/StubMemberRepository.java b/backend/src/test/java/com/twtw/backend/support/fake/FakeMemberRepository.java similarity index 95% rename from backend/src/test/java/com/twtw/backend/support/stub/StubMemberRepository.java rename to backend/src/test/java/com/twtw/backend/support/fake/FakeMemberRepository.java index 016ae1af..ffd90ab4 100644 --- a/backend/src/test/java/com/twtw/backend/support/stub/StubMemberRepository.java +++ b/backend/src/test/java/com/twtw/backend/support/fake/FakeMemberRepository.java @@ -1,4 +1,4 @@ -package com.twtw.backend.support.stub; +package com.twtw.backend.support.fake; import com.twtw.backend.domain.member.entity.AuthType; import com.twtw.backend.domain.member.entity.Member; @@ -7,7 +7,7 @@ import java.util.*; -public class StubMemberRepository implements MemberRepository { +public class FakeMemberRepository implements MemberRepository { private final Map map = new HashMap<>(); diff --git a/backend/src/test/java/com/twtw/backend/support/stub/StubPlanRepository.java b/backend/src/test/java/com/twtw/backend/support/fake/FakePlanRepository.java similarity index 90% rename from backend/src/test/java/com/twtw/backend/support/stub/StubPlanRepository.java rename to backend/src/test/java/com/twtw/backend/support/fake/FakePlanRepository.java index 25348b72..8b0c85e2 100644 --- a/backend/src/test/java/com/twtw/backend/support/stub/StubPlanRepository.java +++ b/backend/src/test/java/com/twtw/backend/support/fake/FakePlanRepository.java @@ -1,4 +1,4 @@ -package com.twtw.backend.support.stub; +package com.twtw.backend.support.fake; import com.twtw.backend.domain.member.entity.Member; import com.twtw.backend.domain.plan.entity.Plan; @@ -6,7 +6,7 @@ import java.util.*; -public class StubPlanRepository implements PlanRepository { +public class FakePlanRepository implements PlanRepository { private final Map map = new HashMap<>(); diff --git a/backend/src/test/java/com/twtw/backend/support/stub/StubRefreshTokenRepository.java b/backend/src/test/java/com/twtw/backend/support/fake/FakeRefreshTokenRepository.java similarity index 87% rename from backend/src/test/java/com/twtw/backend/support/stub/StubRefreshTokenRepository.java rename to backend/src/test/java/com/twtw/backend/support/fake/FakeRefreshTokenRepository.java index 82cef206..5d6c09b0 100644 --- a/backend/src/test/java/com/twtw/backend/support/stub/StubRefreshTokenRepository.java +++ b/backend/src/test/java/com/twtw/backend/support/fake/FakeRefreshTokenRepository.java @@ -1,4 +1,4 @@ -package com.twtw.backend.support.stub; +package com.twtw.backend.support.fake; import com.twtw.backend.domain.member.entity.RefreshToken; import com.twtw.backend.domain.member.repository.RefreshTokenRepository; @@ -8,7 +8,7 @@ import java.util.Optional; import java.util.UUID; -public class StubRefreshTokenRepository implements RefreshTokenRepository { +public class FakeRefreshTokenRepository implements RefreshTokenRepository { private final Map map = new HashMap<>(); diff --git a/backend/src/test/java/com/twtw/backend/support/service/LoginTest.java b/backend/src/test/java/com/twtw/backend/support/service/LoginTest.java index 7d62e8ee..69e8f1a6 100644 --- a/backend/src/test/java/com/twtw/backend/support/service/LoginTest.java +++ b/backend/src/test/java/com/twtw/backend/support/service/LoginTest.java @@ -9,7 +9,7 @@ import com.twtw.backend.domain.member.service.AuthService; import com.twtw.backend.domain.notification.messagequeue.FcmProducer; import com.twtw.backend.fixture.member.MemberEntityFixture; -import com.twtw.backend.support.stub.StubConfig; +import com.twtw.backend.support.fake.FakeConfig; import com.twtw.backend.support.testcontainer.ContainerTestConfig; import org.junit.jupiter.api.BeforeEach; @@ -20,13 +20,13 @@ @ServiceTest @ContextConfiguration( initializers = {ContainerTestConfig.class}, - classes = StubConfig.class) + classes = FakeConfig.class) public abstract class LoginTest { @MockBean protected AuthService authService; + protected Member loginUser; @MockBean private FcmProducer fcmProducer; @Autowired private MemberRepository memberRepository; - protected Member loginUser; @BeforeEach public void setup() {