Skip to content

OnlineMemo/backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

OnlineMemo - Backend Refactoring

β†’Β Β 60x Speed Improvement

Project

Refactor

Β Contents

  1. Β Β βš™ Version
  2. Β Β πŸ—‚οΈ Database
  3. Β Β πŸ“— API
  4. Β Β πŸ“ˆ Performance
  5. Β Β πŸ“‚ Package

βš™ Version

  • JavaΒ :Β Β 11 β†’ 17
  • Spring BootΒ :Β Β 2.7.8 β†’ 3.2.7
  • SwaggerΒ :Β Β Springfox β†’ Springdoc
  • AWS CloudWatchΒ :Β Β Agent β†’ Logs

πŸ—‚οΈ Database

BeforeΒ Β /Β Β After

before DB ERD after DB ERD

λΆ€μ μ ˆν•œ 컬럼λͺ… μˆ˜μ •

  • login_idΒ Β β†’Β Β email
  • first_passwordΒ Β β†’Β Β password
  • usernameΒ Β β†’Β Β nickname

μžλ£Œν˜• λ³€κ²½

  • λ‚ μ§œλ₯Ό λ¬Έμžμ—΄λ‘œ DB에 μ €μž₯ν•  경우, μΆ”ν›„ μ •λ ¬ μ‹œ 속도와 μ •ν™•μ„± μΈ‘λ©΄μ—μ„œ λΆˆλ¦¬ν•¨.
  • LocalDateTime으둜 DB에 μ €μž₯ ν›„, 응닡 μ‹œ μ›ν•˜λŠ” 포맷의 λ¬Έμžμ—΄λ‘œ λ³€ν™˜ν•˜λŠ” λ°©μ•ˆμ„ 채택.
  • modified_date (VARCHAR)Β Β β†’Β Β modified_time (DATETIME)

Enum μ‚¬μš©

  • is_friend : 0Β ,Β Β is_wait : 1Β Β β†’Β Β friendship_state (SEND)
  • is_friend : 1Β ,Β Β is_wait : 0Β Β β†’Β Β friendship_state (FRIEND)

sender_user_id μ‹€μ œ 맀핑

  • κΈ°μ‘΄ id 값을 직접 μ €μž₯ν•˜λŠ” 방식은, μΆ”ν›„ 쑰회 μ‹œ 좔가적인 쿼리 및 λ©”μ†Œλ“œ ν˜ΈμΆœμ„ λ™λ°˜ν•¨.
  • Friendship ν…Œμ΄λΈ”μ— User ν…Œμ΄λΈ”μ„ 두 번 연관관계 λ§€ν•‘ν•˜μ—¬, senderUser도 μ—°κ²°ν•˜λŠ” λ°©μ•ˆμ„ 채택.
  • sender_user_id (Long)Β Β β†’Β Β sender_user_id (User)

refresh_token 컬럼 μΆ”κ°€

  • JWT Access Token만 운용 μ‹œ, 6μ‹œκ°„μ˜ 짧은 둜그인 μœ μ§€μ‹œκ°„μ„ 가지며 λ³΄μ•ˆμ— 취약함.
  • Access Token 만료 μ‹œ, Refresh Token으둜 μž¬λ°œκΈ‰ λ°›μ•„ 2μ£Όλ™μ•ˆ 둜그인 μœ μ§€κ°€ κ°€λŠ₯ν•˜λ©° λ³΄μ•ˆμ΄ 강화됨.
  • Access TokenΒ Β β†’Β Β Access Token + Refresh Token ν•¨κ»˜ 운용.Β Β (FE : Axios Interceptor 적용)

πŸ“— API

Before After
-Β Β λΆˆν•„μš”ν•˜κ²Œ λ§Žμ€ API 호좜둜 μ„±λŠ₯ μ €ν•˜ λ°œμƒ
-Β Β μ‚¬μš©μžμ—κ²Œ userIdκ°€ 자주 λ…ΈμΆœλ˜μ–΄ λ³΄μ•ˆμ„± μ €ν•˜
-Β Β RestFul URI 및 API 개수 λ‹¨μΆ•μœΌλ‘œ μ„±λŠ₯ ν–₯상
-Β Β Security Context μ •λ³΄λ‘œ userIdλ₯Ό λŒ€μ²΄ν•˜μ—¬ λ³΄μ•ˆμ„± ν–₯상

