이번 주에 한 일

Redis의 redisson을 이용한 락 구현

 

redisson의 락은 기본적으로 분산락이다.

 

처음에 락을 구현했을 때는 락을 구분하는 키인 LockKey의 값을 모두 같은 값으로 주고 만들었는데, 서비스에서 락이 가지는 의미를 생각하니 기능마다 다른 값을 주어야 할까? 라는 고민이 되었다.

@Service
@RequiredArgsConstructor
public class ColService {
    public final ColRepository colRepository;
    public final BoardRepository boardRepository;
    public final RedissonClient redissonClient;
    public final BoardService boardService;

    @Transactional
    public ColResponseDto createCol(Long boardId, ColRequestDto requestDto) {
        String lockKey = "ColLock";

        RLock lock = redissonClient.getLock(lockKey);
        boolean lockAcquired = false;
        try {
            if (!lock.tryLock()) {
                throw new RuntimeException(ErrorMessage.LOCK_NOT_ACQUIRED_ERROR_MESSAGE.getErrorMessage());
            }

            lockAcquired = true;
            Board board = boardService.findBoard(boardId);
            Long lastColIndex = colRepository.findLastColIndexByBoardId(boardId);
            Long newColIndex = (lastColIndex != null) ? lastColIndex + 1 : 1;

            Col col = new Col(
                    requestDto.getColName(),
                    newColIndex,
                    board
            );
            Col savedCol = colRepository.save(col);

            return new ColResponseDto(savedCol);
        } finally {
            if (lockAcquired) {
                lock.unlock();
            }
        }
    }


    public List<ColResponseDto> getCols(Long boardId) {
        List<Col> cols = colRepository.findByBoardId(boardId);

        return cols.stream()
                .map(ColResponseDto::new)
                .collect(Collectors.toList());
    }

    public ColResponseDto updateCol(Long boardId, Long columnId, ColRequestDto requestDto) {
        String lockKey = "ColLock";
        RLock lock = redissonClient.getLock(lockKey);

        try {
            if (!lock.tryLock()) {
                throw new RuntimeException(ErrorMessage.LOCK_NOT_ACQUIRED_ERROR_MESSAGE.getErrorMessage());
            }

            Board board = boardService.findBoard(boardId);
            Col col = findCol(columnId);

            if (!col.getBoard().getId().equals(boardId)) {
                throw new IllegalArgumentException(ErrorMessage.ID_MISMATCH_ERROR_MESSAGE.getErrorMessage());
            }

            col.setColName(requestDto.getColName());
            return new ColResponseDto(colRepository.save(col));
        } finally {
            lock.unlock();
        }
    }



    public void deleteCol(Long boardId, Long columnId) {
        String lockKey = "ColLock";
        RLock lock = redissonClient.getLock(lockKey);

        if (!lock.tryLock()) {
            throw new RuntimeException(ErrorMessage.LOCK_NOT_ACQUIRED_ERROR_MESSAGE.getErrorMessage());
        }

        Board board = boardService.findBoard(boardId);
        Col col = findCol(columnId);

        if (!col.getBoard().getId().equals(board.getId())) {
            throw new IllegalArgumentException(ErrorMessage.ID_MISMATCH_ERROR_MESSAGE.getErrorMessage());
        }

        colRepository.deleteById(columnId);
        lock.unlock();
    }

    @Transactional
    public ColResponseDto updateColIdx(Long boardId, Long columnId, Long columnOrderIndex) {
        String lockKey = "ColLock";
        RLock lock = redissonClient.getLock(lockKey);

        try {
            if (!lock.tryLock()) {
                throw new RuntimeException(ErrorMessage.LOCK_NOT_ACQUIRED_ERROR_MESSAGE.getErrorMessage());
            }

            Board board = boardService.findBoard(boardId);
            Col columnToUpdate = findCol(columnId);

            if (!columnToUpdate.getBoard().getId().equals(boardId)) {
                throw new IllegalArgumentException(ErrorMessage.ID_MISMATCH_ERROR_MESSAGE.getErrorMessage());
            }

            List<Col> colList = board.getColList();
            Long currentIndex = columnToUpdate.getColIndex();
            colList.remove(columnToUpdate);
            columnToUpdate.setColIndex(columnOrderIndex);

            for (Col col : colList) {
                if (!col.getId().equals(columnId) && col.getColIndex() >= columnOrderIndex) {
                    col.setColIndex(col.getColIndex() + 1);
                }
            }

            colList.add(columnToUpdate);
            List<Col> savedCols = colRepository.saveAll(colList);

            return new ColResponseDto(columnToUpdate);
        } finally {
            lock.unlock();
        }
    }

