개발일기

[채용 정보 스크랩] 동시 요청 100개 스크랩 해결 방법 본문

취준생 프로젝트

[채용 정보 스크랩] 동시 요청 100개 스크랩 해결 방법

한둥둥 2024. 11. 28. 16:09

Jmeter를 사용해서 똑같은 유저 정보로 동시에 100개 요청을 날리면 어떻게 되는지 테스트 해봤습니다. 

 

100개를 동일하게 요청을 날릴 때,

Jmeter 해당 모습 Error 가 발생하였다. 에러가 발생하는건 사실상 문제는 아니다.  이유는 1초에 요청을 날리고 그 요청을 날리 때, 기준으로 디비에 데이터가 아직 insert되어있지 않기에 여러개의 중복데이터가 저장되어버리고 이에따라 문제가 발생하게 되었다. 

 

대참사는 흠 중복저장 되어버리는 것이 문제였다. 

 

어떻게 해결해야 하는가? 

 

비관적인 락을 걸었다. 사실 Synchorized를 사용할까도 고민이였다. 하지만 Synchorized를 사용해도 똑같은 문제가 발생하였다. 

이유는 Transactional 때문이였다. 

 

Spring AOP 트랙션 관리 기능이 적용되는데 이 과정에서 프록시 객체가 만들어지면서 충돌하게 된다. 

 

JobInfoServiceProxy 예제 클래스이다. 

public class JobInfoServiceProxy extends JobInfoServiceImpl {
    private final JobInfoServiceImpl target;

    public JobInfoServiceProxy(JobInfoServiceImpl target) {
        this.target = target;
    }

    @Override
    public String jobInfoScrap(JobInfoScrapRequest request) {
        // .. 
    }
}

 

이렇게 되어있다. 그러면서 덕분에 Synchorized는 메서드 이름/ 파라미터 타입과 개수가 아니기에 때문에 상속되지 않는다. 따라서 프록시 객체의 jobInfoScrap 메서드는 여러 스레드가 사용할 수 있게 되어버린다...

 

흠 어떻게 해야할까? Transcational은 꼭 걸어줘야 하는데, 그래서 고민하게 된 것이 비관적인락을 걸자였다. 조회하는 부분에 비관적인락을 걸게 된다면 괜찮을 것이라 판단하였다. 

 

QueryDSL에서 비관적인 락을 걸어주었다. 

@Override
    public Optional<JobInfoScrap> findByJobInfoId(Long id, String email) {
        JobInfoScrap scrap = queryFactory.selectFrom(jobInfoScrapEntity)
                .where(
                        (jobInfoScrapEntity.jobInfo.id.eq(id))
                                .and(jobInfoScrapEntity.user.email.eq(email))
                )
                .setLockMode(LockModeType.PESSIMISTIC_WRITE)
                .fetchOne();

        return Optional.ofNullable(scrap);
    }

코드를 이렇게 작성해주었다. 결론적으로 말하지만 똑같은 문제가 발생해버렸다. 왜 발생한거지?? 이유는 알게되었다. 

 

조회가할 조직이 없다면 즉, JobInfoScrap에 조회된 데이터가 없다면 비관적락이 발생하기 때문에 동일한 문제가 계속해서 동일하게 발생하였다. 

그러다가 UniqueKey를 걸어주는 것이였다. 

 

package darkoverload.itzip.feature.job.domain;


import darkoverload.itzip.feature.job.entity.JobInfoEntity;
import darkoverload.itzip.feature.user.domain.User;
import darkoverload.itzip.feature.user.entity.UserEntity;
import darkoverload.itzip.global.entity.AuditingFields;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@ToString
@Entity
@Getter @Setter
@AllArgsConstructor
@NoArgsConstructor(access= AccessLevel.PROTECTED)
@Builder
@Table(
        name="job_info_scraps",
        uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "job_id"})
)
@EntityListeners(AuditingEntityListener.class)
@EqualsAndHashCode(callSuper = false)
public class JobInfoScrap extends AuditingFields {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="user_id")
    private UserEntity user;

    @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
    @JoinColumn(name="job_id")
    private JobInfoEntity jobInfo;

    public static JobInfoScrap createScrap(UserEntity user, JobInfoEntity jobinfo) {
        return JobInfoScrap.builder()
                .user(user)
                .jobInfo(jobinfo)
                .build();
    }

}

테이블에 UniqueConstraint 제약조건을 걸어주었다. 덕분에 최종적으로 중복저장되지 않도록 막을 수 있었다. 

 

다음글은 도메인 테스트 방법과 스크랩에 캐싱을 적용하는 글로 찾아뵈려고 한다.