Skip to content

Commit

Permalink
[Feat] create bookmark #119 (#124)
Browse files Browse the repository at this point in the history
* refactor(ErrorCode): 사용하지 않는 에러코드 제거

* feat(Bookmarks): User의 Bookmarks 구현

* feat(UserQueryRepositoryImpl#findAllBookmarks): User가 구독한 공지를 조회하는 조회 메서드 구현

* feat(DepartmentNoticeRepository): 사용하지 않는 DepartmentNoticeRepository 제거

* feat(NoticeService#getNoticeV1): 사용하지 않는 메서드 제거

* feat(FeedbackService): 사용하지 않는 feedback 서비스 제거

* refactor(AcceptanceTest): 공통된 FirebaseMockBean 상위계층으로 올리기

* test(UserAcceptanceTest): 사용자가 북마크를 저장하는 인수테스트 작성

* test(UserAcceptanceTest): 북마크 인수테스트 릭팩토링

* feat(UserCommandApiV2#saveBookmark): 공지 북마크 기능 구현 완료

* refactor(UserRepositoryTest): 사용자의 id로 북마크한 공지를 조회하는 메서드를 NoticeRepository로 이동한다

* feat: 북마크한 공지 조회 기능 구현

* refactor: test 페키지 리팩토링

* refactor: test의 서식 지정자 제거

* docs(README.md): 내용 일부 업데이트
  • Loading branch information
zbqmgldjfh committed Jan 23, 2024
1 parent 18e60b9 commit 312cbbb
Show file tree
Hide file tree
Showing 39 changed files with 622 additions and 290 deletions.
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,23 @@ https://blogshine.tistory.com/345
**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/664))**

---
# 5. Bulk Query를 통한 성능 개선
## 5. HeapDump를 통해 메모리 누수 원인 찾기 **검색 성능개선**

**문제 상황**

- 애플리케이션에 물리적으로 할당된 메모리를 넘어, swap 메모리까지 사용하고 있는 문제가 발생

**문제 해결**

- 매번 URL 검증을 위한 객체를 생성후 검사 하는것이 아닌, 한번 Compiled된 Pattern 사용을 통한 **메모리 낭비 해결**
- 개선 **** : 전체 512MB중 170MB가 eden space에 주기적으로 생성 → 32%
- 개선 **** : 전체 512MB중 47MB만 생성되도록 개선 → 9%
- 미리 compiled된 Pattern 객체를 활용하여 메모리 누수를 해결하였습니다.

**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/687))**

---
# 6. Bulk Query를 통한 성능 개선

**문제 상황**

Expand All @@ -227,7 +243,7 @@ https://blogshine.tistory.com/345

<br>

### 5-1) Insert 해결책
### 6-1) Insert 해결책

해결책은 2가지가 존재했습니다.
1. Table Id strategy를 SEQUENCE로 변경하고 Batch 작업
Expand All @@ -242,14 +258,14 @@ MySQL과 MariaDB의 Table Id 전략은 대부분이 IDENTITY 전략을 사용하

<br>

### 5-2) Delete 해결책
### 6-2) Delete 해결책
이미 프로젝트에서 queryDsl를 사용하고 있어 이를 이용하는 것이 가장 간단했기 때문에 queryDsl의 delete in 쿼리를 사용하여 해결했습니다.

**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/686))**

---

## 6. 인증, 인가를 비즈니스 로직으로부터 분리하기
## 7. 인증, 인가를 비즈니스 로직으로부터 분리하기

**문제 상황**

Expand All @@ -267,7 +283,7 @@ MySQL과 MariaDB의 Table Id 전략은 대부분이 IDENTITY 전략을 사용하

---

## 7. 흔하디 흔한 N+1 쿼리 개선기
## 8. 흔하디 흔한 N+1 쿼리 개선기

원래 로직에서는 사용자의 Category 이름 목록을 가져오기 위해서 다음과 같이 처리가 되고 잇었습니다!