    public Col findCol(Long id) {
        return colRepository.findById(id).orElseThrow(() ->
                new IllegalArgumentException(ErrorMessage.EXIST_COL_ERROR_MESSGAGE.getErrorMessage()));
    }
}

 

내가 구현하고자 하는 기능의 방향을 생각하니, 크게 몇 가지 방향이 있었는데,

1. 모든 행동에 락을 걸자

장점 : 유저가 어떤 보드에 접근하든, 어떤 컬럼에 접근하든 모든 행동에 락을 건다면 적어도 동시성에 의한 이슈는 없을 것이다

 

단점 : A유저는 boardId가 1인 보드에 접근하고 B유저는 boardId가 2인 보드에 접근한다면 굳이?? 오히려 너무 큰 락으로 인해 유저의 활동이 지나치게 제한되버리는 것 같음.

 

2. 같은 엔티티끼리만 락을 걸자

장점 : 보드라고 예를 든다면, 보드를 건드는 요청이 들어왔다면, 보드 건드리는 요청을 안 받는거지. 그렇게되면 보드를 수정하는 요청과 삭제하는 요청이 같이 들어오더라도 하나의 요청에만 답 할 수 있어서 동시성에 대한 이슈는 어느정도 해결 됨 .

 

단점 : 음.. 생각해보니 A보드에 있는 컬럼을 수정하는 중에  A보드를 삭제한다면 이것도 문제가 될 것  같은데... 

 

글을 정리하다 보니, 하나의 아이디어가 떠올랐다.

 

3. 계층적으로 락을 설정하는 건 어떨까.. ?

지금 구조가 Board > Col > Card 로 포함되어있는데, 

보드에서 락을 걸었다면 ->  Board, Col., Card에선 락 획득 불가

컬럼에서 락을 걸었다면 -> Col, Card에선 락 획득 불가

카드에서 락을 걸었다면 -> Card에서 락 획득 불가.

 

이런 식으로 바꿔야 할 것 같다.

 

구현하려면 boardLockKey, colLockKey, cardLockKey 이런 식으로 따로 키를 다 만들어서

락을 구현해야겠지?

 

반대로 보드를 작업하려면 Board, Col, Card의 모든 락이 해제되어있어야 하는거겠고..,? 

 

내일 한번 구현해봐야겠다.

 

도커 컨테이너 이용해서 서비스 도커에 올리기 ( 실패... ) 

도커를 처음 써봤는데, 현재 프로젝트를 빌드해서 도커 이미지에 올리고, MYSQL, Redis까지 도커 이미지에 올린 뒤,

 

MYSQL과 Redis는 이미지를 통해서 컨테이너에 접속이 되는데, 프로젝트 이미지가 컨테이너에 접속이 안 됐다.

 

프로젝트를 계속 도커에 있는 DB서버에 연결하려고 노력하고 시간도 많이 부었는데 안되는 이유를 많이 배워갔다.

 

비록 성공은 못 했지만, 다음에 시간 나면 또 도전해야지. 

코드 리팩터링

기존에 ColService에서 에러 핸들링을 할 때는 보드 엔티티의 ID를 검증하는 걸 ColService내에서 했는데, 관심사의 분리를 해야한다는 매니저님과 다보미님의 조언으로, Board의 Id는 Board에서 검증하고, Col에서는 BoardService를 주입받아서 그냥 쓰기만 하면 되도록 리팩터링 했다. 

 

OOP 스터디도 친구들이랑 했는데, 막상 실전 가니까 코드에만 집중해서 잊어먹었나 보다. 

관심사의 분리를 꼭 생각하도록 하자! 

 

이번 주 느낀 점

아무리 시간 많이 부어도 해결되지 않다가 갑자기 어!? 하는 순간에 풀리는 문제들이 꽤 있었다.

 

머리 싸매고 끙끙댈 때는 너무나도 힘들지만, 한 순간 찾아오는 해답을 잡는 순간에 성취감이 너무 좋았던 한 주였다.

 

오는 주도 열심히 해서, 목요일에 발표니까 서비스 완성까지 조그만 더 힘내자!