올해 초부터 학교 비교과 프로그램으로 동기들과 함께 진행중인 프로젝트 모카콩(https://github.com/mocacong/Mocacong-Backend)은 동시성 문제를 방지할 코드가 없었습니다.
동시성 문제는 여러 스레드나 프로세스가 동시에 접근하고 수정하는 상황에서 발생할 수 있는 문제입니다. 데이터베이스에서의 동시성 문제는 동시에 여러 개의 트랜잭션이 동일한 데이터를 변경하거나 중복된 데이터를 삽입하려고 할 때 발생할 수 있습니다.
배포하기 전, save 관련 메서드에서 동시성 문제가 발생한다는 것을 발견했고 이를 방지하기 위해 unique 속성을 걸었습니다.
1. unique 속성이란
Spring에서 unique 속성은 데이터베이스 테이블의 컬럼에 대한 고유성(유일성)을 보장하는 데 사용됩니다. 이 속성은 데이터베이스 스키마를 정의할 때 사용되며, 특정 컬럼이 중복된 값을 가질 수 없도록 제약 조건을 설정합니다.
현재 모카콩의 도메인은 BaseTime, Cafe, CafeDetail, CafeImage, Comment, Favorite, Member, MemberProfileImage, Platform, Review, Score 가 있습니다. 그리고 save 동시성 문제를 방지하기 위해 Cafe, Favorite, Member, Review 엔티티에만 unique 속성을 적용했습니다.
2. unique 속성 적용 코드
1) unique 속성
Cafe
@Entity
@Table(name = "cafe")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Cafe extends BaseTime {
private static final int NONE_REVIEW_SCORE = -1;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cafe_id")
private Long id;
@Column(name = "name", nullable = false)
private String name;
// unique 속성 true 추가 (default: false)
@Column(name = "map_id", unique = true, nullable = false)
private String mapId;
@OneToMany(mappedBy = "cafe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Score> score;
@Embedded
private CafeDetail cafeDetail;
@OneToMany(mappedBy = "cafe", fetch = FetchType.EAGER)
private List<CafeImage> cafeImages;
@OneToMany(mappedBy = "cafe", fetch = FetchType.LAZY)
private List<Review> reviews;
@OneToMany(mappedBy = "cafe", fetch = FetchType.LAZY)
private List<Comment> comments;
...
}
unique 속성은 유일성을 보장할 수 있는 필드에 적용해야 합니다.
- Cafe 엔티티의 mapId 필드는 해당 카페의 위도와 경도를 합한 아이디이므로 Cafe 엔티티에서 고유성을 보장해주는 필드입니다.
- 위의 코드처럼 @Column(unique = true)을 추가합니다.
2) uniqueConstraints 속성
Favorite
@Entity
@Table(name = "favorite", uniqueConstraints = {
@UniqueConstraint(columnNames = { "member_id", "cafe_id" })
})
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Favorite extends BaseTime {
@Id
@Column(name = "favorite_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cafe_id")
private Cafe cafe;
...
}
Favorite 엔티티는 카페 즐겨찾기를 담당합니다. 모카콩 서비스에서는 회원은 한 카페에 대해 즐겨찾기가 한 번 가능해야 합니다.
- 그렇기에 Cafe 엔티티와 달리 Favorite는 Member 엔티티와 Cafe 엔티티와 관련이 있으므로 다중 컬럼에 대해 고유성 제약 조건을 설정해야 합니다.
- 위의 @Table 에 uniqueConstraints = { @UniqueConstraint(columnNames = { "member_id", "cafe_id" }을 추가합니다.
3. DataIntegrityViolationException 핸들링
1) DataIntegrityViolationException 이란
DataIntegrityViolationException은 데이터베이스 무결성 제약 조건을 위반하는 작업을 수행할 때 발생하는 예외입니다. 데이터베이스 무결성은 데이터베이스의 일관성, 정확성, 유효성을 보장하기 위해 적용되는 규칙 집합을 의미합니다. 예를 들어, 고유성 제약 조건, 외래 키 제약 조건 등이 데이터베이스 무결성의 일부입니다.
`DataIntegrityViolationException` 이 발생하는 경우
1. 고유성 제약 조건 위반
2. 외래 키 제약 조건 위반
3. 체크 제약 조건 위반
모카콩 서비스는 중간에 unique 속성을 추가했습니다. 만약 실행 도중에 데이터베이스에서 고유성 제약 조건을 위반하는 작업을 시도하면 데이터베이스에서 고유성 제약 조건을 위반하는 작업으로 간주되어 DataIntegrityViolationException이 발생하는 경우가 생깁니다.
이 같은 예외를 방지하기 위해 핸들링을 해주어야 합니다.
2) DataIntegrityViolationException 핸들링
CafeServiece.save 메서드
@Transactional
public void save(CafeRegisterRequest request) {
Cafe cafe = new Cafe(request.getId(), request.getName());
try {
cafeRepository.save(cafe);
} catch (DataIntegrityViolationException e) { // 예외 핸들링 추가
throw new DuplicateCafeException();
}
}
DataIntegrityViolationException 이 터지는 경우엔 try-cathc 문을 통해 커스텀 예외 코드를 던집니다.
'백엔드 개발일지' 카테고리의 다른 글
[Spring] Code Coverage 측정을 위한 JaCoCo 적용하기 (0) | 2023.12.04 |
---|---|
[Spring] 멀티 모듈 프로젝트 도입하기 (3) | 2023.11.20 |
[JPA] 코멘트 신고 기능 도입 및 벌크 연산과 배치 작업을 통한 기간 정지 구현 (0) | 2023.07.22 |
[JPA] 인터셉터를 활용한 JWT Token 기반 로그인 구현 정리 (0) | 2023.07.17 |
[JPA] JPA 변경 감지를 이용하여 상태 변경을 반영 (0) | 2023.07.17 |