πŸ“ˆ Performance

N+1 Query

Β CodeΒ :Β Open!

Repository

// < Before - JPA 쿼리 λ©”μ†Œλ“œ (Lazy 쑰회) >
Optional<User> findById(Long userId);              // User

// < After - Fetch Join λ©”μ†Œλ“œ (Eager 쑰회) >
@Query("SELECT u FROM User u " +                   // User
        "LEFT JOIN FETCH u.userMemoList uml " +    // + User.userMemoList
        "LEFT JOIN FETCH uml.memo m " +            // + User.userMemoList.memo
        "LEFT JOIN FETCH m.userMemoList umll " +   // + User.userMemoList.memo.userMemoList
        "LEFT JOIN FETCH umll.user " +             // + User.userMemoList.memo.userMemoList.user
        "WHERE u.id = :userId")
Optional<User> findByIdToDeepUserWithEager(@Param("userId") Long userId);

Service

@Transactional(readOnly = true)
@Override
public List<MemoDto.MemoPageResponse> findMemos(String filter, String search) {  // λ©”λͺ¨ λͺ©λ‘ 쑰회,μ •λ ¬,검색 둜직
    if(filter != null && search != null) throw new Exception400.MemoBadRequest("잘λͺ»λœ μΏΌλ¦¬νŒŒλΌλ―Έν„°λ‘œ APIλ₯Ό μš”μ²­ν•˜μ˜€μŠ΅λ‹ˆλ‹€.");
    Predicate<Memo> memoPredicate = (filter != null) ? filterMemos(filter) : searchMemos(search);
    Long loginUserId = SecurityUtil.getCurrentMemberId();

    // < Before - JPA 쿼리 λ©”μ†Œλ“œ (Lazy 쑰회) >  N+1 문제 O
    User user = userRepository.findById(loginUserId).orElseThrow(() -> new Exception404.NoSuchUser(String.format("userId = %d", loginUserId)));

    // < After - Fetch Join λ©”μ†Œλ“œ (Eager 쑰회) >  N+1 문제 X
    User user = userRepository.findByIdToDeepUserWithEager(loginUserId).orElseThrow(() -> new Exception404.NoSuchUser(String.format("userId = %d", loginUserId)));

    List<MemoDto.MemoPageResponse> memoPageResponseDtoList = user.getUserMemoList().stream()
            .map(UserMemo::getMemo)              // User.userMemoList (N+1 쿼리 λ°œμƒ)
            .filter(memoPredicate)               // User.userMemoList.memo (N+1 쿼리 λ°œμƒ)
            .sorted(Comparator.comparing(Memo::getModifiedTime, Comparator.reverseOrder())
                    .thenComparing(Memo::getId, Comparator.reverseOrder()))
            .map(MemoDto.MemoPageResponse::new)  // User.userMemoList.memo.userMemoList & User.userMemoList.memo.userMemoList.user (λ‚΄λΆ€μ—μ„œ N+1 쿼리 λ°œμƒ)
            .collect(Collectors.toList());

    return memoPageResponseDtoList;
}
Before
(JPA method)
After
(JPQL Fetch Join)
-Β Β JPA 쿼리 λ©”μ†Œλ“œλ‘œ μƒμœ„ μ—”ν‹°ν‹°λ₯Ό μ‘°νšŒν•œ 경우
-Β Β ν•˜μœ„ 엔티티에 μ ‘κ·Όμ‹œ 좔가적인 N+1 쿼리 λ°œμƒ
-Β Β μž¦μ€ DB μ ‘κ·ΌμœΌλ‘œ μ„±λŠ₯ μ €ν•˜ λ°œμƒ
-Β Β Fetch Join을 ν™œμš©ν•œ JPQL λ©”μ†Œλ“œλ‘œ μƒμœ„ μ—”ν‹°ν‹°λ₯Ό μ‘°νšŒν•œ 경우
-Β Β μ§€μ •ν•œ ν•˜μœ„ μ—”ν‹°ν‹°κΉŒμ§€ Eager μ‘°νšŒν•˜μ—¬ N+1 문제 ν•΄κ²°
-  쿼리 κ°œμ„ μœΌλ‘œ DB 접근을 μ΅œμ†Œν™”ν•˜μ—¬ μ„±λŠ₯ μ΅œμ ν™”

