[트러블 슈팅] Trello(칸반보드) 프로젝트
1. GreenHopper 알고리즘 정밀도 문제
1)개요
리스트 정렬을 위해 GreenHopper 알고리즘을 사용하여 position 값을 생성하였는데, 처음엔 int 자료형을 사용하여 중간값을 계산했지만, 정밀도 문제로 인해 예상치 못한 값이 저장되는 문제 발생
2)문제상황
double 타입으로 변환하여 position 값을 저장했는데, 연산 과정에서 부동소수점 오차 발생.
즉, 데이터가 많아질수록 중간값을 정확히 계산하지 못하고 값이 비정상적으로 나오는 문제가 있었음
이럴 경우 리스트 두개가 같은 위치를 가지게 되어 정렬이 깨질 가능성이 있음
3)해결
방법1. BigDecimal을 사용하여 정밀도 향상
-기존 double을 BigDecimal로 변경하여 소수점 오차를 방지
방법2. 기본값을 1에서 100으로 변경
-기존에는 1.0, 1.5, 2.0 등으로 저장되었는데, 이를 100, 150, 200으로 변경하여 더 정밀하게 정렬 가능하도록 개선
//원래 코드
private Double calculateNewPosition(List<BoardList> lists, int targetIndex) {
if (lists.isEmpty()) {
return 1.0;
}
if (targetIndex == 0) {
return lists.get(0).getPosition() / 2.0;
}
if (targetIndex >= lists.size()) {
return lists.get(lists.size() - 1).getPosition() + 1.0;
}
Double prevPosition = lists.get(targetIndex - 1).getPosition();
Double nextPosition = lists.get(targetIndex).getPosition();
return (prevPosition + nextPosition) / 2.0;
}
//최종 코드
private BigDecimal calculateNewPosition(List<BoardList> lists, int targetIndex) {
if (lists.isEmpty()) {
return BigDecimal.valueOf(100);
}
if (targetIndex == 0) {
BigDecimal newPosition = lists.get(0).getPosition().divide(BigDecimal.valueOf(2));
if (newPosition.compareTo(BigDecimal.ZERO) <= 0) {
throw new CustomException(ErrorCode.BAD_REQUEST);
}
return newPosition;
} else if (targetIndex >= lists.size()) {
return lists.get(lists.size() - 1).getPosition().add(BigDecimal.valueOf(100));
} else {
BigDecimal prevPosition = lists.get(targetIndex - 1).getPosition();
BigDecimal nextPosition = lists.get(targetIndex).getPosition();
return prevPosition.add(nextPosition).divide(BigDecimal.valueOf(2), 6, RoundingMode.HALF_UP);
}
}
정확한 중간 값을 유지하며, 정렬이 깨지지 않도록 개선
4)결론
- BigDecimal을 사용하여 부동소수점 오차 문제 해결
- 기본값을 100으로 변경하여 더 정밀한 정렬 가능
- 정렬이 안정적으로 유지되면서 오차 없이 지속적으로 사용 가능
2. 파일 업로드 문제
1)개요
파일 업로드 기능을 구현하는 과정에서 여러 개의 파일을 업로드해야하는데 한 개만 업로드되는 문제 발생
2)문제상황
MultipartFile을 단일 객체로 받아서 하나의 파일만 저장되도록 구현
여러 개의 파일을 업로드하는 경우, 리스트 형태로 관리되지 않아 나머지가 무시되고 하나의 파일만 저장
3)해결
List<MultipartFile>로 변경하여 다중 업로드 가능하도록 수정
//원래 코드
@PostMapping
public ResponseEntity<CommonResDto<FileResponseDto>> uploadFile(
@RequestParam MultipartFile file,
@RequestParam Long cardId,
Authentication authentication
) {
FileResponseDto response = fileService.uploadFile(file, cardId, authentication.getName());
return new ResponseEntity<>(new CommonResDto<>("파일 업로드 완료", response), HttpStatus.CREATED);
}
//최종 코드
@PostMapping
public ResponseEntity<CommonListResDto<FileResponseDto>> uploadFiles(
@RequestParam List<MultipartFile> files,
@RequestParam Long cardId,
Authentication authentication
) {
List<FileResponseDto> response = fileService.uploadFiles(files, cardId, authentication.getName());
return new ResponseEntity<>(new CommonListResDto<>("파일 업로드 완료", response), HttpStatus.CREATED);
}
4)결론
- 다중 파일 업로드가 정상적으로 동작하도록 수정
- 파일을 한 번에 여러 개 업로드하는 기능 지원
3. S3 환경변수 설정 문제
1)개요
AWS S3를 사용하여 파일 업로드 기능을 구현하는 과정에서, 초기에 환경 변수를 직접 설정하는 방법을 몰라 AWS Parameter Store를 사용하여 복잡하게 키를 관리하는 실수를 함
2)문제상황
환경 변수를 직접 설정하는 방법을 몰라 AWS Parameter Store를 사용하였는데 단순히 환경 변수 설정만 하면 해결될 문제를 어렵게 접근
3)해결
방법1. AWS Parameter Store에서 환경 변수 방식으로 변경
AWS System Manager Parameter Store에서 키 값을 가져오도록 했으나, 단순한 환경 변수 설정으로도 충분해 Parameter Store를 제거하고 환경 변수 적용
방법2. Spring @Value 어노테이션 적용
환경 변수에서 S3 키 값을 직접 관리하도록 변경하여 설정이 단순하고 직관적으로 개선됨
//원래 코드
@Bean
public String awsAccessKey() {
return AwsParameterStoreUtil.getParameterValue("/dev/s3-access-key", true);
}
@Bean
public String awsSecretKey() {
return AwsParameterStoreUtil.getParameterValue("/dev/s3-secret-key", true);
}
//최종 코드
@Value("${aws.s3.access-key}")
private String accessKey;
@Value("${aws.s3.secret-key}")
private String secretKey;
4)결론
- 환경 변수 설정만으로 충분했지만, 불필요하게 Parameter Store를 사용하여 복잡성을 증가시켰던 실수
- 기본적인 환경 변수 설정 방법을 숙지하는 것이 중요하다는 것을 배움
4. 리스트 Position 관리
1)개요
리스트를 생성할 때, 현재 리스트 개수를 기준으로 position 값을 부여하도록 개선
2)문제상황
lastPosition을 기준으로 새로운 position을 계산했지만, 불필요한 연산이 많아 성능 저하
3)해결
리스트 개수를 세서 count + 1 값으로 position을 부여, position 값이 항상 1부터 시작하도록 기본값 설정
//원래 코드
@Transactional
public ListResponseDto createList(Long workspaceId, Long boardId, String title, String email) {
User user = userRepository.findByEmailOrElseThrow(email);
Workspace workspace = workspaceRepository.findByIdOrElseThrow(workspaceId);
Board board = boardRepository.findByIdOrElseThrow(boardId);
if (!board.getWorkspace().getId().equals(workspace.getId())) {
throw new CustomException(ErrorCode.BAD_REQUEST);
}
List<BoardList> lists = listRepository.findByBoardId(boardId);
lists.sort(Comparator.comparing(BoardList::getPosition));
BigDecimal newPosition;
if (lists.isEmpty()) {
newPosition = BigDecimal.valueOf(100);
} else {
BigDecimal lastPosition = lists.get(lists.size() - 1).getPosition();
newPosition = lastPosition.add(BigDecimal.valueOf(100));
}
BoardList boardList = new BoardList(title, newPosition, user, board);
return new ListResponseDto(listRepository.save(boardList));
}
//최종 코드
@Transactional
public ListResponseDto createList(Long workspaceId, Long boardId, String title, String email) {
User user = userRepository.findByEmailOrElseThrow(email);
Workspace workspace = workspaceRepository.findByIdOrElseThrow(workspaceId);
Board board = boardRepository.findByIdOrElseThrow(boardId);
if (!board.getWorkspace().getId().equals(workspace.getId())) {
throw new CustomException(ErrorCode.BAD_REQUEST);
}
Long listCount = listRepository.countByBoardId(boardId);
BigDecimal newPosition = BigDecimal.valueOf((listCount + 1) * 100);
BoardList boardList = new BoardList(title, newPosition, user, board);
return new ListResponseDto(listRepository.save(boardList));
}
4)결론
- 리스트 정렬 로직이 더 단순하고 빠르게 개선됨
- 불필요한 연산을 줄여 성능 최적화, position 값이 항상 1부터 시작하여 정렬이 안정적으로 유지