로깅 강화
로깅을 통해 문제 발생 지점을 확인하려 하였다. 로그에는 스택 트레이스, 입력 값, 쓰레드 이름 및 ID 등을 포함시켰다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PerformanceLoggingUtil {
private static Logger getLogger(Class<?> clazz) {
return LoggerFactory.getLogger(clazz);
}
public static void logPerformanceInfo(Class<?> clazz, String message) {
Logger logger = getLogger(clazz);
String threadName = Thread.currentThread().getName();
long threadId = Thread.currentThread().getId();
long timestamp = System.currentTimeMillis();
String formattedMessage = String.format("[Timestamp: %d, Thread Name: %s, Thread ID: %d] %s", timestamp, threadName, threadId, message);
if (logger.isInfoEnabled()) {
logger.info(formattedMessage);
}
}
public static void logPerformanceError(Class<?> clazz, String message, Throwable t) {
Logger logger = getLogger(clazz);
String threadName = Thread.currentThread().getName();
long threadId = Thread.currentThread().getId();
long timestamp = System.currentTimeMillis();
String formattedMessage = String.format("[Timestamp: %d, Thread Name: %s, Thread ID: %d] %s", timestamp, threadName, threadId, message);
if (logger.isErrorEnabled()) {
logger.error(formattedMessage, t);
}
}
}
해당 쓰레드 정보가 보인다는 것을 알 수 있다
동기화와 락
synchronized
키워드와 ReentrantLock
을 사용하여 문제를 해결하려 시도하였으나 문제는 계속 발생하였다.
synchronized 키워드 사용
@Transactional(isolation = Isolation.REPEATABLE_READ)
public MessageDto updateAnswer(AnswerRequestDto requestDto,Long answerId, User user) {
try{
PerformanceLoggingUtil.logPerformanceInfo(AnswerService.class, "설문지 응답 업데이트 시작");
Answer answer;
synchronized (answerRepository) { // 해당 로직 블록을 동시에 하나의 스레드만 실행할 수 있게 한다.
answer = answerRepository.findById(answerId).orElseThrow(() -> new NullPointerException("예외가 발생하였습니다."));
}
// Answer answer = answerRepository.findByIdForUpdate(answerId)
// .orElseThrow(() -> new IllegalArgumentException("해당 ID에 대한 답변을 찾을 수 없습니다."));
if (!answer.getUser().getUserId().equals(user.getUserId())){
throw new IllegalArgumentException("예외가 발생하였습니다.");
} // 사용자가 응답자가 아닐 시 에러 출력
if(answer.getSurvey().getMaxChoice() < requestDto.getAnswer()){
throw new IllegalArgumentException("예외가 발생하였습니다.");
} // 선택지에 없는 응답으로 변경 시 에러 출력
answer.update(requestDto.getAnswer());
MessageDto message = new MessageDto("수정이 완료되었습니다.");
PerformanceLoggingUtil.logPerformanceInfo(AnswerService.class, "설문지 응답 업데이트 완료");
return message;
}
catch(Exception e){
PerformanceLoggingUtil.logPerformanceError(AnswerService.class, "설문지 응답 업데이트 중 오류 발생", e);
throw e;
}
}
Isolation.REPEATABLE_READ
대신 Isolation.SERIALIZABLE
을 사용하여 트랜잭션 격리 수준을 더 높게 설정할 수 있다.@Transactional(isolation = Isolation.SERIALIZABLE)
SERIALIZABLE
수준은 성능에 영향을 줄 수 있다.Isolation.SERIALIZABLE
로 변경하여 동시성 문제를 해결하려고 시도하였으나 해결되지 않았다.이를 이용해서 에러 줄이는 걸 시도해 보겠다
@Service
@RequiredArgsConstructor
public class AnswerService {
private final AnswerRepository answerRepository;
private final SurveyRepository surveyRepository;
private final Semaphore semaphore = new Semaphore(3); // 동시에 3개의 스레드만 허용
public MessageDto createAnswer(AnswerRequestDto requestDto, User user) throws InterruptedException {
semaphore.acquire(); // Semaphore 획득
try {
Survey survey = surveyRepository.findById(requestDto.getSurveyId()).orElseThrow(() -> new NullPointerException("예외가 발생하였습니다."));
if (answerRepository.findByUserAndSurvey(user, survey).isPresent()) {
throw new IllegalArgumentException("예외가 발생하였습니다.");
} // 이미 선택한 설문지를 중복 응답 시 에러 출력
if (survey.getMaxChoice() < requestDto.getAnswer()) {
throw new IllegalArgumentException("예외가 발생하였습니다.");
} // 선택지에 없는 응답 시 에러 출력
Answer answer = new Answer(requestDto.getAnswer(), user, survey);
if (survey.getDeadline().isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("예외가 발생하였습니다.");
}
Answer savedAnswer = answerRepository.save(answer);
MessageDto message = new MessageDto("작성이 완료되었습니다.");
return message;
} finally {
semaphore.release(); // Semaphore 해제
}
}
동시 접근 가능한 스레드를 3개로 설정 했을때 문제가 발생한다 여전히
그럼 1개로 수정해 보겠다
1개로 수정해도 여전히 오류가 발생한다
Locking
정의
Java의 java.util.concurrent.locks
패키지에 있는 Lock
인터페이스와 구현체들을 사용하여 명시적으로 락을 관리하는 방법이다.
방법
ReentrantLock
등의 구현체를 사용하여 락을 얻고 해제한다고 한다.
private final Lock lock = new ReentrantLock();
public MessageDto createAnswer(AnswerRequestDto requestDto, User user) {
lock.lock();
try {
Survey survey = surveyRepository.findById(requestDto.getSurveyId()).orElseThrow(() -> new NullPointerException("예외가 발생하였습니다."));
if (answerRepository.findByUserAndSurvey(user, survey).isPresent()) {
throw new IllegalArgumentException("예외가 발생하였습니다.");
} // 이미 선택한 설문지를 중복 응답 시 에러 출력
if (survey.getMaxChoice() < requestDto.getAnswer()) {
throw new IllegalArgumentException("예외가 발생하였습니다.");
} // 선택지에 없는 응답 시 에러 출력
Answer answer = new Answer(requestDto.getAnswer(), user, survey);
if (survey.getDeadline().isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("예외가 발생하였습니다.");
}
Answer savedAnswer = answerRepository.save(answer);
MessageDto message = new MessageDto("작성이 완료되었습니다.");
return message;
} finally {
lock.unlock();
}
}
주의점
항상 finally
블록에서 락을 해제해야 한다.
여전히 오류는 잡히지 않는다.
@Override
public String toString() {
return "Answer{" +
"answerId=" + answerId +
", answerNum=" + answerNum +
", survey=" + (survey != null ? survey.getId() : "null") +
", user=" + (user != null ? user.getId() : "null") +
", version=" + version +
'}';
}
단일 스레드에서는 create -> update로 정상적으로 요청이 실행되지만
다중 스레드에서는 서버가 정상적으로 작동하지 않아서 순서가 보장 되지 않는다는 것을 알 수 있다.
=> constant Timer를 이용하면 순서 보장이 가능하다
하지만 여전히 문제가 발생한다.
다중 스레드 환경에서 쓰레드간에 병렬 처리로
처리가 완료 되는 순서가 보장되지 않기 때문이다.
스레드가 1, 2, 3, 4, 5로 시작 되더라도
DB에 저장이 완료되는 시간이 항상 1,2,3,4,5로 되지는 않는다
즉
스레드는 1, 3, 2, 4, 5 형태로 완료 되면 DB에서 각 요청에 대해
1, 3, 2, 4, 5 형태로 저장된다.
하지만 지금은 csv 파일을 읽어 업데이트할 survey Id를 가지고 오는데
이는 1, 2, 3, 4, 5로 고정 되어 있기에 오류가 발생한 것이다.
그래서 요청간의 순서를 보장하더라도 여전히 오류가 발생한다.
스레드의 문제가 아니라 로직의 문제였다. 다중 스레드 환경에서는 서버가 정상적으로 작동하지 않아 요청 처리 순서가 보장되지 않았다. 이로 인해 데이터의 일관성 문제가 발생하였다.
응답 형태 변경
기존 메시지 반환 방식에서 Entity Dto로 바꾸어서 반환하였다.
이전 요청의 결과를 다음 요청에 반영
JMeter의 Json Extractor를 사용하여 이전 요청의 결과를 다음 요청에 활용하였다.
JMeter에서 이전 요청 결과를 다음 요청에 적용하기
스레드 조절
요청 처리 순서를 보장하기 위해 JMeter의 스레드 설정을 조절하였다.
멀티 스레드 환경에서 발생하는 문제를 해결하기 위해서는 동시성 문제뿐만 아니라 로직의 문제도 함께 고려해야 한다. 이번 문제에서는 로직의 문제가 주 원인이었으며, 이를 해결하여 문제를 해결할 수 있었다.