Bulk Query

Β CodeΒ :Β Open!

Repository

// < Before - JPA saveAll >
void saveAll(List<UserMemo> userMemoList);

// < Before - JPA deleteAll >
void deleteAll(List<Memo> memoList);  // deleteAllInBatch()λŠ” OR절의 μ„±λŠ₯ μ €ν•˜μ™€ μ˜€λ²„ν—€λ“œμ˜ κ°€λŠ₯μ„±μœΌλ‘œ μ‚¬μš©ν•˜μ§€ μ•Šμ•˜μŒ.

// < After - JDBC Batch Insert >
public void batchInsert(List<UserMemo> userMemoList) {
    String sql = "INSERT INTO user_memo (user_id, memo_id) VALUES (?, ?)";

    for (int i=0; i<userMemoList.size(); i+=BATCH_SIZE) {  // 'BATCH_SIZE = 1000' 배치 크기 μ„€μ • (λ©”λͺ¨λ¦¬ μ˜€λ²„ν—€λ“œ 방지)
        List<UserMemo> batchList = userMemoList.subList(i, Math.min(i+BATCH_SIZE, userMemoList.size()));

        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                UserMemo userMemo = batchList.get(i);
                ps.setLong(1, userMemo.getUser().getId());
                ps.setLong(2, userMemo.getMemo().getId());
            }

            @Override
            public int getBatchSize() {
                return batchList.size();
            }
        });
    }
}

// < After - JDBC Batch Delete >
public void batchDelete(List<Memo> memoList) {

    for (int i=0; i<memoList.size(); i+=BATCH_SIZE) {
        List<Long> batchList = memoList.subList(i, Math.min(i+BATCH_SIZE, memoList.size()))
                .stream()
                .map(Memo::getId)
                .collect(Collectors.toList());

        String sql = String.format("DELETE FROM memo WHERE memo_id IN (%s)",  // OR절이 μ•„λ‹Œ IN절 μ‚¬μš©.
                batchList.stream()
                        .map(String::valueOf)
                        .collect(Collectors.joining(",")));

        jdbcTemplate.update(sql);
    }
}
BeforeΒ Β β†’Β Β After
(JPA saveAll)Β Β β†’Β Β (JDBC Batch Insert)
BeforeΒ Β β†’Β Β After
(JPA deleteAll)Β Β β†’Β Β (JDBC Batch Delete + IN절)
-Β Β λ©”λͺ¨μ— μ—¬λŸ¬ 친ꡬλ₯Ό μ΄ˆλŒ€ν•  λ•Œ Bulk Insert ν•„μš”
-Β Β idμ „λž΅μ΄ IDENTITY라 saveAll이 벌크둜 λ™μž‘ν•˜μ§€ μ•ŠμŒ
-Β Β JPA saveAll λŒ€μ‹  JDBC Batch Insert μ‚¬μš©
-Β Β 10λͺ…μ˜ 친ꡬ μ΄ˆλŒ€ μ‹œ 쿼리가 10νšŒμ—μ„œ 1회둜 κ°μ†Œ
-Β Β νšŒμ› νƒˆν‡΄ μ‹œ λͺ¨λ“  λ©”λͺ¨λ₯Ό Bulk Delete ν•„μš”
-Β Β JPA의 λ‚΄λΆ€ 순회둜 deleteAll이 벌크둜 λ™μž‘ν•˜μ§€ μ•ŠμŒ
-Β Β JPA deleteAll λŒ€μ‹  JDBC Batch Delete μ‚¬μš©
-Β Β OR λŒ€μ‹  IN 절둜 μ˜€λ²„ν—€λ“œμ™€ μ„±λŠ₯ 이점 확보
-Β Β 10개의 λ©”λͺ¨ μ‚­μ œ μ‹œ 쿼리가 10νšŒμ—μ„œ 1회둜 κ°μ†Œ

