TDD(Test Driven Development)를 시작하고 있다. 


C++에서의 TDD의 힘겨움

그런데 유명한 TDD 책들을 보니 죄다 Java 언어로 되어 있다.
그 유명한 녹색 막대도 Eclipse IDE에 프로그래스바 플러그인이 JUnit과 함께 녹아들어가 있어서 가능했던 것이었다.

C++ 진영에서는 테스팅 프레임워크가 좀 많다. 그렇지만 IDE 애드온을 제공하는 곳은 없기 때문에 아무래도 까만 콘솔 화면밖에 볼 수 없는 것 같았다.

TDD는 안그래도 시작하기가 힘든데, 이렇게 프레임워크가 다양해서 막막했다. 대체 뭘 골라야 잘 골랐다는 소리를 들을까? Java의 JUnit과 같이 표준스러운게 있다면 선택의 여지도 좁지만 반대로 시작하기는 훨씬 쉬웠을 것이다.

노엘의 홈페이지
CppUnit
CppUnitLite
CxxUnit
UnitTest++
GoogleTest
등등...

너무 녹색 막대가 보고 싶어서(나도 'Green Bar' 보고 싶다고 ㅠㅠ) 처음에는 vutpp + CppUnitLite를 사용했다. 처음에는 녹색 막대를 본다는 사실에 많이 들떠있었지만, 버그도 좀 있고 사용하기가 생각보다 그닥 편하지가 않아서 다른 것을 찾아보게 되었다.

찾아보다 느낀 사실은 Windows + VisualStudio + C++ 조합의 개발자가 적지 않은데도 제대로 된 오픈소스 VS IDE Addon이 없다는 사실이다. TDD 책에 보면 빨간 막대 -> 녹색 막대 -> 리팩토링 이라고 말하는데 그건 자바 사정이고 C++에서 녹색 막대 보기는 물건너간것 같다.


VisualAssert의 발견

라고 생각하던 와중에 VisualAssert라는 VS Addon형 테스팅 프레임워크를 발견했다. 이거 아주 쓸만하다. 초보자라면 CppUnitLite 만큼 강추한다.

이것도 좀 쓰다 보니 문제가 있었는데, 테스트 도중 하나라도 실패하면 전체 테스트가 멈춘다는 것이다.


GoogleTest의 발견

그래서 결국 GoogleTest로 왔다.

역시 구글은 다르다는 느낌을 소스만 보고도 느꼈다. 비록 콘솔창으로 결과가 출력되지만 텍스트에 색을 입혀서 성공과 실패를 금방 알 수 있게 해준다. 녹색 막대를 못 보는 사람들을 세심한 배려(?)가 아닐 수 없다.

C++에는 리플렉션이 없어서 테스팅 프레임워크마다 하나씩 아쉬운 점이 보였는데, GoogleTest에서는 그런 점을 상당 부분 개선하려고 한 부분이 보인다.

예를 들면 특정 문자열로 시작하는 테스트만 실행시킬 수 있는 등 필터 기능과, 에러가 발생했을 때 디버거를 붙일 수 있는 기능 등 실제로 필요하다고 생각되는 기능은 다 있는 것 같다. 그리고 Windows도 차별 없이 잘 지원해주고 있다.

현재 모든 프로젝트는 GoogleTest를 사용해서 진행하고 있다.

반응형
결론부터 말하자면
 
1. 링크드 리스트 당 하나의 동기화 객체를 사용할 것.

2. 성능을 위해 Read Write Lock(또는 Shared Exclusive Lock이라고도 함)을 사용할 것.
 (동시에 여러명이 Read 가능, 한명만 Write 가능)

3. 리스트가 길어져서 탐색 시간과 대기 시간이 증가할 것 같으면 해시 테이블을 사용하고, 각 버킷별로 하나의 동기화 객체를 사용할 것.



멀티스레드 프로그래밍
에서 링크드 리스트(Linked List)를 다루다 보면 마주치는 문제가 있다. 바로 동기화이다.

이게 접근할 수 있는 스레드가 한개면 상관이 없는데, 여러개일 경우에는 push와 pop이 동시에 진행되면서 잘못된 메모리 참조를 하고 프로그램이 죽어버리는 상황이 발생할 수 있다. 실제로 이 점을 전혀 염두하지 않고 프로그램을 만들었다가 프로그램이 피를 토하며 죽고 나서야 실수를 깨달았던 경우가 있었다. 그 후부터는 항상 리스트를 만들 때 동기화가 필요한지에 대해 생각한다.

