공통코드 동시성 실패

Reading-Snail·2023년 11월 17일
1

동시성 문제

접속자들이 동시에 조회 또는 등록을 하는 작업을 했을 떄 동시성이 꺠졌습니다. 일반적인 상황에서는 일어나지 않고, 여러 개의 프로시저를 순차적으로 진행하는 메서드에서 프로시저들 사이에 타 접속자가 로그인 할 시 해당 정보가 덮어 씌워지면서 문제가 발생했습니다. 프로시저는 소요되는 시간이 초 단위로 길다 보니 더 쉽게 이러한 결과가 자주 나타난 듯 했습니다.

Service단이 ServiceExecutor를 상속 받아 공통된 정보를 조회하도록 설계 된 구조였습니다. 여기서 문제점이 Map형식으로 되어있는 공통코드 변수가 전역변수로 선언되어져 있었습니다. 전역변수에 수정 또는 삭제가 타 접속자의 로그인으로 인해 발생했을 때 변경된 데이터로 다음 프로시저가 작동되는 문제였습니다. 이전의 타 언어로 작성된 프로그램을 컨버팅하여 마이그레이션 하다보니 구조적으로 문제가 발생한 것으로 보여졌습니다. 또한 요구되는 비즈니스 로직에도 변경이 있어서 이전에는 발생하지 않았던 오류가 확인 된 것으로 보여졌습니다. (이전에도 존재했던 구조적 결함이었으나 오류가 발생될 수 있는 사례가 없었던 것 같습니다.)

우선 적용된 해결책은 전역변수를 직접 사용하는 부분을 제거하고, 지역변수로 데이터를 받아오도록 하는 것이었습니다. 덕분에 프로시저에 해당하는 동시성 문제는 해결 되었습니다.

그렇다면 정말로 동시성 문제가 사라졌는가에 대해서는 의문이 남았다. 동시 접속자가 많지 않은 프로그램이고, 일반 조회의 경우 소요시간이 매우 짧기 때문에 동시성 문제가 발생할 확률은 현저히 낮아 보였습니다. 그러나 여러 부분에서 문제점이 많은 설계와 코드라고 느껴졌습니다. 우연과 우연이 겹치면 일반적인 CRUD 메서드에서도 충분히 오류가 발생할 가능성이 있다고 판단되었습니다.

추가적인 개선사항

클린코드 - Rober C. Martin :: 동시성

동시성 방어 원칙

  • SRP - 동시성 코드와 일반 코드를 분리
  • Corollary
    - 자료를 제한하라: 자료를 캡슐화하고, 공유 자료를 최대한 줄인다.
    - 자료 사본을 사용하라: 자료를 복사해 읽기전용으로 사용 <- 사용된 해결책
    - 스레드는 가능한 독립적으로 구현히리: 쓰레드 간 동기화를 최소화한다.
  • 라이브러리를 이해하라
    - 자바에서는 다중 스레드에서도 안전한 컬렉션을 제공한다.
    .e.g ConcurrentHashMap <- 가능성이 있어보여 적용해봤으나 Null값이 있는 경우가 있어 적용이 즉각적인 적용은 어려웠다.
  • 실행 모델을 이해하라
    - 생산자-소비자 / 읽기-쓰기 / 식사하는 철학자들 모델과 알고리즘 해법을 이해한다.
  • 동기화하는 메서드 사이에는 존재하는 의존성을 이해하라
    - 공유 객체 하나에는 메서드 하나만 사용한다.
    - 메서들 꼭 여러개 사용해야 된다면
    이후 내용은 동시성과 관련된 책을 하나 별도로 읽어야 될 정도로 깊은 내용이라고 생각이 들었습니다.

위의 방법들에서는 '자료사본을 사용하라'를 통해 문제를 해결 할 수 있었습니다. 추가적으로 ConcurrentHashMap를 적용하는 해결책도 가능성이 있어 적용해봤으나 Null값이 있는 경우가 사용할 수 없는 방식이어서 불가했습니다.

'토비의 스프링 3.1'을 참고한다면

  • 싱글톤으로 접근하여 동시성이 꺠진 것이었기 때문에 SessionScope 방식을 공용데이터 객체에 적용하여 각 접속자별로 독립적으로 데이터를 관리 할 수도 있었을 것 같습니다. 다만 해당 객체가 ServiceExecutor에서 상속받아서 사용하는 방법이고, POJO 클라스가 아닌 Map으로 구현 되어 있어. 너무나도 큰 공사가 되는 부분이었습니다. Map을 CommonMap으로 감싸는 방법을 사용하여 SessionScope를 적용하여 정상 작동하는 것도 잘 확인 하였습니다. 변경되는 사항이 다른 동료들을 납득시킬 수 있을 만큼 안정하다는 자신감이 없어 적용하지는 못하였습니다.

ThreadLocal도 해결책이 될 수 있을 것 같다.

  • ThreadLocal은 JDK1.2 이후에 추가된 스레드 별로 독립된 변수를 사용할 수 있게 해주는 클래스이다. 이를 활용하면 동시성 문제를 해결 할 수 있을 것으로 짐작되었습니다. 다만 이 또한 코드 전체에 너무나도 많은 수정을 요구하게 되게 때문에 활용하기는 어려워 보였습니다. ConcurrentHashMap처럼 null을 허용하지 않는 것도 문제였습니다.

결론

임시 방책으로 문제는 해결 했으나, 결국 처음에 얼마나 정교한 설계를 구현해 놓았는가가 무었보다 중요하다는 사실을 다시 느끼게 되었습니다. 프로젝트가 진행 될 수록 새로운 코드가 추가되어질 수 밖에 없고, 하나의 코드가 변경될 때 갖게 되는 파급효과가 상당하다는 사실을 느낄 수 있었습니다. 사실 STS를 사용하고, Spring 기능을 일부 구현했을지는 모르지만 현재 진행하고 있는 스프링 프레임워크 철학을 녹여낸 코드와는 매우 거리가 있지 않나 싶었습니다.

또한, 테스트이 중요성을 더 크게 체감하게된 경험이었습니다. 아무리 좋다고 판단되는 코드이 변형을 진행하고 성공하더라도 나와 동료들을 설득하기 위해서는 안정성이 그 무엇보다 중요하다는 생각이 들었습니다. 그리고 안정성을 수치화 하고 시각화 할 수 있는 사실상 유일한 방법은 테스트 뿐이고 그래서 개발에 있어서 TDD가 필수임을 더 크게 마음에 새길 수 있었습니다.

profile
책읽는 달팽이 || 공학도에서 개발자로! || 결국 과거의 흐름을 이해했을 때 지금의 것들을 통찰력있게 바라볼 수 있다고 믿습니다.

0개의 댓글