→ 60x Speed Improvement
- Java : 11 → 17
- Spring Boot : 2.7.8 → 3.2.7
- Swagger : Springfox → Springdoc
- AWS CloudWatch : Agent → Logs
- login_id → email
- first_password → password
- username → nickname
- 날짜를 문자열로 DB에 저장할 경우, 추후 정렬 시 속도와 정확성 측면에서 불리함.
- LocalDateTime으로 DB에 저장 후, 응답 시 원하는 포맷의 문자열로 변환하는 방안을 채택.
- modified_date (VARCHAR) → modified_time (DATETIME)
- is_friend : 0 , is_wait : 1 → friendship_state (SEND)
- is_friend : 1 , is_wait : 0 → friendship_state (FRIEND)
- 기존 id 값을 직접 저장하는 방식은, 추후 조회 시 추가적인 쿼리 및 메소드 호출을 동반함.
- Friendship 테이블에 User 테이블을 두 번 연관관계 매핑하여, senderUser도 연결하는 방안을 채택.
- sender_user_id (Long) → sender_user_id (User)
- JWT Access Token만 운용 시, 6시간의 짧은 로그인 유지시간을 가지며 보안에 취약함.
- Access Token 만료 시, Refresh Token으로 재발급 받아 2주동안 로그인 유지가 가능하며 보안이 강화됨.
- Access Token → Access Token + Refresh Token 함께 운용. (FE : Axios Interceptor 적용)
Before | After |
---|---|
- 불필요하게 많은 API 호출로 성능 저하 발생 - 사용자에게 userId가 자주 노출되어 보안성 저하 |
- RestFul URI 및 API 개수 단축으로 성능 향상 - Security Context 정보로 userId를 대체하여 보안성 향상 |
Code : Open!
// < 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);
@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;
}
Code : Open!
// < 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 |
---|---|
- Entity와 Repository의 패키징 혼용 - Entity명과 동일한 상위 디렉토리 할당 |
- Entity와 Repository의 패키징 분리 - 역할에 따른 Entity 상위 디렉토리 할당 |
Before | After |
---|---|
- 잘못된 도메인별 DTO 분배 - 동일 디렉토리 내 요청&응답 DTO 혼용 - 무분별한 네이밍으로 복잡성 증가 |
- Inner Class를 활용한 DTO 분리 - 동일 클래스 내 static DTO 네이밍 규칙 준수 |
Before | After |
---|---|
- 역할 없는 무분별한 Exception 생성 - Handler에 예외처리 응답을 일일이 작성 |
- 추상화 CustomException 클래스 상속 - inner 방식으로 static Exception 생성 - Handler는 400,404,500 클래스만 타겟팅 |
< 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