반응형

동시성 문제를 해결하는 4가지 방법

안녕하세요, 오늘은 멀티스레드 환경에서 발생하는 동시성 문제를 해결하는 네 가지 방법에 대해 이야기해보려고 합니다. 동시성 문제는 여러 스레드가 동시에 같은 자원에 접근하면서 데이터 무결성이 깨질 수 있는 상황을 말하는데요. 이를 해결하기 위해 자바와 Spring Boot에서 사용할 수 있는 다양한 접근법을 예시와 함께 알아보겠습니다.

1. 자바 synchronized 키워드

가장 기본적인 동시성 제어 방법 중 하나는 자바의 synchronized 키워드를 사용하는 것입니다. 이 방식은 특정 코드 블록이나 메서드에 대해 한 번에 하나의 스레드만 접근할 수 있도록 잠금을 걸어줍니다.

언제 사용하나요?

  • 간단한 동기화가 필요한 경우
  • 객체 단위로 락을 걸어 데이터 무결성을 지키고 싶을 때

Spring Boot 예시

재고 관리 시스템에서 동시에 재고를 감소시키는 상황을 생각해봅시다.

@Service
public class StockService {
    private int stock = 100;

    public synchronized void decreaseStock(int quantity) {
        if (stock >= quantity) {
            stock -= quantity;
            System.out.println("재고 감소! 남은 재고: " + stock);
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}
 

위 코드에서 synchronized를 사용하면 여러 스레드가 동시에 decreaseStock 메서드에 접근하더라도 순차적으로 실행됩니다. 단점은 성능이 중요한 대규모 시스템에서는 병목 현상이 발생할 수 있다는 점입니다.


2. 낙관적 락 (Optimistic Lock)

낙관적 락은 "충돌이 별로 없을 거야!"라는 낙관적인 가정을 기반으로 동시성을 제어합니다. 데이터를 수정할 때 충돌 여부를 버전 번호나 타임스탬프를 통해 체크하고, 충돌이 발생하면 예외를 던져서 처리합니다.

언제 사용하나요?

  • 충돌 가능성이 낮은 환경
  • 성능 최적화가 중요한 경우

Spring Boot 예시

JPA를 사용해 낙관적 락을 구현해보겠습니다. @Version 어노테이션을 활용하면 됩니다.

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int stock;

    @Version
    private int version; // 버전 필드 추가
}

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void decreaseStock(Long productId, int quantity) {
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("상품 없음"));
        if (product.getStock() >= quantity) {
            product.setStock(product.getStock() - quantity);
            productRepository.save(product);
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}
 

위 코드에서 트랜잭션이 끝날 때 JPA가 버전을 체크합니다. 다른 스레드가 먼저 수정해서 버전이 달라지면 OptimisticLockException이 발생하고, 이를 통해 충돌을 감지합니다. 성능이 비관적 락보다 좋지만, 충돌이 자주 발생하면 롤백 처리가 늘어날 수 있어요.


3. 비관적 락 (Pessimistic Lock)

비관적 락은 "충돌이 자주 발생할 거야!"라는 비관적인 가정을 하고, 아예 트랜잭션 시작 시점에 데이터베이스에 락을 겁니다. 다른 스레드는 락이 해제될 때까지 기다려야 합니다.

언제 사용하나요?

