
기술 면접에서 종종 동시성에 대해 설명해주세요 라는 질문을 들을 수 있다. 그리고, 실제 기능을 구현하면서 동시성을 제어해야되는 상황(ex) 좋아요, 조회수)이 놓일 때가 있다. 머릿속으로는 어렴풋이 알고 있으나 제대로 설명을 하지 못하는 나를 위해 이 글을 작성함으로써 정리할 수 있도록 하고자 한다.
동시성은 멀티 스레드 환경에서 작업이 동시에 실행되고 있는 것처럼 보이는 것이다. 이것을 이해하기 위해선 우선 프로세스와 스레드에 대해 알아야한다.

실제로 실행되고 있는 프로그램을 의미한다.
우리가 흔히 바탕화면에서 자주 볼 수 있는 프로그램은 실행되지 않는 정적인 상태이다. 이를 더블클릭 하여 프로그램이 실행되면, 운영체제는 프로그램에 메모리 공간을 할당하고, 이를 '프로세스'라고 부른다. 각 프로세스는 독자적인 영역을 가지고 있으며 여기에 스택, 힙, 코드 등이 포함된다.
윈도우 환경에서 작업관리자를 실행하면 보이는 프로세스가 바로 위에서 설명한 프로세스다.
그렇다면 스레드는 무엇일까?