Expand Down Expand Up @@ -325,7 +341,7 @@ public List<String> getCategoryNamesFromCategories(List<Category> categories) {

쿼리가 총 1 + 2N 만큼 발생중이다.

### 7 - 1) 변경 전 쿼리
### 8 - 1) 변경 전 쿼리

```bash
Hibernate:
Expand Down Expand Up @@ -382,7 +398,7 @@ Connection: keep-alive
N+1 문제로 User한번 조회하는데 위와 같이 쿼리가 3번 나가게 됨
### 7 - 2) 변경 후
### 8 - 2) 변경 후
변경 후 한방 쿼리로 조회 끝
```java
Expand All @@ -399,7 +415,7 @@ public List<String> getUserCategoryNamesByToken(String token) {
___
## 8. Test Container를 통한 테스트의 멱등성 보장하기
## 9. Test Container를 통한 테스트의 멱등성 보장하기
테스트와, 실제 운영 DB를 둘다 MariaDB 환경으로 사용하여 문제가 발생할 일이 없다 생각했었습니다.
하지만, utf8과 같은 인코딩 방식이 로컬과 프로덕션이 달라 문제가 발생하였으며, 이또한 테스트 환경에서 걸러내지 못한 것이 문제라 생각하였습니다.
Expand All @@ -408,7 +424,7 @@ ___
---
## 9. CI / 정적분석기(SonarCloud, jacoco)를 사용한 코드 컨벤션에 대한 코드리뷰 자동화
## 10. CI / 정적분석기(SonarCloud, jacoco)를 사용한 코드 컨벤션에 대한 코드리뷰 자동화
**문제 상황**
Expand All @@ -425,7 +441,7 @@ ___
---
## 10. 서버 모니터링
## 11. 서버 모니터링
**문제 상황**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public enum ResponseCodeAndMessages {
/* Category */
CATEGORY_SEARCH_SUCCESS(HttpStatus.OK.value(), "지원하는 학교 공지 카테고리 조회에 성공하였습니다"),
CATEGORY_SUBSCRIBE_SUCCESS(HttpStatus.OK.value(), "사용자의 학교 공지 카테고리 구독에 성공하였습니다"),

CATEGORY_USER_SUBSCRIBES_LOOKUP_SUCCESS(HttpStatus.OK.value(), "사용자가 구독한 학교 공지 카테고리 조회에 성공하였습니다"),

/* Department */
Expand All @@ -21,17 +20,17 @@ public enum ResponseCodeAndMessages {
/* Staff */
STAFF_SEARCH_SUCCESS(HttpStatus.OK.value(), "교직원 조회에 성공하였습니다"),

/* Feedback */
FEEDBACK_SAVE_SUCCESS(HttpStatus.OK.value(), "피드백 저장에 성공하였습니다"),
FEEDBACK_SEARCH_SUCCESS(HttpStatus.OK.value(), "피드백 조회에 성공하였습니다"),

/* Admin */
ADMIN_TEST_NOTICE_CREATE_SUCCESS(HttpStatus.OK.value(), "테스트 공지 생성에 성공하였습니다"),
ADMIN_REAL_NOTICE_CREATE_SUCCESS(HttpStatus.OK.value(), "실제 공지 생성에 성공하였습니다"),

/* User */
USER_REGISTER_SUCCESS(HttpStatus.OK.value(), "회원가입에 성공하였습니다"),
USER_REGISTER_FAIL(HttpStatus.BAD_REQUEST.value(), "회원가입에 실패하였습니다"),
BOOKMAKR_SAVE_SUCCESS(HttpStatus.OK.value(), "북마크 저장에 성공하였습니다"),
BOOKMARK_LOOKUP_SUCCESS(HttpStatus.OK.value(), "북마크 조회에 성공하였습니다"),
FEEDBACK_SAVE_SUCCESS(HttpStatus.OK.value(), "피드백 저장에 성공하였습니다"),
FEEDBACK_SEARCH_SUCCESS(HttpStatus.OK.value(), "피드백 조회에 성공하였습니다"),

/**
* ErrorCodes about auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public enum ErrorCode {


API_NOTICE_NOT_EXIST_CATEGORY(HttpStatus.BAD_REQUEST, "해당 공지 카테고리를 지원하지 않습니다."),
// API_NOTICE_CANNOT_FIND_CATEGORY(HttpStatus.INTERNAL_SERVER_ERROR, "해당 공지 카테고리를 찾을 수 없습니다."),
API_MISSING_PARAM(HttpStatus.BAD_REQUEST, "필수 파라미터가 없습니다."),
API_INVALID_PARAM(HttpStatus.BAD_REQUEST, "파라미터 값 중 잘못된 값이 있습니다."),
API_BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
Expand All @@ -37,13 +36,6 @@ public enum ErrorCode {

UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 오류"),

/**
* ErrorCodes about WebSocket
*/
WS_MISSING_PARAM(HttpStatus.BAD_REQUEST, "웹소켓 메세지의 파라미터가 누락되어있습니다."),
WS_INVALID_PARAM(HttpStatus.BAD_REQUEST, "웹소켓 메세지 파라미터 중 유효하지 않은 값이 있습니다."),
WS_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."),

/**
* ErrorCodes about InternalLogicException
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.kustacks.kuring.notice.business;

import com.kustacks.kuring.notice.domain.CategoryName;
import com.kustacks.kuring.common.exception.InternalLogicException;
import com.kustacks.kuring.common.exception.NotFoundException;
import com.kustacks.kuring.common.exception.code.ErrorCode;
import com.kustacks.kuring.common.exception.InternalLogicException;
import com.kustacks.kuring.notice.common.OffsetBasedPageRequest;
import com.kustacks.kuring.notice.common.dto.NoticeDto;
import com.kustacks.kuring.notice.common.dto.NoticeListResponse;
import com.kustacks.kuring.notice.common.dto.NoticeSearchDto;
import com.kustacks.kuring.notice.domain.CategoryName;
import com.kustacks.kuring.notice.domain.DepartmentName;
import com.kustacks.kuring.notice.domain.DepartmentNoticeRepository;
import com.kustacks.kuring.notice.domain.NoticeRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
Expand All @@ -18,14 +15,12 @@

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Transactional(readOnly = true)
public class NoticeService {

private final NoticeRepository noticeRepository;
private final DepartmentNoticeRepository departmentNoticeRepository;
private final CategoryName[] supportedCategoryNameList;
private final DepartmentName[] supportedDepartmentNameList;
private final String SPACE_REGEX = "[\\s+]";
Expand All @@ -36,9 +31,8 @@ public class NoticeService {
@Value("${notice.library-base-url}")
private String libraryBaseUrl;

public NoticeService(NoticeRepository noticeRepository, DepartmentNoticeRepository departmentNoticeRepository) {
public NoticeService(NoticeRepository noticeRepository) {
this.noticeRepository = noticeRepository;
this.departmentNoticeRepository = departmentNoticeRepository;
this.supportedCategoryNameList = CategoryName.values();
this.supportedDepartmentNameList = DepartmentName.values();
}
Expand All @@ -47,23 +41,14 @@ public List<DepartmentName> lookupSupportedDepartments() {
return List.of(supportedDepartmentNameList);
}

public NoticeListResponse getNotices(String type, int offset, int max) {
String categoryName = convertShortNameIntoLongName(type);

List<NoticeDto> noticeDtoList = noticeRepository
.findNoticesByCategoryWithOffset(CategoryName.fromStringName(categoryName), new OffsetBasedPageRequest(offset, max));

return new NoticeListResponse(convertBaseUrl(categoryName), noticeDtoList);
}

public List<NoticeDto> getNoticesV2(String type, String department, Boolean important, int page, int size) {
public List<NoticeDto> getNotices(String type, String department, Boolean important, int page, int size) {
if (isDepartmentSearchRequest(type, department)) {
DepartmentName departmentName = DepartmentName.fromHostPrefix(department);

if (Boolean.TRUE.equals(important)) {
return departmentNoticeRepository.findImportantNoticesByDepartment(departmentName);
return noticeRepository.findImportantNoticesByDepartment(departmentName);
} else {
return departmentNoticeRepository.findNormalNoticesByDepartmentWithOffset(departmentName, PageRequest.of(page, size));
return noticeRepository.findNormalNoticesByDepartmentWithOffset(departmentName, PageRequest.of(page, size));
}
}

Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class Notice {
private Long id;

@Getter(AccessLevel.PUBLIC)
@Column(name = "article_id", length = 15, nullable = false)
@Column(name = "article_id", length = 32, nullable = false)
private String articleId;

@Getter(AccessLevel.PUBLIC)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.kustacks.kuring.notice.common.dto.NoticeDto;
import com.kustacks.kuring.notice.common.dto.NoticeSearchDto;
import com.kustacks.kuring.user.common.dto.BookmarkDto;
import org.springframework.data.domain.Pageable;

import java.util.List;
Expand All @@ -15,4 +16,16 @@ public interface NoticeQueryRepository {
List<String> findNormalArticleIdsByCategory(CategoryName categoryName);

void deleteAllByIdsAndCategory(CategoryName categoryName, List<String> articleIds);

List<NoticeDto> findImportantNoticesByDepartment(DepartmentName departmentName);

List<NoticeDto> findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable);

List<Integer> findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum);

List<Integer> findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum);

void deleteAllByIdsAndDepartment(DepartmentName departmentName, List<String> articleIds);

List<BookmarkDto> findAllByBookmarkIds(List<String> ids);
}
Loading

0 comments on commit 312cbbb

Please sign in to comment.