우테코 Lv1 3번째 미션인 블랙잭을 구현하다가
카드의 자료구조를 정하는데 의견이 갈렸다.
나는 Deque을 주장했고 페어는 Stack을 제안했다
Stack을 사용하자는 페어 의견에 설득이 되었는데 그 이유는 다음과 같다
- 카드 덱의 멘탈모델이 Stack과 유사하다
- 양쪽에 넣고 뺄 수 있는 확장 기능을 가진 Deque은 오용의 위험성이 있다
- 카드 덱 객체는 Stack 이 제공하는 기능만으로 충분하며 그 이상의 확장을 상상하기 힘들다
그러나 Stack을 사용하자 SonarLint에서 경고를 띄웠다.
그것도 Deque을 사용하라고.
왜 그럴까?
이유1. Stack 은 Vector 컬렉션을 상속받아 정의에서 벗어난 오용가능성이 있다.
Vector는 자바 버전 1부터 있었던 굉장히 오래된 클래스이기 때문에 여러모로 취약점이 많다.
(이 취약점에 대해서는 밑에서 다루어보고자 한다)
문제는 상속으로 인한 부모 메서드 공유문제로 인해
개발자는 Stack에 알맞는 사용을 기대했으나,
사용자가 잘못되게 사용할 수 있다는 문제가 있다.
예를 들어 Vector에는 원하는 index에 객체를 끼워넣을 수 있는
insertElementAt이라는 메서드가 있다.
문제는 Stack이 Vector를 상속받았기 때문에
insertElementAt이 사용가능하다는 것이다.
Stack은 그 정의상 후입선출이 보장되어야 하는데,
원하는 위치에 객체를 끼워넣는다면 그 자료구조를 더이상 스택이라고 부를 수 있을까?
이렇듯 Stack은 Vector를 활용하여 구현된 만큼
Stack의 정의에서 어긋난 메서드를 활용하여 자료구조의 의미에서 벗어날 수 있는 위험성이 있다.
이유2. Vector 메서드는 동기화되어 있어 속도가 느리다
Vector의 소스코드를 보면 메서드에 synchronized 키워드가 걸려있는 것을 볼 수 있다.
synchornized 키워드는 멀티 쓰레드 환경에서 두 개 이사으이 쓰레드가 하나의 변수에 동시에 접근할 때 경쟁상태가 발생하지 않게 한다. 더 쉽게는 다른 쓰레드가 접근하지 못하도록 메서드를 잠그는 것을 의미한다.
비유를 통해 동기화를 쉽게 설명해보자
이는 우리가 화장실에서 줄을 서는 원리와 비슷하다. 화장실 1칸은 1명의 사람만 사용할 수 있고, 그 사람이 사용할 때는 락을 걸고 화장실을 잠근다. 볼일을 다 보고 온 사람은 락을 풀고, 대기하고 있는 다음 사람에게 칸을 넘겨준다. 이 규칙은 2사람이 동시에 화장실 칸을 사용하는 상황을 예방해준다. 우리는 이 규칙 덕분에 화장실 칸 안에 들어가서 누가 먼저 볼일을 볼 것인지 실랑이 하지 않아도 된다.
동기화도 마찬가지로 단 한 쓰레드만이 메서드를 사용하게 만든다. 메서드를 모두 사용한 하나의 쓰레드는 이제 락을 풀고 다음 대기 쓰레드에게 메서드를 넘겨준다. 화장실 칸의 규칙과 마찬가지로 동기화는 멀티 쓰레드 환경에서 메서드가 겹치서 무시되거나 충돌되지 않고 안전하게 사용도록 해준다.
동기화가 '무엇인지'에 대해서는 어느정도 감을 잡았으리라 기대한다.
그러나, 동기화를 "왜" 사용하는지에 대한 추가적인 학습이 있으면 좋다.
이 글을 모두 읽고 동기화에 더 궁금하다면 인파님의 블로그 글을 통해 더 학습해보자!
다시 본론으로 돌아와서 Stack이 상속받는 Vector 컬렉션의 메서드는 모두 강제 동기화되어 있다.
문제는 이 동기화로 인해 싱글 스레드 환경에서도 수많은 오버헤드가 발생하게 된다는 점이며 동기화여부를 따지느라(이 메서드를 다른 쓰레드가 쓰는지 확인하느라) 일반 메서드보다 속도가 느려질 수 있다는 점이다.
실제로 천만번 add 메서드를 호출한 시간이 ArrayList가 Vector보다 더 빠르다
자바 문서도 비슷한 이야기를 한다.
멀티 쓰레드 환경에서 안전성을 추구하지 않는다면 웬만하면 ArrayList를 쓰라고
이유3. Vector는 동기화 자체도 온전하지 않다
그런데 한가지 의문이 드는 것은 Vector가 synchronized 강제 동기화로 속도가 느려졌다고 하면, 안전성을 추구하는 멀티 쓰레드 환경에서는 Vector가 효용성이 있지 않을까?
왜 Vector는 왜 지양되고 있는 것일까?
그 이유는 Vector의 동기화가 온전하지 않기 때문이다.
Vector의 동기화는 메서드에만 synchornized 키워드로 되어 있어 메서드 자체 실행에는 안전하지만 Vector 인스턴스 객체 자체에는 동기화가 되어 있지 않아 동시다발적으로 객체에 접근해 메서드 호출이 가능하다
이 말을 해석해보면 다음과 같다.
vector.add에서 add 메서드는 동기화가 되어 있다. 따라서 누군가 add 기능을 사용하고 있으면 다른 쓰레드는 기다린다. 그러나 vector 인스턴스자체는 동기화처리가 안되어 있어 동시에 두 군데에서 다른 기능을 호출할 수 있다. 즉, vector.remove 기능 호출은 가능하다. 메서드 하나하나만 동기화처리되어 있지 vector 인스턴스 자체는 동기화처리가 안되어 있어 다른 기능이라면 객체를 여기저기서 동시에 부를 수 있는 것이다.
즉, Vector는 온전한 동기화 처리가 안되어 있다. 이말은 안전성을 위해서라도 성능을 위해서라도 Vector를 사용할 합리적인 이유가 없음을 의미한다
정리해보자
1. Vector는 동기화로 인해 속도가 느리다
2. 그러나, 그 동기화조차 온전하지 못하다는 취약점이 있다
따라서 대부분 우리가 Collection을 쓸때는 ArrayList를 쓰는 것을 권장받으며
동기화처리된 List의 경우도 SychornizedCollection을 주로 사용하지 vector를 사용하진 않는다
해결방안 : Deque을 사용하자
지금까지의 이야기를 정리해보자
- Stack은 Vector를 상속받아 구현된다
- 따라서 Stack은 LIFO 구조에 맞지 않는 메서드 지원 가능성이 있다
- Vector는 멀티스레드 환경여부와 상관없이 성능저하를 일으킨다
그래서 SonarLint와 자바 공식문서에서는 stack 대신 deque을 사용하는 것을 추천한다.
덱은 큐를 상속받고 큐는 Collection을 상속받기에 더욱 안정적인 LIfO 형태의 기능을 제공할 수 있다는 것이다
Reference)
'우테코 > Level1' 카테고리의 다른 글
상속과 조합 : is-a / has-a (8) | 2024.03.18 |
---|---|
[스크랩] 풀링 / 캐싱 (0) | 2024.03.14 |
.collect(Collectors.toList()) vs Stream.toList() (0) | 2024.03.04 |
[TDD] JUnit - @ParameterizedTest 공식문서 정리하기 (1) | 2024.02.28 |
[스크랩] 페어프로그래밍 환경설정 - 공통 커밋 Co-authored-by (0) | 2024.02.23 |