Lock 이란?

하나의 자원에 여러 요청이 들어올 때 데이터의 일관성과 무결성을 보장하기 위해 하나의 커넥션에게만 변경할 수 있는 권한을 주는 기능으로 동시성을 제어하기 위한 목적으로 사용됩니다.  간단한 예시를 보면서 이해해 보도록 하겠습니다.

사용자에게 송금하는 메소드 transfer 가 있습니다. 순서는 다음과 같습니다. 잔고조회 -> 잔고 + amount

 

사용자A사용자B가 동시에 사용자C에게 송금한다고 했을 때, 사용자A, 사용자B는 1000 씩 송금을 했으니 사용자C의 잔고가 4000이 될 것으로 예상했으나 최종잔고는 3000이 되었습니다. 이는 동시성 문제로 데이터의 일관성과 무결성이 보장되지 않아서 생기는 문제입니다. 오늘은 이 문제를 해결하는 Lock 에 대해서 알아보도록 하겠습니다.

 

 

Lock의 종류

1. Shared Lock (공유 락)

  • 읽기 작업을 위한 Lock
  • 여러 트랜잭션이 동시에 데이터를 읽을 수 있음
  • 다른 Shared Lock과는 호환되지만 Exclusive Lock과는 호환되지 않음
    • 읽기 작업 중일 때, 새로운 읽기 작업이 들어온 경우 -> 가능
    • 읽기 작업 중일 때, 새로운 쓰기 작업이 들어온 경우 -> 불가능 (쓰기 작업으로 멱등성 보장x)

2. Exclusive Lock (배타적 락)

  • 쓰기 작업을 위한 Lock
  • 한 번에 하나의 트랜잭션만 데이터를 수정할 수 있음
  • 다른 어떤 Lock과도 호환되지 않음
    • 쓰기 작업 중일 때, 새로운 읽기 작업이 들어온 경우 -> 불가능
    • 쓰기 작업 중일 때, 새로운 쓰기 작업이 들어온 경우 -> 불가능

 

그렇다면 호환되지 않은 Lock의 요청이 들어올 때 불가능하다고 했는데 어떤 상황이 벌어질까?

 

 

Blocking

호환되지 않은 Lock 이 들어올 경우 블로킹(Blocking)이 발생합니다. 새로운 Lock의 요청이 들어올 때, 이미 Lock이 선행되고있으면 선행되고있는 Lock이 종료될떄까지 대기하고있다가 종료가 되면 새로운 Lock을 획득해 진행하게됩니다.

 

위의 예시에 Lock을 적용한다면 순서는 다음과 같습니다.

사용자A -> transfer(사용자A, 사용자C, 1000) -> Exclusive Lock 획득 -> 사용자B -> transfer(사용자B, 사용자C, 1000) -> Blocking (사용자A의 Lock이 해제될 때까지 대기) -> 송금 -> Blocking 해제, Exclusive Lock 획득 -> 송금 

 

위에서 Shared Lock은 여러 Shared Lock과 호환된다고했습니다. 만약 100개의 트랜잭션에서 Shared Lock을 획득하고 있을때 1개의 Exclusive Lock 요청이 들어온다면 100개의 Shared Lock이 모두 해제될때까지 대기하게 됩니다. 또는 끊기지 않고 Shared Lock이 들어온다면 Exclusive Lock은 계속 획득하지 못하게 됩니다. 따라서 Lock을 설정하는 범위를 알맞게 설정하는 것이 중요합니다.

 

 

그 다음으로 두개의 자원에 대한 Lock을 다룰때는 어떤 문제가 생길 수 있을까요?

 

 

Dead Lock

두 개 이상의 자원에서 서로 Blocking 되어 영구적으로 접근할 수 없는 상태(교착상태)입니다.

 