Benchmark

Before
(MemoPage - 30 memos)
After
(MemoPage - 30 memos)
-Β Β FE : 각각의 λͺ¨λ“  ν•˜μœ„ μ»΄ν¬λ„ŒνŠΈμ—μ„œ API 닀쀑 호좜
-  Result :  Request = 91번  &  Finish Time = 11.27s
-Β Β FE : μƒμœ„ μ»΄ν¬λ„ŒνŠΈμ—μ„œ API 호좜 ν›„ ν•˜μœ„λ‘œ props 전달
-Β Β BE : 전체적인 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 및 쿼리 κ°œμ„ 
-  Result :  Request = 2번  &  Finish Time = 193ms

β†’Β Β λΆˆκ³Ό 30개의 λ©”λͺ¨μž„에도, 무렀 58.4배의 μ„±λŠ₯ κ°œμ„ 
β†’Β Β Prod μ„œλ²„ 재배포 μ‹œ, μ΅œμ†Œ 60λ°° μ΄μƒμ˜ 속도 ν–₯상 μ˜ˆμƒ

πŸ“‚ Package

DomainΒ Β &Β Β Repository

Before After
-Β Β Entity와 Repository의 νŒ¨ν‚€μ§• 혼용
-Β Β Entityλͺ…κ³Ό λ™μΌν•œ μƒμœ„ 디렉토리 ν• λ‹Ή
-Β Β Entity와 Repository의 νŒ¨ν‚€μ§• 뢄리
-  역할에 λ”°λ₯Έ Entity μƒμœ„ 디렉토리 ν• λ‹Ή

DTO

Before After
-  잘λͺ»λœ 도메인별 DTO λΆ„λ°°
-  동일 디렉토리 λ‚΄ μš”μ²­&응닡 DTO 혼용
-Β Β λ¬΄λΆ„λ³„ν•œ λ„€μ΄λ°μœΌλ‘œ λ³΅μž‘μ„± 증가
-Β Β Inner Classλ₯Ό ν™œμš©ν•œ DTO 뢄리
-  동일 클래슀 λ‚΄ static DTO 넀이밍 κ·œμΉ™ μ€€μˆ˜

Exception

Before After
-Β Β μ—­ν•  μ—†λŠ” λ¬΄λΆ„λ³„ν•œ Exception 생성
-Β Β Handler에 μ˜ˆμ™Έμ²˜λ¦¬ 응닡을 일일이 μž‘μ„±
-  좔상화 CustomException 클래슀 상속
-Β Β inner λ°©μ‹μœΌλ‘œ static Exception 생성
-Β Β HandlerλŠ” 400,404,500 클래슀만 νƒ€κ²ŸνŒ…

Directory Structure

