패스트캠퍼스 챌린지

패스트캠퍼스 환급 챌린지 21일차 : 9개 도메인 프로젝트로 끝내는 백엔드 웹 개발 (Java/Spring) 초격차 패키지 Online 강의 후기

ssoonearth 2025. 4. 21. 23:59

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

https://abit.ly/lisbva

 

 

 

 

패스트캠퍼스의 '9개 도메인 프로젝트로 끝내는 백엔드 웹 개발 (Java/Spring) 초격차 패키지 Online' 강의 수강 21일차!
오늘은 동시성 문제에 대한 개념을 학습하고, 좋아요를 동시에 눌렀을 때의 해결 방법을 구현하는 작업을 진행하였다.

 

두 사용자가 같은 자료에 대해 동시에 업데이트 요청을 보낸 경우, 첫번째 업데이트 요청에 대한 결과가 두 번째 요청의 처리로 인해 덮어써지는 현상이 발생하게 되는데, 이를 갱신 유실 (update lost) 이라 한다.

 

이러한 동시성 문제를 해결하기 위해 DB단에서 처리하는 방법들이 있다.

 

1. Lock

(MySQL 기준) DB의 Lock이란, unique한 대상의 insert/update 요청이 들어오면 row 한 개의 데이터에 lock을 걸어주는 것을 말한다.

동시에 insert/update 작업 요청이 들어온다면 먼저 들어온 요청을 처리하는 동안 다른 요청은 데이터에 접근할 수 없게 대기가 걸리게 된다.

같은 자료에 대한 작업을 동시에 진행하지 못하게 함으로써, 중복 업데이트를 방지한다.

 

2. Isolation Level을 Serializable로 올리기

트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때, 서로에게 어떤 영향을 줄 수 있는지를 제어하는 설정이다.

 

이상현상

- Dirty Read: A 트랜잭션이 아직 커밋하지 않은 B 트랜잭션의 데이터를 읽음

- Non-Repeatable Read: A 트랜잭션이 같은 데이터를 두 번 읽을 때, 중간에 B 트랜잭션이 값을 수정하여 값이 바뀜

- Phantom Read: A 트랜잭션이 같은 조건으로 데이터를 두 번 조회할 때, 중간에 B 트랜잭션이 새로운 row를 insert하여 2번의 조회 결과가 다름

 

격리 수준은 위의 3개의 이상 현상 중에 어떤 현상을 허용하는지에 따라 구분할 수 있다.

- Read Uncommitted: 3개 모두 허용

- Read Commited: Dirty Read 비허용

- Repeatable Read: Dirty Read, Non-Repeatable Read 비허용

- Serializable: 모두 비허용

 

 

3. 낙관적 락 / 비관적 락 사용

- 낙관적 락: 락을 걸지 않고 일단 작업을 진행한 후, 커밋 직전에 충돌 여부를 확인해서 다른 트랜잭션이 이미 수정하였다면 실패 처리

- 비관적 락: 다른 트랜잭션이 수정하지 못하도록 선제적으로 락을 걺

 

 

하지만 이렇게 DB단에서 처리하는 경우 성능 이슈를 야기할 수 있다.

이번 실습의 경우 간단한 동시성 제어이기 때문에 쿼리를 수정하여 해결해보았다.

 

// 기존
@Modifying
@Query(value = "UPDATE PostEntity p "
        + "SET p.likeCount = :#{#postEntity.likeCount}, "
        + "p.updDt = now() "
        + "WHERE p.id = :#{#postEntity.getId()}")
void updateLikeCount(PostEntity);
// 수정
@Modifying
@Query(value = "UPDATE PostEntity p "
        + "SET p.likeCount = p.likeCount + :likeCount, "
        + "p.updDt = now() "
        + "WHERE p.id = :#{#postEntity.getId()}")
void updateLikeCount(Long postId, Integer likeCount);

 

쿼리상에서 직접 연산을 수행하게끔 수정하여서 갱신 유실을 방지할 수 있게 하였다.

하지만 이렇게 쿼리상에서 연산을 처리하는 것은 기존의 객체로 구현한 것보다 유연성이 떨어지고, 테스트를 위해 외부 라이브러리를 사용해야 하는 단점이 있다.

 

쿠폰 기능의 경우 두 가지의 핵심 검증이 필요하다.

유효성 검증은 어느 상품에 사용하는지, 결제 금액이 얼마인지 등의 비즈니스 목적에 따라 변할 확률이 높기 때문에 자바 객체에 넣어 도메인 로직에서 처리하는 것이 좋고, 사용 여부를 검증하는 것은 로직이 쉽게 변하지 않고, 두 번 처리되지 않아야 하기 때문에 레파지토리에 직접 쿼리문을 작성해서 처리하는 것이 좋다.

 

기능이 정합성과 일치가 중요한지, 도메인 로직의 유연성이 중요한지 잘 구분하여 구현하는 것이 중요하다는 것을 깨달았다.