위의 transfer 메소드에서 사용자의 잔고를 줄이는 로직추가하여 예시로 들어보겠습니다. 로직은 다음과 같습니다. 그림그리는 것보다 코드가 더 읽기 쉽고 이해하기 편하니 코드로 작성하겠습니다.

    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // 잔액 확인 및 업데이트를 위해 Exclusive Lock
        User fromUser = userRepository.findByIdWithLock(fromId);

        // 수신자의 계좌에 Exclusive Lock
        User toUser = userRepository.findByIdWithLock(toId);

        // 송금 처리
        fromUser.withdraw(amount);
        toUser.deposit(amount);
    }

 

시간 사용자A
(사용자A -> 사용자B에게 100원 송금)
사용자B
(사용자B -> 사용자A에게 100원 송금)
1 사용자A 락 획득 사용자B 락 획득
2 사용자B 락 획득시도 (Blocking) 사용자A 락 획득시도 (Blocking)
3 대기 대기
4 Dead Lock

 

그러면 위의 코드에서 Dead Lock이 발생하지 않게 하기 위해서는 어떻게 해아할까요?

락 획득의 순서에서 일과성을 유지하는 것입니다.

    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        User fromUser, toUser;
        if (fromId < toId) {
            fromUser = userRepository.findByIdWithLock(fromId);
            toUser = userRepository.findByIdWithLock(toId);
        } else {
            toUser = userRepository.findByIdWithLock(toId);
            fromUser = userRepository.findByIdWithLock(fromId);
        }
        fromUser.withdraw(amount);
        toUser.deposit(amount);
    }

 

사용자A ID = 1

사용자B ID = 2 라고 했을 때,

시간 사용자A
(사용자A -> 사용자B에게 100원 송금)
사용자B
(사용자B -> 사용자A에게 100원 송금)
1 사용자A 락 획득 사용자A 락 획득시도 Blocking
2 사용자B 락 획득 대기
3 송금 (락 반환) 사용자A 락 획득
4 - 사용자B 락 획득
5 - 송금 (락 반환)

 

 

결론

동시성 문제와 대용량 트래픽을 공부하기 위해 한정 수량 쿠폰과 대기열을 만들어보는 와중에 DeadLock와 데이터 무결성에 문제가 생겼고, 이를 해결하는 과정에서 Lock에 대해서 공부하게되었습니다. 다음 글은 JPA Lock 전략과 JAVA synchrozied, 그리고 JMeter 에 대해서 작성하려고합니다.

이전 글

 

다대다(N:N) 관계는 왜 피해야할까?

다대다 관계 데이터베이스를 설계에서 다대다(N:N)관계는 피하는것은 일반적으로 권장되고있습니다. 하지만 자세한 설명이 추가되지않는다면 그 이유가 헷갈릴 수 있는데요. 이는 데이터의 무

tmd8633.tistory.com

 

이전 글에서 다대다(M:N) 관계를 해결하기 위해 1:N, N:1로 관계를 풀어주었습니다.

 

이를 통해 데이터 무결성과 중복성을 해결할 수 있었고, 유지보수와 확장에 대한 장점을 얻을 수 있었습니다.

하지만 이런 관계가 무한정 증가하다보면 관계가 복잡해지면서 이해하기가 어려워지는 단점이 존재합니다.

 

이번에는 1:N, N:1로써 해결하는것이 아닌 2진법을 통해 이 관계를 풀어보도록 하겠습니다.

 

 

2진법과 비트연산

컴퓨터를 조금이라도 공부해본사람이라면 2진법에 대해서 익숙하실겁니다. 따라서 간단하게 짚고 넘어가겠습니다.

 

2진법은 위와 같이 나타낼 수 있습니다. 다음은 이 수를 비트연산으로 표현해보겠습니다.

 

 

AND

두개의 2진수의 자리수가 둘다 1이면 1, 나머지는 0이되는 연산입니다.

 

 

OR

어느 하나라도 1이라면 1이되고, 둘다 0이라면 0이 되는 연산입니다.

 

 

XOR

두개의 값이 서로 다르면 1을 반환하고 그렇지 않으면 0을 반환합니다.

 

 

 

연산자는 더 있지만 이 글에서는 비트연산이 주제가 아니기때문에 생략하도록 하겠습니다.

 