대개 이런 상황에서는 리스트 한개당 하나의 크리티컬 섹션을 사용해서 동기화하는 방법을 사용했다. 그런데 리스트가 길어지다 보면 탐색 시간이 오래 걸리기 마련인데, 만약 여러 스레드가 하나의 리스트를 '읽으려' 할 때 동시에 읽어도 되지만 크리티컬 섹션에 막혀서 하나씩 대기를 타고 있는 비효율적인 상황이 발생한 적이 있다.


이 문제를 해결하기 위해서 처음에 생각해 낸 방법은 '각 노드마다 동기화 객체 사용'이었다. 리스트 전체를 잠그는 대신 하나의 노드만 잠그고 읽거나 쓰면, 잠기지 않은 다른 노드는 다른 스레드가 접근할 수 있기 때문이다.

그런데 생각하다보니 문제가 하나 생겼는데, 일단 노드들이 주루룩 있는 상태에서는 각 노드를 잠가서 읽거나 쓰는(노드의 내용을 수정하는)데는 문제가 없었다. 하지만 링크를 끊거나 연결하는 과정에서 문제가 발생할 것이 분명했다.

동기화라는 것은 원자적(아토믹, Atomic)이지 않은 여러 개의 명령을 원자성을 가지도록 해주는 것이다. 원자적인 명령 단위는 멀티스레드 환경에서도 안전하다. 자세한 내용은 생략.

위에서 말한 그 문제는, 링크드 리스트에서 노드를 추가하거나 삭제할 때 연결을 맺고 끊는 과정이 원자적이지 않아서 발생한 것이다. 앞 노드의 연결을 수정하고, 뒤 노드의 연결을 수정하는 두개의 과정이 있기 때문이다. 그럼 락 걸고 앞뒤로 수정한 다음에 언락하면 되는거 아냐? 말은 쉽지만 1번 노드에 락 건다고 2번 노드까지 잠기는게 아니니까.

고민하던 끝에... 그래! 그냥 락 앞뒤로 걸어버리자! 하고 생각했다.

이론적으로 될 거라고 생각했다. 하지만 데드락과의 사투 끝에... 결국 내린 결론은 리스트 통째로 락 거는게 가장 낫다는 것이었다. 동기화 로직에 들어가는 코드의 양이 비대해지고 복잡해지면서 과연 다른 사람이 이 코드를 보고 이해할까 하는 생각이 드는 것은 물론이었다. 어찌어찌 해서 사실 완성은 했지만, 결국 이 코드를 버렸다.

쓰기를 할 때 뿐만이 아니라 읽기를 할 때도 필요한 노드를 포함해서 앞뒤 3개의 노드에 락을 걸어야 했다. 물론 데드락이 생기지 않게 왼쪽 오른쪽 자신 순서로 걸도록 했지만...

이제 여러 개의 스레드가 읽는다고 쳐보자. 락과 언락에 걸리는 시간때문에 오히려 퍼포먼스가 떨어질 지경이다.

그래서 버렸다. 사실 성능 측정으로 비교해보지는 않았다. 하지만 그렇게 직감이 들었다.


그 대안으로 해시 테이블을 선택했다. 말하자면 리스트를 쪼개서 분산 처리하는 것이다. 테이블의 각 버킷을 링크드 리스트로 만들면 된다. 그리고 각 버킷별로 동기화 객체를 사용하면 된다. 동시에 읽을 수 있게 Read Write Lock을 사용하면 더 좋다(Read Write Lock은 Interlock을 사용해서 직접 구현했다).

말은 마치 해시 테이블을 교묘하게 이용한 링크드 리스트처럼 했지만, 그냥 해시 테이블을 쓰는거다. 충돌이 발생하면 링크드 리스트로 이어가는 것일 뿐이지.

해시 테이블은 링크드 리스트보다 조금 더 메모리를 소모한다. 하지만 많은 데이터가 있어도 검색과 삽입, 삭제가 빠르므로 현업에서 자주 사용하고 있다.

반응형

'C, C++ 일반 > 알고리즘' 카테고리의 다른 글

알고리즘 문제 사이트  (0) 2010.11.04

High Cohesion, Low Coupling

진리이다. 항시 기억하자.


예를 들면 인체는 결합도가 높다. 뭔가 하나 수정하려면 주변에 영향을 주는 것이 많아서 굉장히 힘들다. 의사들을 보면 엄청나게 공부를 해야 하고 기억해야 할 것도 많다. 게다가 크고 작은 의료 사고(버그)도 끊이지를 않는다. 환자가 알아서 잘 설명하지 않으면 의사도 어디가 문제인지 모른다.

이번엔 결합도가 아주 낮은 전구와 소켓을 보자. 전구가 나갔으면 전구를 돌려서 슥 뺀다음 새 전구를 끼우면 된다.

반응형

+ Recent posts