gony-dev 님의 블로그

공유 자원 접근 시 일어나는 Race Condition? 본문

TroubleShooting

공유 자원 접근 시 일어나는 Race Condition?

minarinamu 2026. 2. 1. 14:39

😭 Situation

프로젝트 리팩터링 작업 중, 댓글 생성 기능을 검토하다가 쿼리문에 문제 발생 우려가 제기되었다.

 

댓글 생성 기능은 댓글과 대댓글의 생성 로직이 달라 API를 분리하였다.

댓글 객체에는 댓글 그룹을 지정하기 위해 commentGroup 필드를 생성하였다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "writer_id", nullable = false)
    private Member writer;

    @Column(nullable = false, length = 500)
    private String content;

    @Column(nullable = false)
    private Long authorId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "feed_id", nullable = false)
    private Feed feed;

    @Column
    private Long commentGroup; // 대댓글을 위한 댓글 ID

    public Comment(Member writer, String content, Long author, Feed feed, Long commentGroup) {
        this.writer = writer;
        this.content = content;
        this.authorId = author;
        this.feed = feed;
        this.commentGroup = commentGroup;
    }
}

 

문제는 (대댓글이 아닌) 댓글 생성 로직이었다.

commentGroup을 지정하기 위해 repository 내에서 다음 그룹을 지정하는 쿼리문에서 "레이스 컨디션"이 일어난 것이다.

 

레이스 컨디션(Race Condition)이란?

레이스 컨디션은 2개 이상의 프로세스나 스레드가 공유 자원을 서로 사용하려고 경쟁하는 현상을 의미한다.

이때 공유 자원은 멀티 스레드 환경에서 프로세스 내의 자원 사용을 모두 공유한다는 점에서 동기화 문제가 발생한다.

public Long nextCommentGroup(Feed feed) {
        Long max = queryFactory
                .select(comment.commentGroup.max())
                .from(comment)
                .where(comment.feed.eq(feed))
                .fetchOne();
        return (max == null) ? 1L : max + 1;
}

 

위의 코드의 경우에는 댓글을 생성하기 위해 피드에 해당하는 댓글 그룹의 최대값 + 1을 반환하는 쿼리문이다. 이때 발생하게 될 문제를 알아보자.

동시에 댓글이 2개 이상 생성될 경우, 동시에 max를 읽을 것이고 둘다 같은 값을 반환한다.

이러면 동일한 commentGroup을 반환할 테고 동시성 문제가 발생하게 된다.

 

물론 댓글이 생성되지 않는 문제는 아니다. 현재 같은 commentGroup 내의 댓글은 만들어진 순서에 따라 댓글과 대댓글로 구분되기
때문에 조금이라도 더 늦은 댓글이 먼저 생성된 댓글에 대한 대댓글로 만들어지게 된다.

 

이러한 문제는 UX적으로도 사용자에게 불편함을 증대시키기 때문에 해결해야할 필요성을 느꼈다.


🧐 Task

우선 당장의 해결 방안을 모색하기 보다는 현재 주어진 상황을 다시 한 번 되새겨 보기로 했다.

댓글은 만들어진 댓글들에 대한 commentGroup 중 최댓값 다음으로 배치되어 생성된다. 그런 과정에서 querydsl을 사용하여 최댓값을 도출하게 되는데 이때 문제가 발생한다.

 

사실 이러한 문제는 "락(Lock)"을 사용하여 문제를 해결하는 흔한 동시성 문제이다.

하지만 락은 완벽한 기술이 아니다.

  • 동시에 들어온 요청이 락 대기 상태에 걸림 -> TPS 저하
  • 지연 시간 증가
  • 데드락 위험
  • 운영/디버깅 난이도 상승

위와 같은 단점으로 인해 필자는 구현시에 마지막 선택지로서 락을 선택한다.

그래서 락 대신 commentGroup 생성 기준을 다시 생각해보기로 했다.


😠 Action

기존의 comment 객체는 comment 테이블 내에 있는 commentGroup 중 최댓값 + 1을 해서 만들어진다.
그리고 대댓글은 댓글의 commentGroup + 1씩하여 늘어나게 되는 구조였다.

 

지금 생각해보면 왜 저렇게 짰을까?

commentGroup은 주요 목적이 댓글에 대한 대댓글을 조회해주기 위한 것이다.

그래서 쿼리문으로 최댓값을 조회하는 것이 아닌 댓글 생성 시 만들어지는 pk를 기준으로 만들기로 하였다.

그렇게 되면 별도의 시퀀스가 없이도 동시성 이슈가 없다!

@Transactional
@Override
public void createComment(String email, Long postId, CommentReqDto reqDto) {

    Member writer = findIfEmailExists(email);
    Feed feed = findIfFeedExist(postId);
    Long authorId = feed.getMember().getId();

    Comment comment = new Comment(writer, reqDto.content(),
            authorId, feed);
    commentRepository.save(comment);
    comment.updateCommentGroup(comment.getId());

    log.info("{} 피드에 대한 댓글 생성 완료", feed.getId());
}


// Comment.updateCommentGroup
public void updateCommentGroup(Long groupId) {
    this.commentGroup = groupId;
}

 


🙃 Result

TestCode

@Test
void concurrentTest() {
	ExecutorService es = Executors.newFixedThreadPool(32);
    
    for(int i=0;i<10;i++){
    	es.submit(() -> {
        	commentService.createComment("email", "postId", CommentReqDto reqDto);
        });
    }
    
    assertThat(commentRepository.).isEqualTo(10);
}

 

테스트 결과 성공이었다!