그렇다면이 2진수 연산을 통해 어떻게 N:N관계를 풀수 있을까요?

 

 

 

2진법의 적용

기존의 1:N, N:1의 관계입니다.

1번의 홍길동 학생이 수강하는 과목은 JAVA, JS, Flutter, React 를 수강하고 그 과목코드는 1,2,3,4 입니다.

2번의 홍길순 학생이 수강하는 과목은 JAVA, JS 를 수강하고 그 과목코드는 1,2 입니다.

 

지금부터 한 단계씩 밟으며 관계를 정리해보겠습니다.

 

 

1. 과목코드 수정

현재 과목코드는 1 부터 오름차순으로 정의되어있습니다. 이를 2진수로 변경하겠습니다.

기존의 1,2,3,4 처럼 증가하는 수를 2의 지수로 표현하여 2의 0, 2의 1, 2의 2, 2의 3 로 나타내었습니다.

 

과목코드 과목명
0001 JAVA
0010 JS
0100 Flutter
1000 React

 

 

 

 

2. 수강테이블 합치기

우리는 위에서 잠깐 비트연산자 OR에 대해서 짚고 넘어갔습니다. 이 OR 연산자를 이용해서 수강테이블을 하나로 정리할 수 있습니다.

1번 홍길동의 수강목록 0001 + 0010 + 0100 + 1000 = 1111

2번 홍길순의 수강목록  0001 + 0010 = 0011

 

학번 수강코드
1 15 ( = 1111b)
2 3 ( = 0011b)

 

 

 

3. 수강테이블 삭제

학생과 수강 테이블간의 관계가 1:N에서 1:1로 변경되었습니다. 따라서 수강테이블을 삭제하더라도 1:N관계가 유지되므로 삭제해도 무방합니다.

 

과목코드를 직접 매핑시키지 않고 이와같이 사용할 수 있습니다.

최종적으로 수강테이블이 사라져 깔끔해졌습니다.

 

 

 

실제로 어떻게 사용해야할까?

실제로 조회, 추가, 삭제를 하려면 어떻게 해야할까요? 하나하나 알아봅시다.

 

아래부터 MySQL 기준으로 설명하겠습니다.

 

조회

홍길순 학생이 어떤과목을 수강하고 있는지 조회하려고합니다. 

 

SELECT *
FROM 학생
JOIN 과목 ON (학생.과목코드 & 과목.과목코드 = 과목.과목코드)

 

JOIN에서 비트연산을 해줍니다. 홍길순 학생의 수강하는 과목코드 3 ( = 0011b) 과 과목테이블의 과목코드 (0001, 0010, 0100, 1000)  가 같은지 확인해보면 됩니다. 

홍길순의 과목코드 첫번째자리는 JAVA의 1에 해당하는 위치입니다. 따라서 (홍길순의 과목코드) AND (JAVA 과목코드) = JAVA 과목코드 가 성립됩니다.

 

Flutter와 AND 연산을 했지만 그 결과는 0이고 이해 해당하는 과목은 존재하지 않으니 출력이 되지않습니다.

 

 

 

 

추가

홍길순 학생이 Flutter를 추가로 신청했을때 코드를 보겠습니다.

UPDATE 학생
SET 과목코드 = 과목코드 | ( SELECT 과목코드 FROM 과목 WHERE 과목명 = 'Flutter' )
WHERE 학번 = 2

 

서브쿼리에는 Flutter에 해당하는 과목코드만 존재하면 됩니다. 과목코드를 이미 알고있다면 서브쿼리를 사용하지 않고 바로 넣어줄 수 있습니다.

 

OR 연산자를 사용해서 기존 3 ( = 0011b) 에서 7 ( = 0111b) 로 업데이트 되었습니다. 

OR 연산의 장점은 만약 문제가 발생하여 Flutter 과목이 중복 수강이 되었어도, 중복으로 추가되지 않는다는것입니다. 

(0111 | 0100 = 0111)

 

 

 

삭제

이번엔 홍길동 학생이 JS 과목을 수강취소했을때의 코드입니다.

