반응형

멀티스레드 문제 예시 - 은행 계좌 잔액 갱신 문제

class BankAccount {
    private int balance = 100;

    public void withdraw(int amount) {
        if (balance >= amount) { // 조건 검사
            balance -= amount;  // 잔액 갱신
        }
    }
}

의도된 동작

  1. 스레드 A가 50을 출금.
  2. 스레드 B가 50을 출금.
  3. 결과적으로 잔액은 0이 되어야 함.

문제 상황

  • 스레드 A와 스레드 B가 동시에 withdraw(50) 메서드를 호출하면, 잔액 조건 검사와 갱신 작업이 중첩될 수 있음.
  • 이로 인해 두 스레드가 잔액 조건을 동시에 검사한 후, 동시에 출금을 진행.
  • 결과적으로, 잔액이 50으로 잘못 계산됨.

 

**정상적인 단일 스레드 실행**

-------
스레드 A 실행:
[검사] 잔액: 100 >= 50  -> True
[갱신] 잔액 -= 50       -> 잔액: 50

**멀티스레드에서의 충돌**
스레드 A 실행:                          스레드 B 실행:
[검사] 잔액: 100 >= 50  -> True        [검사] 잔액: 100 >= 50  -> True
[갱신] 잔액 -= 50       -> 잔액: 50    [갱신] 잔액 -= 50       -> 잔액: 50

결과: 두 스레드가 모두 출금을 완료했지만, 최종 잔액은 50으로 계산됨 (잘못된 결과).

멀티스레드 환경의 위험 요소

  1. Race Condition (경쟁 상태)
    • 여러 스레드가 동시에 동일한 자원에 접근하고 이를 수정할 때, 실행 순서에 따라 결과가 달라질 수 있음.
    • 예: 위의 은행 계좌 예제에서 스레드 간 동기화가 없을 경우 발생.
  2. Deadlock (교착 상태)
    • 두 스레드가 서로 자원을 기다리며 무한히 대기하는 상황.
    • 예: 스레드 A가 자원 1을 점유하고 자원 2를 기다리는 동안, 스레드 B는 자원 2를 점유하고 자원 1을 기다림.
  3. Data Corruption (데이터 손상)
    • 여러 스레드가 동일한 메모리 위치를 수정하면, 결과값이 예기치 않게 손상될 수 있음.
    • 예: 공유 데이터 구조의 상태가 불완전하거나 잘못된 상태로 유지됨.
  4. Thread Interleaving (스레드 중첩 실행)
    • 스레드가 실행 중에 문맥 교환(Context Switching)으로 인해 실행 흐름이 중첩됨.
    • 예: 스레드 A와 B가 교차 실행되며 연산이 중간 상태에서 중단됨.

해결 방법

1. 동기화 사용

  • synchronized 키워드: 공유 자원에 한 번에 하나의 스레드만 접근하도록 보장.
  • ReentrantLock: 더 세밀한 락 제어 가능.
public synchronized void withdraw(int amount) {
    if (balance >= amount) {
        balance -= amount;
    }
}

2. 원자적 연산 사용

  • Java의 AtomicInteger 또는 AtomicLong과 같은 클래스 사용.
  • 내부적으로 CAS(Compare-And-Swap)를 활용하여 동기화 문제를 해결.
import java.util.concurrent.atomic.AtomicInteger;

class BankAccount {
    private AtomicInteger balance = new AtomicInteger(100);

    public void withdraw(int amount) {
        balance.addAndGet(-amount); // 원자적 연산
    }
}

3. 동시성 제어 도구

  • CountDownLatch, Semaphore, CyclicBarrier 등 사용.
  • 특정 조건에서 스레드의 실행을 제어하여 동기화 문제를 방지.
반응형
반응형

AtomicInteger.class

Java에서 사용하는 클래스로 멀티 스레드 환경에서 안전하게 사용되기 위한 원자성을 보장받는 자료형이다

클래스의 주요 기능 및 특징

  • 원자적 연산
    • 다른 스레드가 해당 연산의 중간 상태를 볼 수 없음
  • 스레드 안전
    • 여러 스레드에서 동시에 접근하더라도 데이터 경합 없이 안전하게 값을 읽고 쓸 수 있음
  • 락 없이 동작
    • 기존의 동기화 방식(synchronized 키워드)은 락을 사용하지만, 아토믹 연산은 CPU가 제공하는 하드웨어 수준의 락-프리(Lock-Free) 매커니즘을 사용
  • CAS 알고리즘
    내부적으로 CAS(Compare-And-Swap) 알고리즘을 사용하여 값을 업데이트
    • 특정 메모리 위치 값이 예상값과 같은지 확인한 후, 같다면 새 값으로 교체
    • 이 과정에서 충돌을 감지

주요 메서드

  • get()
  • set(int newValue)
  • getAndIncrement()
    • 현재 값 반환 후 1 증가
  • incrementAndGet()
    • 값을 1 증가시키고 증가된 값 반환
  • compareAndSet(int expect, int update)
    • 현재 값이 expect와 같으면 update 값으로 변경 (CAS 연산)

사용 고려 시점

  • 동시성 프로그래밍에서 데이터를 안전하게 업데이트해야 하는 경우
    • 멀티 스레드 환경에서 카운터 증가
    • 통계 집계
  • 락 기반 동기화보다 가벼운 대안
  • 성능이 중요한 환경에서 데이터 무결성을 유지해야 할 때

❓CAS (Compare-And-Swap)

CAS는 값의 업데이트가 특정 조건에서만 이루어지도록 보장하는 하드웨어 지원 원자적 연산
따라서 자바에만 있는 것이 아니라, C 계열의 언어에서도 std::atmoic 클래스와 같이 사용되고 있음