  • 충돌 가능성이 높은 환경
  • 데이터 무결성이 매우 중요한 경우 (예: 금융 시스템)

Spring Boot 예시

JPA에서 @Lock 어노테이션을 사용해 비관적 락을 설정할 수 있습니다.

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdWithPessimisticLock(@Param("id") Long id);
}

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void decreaseStock(Long productId, int quantity) {
        Product product = productRepository.findByIdWithPessimisticLock(productId)
                .orElseThrow(() -> new RuntimeException("상품 없음"));
        if (product.getStock() >= quantity) {
            product.setStock(product.getStock() - quantity);
            productRepository.save(product);
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}

 

PESSIMISTIC_WRITE 락을 걸면 해당 레코드는 트랜잭션이 끝날 때까지 다른 스레드가 수정할 수 없습니다. 성능은 낙관적 락보다 떨어지지만, 중요한 데이터의 정확성을 보장할 수 있어요.


4. 캐시 서버 도입

마지막으로, 캐시 서버(예: Redis)를 활용하는 방법입니다. 이건 돈이 좀 들 수 있는 솔루션이지만, 동시성 문제를 근본적으로 줄이고 성능을 크게 향상시킬 수 있습니다. 캐시는 자주 조회되는 데이터를 메모리에 저장해 DB 부하를 줄여줍니다.

언제 사용하나요?

  • 읽기 요청이 많은 시스템
  • DB 부하를 줄이고 응답 속도를 높이고 싶을 때

Spring Boot 예시

Redis를 사용해 재고를 캐싱하고 동시성을 제어해보겠습니다.

@Service
public class StockService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String STOCK_KEY = "stock:product:1";

    public void decreaseStock(int quantity) {
        String stockStr = redisTemplate.opsForValue().get(STOCK_KEY);
        int stock = stockStr != null ? Integer.parseInt(stockStr) : 100;

        if (stock >= quantity) {
            redisTemplate.opsForValue().set(STOCK_KEY, String.valueOf(stock - quantity));
            System.out.println("재고 감소! 남은 재고: " + (stock - quantity));
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}
 

여기서는 단순히 Redis에 재고를 저장했지만, 실제로는 Redis의 SETNX(Set if Not Exists) 같은 명령어나 분산 락(Distributed Lock)을 활용해 동시성을 더 철저히 제어할 수 있습니다. 비용이 들지만 확장성과 성능 면에서 큰 이점이 있어요.


정리: 어떤 방법을 선택할까?

  • synchronized: 간단하고 비용 없음, 소규모 시스템에 적합
  • 낙관적 락: 충돌이 적을 때 성능 좋음, 무료
  • 비관적 락: 충돌이 많고 데이터 무결성이 중요할 때, 무료지만 성능 저하 주의
  • 캐시 서버: 돈은 들지만 성능과 확장성 극대화

프로젝트의 요구사항과 예산에 따라 적절한 방법을 선택하면 됩니다. 예를 들어, 간단한 앱이라면 synchronized나 낙관적 락으로 충분할 수 있고, 대규모 트래픽을 감당해야 한다면 Redis 같은 캐시 서버를 고려해볼 만해요.

이 글이 동시성 문제 해결에 도움이 되셨길 바랍니다.

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Kafka] Kafka 이용하여 Spring Boot로 메시지 보내고 받기  (0) 2025.02.27
[MSA] OpenFeign  (0) 2025.02.27
[Trace] Jaeger  (0) 2025.02.21
[Log] Log 중앙화  (0) 2025.02.21
[Trace] 핀포인트(Pinpoint)  (0) 2025.02.21
반응형

백엔드란?

  • 프론트엔드는 폭넓은 의미로는 사용자가 서비스를 이용할 때 쓰게 되는 웹, 앱 등을 의미
  • 백엔드는 서비스에 필요한 모든 데이터를 저장하고 다루는 공간

  • 많이 사용하는 서비스들을 이용한 예
    • 네이버 - 회원가입한 유저들 데이터, 웹툰 데이터들, 카페 데이터들, 블로그 데이터들 등 수많은 데이터
    • 카카오톡 - 유저 데이터, 유저가 들어간 채팅방 데이터, 유저의 친구들 데이터 등 서비스에 필요한 많은 데이터
  • 백엔드는 서비스에 필요한 데이터들을 저장하고 클라이언트(사용자, 관리자 등)에게 알맞게 데이터를 가공하는 역할

  • 데이터를 가공한다는 것은 원형의 데이터들을 목적에 맞게 가공하는 걸 의미
    • 데이터들을 바탕으로 목적에 맞게 데이터들을 끌어와서 필요없는 건 빼고 필요한 건 살리는 작업

 

백엔드의 구성

  • 프론트엔드는 보통 웹, 앱 등 특정한 프로그램을 하나 개발하면 되는 반면 백엔드는 여러가지 서버 프로그램들이 유기적으로 연결되어 있음

 

데이터베이스

  • 데이터베이스는 서비스에 필요한 데이터를 저장하는 역할
    • 온라인 클래스를 제공하는 서비스라면 데이터 중에 클래스 관련 정보들을 저장하는 데이터베이스가 존재
  • 데이터베이스도 데이터를 제공하는 서버이므로 즉, 하나의 서버 프로그램이고 운영체제(OS) 위에서 동작함

  • 데이터베이스는 서비스에 필요한 중요한 정보들이 전부 들어으므로 철저하게 보안 유지 필요
    • IT 회사에서 데이터베이스가 해킹 당하는 경우, 회사의 모든 중요한 정보가 유출 됨
    • 데이터베이스에서 직접 정보를 가져올 수 있는 주체 제한 필요

 

API 서버

  • 기본적으로 데이터베이스는 웹, 앱을 사용하는 클라이언트는 접근을 막는 대신에 클라이언트에게 대신 데이터를 전달해주는 API 서버에게는 연결을 허용해줌
    • 보통 데이터베이스는 외부 클라이언트의 네트워크 연결을 하지 못하도록 방화벽을 설정
    • 보안을 위해서 데이터베이스에 접근할 수 있는 건 API 서버(WAS 서버), 서비스 관리자 등으로 한정함
    • 이 때 보통 특정 IP주소를 허용하는 방식으로 데이터베이스 연결을 관리

  • 위 사진처럼 API 서버는 데이터베이스의 데이터들을 가공해서 클라이언트에게 전달하는 역할을 함

  • API 서버에는 다양한 API 요청을 처리하도록 개발됨
    • 위 사진처럼 로그인, 상품 정보 받아오기 등 다양한 요청을 처리할 수 있도록 백엔드 개발자가 API 서버 개발을 함
  • 프론트엔드(웹, 앱)에게 데이터를 제공하는 API 서버를 WAS(Web Application Server)라고 하며, API 서버가 조금 더 넓은 개념이고 WAS는 프론트엔드에게 데이터를 건네주는 서버를 한정지어서 얘기함

 

파일 스토리지

  • 스토리지 서버: 이미지 등의 파일들을 전문적으로 저장하는 서버로 파일들을 저장한 후 URL 주소를 통해 다운을 받는 방식
    • 대표적으로 우리가 사용하는 웹에서 이미지의 경우 웹 서버, API 서버를 통해 URL 주소를 전달받고 URL 주소를 통해 파일 스토리지에 접근해서 이미지를 다운받고 보여줌(이미지 자체를 받기에는 용량이 큼)
    • 실제로 데이터베이스에도 파일을 저장할 수 있지만 데이터베이스는 서비스에 필요한 핵심 데이터들, 즉 텍스트 위주로 이루어진 내용들을 다루는데 최적화되어 있음

 

캐시

  • 캐시: 자주 쓰이는 정보를 저장해 놓는다는 개념
    • 보통 API 서버에서는 데이터베이스에서 정보를 꺼내고, 가공을 하는 작업을 하는데 이 때 API 서버에서 항상 동일한 결과를 제공해준다면 매번 데이터베이스, API 서버가 작업할 필요가 없음
    • 캐시 서버를 이용하면 API 서버와 데이터베이스가 일을 따로 하지 않고 바로 저장된 데이터를 제공해줄 수 있음

  • 캐시 서버는 사용자와 가까운 곳에 임시 데이터를 캐시 형태로 저장하여 빠르게 제공
    • 정적 컨텐츠에 대해 활용도가 높음

 

Reference

https://www.inflearn.com/course/it-%EA%B0%9C%EB%B0%9C%EC%A7%80%EC%8B%9D#

 

IT 회사에서 비개발자가 살아남기 위한 모든 개발 지식 A to Z - 인프런 | 강의

본 강의는 멤버십 구독 개념으로 한 번 구매하면 계속 추가되는 수업도 수강할 수 있습니다😃, [사진] 이 강의는!  단순히 개발 용어만을 알려주지 않습니다.IT 회사에서 개발자들이 실제로 하

www.inflearn.com

https://www.grabbing.me/IT-A-to-Z-By-1e1fbc981b7c4c03ac44943085ac8304

 

[IT 개발자와 일할 때 필요한 모든 개발지식] A to Z 자료 모음집 By 그랩

장담하건대 이 내용들만 알고 계시면 IT 개발의 전체적인 흐름은 전부 파악한다고 보셔도 무방합니다.

www.grabbing.me

 

반응형

'비개발자의 개발 지식 스터디 > 백엔드' 카테고리의 다른 글

클라우드 서비스(SaaS, PaaS, IaaS)  (0) 2023.07.24
서버 스케일링 & 모니터링  (0) 2023.07.14
클라우드  (0) 2023.07.14

+ Recent posts