찜하기 기능을 구현하기 위해 하나의 PATCH API를 사용하였고, 이를 통해 찜하기와 해제 기능을 동시에 처리하도록 설계했습니다. 그러나 다량의 찜하기/해제 요청이 동시에 들어오면서, user_id와 house_id가 같은 pin 엔티티가 중복 생성되는 문제가 발생하였고, 이에 따라 서버 오류가 발생했습니다.
아래와 같이 다대다 관계로 찜/좋아요 기능을 위해 엔티티를 설계했습니다.
@Entity
@Table(name="pin")
@Data
public class Pin {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "house_id")
private House house;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "user_id")
private User user;
}
문제 원인
문제의 원인은 찜하기/해제 기능을 하나의 API로 처리한 것에서 발생했습니다. pin 엔티티는 house_id와 user_id 필드를 이용하여 찜 여부를 기록하고, 찜 해제 시 해당 엔티티를 삭제하는 방식으로 구현되었습니다.
Optional<Pin> pin = pinRepository.findByUserIdAndHouseId(Long userId, Long houseId);
이 과정에서 발생한 동시성 문제는 다음과 같습니다.
- 찜1: DB에서 pin을 읽고, 찜 엔티티가 없는 것을 확인한 후 새로운 찜 객체를 작성합니다.
- 찜2: DB에서 동일한 user_id와 house_id로 pin을 읽는데, 찜1이 DB에 새로운 객체를 작성하기 전에 읽었기 때문에 찜이 없다고 판단하고 새로운 찜 객체를 작성합니다.
결과적으로 두 개의 pin 객체가 중복 생성되며, 다음 찜하기 요청 시 IncorrectResultSizeDataAccessException 오류가 발생합니다. 이는 동일한 user_id와 house_id에 대해 여러 개의 pin 객체가 존재하기 때문입니다.
해결 방안
가장 이상적인 해결책은 찜하기 API와 찜해제 API를 분리하는 것이지만(읽을 필요가 없도록), 이미 클라이언트 측에서 사용하는 API가 있기 때문에 뒤늦게 변경하는 것은 어려운 상황이었습니다.
따라서 찜 해제 시 동일한 찜 객체를 모두 삭제하고, 찜 API에서는 아래와 같은 함수로 찜 여부를 확인하도록 수정하였습니다.
기존 함수 선언부를 Optional -> List로 바꾸면 끝입니다.
List<Pin> findByUserIdAndHouseId(Long userId, Long houseId);
이 함수는 userId와 houseId를 기준으로 해당하는 모든 찜 객체를 조회합니다. List의 empty() 함수를 사용하여 찜 여부를 확인하고, 찜 해제 시에는 해당 pin 객체들을 모두 삭제하는 방식으로 해결하였습니다.
if (!existingPins.isEmpty()) {
pinRepository.deleteAllInBatch(existingPins); // 모든 엔티티 삭제
}
이와 같이 batch 방식으로 한 번에 여러 엔티티를 삭제하는 방법을 적용하여, 동시성 문제를 해결할 수 있었습니다.
결론
- 문제 원인: 하나의 API에서 찜하기와 찜 해제를 동시에 처리하는 과정에서 발생한 race condition.
- 해결 방법: 찜하기/해제 API의 로직을 수정하여 찜 객체를 정확하게 처리하도록 변경. findByUserIdAndHouseId()로 찜 여부를 확인하고, 찜 해제 시 모든 관련 객체를 삭제하는 방식으로 동시성 문제를 해결.
이 방법을 통해 찜하기와 찜 해제의 안정성을 높일 수 있었으며, 동시 다발적인 요청에서도 정상적으로 동작하도록 개선되었습니다.
'Server > 🌱 Spring Boot (java)' 카테고리의 다른 글
[Gradle] common을 사용하는 멀티-모듈 프로젝트에서 순환참조 문제 해결 및 최적화 (0) | 2025.01.24 |
---|---|
[Gradle] 멀티-모듈 프로젝트에서 Gradle 설정 (0) | 2025.01.24 |
[Spring] 스프링 MVC 예외 처리 시 인터셉터 재호출 해결법 (0) | 2024.08.09 |
[Spring] 서블릿 예외 처리 시 필터 재호출 해결법 (0) | 2024.07.28 |
[Spring] 서블릿 예외 처리와 오류 페이지 (0) | 2024.07.28 |
댓글