< Before >                                        < After >
----------------------------------------------------------------------------------------------
:                                                 :
β”œβ”€β”€ config                                        β”œβ”€β”€ config
β”‚   β”œβ”€β”€ JwtSecurityConfig.java                    β”‚   β”œβ”€β”€ SecurityConfig.java
β”‚   β”œβ”€β”€ SwaggerConfig.java                        β”‚   └── SwaggerConfig.java
β”‚   └── WebSecurityConfig.java                    β”œβ”€β”€ controller
β”œβ”€β”€ controller                                    β”‚   β”œβ”€β”€ AuthController.java
β”‚   β”œβ”€β”€ AuthController.java                       β”‚   β”œβ”€β”€ FriendshipController.java
β”‚   β”œβ”€β”€ FriendshipController.java                 β”‚   β”œβ”€β”€ MemoController.java
β”‚   β”œβ”€β”€ MemoController.java                       β”‚   β”œβ”€β”€ TestController.java
β”‚   β”œβ”€β”€ TestController.java                       β”‚   └── UserController.java
β”‚   └── UserController.java                       β”œβ”€β”€ domain
β”œβ”€β”€ domain                                        β”‚   β”œβ”€β”€ Friendship.java
β”‚   β”œβ”€β”€ DefaultFriendshipEntity.java              β”‚   β”œβ”€β”€ Memo.java
β”‚   β”œβ”€β”€ DefaultMemoEntity.java                    β”‚   β”œβ”€β”€ User.java
β”‚   β”œβ”€β”€ friendship                                β”‚   β”œβ”€β”€ common
β”‚   β”‚   β”œβ”€β”€ Friendship.java                       β”‚   β”‚   └── BaseEntity.java
β”‚   β”‚   └── FriendshipJpaRepository.java          β”‚   β”œβ”€β”€ enums
β”‚   β”œβ”€β”€ memo                                      β”‚   β”‚   β”œβ”€β”€ Authority.java
β”‚   β”‚   β”œβ”€β”€ Memo.java                             β”‚   β”‚   └── FriendshipState.java
β”‚   β”‚   └── MemoJpaRepository.java                β”‚   └── mapping
β”‚   β”œβ”€β”€ user                                      β”‚       └── UserMemo.java
β”‚   β”‚   β”œβ”€β”€ Authority.java                        β”œβ”€β”€ dto
β”‚   β”‚   β”œβ”€β”€ User.java                             β”‚   β”œβ”€β”€ AuthDto.java
β”‚   β”‚   └── UserJpaRepository.java                β”‚   β”œβ”€β”€ FriendshipDto.java
β”‚   └── userandmemo                               β”‚   β”œβ”€β”€ MemoDto.java
β”‚       β”œβ”€β”€ UserAndMemo.java                      β”‚   └── UserDto.java
β”‚       └── UserAndMemoJpaRepository.java         β”œβ”€β”€ jwt
β”œβ”€β”€ dto                                           β”‚   β”œβ”€β”€ CustomUserDetailsService.java
β”‚   β”œβ”€β”€ friendship                                β”‚   β”œβ”€β”€ JwtFilter.java
β”‚   β”‚   β”œβ”€β”€ FriendshipRequestDto.java             β”‚   β”œβ”€β”€ TokenProvider.java
β”‚   β”‚   β”œβ”€β”€ FriendshipResponseDto.java            β”‚   └── handler
β”‚   β”‚   β”œβ”€β”€ FriendshipSendRequestDto.java         β”‚       β”œβ”€β”€ JwtAccessDeniedHandler.java
β”‚   β”‚   β”œβ”€β”€ FriendshipSendResponseDto.java        β”‚       β”œβ”€β”€ JwtAuthenticationEntryPoint.java
β”‚   β”‚   └── FriendshipUpdateRequestDto.java       β”‚       └── JwtExceptionFilter.java
β”‚   β”œβ”€β”€ memo                                      β”œβ”€β”€ repository
β”‚   β”‚   β”œβ”€β”€ MemoInviteResponseDto.java            β”‚   β”œβ”€β”€ FriendshipBatchRepository.java
β”‚   β”‚   β”œβ”€β”€ MemoResponseDto.java                  β”‚   β”œβ”€β”€ FriendshipRepository.java
β”‚   β”‚   β”œβ”€β”€ MemoSaveRequestDto.java               β”‚   β”œβ”€β”€ MemoBatchRepository.java
β”‚   β”‚   β”œβ”€β”€ MemoSaveResponseDto.java              β”‚   β”œβ”€β”€ MemoRepository.java
β”‚   β”‚   β”œβ”€β”€ MemoUpdateRequestDto.java             β”‚   β”œβ”€β”€ UserMemoBatchRepository.java
β”‚   β”‚   └── MemoUpdateStarRequestDto.java         β”‚   β”œβ”€β”€ UserMemoRepository.java
β”‚   β”œβ”€β”€ token                                     β”‚   └── UserRepository.java
β”‚   β”‚   └── TokenDto.java                         β”œβ”€β”€ response
β”‚   β”œβ”€β”€ user                                      β”‚   β”œβ”€β”€ GlobalExceptionHandler.java
β”‚   β”‚   β”œβ”€β”€ UserIdResponseDto.java                β”‚   β”œβ”€β”€ ResponseCode.java
β”‚   β”‚   β”œβ”€β”€ UserLoginRequestDto.java              β”‚   β”œβ”€β”€ ResponseData.java
β”‚   β”‚   β”œβ”€β”€ UserRequestDto.java                   β”‚   β”œβ”€β”€ exception
β”‚   β”‚   β”œβ”€β”€ UserRequestDtos.java                  β”‚   β”‚   β”œβ”€β”€ CustomException.java
β”‚   β”‚   β”œβ”€β”€ UserResponseDto.java                  β”‚   β”‚   β”œβ”€β”€ Exception400.java
β”‚   β”‚   β”œβ”€β”€ UserSignupRequestDto.java             β”‚   β”‚   β”œβ”€β”€ Exception404.java
β”‚   β”‚   β”œβ”€β”€ UserUpdateNameRequestDto.java         β”‚   β”‚   └── Exception500.java
β”‚   β”‚   └── UserUpdatePwRequestDto.java           β”‚   └── responseitem
β”‚   └── userandmemo                               β”‚       β”œβ”€β”€ MessageItem.java
β”‚       β”œβ”€β”€ UserAndMemoRequestDto.java            β”‚       └── StatusItem.java
β”‚       └── UserAndMemoResponseDto.java           β”œβ”€β”€ service
β”œβ”€β”€ jwt                                           β”‚   β”œβ”€β”€ AuthService.java
β”‚   β”œβ”€β”€ JwtAccessDeniedHandler.java               β”‚   β”œβ”€β”€ FriendshipService.java
β”‚   β”œβ”€β”€ JwtAuthenticationEntryPoint.java          β”‚   β”œβ”€β”€ MemoService.java
β”‚   β”œβ”€β”€ JwtFilter.java                            β”‚   β”œβ”€β”€ UserMemoService.java
β”‚   └── TokenProvider.java                        β”‚   β”œβ”€β”€ UserService.java
β”œβ”€β”€ response                                      β”‚   └── impl
β”‚   β”œβ”€β”€ GlobalExceptionHandler.java               β”‚       β”œβ”€β”€ AuthServiceImpl.java
β”‚   β”œβ”€β”€ ResponseCode.java                         β”‚       β”œβ”€β”€ FriendshipServiceImpl.java
β”‚   β”œβ”€β”€ ResponseData.java                         β”‚       β”œβ”€β”€ MemoServiceImpl.java
β”‚   β”œβ”€β”€ exception                                 β”‚       β”œβ”€β”€ UserMemoServiceImpl.java
β”‚   β”‚   β”œβ”€β”€ FriendshipBadRequestException.java    β”‚       └── UserServiceImpl.java
β”‚   β”‚   β”œβ”€β”€ FriendshipDuplicateException.java     └── util
β”‚   β”‚   β”œβ”€β”€ LoginIdDuplicateException.java            β”œβ”€β”€ SecurityUtil.java
β”‚   β”‚   β”œβ”€β”€ MemoSortBadRequestException.java          └── TimeConverter.java
β”‚   β”‚   β”œβ”€β”€ NoSuchFriendshipException.java
β”‚   β”‚   β”œβ”€β”€ NoSuchMemoException.java
β”‚   β”‚   β”œβ”€β”€ NoSuchUserException.java
β”‚   β”‚   └── UserAndMemoDuplicateException.java
β”‚   └── responseitem
β”‚       β”œβ”€β”€ MessageItem.java
β”‚       └── StatusItem.java
β”œβ”€β”€ service
β”‚   β”œβ”€β”€ FriendshipService.java
β”‚   β”œβ”€β”€ MemoService.java
β”‚   β”œβ”€β”€ UserAndMemoService.java
β”‚   β”œβ”€β”€ UserService.java
β”‚   β”œβ”€β”€ auth
β”‚   β”‚   β”œβ”€β”€ AuthService.java
β”‚   β”‚   └── CustomUserDetailsService.java
β”‚   └── logic
β”‚       β”œβ”€β”€ FriendshipServiceLogic.java
β”‚       β”œβ”€β”€ MemoServiceLogic.java
β”‚       β”œβ”€β”€ UserAndMemoServiceLogic.java
β”‚       └── UserServiceLogic.java
└── util
    └── SecurityUtil.java

Releases

No releases published

Packages

No packages published

Languages