본문 바로가기

백엔드 개발일지

[JPA] save 관련 메서드 동시성 문제 해결

올해 초부터 학교 비교과 프로그램으로 동기들과 함께 진행중인 프로젝트 모카콩(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 문을 통해 커스텀 예외 코드를 던집니다.