프로세스 내부에서 실행되는 독립적인 작업의 단위다.
스레드는 프로세스 내부에 존재하며, 프로세스의 자원을 공유하면서 병렬적으로 작업을 수행하는 독립적인 실행 단위다.
하나의 프로세스 내에는 여러 개의 스레드가 존재할 수 있으며, 이들은 프로세스가 가진 메모리 공간(힙 영역)을 공유하게 된다. 그러나 각 스레드는 독립적인 스택 공간을 가지고 있어, 자신만의 실행 흐름을 관리한다.
스레드는 프로세스처럼 완전히 독립적인 구조가 아니라 일부 공간을 공유하며 사용하기 때문에 스레드간 데이터 교환이 쉬워 리소르를 덜 잡아먹는다.
하지만, 일부 메모리 공간을 공유하기 때문에 각 스레드가 리소스를 점유하려는 현상이 발생하여 스레드가 무한정 대기하는 DeadLock(교착상태)에 빠질 수 있다.
이제 프로세스와 스레드에 대해 어느정도 알게되었다. 그렇다면 동시성은 무엇일까?
싱글 코어에서 멀티 스레드를 동작시키기 위한 방법으로,
작업이 동시에 실행되고 있는 것처럼 보이는 것이다.
유저 A와 유저 B가 게시판에 좋아요를 누른다고 가정해보자. 이 둘이 동시에 좋아요를 누른다면 좋아요 수는 2가 증가되어야 정상일 것이다. 하지만, 별도의 동시성 처리를 하지 않았다면 실제로 증가하는 것은 2가 아닌 1이 증가한다. 이와 같은 상황을 경쟁조건이라고 한다.
경쟁조건
두 개 이상의 프로세스나 스레드가 공유 자원에 동시에 접근하려고 할 때 발생하는 상황을 의미한다. 이 경우, 프로세스나 스레드의 실행 순서나 타이밍에 따라 결과가 달라질 수 있다.
자바에서 이러한 동시성을 다루기 위해선 어떠한 방법이 있을까?
하나하나씩 알아보도록 하자.
성하님의 글 - DB 락이란?
락에 관한 자세한 사항은 해당 글에서 자세히 설명되어있다.
synchronized 키워드를 통해 하나의 스레드에서만 접근이 가능하도록 Lock을 걸어버리는 방식이다.
간단하게 문제가 되는 메소드, 변수 등에 synchronized 키워드를 걸면 된다. synchronized 키워드는 메소드가 끝날 때까지 동기화를 보장한다.
하지만 @Transactional과 같이 사용할 경우 문제가 해결되지 않아 @Transactional을 지워줘야 한다. 그리고, 하나의 프로세스에서만 동기화를 보장하기 때문에 MSA같은 다중 서버 환경에서는 좋은 선택지는 아니다.
왜 Transactional을 지워야할까?
Transactional이 붙은 메소드는 해당 메소드의 호출이 완전히 종료되어야commit이 되거나rollback이 이루어진다. 즉, 메소드가 끝나고 트랜잭션이 커밋되는 사이의 시간, 다른 스레드가 그 메소드를 호출하고 실행할 수 있다. 그 사이에 조회된 사항은 아직 변경 전에 대한 정보이므로 여전히 동시성 문제가 발생할 수 있다.
synchornized 키워드는 문제점이 존재하여 완전히 동시성 문제를 해결할 수 없다.
따라서,DB에서도 해결할 수 있는 방법들이 있는데 다음과 같다.
하나하나씩 알아보도록 하자.
자원 요청에 따른 동시성문제가 발생할것이라고 예상하고 락을 걸어버리는 방법론이다. Spring DATA JPA를 사용한다면 Respositroy를 사용하는 곳에 @Lock 어노테이션을 붙이면 된다.
@Repository
public interface PerformanceRepository extends JpaRepository<Performance, UUID> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Page<Performance> findByIsReserve(String isReserve, Pageable pageable);
}
비관적 락은 하나의 트랜잭션이 자원에 접근시 락을 걸고, 다른 트랜잭션이 접근하지 못하게 하여 조회 또는 갱신 처리가 완료될 때까지 락을 유지한다.
데이터베이스에서 Shared Lock(공유, 읽기 잠금) 이나 Exclusive Lock(배타, 쓰기 잠금) 을 걸 수 있다.
Shared Lock
다른 트랜잭션에서 읽기만 가능. 또한Exclusive lock적용이 불가능 = 읽는동안 변경하는것을 막기 위함
Exclusive lock
다른 트랜잭션에서 읽기, 쓰기가 둘다 불가능. 또한 Shared, Exclusive Lock 적용이 추가적으로 불가능 = 쓰는동안 읽거나, 다른 쓰기가 오는것을 막기 위함
정리하자면
장점
단점
데드락이 일어날 가능성이 있다.하지만, 조건을 넣어주면 무작정 대기하지 않고 대기시간을 주거나 바로 예외처리가 가능하다.
NO WIAT
잠금을 획득하지 못하면 바로 예외를 발생시킨다.
WAIT [n]
n초 이후에 예외를 발생시켜다른 사용자에 의해 변경중이므로 다시 시도하십시오와 같은 예외 처리를 할 수 있다.
자원에 락을 걸어서 선점하지말고, 동시성 문제가 발생하면 그때 가서 처리 하자는 방법론이다.
그러므로 데이터를 수정하고자 하는 시점에 앞서 반드시 읽은데이터가 다른 사용자에 의해 변경 되었는지를 검사해야 한다.
일반적으로 version의 상태를 보고 충돌을 확인하며, 충돌이 확인된경우 롤백을 진행시킨다.
하지만, 데이터베이스에서 동시성을 처리하는것이 아닌, 어플리케이션에서 처리하기 때문에 여러 작업이 묶인 트랜잭션으로 요청에서 에러가 발생하면, 개발자가 직접 롤백 처리를 해주어야 한다.
사용 방법은 비관적 락과 비슷하다.
Repository를 사용하는 곳에서 Lock 관련 어노테이션을 다음과 같이 선언해주면 된다.
@Repository
public interface PerformanceRepository extends JpaRepository<Performance, UUID> {
@Lock(LockModeType.OPTIMISTIC)
Page<Performance> findByIsReserve(String isReserve, Pageable pageable);
}
여기서 Lock 어노테이션 뿐만 아니라 한 가지 설정을 더 해줘야 한다.
낙관적 락은 버전을 비교하여 동시성을 처리한다고 했는데, 해당 Version 컬럼을 수동으로 엔티티에 추가해줘야한다.
컬럼을 추가한 후에 @Version 어노테이션을 선언해주면 된다.
public class Alarm extends Time {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Comment("알람 Id")
private Long id;
@Version
private Long version;
이렇게 낙관적 락을 설정한 상태에서 동시성 요청이 들어오면 버전이 맞지 않아 에러를 발생시킨다.
하지만 예외가 발생하면 서비스가 멈추는 것이 아니라 다시 재조회를 하여 버전을 업데이트하고 로직을 수행하는 것을 원하기 때문에 적절한 수정이 필요하다.
그러므로 반복문과 try-catch를 조합하여 로직이 성공할 때까지 수행하도록 다음과 같이 구현해줘야 한다.
public void increase(Long id, Long quantity) throws InterruptedException {
while (true) {
try {
optimisticLockStockService.decrease(id, quantity);
break;
} catch (Exception e) {
Thread.sleep(50);
}
}
}
정리하자면
장점
단점
싱글 스레드 활용의 경우 redis와 관련하여 설명할 것이기 때문에 다음 글에서 이어서 작성하도록 하겠다.