CAS 작동 방식

  • 특정 메모리 위치에 대해, 세 값을 비교 및 업데이트
    • 현재 값 (current): 현재 메모리에 저장된 값
    • 기대 값 (expected): 우리가 예상하는 값
    • 새 값 (new): 우리가 저장하려는 값
  • CAS 연산의 과정
    • 메모리의 현재 값이 기대 값(expected)과 동일하면, 새 값으로 업데이트
    • 메모리의 현재 값이 기대 값과 다르면 실패를 반환

CAS 장점

  • 스케일링 가능성: 여러 스레드가 동시에 접근해도 성능 저하가 적다.
  • 스레드가 블로킹되지 않으므로 데드락 위험성이 없다

CAS 단점

  • Busy-waiting:
    • 실패할 경우 재시도하는 루프를 사용하므로 CPU 자원을 소비할 수 있다.
    • 특히 충돌이 빈번하면 성능 저하가 발생할 수 있다.
  • ABA 문제:
    • 메모리 값이 A -> B -> A로 변경된 경우, CAS는 값이 바뀌지 않았다고 판단할 수 있다.
    • 이 문제는 태그(tag) 또는 버전 관리를 통해 해결

코드 예시 - 과정만 참고

public final int incrementAndGet() {
    for (;;) {
        int current = get(); // 현재 값 읽기
        int next = current + 1; // 새로운 값 계산
        if (compareAndSet(current, next)) // CAS 연산
            return next; // 성공 시 반환
    }
}

CPU의 원자적 명령어

CPU는 CAS 연산을 지원하기 위해 특정 명령어(기계어)를 제공

  • x86 아키텍처 - CMPXCHG(Compare and Exchange)
  • ARM 아키텍처 - LDREX/STREX

메모리 값을 읽고 수정하는 작업을 한 번에 처리하므로, synchronized 키워드 없이 안전하게 업데이트


반응형

'호기심 천국 > Java' 카테고리의 다른 글

멀티스레드 문제 상황 예시 1  (1) 2025.01.20
StringBuilder vs String 문자열합치기  (0) 2023.09.19
반응형
  • 컨트롤러 테스트에서 사용한 기법

    • Mocking을 통해 Controller 단위 테스트 시행
    • 통합테스트가 아닌 단위 테스트를 진행할 때는 어노테이션에 주의해야합니다.
    • 이번 테스트에 사용된 UserControllerTest.java 파일에서 사용된 WebMvcTest를 명시해 사용하면, 컨트롤러 레이어의 단위테스트 형태를 갖출 수 있습니다
  • Mocking 기법 예시

    • Mocking을 통해 Controller 레이어의 단위테스트가 목적
    • 필요없는 내부동작을 임의로 동작한 것으로 치고 반환
    • Controller에서 필요없는 로직인 UserService안의 로직을 Mocking하여, Controller 레이어에서 사용되는 Request, Response, Valid 등을 테스트할 수 있음

@Slf4j
@WebMvcTest(UserController.class) // WebMvcTest는 컨트롤러 레이어만 테스트하기 위해 스프링의 WebMvc 관련 빈만 로드
@MockBean(JpaMetamodelMappingContext.class) // jpa동작하지않도록
@AutoConfigureMockMvc(addFilters = false) // 시큐리티 필터 제외
@ActiveProfiles("test")
class UserControllerTest {
    ...... 중략
    @Autowired
    private MockMvc mockMvc; //실제 서블릿 컨테이너 없이도 HTTP 요청과 응답을 테스트하기 위해 사용

    @Autowired
    private ObjectMapper objectMapper; // Response 매핑을 위한 객체

    @MockBean
    private UserService userService; // @MockBean을 활용하여 UserService를 Mocking


    private final User mockUser = User.builder()
                .id(UUID.randomUUID())
                .email("mockUser@naver.com")
                .nickname("mockUser")
                .password("abcd1234!")
                .isMarketing(true)
                .isAlarm(true)
                .state(State.ACTIVE)
                .build();

    @Test
    @DisplayName("회원가입 성공 response 테스트")
    void testCreateUserSuccess() throws Exception {
    CreateUserReq createUserReq = CreateUserReq.testCreate(); // 임의의 UserReq 생성

    /** CreateUserReq 타입에 맞는 객체가 UserService.create에 들어가면 mockUser를 반환하도록
    *    given 메서드는 BDD 스타일 테스트에서 사용되는 Mockito의 메서드
    *    mocking객체(UserService)안의 파라미터에 any(), eq() 두 메서드를 통해 값을 넣어줄 수 있고
    *    any(CreateUserReq.class) 선언 시 CreateUserReq 타입이 userService.create() 안에 들어가야 모킹된 mockUser가 반환된다
    **/
    given(userService.create(any(CreateUserReq.class))).willReturn(mockUser);

    /**
    *  "/users"의 위치로 헤더에 MediaType 타입에 APPLICATION_JSON, body에 createUserReq의 값을 담아 요청
    *  andExpect() 안에서 값이 어떻게 반환되는지 확인 현재 예시에서는 201반환하는지 확인
    **/
    mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createUserReq)))
            .andExpect(status().isCreated());
    ...... 중략
}

❗구현이슈

  • @AutoConfigureMockMvc(addFilters = false) // 시큐리티 필터 제외
  • 임의의 시큐리티 필터를 생성했을 시엔, 해당 어노테이션을 넣어줘야 한다.
  • 넣어주지 않을 시 실제 시큐리티 필터를 타서 문제 발생
반응형

'LTF(learn through failure) > Spring' 카테고리의 다른 글

SQL Injection  (0) 2023.09.30
스프링부트 Lombok @RequiredArgsConstructor잘 알고쓰자  (0) 2023.07.26

+ Recent posts