이번 프로젝트에서는 이용자들이 쓰레기통에 좋아요, 싫어요, 북마크를 할 수 있도록 했다.
문제는 각각 중복해서 좋아요를 하거나 싫어요를 할 수 있는 상황이 있다는 것이다.
그래서, DB에서 먼저 이 이용자가 해당 쓰레기통을 좋아요/싫어요/북마크 했는지 확인하고 그렇지 않으면 가능하도록 구현을 했다.
public void saveLike(String email, Long binId) {
Member sender = memberService.findByEmail(email);
if (memberLikeBinRepository.existsByMember_IdAndBin_Id(sender.getId(), binId)) {
throw new BadRequestException("이미 좋아요를 누른 쓰레기통입니다.");
}
Bin bin = binService.findById(binId);
bin.increaseLike();
MemberLikeBin memberLikeBin = MemberLikeBin.builder()
.member(sender)
.bin(bin)
.build();
memberLikeBinRepository.save(memberLikeBin);
if (!notificationService.hasLikeNotification(sender, bin)) {
notificationService.sendNotification(sender, getReceiver(bin), bin, NotificationType.BIN_LIKED, null);
}
}
이런 방식이다.
그런데, 같이 프로젝트를 하는 팀원이 이 방법 대신 새로운 방법을 활용할 수 있다고 알려주었다.
안녕하세요. Unique Index 데이터 저장에 대해서 질문드립니다.
이 글을 보면
내가 한 방식처럼 select를 통해서 먼저 데이터가 db에 있는지를 확인하는 방식이 있고, 유니크 인덱스를 걸어서 중복 데이터를 넣으면 예외가 터지도록 하는 방식이 있다.
@Table(
uniqueConstraints = {
@UniqueConstraint(
name = "sameBookmark",
columnNames = {"member_id", "bin_id"}
)
}
)
public class Bookmark extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bin_id")
private Bin bin;
@Builder
public Bookmark(Member member, Bin bin) {
this.member = member;
this.bin = bin;
}
나는 이 두가지 방식을 혼용해서 썼는데 어떤 방식을 쓰는 게 더 좋을지...조금 막연했다.
영한샘의 설명에 따르면, select 방식도 성능적으로 영향을 거의 주지 않는다고 한다.
성능 측면에서 1번이 select 쿼리를 발생시키기 때문에 성능에 영향을 줄 것 같지만, 암달의 법칙이라고 하지요. 애플리케이션 전체를 볼때 PK나 index에서 단건을 조회하는 경우 성능에 미치는 영향은 거의 미미합니다. 그리고 대부분의 애플리케이션은 조회가 많지, 이렇게 저장하는 비즈니스 로직의 호출은 상대적으로 매우 적습니다.
또한, 이렇게 저장하는 비즈니스 로직의 호출도 생각보다 많지 않아서 성능적인 이슈가 될 법하지는 않다고 한다.
이해하기 쉬운 도메인 코드의 관점에선, select를 통해서 한번 검증하는 게 이해하기 더 쉽다는 영한샘의 말에도 동의한다.
코드에 체크 로직이 들어가는데 이 체크 로직 자체가 이미 비즈니스를 반영하기 때문입니다. 반면에 2번은 예외를 잡아서 처리해야 하는데, 아... 이해하기도 좀 애매합니다. 그리고 DB 관련 예외가 막상 터지면 이걸 뭔가 잡아서 해결하기 보다는 그냥 웹 애플리케이션 앞쪽까지 던지고, 공통 예외로 처리하는게 더 깔끔한 경우가 많습니다.
다만, 중요하게 고려할 지점은 동시성 문제다. select 방식은 동시성 문제에서 자유롭지 않았다고 한다!!..(몰랐다)
같은 이름의 사용자가 동시에 insert 처리를 하면 체크로직을 둘다 통과해서 DB까지 이동하게 됩니다. 이후에 DB에서 어떤 문제가 발생하겠지요.
북마크, 좋아요, 싫어요 이런 것들이 모두 여러 서버에서 작동하면서 요청이 저 if 절을 동시에 통과하면 중복 데이터가 DB에 쌓일 수 있는 구조인 것이다.
그래서?
제가 권장하는 방법은 체크 로직을 사용하는 것입니다. 체크 로직을 사용하면 예외를 터트러던, 아니면 예외라는 값을 반환하던 간에 원하는 상황을 깔끔하게 제어할 수 있습니다. 하지만 체크 로직만으로는 동시성 문제가 해결이 안되지요. 그런데 생각해보면 이런 동시성 문제는 진짜 거의 터질일이 없습니다. 따라서 DB에서 동시성 문제가 발생하면(Unique 제약조건 위배) 따로 catch로 잡지말고, 그냥 공통 예외로 컨트롤러 끝까지 보내서 공통 예외로 처리되도록 만듭니다. 그리고 공통 예외 처리에서 고객에게는 시스템에 문제가 있습니다. 정도로 뿌리고 대신 디버깅을위해 시스템에 로그로 자세히 남기는 정도로 마무리하면 됩니다^^
내가 이해한 바로는 select를 통해서 애플리케이션 코드로 한번 확인을 하면서도 유니크 제약을 걸어서 이걸 공통 예외로 처리되도록 하라는 것인거 같다.