UPDATE 학생
SET 과목코드 = 과목코드 ^ ( SELECT 과목코드 FROM 과목 WHERE 과목명 = 'JS' )
WHERE 학번 = 2

XOR 연산을 통해 서로 다른값이어야 1을 반환하고 같은 값은 0으로 반환되었습니다.

 

 

 

결론

이 이진법을 이용한 테이블 관계매핑은 다대다 관계를 해소하기위한 보조 테이블들을 생성하지 않아 최적화에 도움이 되고, 데이터를 하나로 관리하는 편리함이 존재합니다.

하지만 단점으로는 데이터를 보관할 수 있는 한계가 존재한다는 점입니다.

데이터를 30개만 넣어도 값은 10억이 넘어가기 때문에 확장성을 고려해야하는 상황이라면 비트연산을 통한 매핑은 사용되어선 안됩니다.

오로지 한계가 정해져있는 부분에 사용되어야합니다.

 

최근 비트연산에 대해서 공부하고있는데 재미있어서 이렇게 글을 써보았는데, 혼자 공부하고 작성한 글이라 분명히 틀린부분이 존재할겁니다. 그대로 받아드리지 말고 참고만 해주시길 바랍니다. 틀린부분이 있다면 지적부탁드립니다.

'일반 > DB' 카테고리의 다른 글

[DB] Lock 이해하기  (0) 2025.01.15
다대다(N:N) 관계는 왜 피해야할까?  (1) 2024.02.29

다대다 관계

데이터베이스를 설계에서 다대다(N:N)관계는 피하는것은 일반적으로 권장되고있습니다. 하지만 자세한 설명이 추가되지않는다면 그 이유가 헷갈릴 수 있는데요. 이는 데이터의 무결성, 효율성, 유지보수 등 다양한 측면에서 중요한 이유가 있어 권장하지 않는것입니다.

 

 

 

다대다 관계란?

다대다 관계란 하나의 엔티티(Entity)가 다수의 다른 엔티티와 관계를 맺고 있는 상황을 나타냅니다. 예를들어, 학생과 강좌간의 관계에서 한 학생이 여러 강좌를 수강하고, 한 강좌에는 여러 학생이 수강할 수 있는것이 다대다 관계입니다.

 

 

다대다 관계는 왜 피해야할까?

위에 연관관계를 통해 완성된 테이블을 만들어보겠습니다.

 

1. 데이터 중복성과 무결성 문제

위 테이블에 기본키(PK)를 포함해서 데이터의 중복이 발생했고,  데이터의 무결성이 손상되었습니다. 이로 인해 학번만으로 데이터를 구분할 수 없어졌습니다. 

 

 

2. 유지보수 및 확장성

데이터의 추가 또는 변경이 일어난다면 중복된 데이터를 모두 수정 및 추가해야합니다. 예를 들어, 홍길동 학생의 학과가 '수학과'로 변경된다면 2군데 모두 수정해야하고, 학생 테이블에 성별을 추가한다면 중복데이터를 추가해야하는 일이 발생합니다. 

 

 

 

 

결론

학생과 과목테이블 사이에 수강테이블을 만들어 다대다 관계를 일대다(1:N), 다대일(N:1) 관계로 풀어주는 것으로 해결 할 수 있다.

 

 

다음 글을 읽어보는걸 추천드립니다.

 

 

다대다(N:N) 관계를 조금 더 쉽게 풀어보자 (2진법의 활용)

이전 글 다대다(N:N) 관계는 왜 피해야할까?다대다 관계 데이터베이스를 설계에서 다대다(N:N)관계는 피하는것은 일반적으로 권장되고있습니다. 하지만 자세한 설명이 추가되지않는다면 그 이유

tmd8633.tistory.com

 

'일반 > DB' 카테고리의 다른 글

[DB] Lock 이해하기  (0) 2025.01.15
다대다(N:N) 관계를 조금 더 쉽게 풀어보자 (2진법의 활용)  (0) 2024.05.18

+ Recent posts