package com.sparta.springtrello.domain.ticket.service;
import com.sparta.springtrello.domain.board.entity.Board;
import com.sparta.springtrello.domain.board.repository.BoardRepository;
import com.sparta.springtrello.domain.kanban.entity.Kanban;
import com.sparta.springtrello.domain.kanban.repository.KanbanRepository;
import com.sparta.springtrello.domain.member.entity.Member;
import com.sparta.springtrello.domain.member.entity.MemberRole;
import com.sparta.springtrello.domain.member.repository.MemberRepository;
import com.sparta.springtrello.domain.ticket.dto.TicketRequestDto;
import com.sparta.springtrello.domain.ticket.entity.Ticket;
import com.sparta.springtrello.domain.ticket.repository.TicketRepository;
import com.sparta.springtrello.domain.user.dto.AuthUser;
import com.sparta.springtrello.domain.user.entity.User;
import com.sparta.springtrello.domain.user.enums.UserRole;
import com.sparta.springtrello.domain.user.enums.UserStatus;
import com.sparta.springtrello.domain.user.repository.UserRepository;
import com.sparta.springtrello.domain.workspace.entity.Workspace;
import com.sparta.springtrello.domain.workspace.repository.WorkspaceRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
public class TicketLockTest {
@Autowired
private TicketRepository ticketRepository;
@Autowired
private KanbanRepository kanbanRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
private TicketService ticketService;
@Autowired
private WorkspaceRepository workspaceRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private BoardRepository boardRepository;
@Test
void 비관락() throws InterruptedException {
User user = new User(1L, "email@email.com", UserRole.ROLE_ADMIN);
ReflectionTestUtils.setField(user, "name", "User");
ReflectionTestUtils.setField(user, "password", "Password");
ReflectionTestUtils.setField(user, "status", UserStatus.ACTIVATED);
userRepository.save(user);
Workspace workspace = new Workspace(1L, "title", "content", null);
workspaceRepository.save(workspace);
Member member = new Member(user, workspace, MemberRole.ROLE_WORKSPACE);
ReflectionTestUtils.setField(member, "id", 1L);
memberRepository.save(member);
Board board = new Board(member, workspace, "boardtitle", "background", "image");
ReflectionTestUtils.setField(board, "id", 1L);
boardRepository.save(board);
Kanban kanban = new Kanban(1, "kanbantitle", board);
ReflectionTestUtils.setField(kanban, "id", 1L);
kanbanRepository.save(kanban);
Ticket ticket = new Ticket("tickettitle","ticketcontent","2000-00-00",member,kanban);
ReflectionTestUtils.setField(ticket, "id", 1L);
Ticket savedTicket = ticketRepository.save(ticket);
AuthUser authUser = new AuthUser(1L, "email@email.com", UserRole.ROLE_ADMIN);
Long id = savedTicket.getId();
//when
int testCount = 1000;
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(testCount);
AtomicInteger successfulUpdates = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
for (int i = 0; i < testCount; i++) {
executorService.submit(() -> {
try {
//새로운 데이터를 보내기위한 랜덤문자 덧붙임
TicketRequestDto requestDto = new TicketRequestDto("title" + UUID.randomUUID(),
"UpContents","3000-00-00",1L);
ticketService.updateTicket(authUser, id, requestDto);
successfulUpdates.incrementAndGet();
} catch (Exception e) {
System.out.println("비관적 락 충돌: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await(); // 모든 스레드가 작업을 완료할 때까지 대기
executorService.shutdown();
long endTime = System.currentTimeMillis();
long durationInMillis = endTime - startTime;
double durationInSeconds = durationInMillis / 1000.0;
System.out.println("성공한 업데이트 수: " + successfulUpdates.get());
System.out.println("테스트 실행 시간: " + durationInSeconds + "초");
assertEquals(successfulUpdates.get(), 1000);
}
@Test
void 낙관락() throws InterruptedException {
User user = new User(1L, "email@email.com", UserRole.ROLE_ADMIN);
ReflectionTestUtils.setField(user, "name", "User");
ReflectionTestUtils.setField(user, "password", "Password");
ReflectionTestUtils.setField(user, "status", UserStatus.ACTIVATED);
userRepository.save(user);
Workspace workspace = new Workspace(1L, "title", "content", null);
workspaceRepository.save(workspace);
Member member = new Member(user, workspace, MemberRole.ROLE_WORKSPACE);
ReflectionTestUtils.setField(member, "id", 1L);
memberRepository.save(member);
Board board = new Board(member, workspace, "boardtitle", "background", "image");
ReflectionTestUtils.setField(board, "id", 1L);
boardRepository.save(board);
Kanban kanban = new Kanban(1, "kanbantitle", board);
ReflectionTestUtils.setField(kanban, "id", 1L);
kanbanRepository.save(kanban);
Ticket ticket = new Ticket("tickettitle","ticketcontent","2000-00-00",member,kanban);
ReflectionTestUtils.setField(ticket, "id", 1L);
Ticket savedTicket = ticketRepository.save(ticket);
AuthUser authUser = new AuthUser(1L, "email@email.com", UserRole.ROLE_ADMIN);
Long id = savedTicket.getId();
//when
int testCount = 10000;
ExecutorService executorService = Executors.newFixedThreadPool(20);
CountDownLatch latch = new CountDownLatch(testCount);
// 성공 횟수 추적 변수
AtomicInteger successfulUpdates = new AtomicInteger(0);
// 예외 발생 횟수를 추적하기 위한 변수
AtomicInteger optimisticLockExceptionCount = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
for (int i = 0; i < testCount; i++) {
executorService.submit(() -> {
try {
//새로운 데이터를 보내기위한 랜덤문자 덧붙임
TicketRequestDto requestDto = new TicketRequestDto("title" + UUID.randomUUID(),
"UpContents","3000-00-00",1L);
ticketService.updateTicket(authUser, id, requestDto);
successfulUpdates.incrementAndGet();
} catch (OptimisticLockingFailureException e) {
optimisticLockExceptionCount.incrementAndGet();
System.out.println("낙관적 락 충돌: " + e.getMessage());
}finally {
latch.countDown();
}
});
}
latch.await(); // 모든 스레드가 작업을 완료할 때까지 대기
executorService.shutdown();
long endTime = System.currentTimeMillis();
long durationInMillis = endTime - startTime;
double durationInSeconds = durationInMillis / 1000.0;
System.out.println("성공한 업데이트 수: " + successfulUpdates.get());
System.out.println("실패한 업데이트 수: " + optimisticLockExceptionCount.get());
System.out.println("테스트 실행 시간: " + durationInSeconds + "초");
assertNotNull(successfulUpdates.get());
}
}
Service계층에서 비관락 설정
@Transactional
public TicketResponseDto updateTicket(AuthUser authUser, Long id, TicketRequestDto requestDto) {
//비관락 대상
Ticket ticket = ticketRepository.findByIdWithPessimisticLock(id).orElseThrow(() ->
new HotSixException(ErrorCode.TICKET_NOT_FOUND));
//ticket entity에 등록될 kanban 찾기
Kanban kanban = kanbanRepository.findById(requestDto.getKanbanId()).orElseThrow(() ->
new HotSixException(ErrorCode.KANBAN_NOT_FOUND));
//ticket을 등록하는 멤버 찾기
Member member = memberRepository.findByWorkspaceIdAndUserId(kanban.getBoard().getWorkspace().getId(),authUser.getId())
.orElseThrow(()-> new HotSixException(ErrorCode.USER_NOT_FOUND));
//ticket을 등록하려는 유저의 role이 reader인지 확인
if (member.getMemberRole().equals(MemberRole.ROLE_READER)){
throw new RuntimeException();
}
ticket.update(
requestDto.getTitle(),
requestDto.getContents(),
requestDto.getDeadline(),
kanban
);
return new TicketResponseDto(ticket);
}
Repository계층에서 비관락 설정 : @Lock 어노테이션 입력
public interface TicketRepository extends JpaRepository<Ticket, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select t from Ticket t where t.id = :id")
Optional<Ticket> findByIdWithPessimisticLock(Long id);
}
Entity계층에서 낙관락 설정 : @Version 어노테이션 입력
public class Ticket extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private int version;
}
트러블슈팅