티스토리 뷰

JDK6로 돌아가고있는 서버를 JDK8로 업그레이드를 해야했다. 자세히말하면 JDK 뿐만아니라 Spring3을 Spring5로, Tomcat6를 Tomcat8로 함께 업그레이드를 진행했다.


Spring 버전이 변경되면서 인터페이스자체가 변경된 몇가지 부분외엔 크게 수정이 필요하지않았다. 수정해야할 부분을 모두 수정하고 서비스에 투입하자 갑자기 예외로그가 쭉쭉 올라오기 시작했다. 기존 JDK6에서는 발생하지않았던 문제이며, 발생한 예외는 ConcurrentModificationException 이었다.


예외로그가 쭉쭉 쌓이다보니 일단 서비스에서 제외하고 원인을 찾기시작했다. 보통 ConcurrentModificationException은 Iterator가 반복하고있을때 내부에서 remove등으로 Collection을 건드릴때 발생한다. 문제의 코드는 대략 이런 형태였다.


List<Object> list = // db select

Collections.sort(list);

for(Object obj : list){ // ConcurrentModificationException 발생

  // ...

}


for-each 문은 내부적으로 Iterator를 이용하므로 문제의 코드가 존재한다면 깔끔하게 해결할 수 있을것 같았다. 하지만 for-each 내부에서 Collection을 조작하는 코드는 없었다. 그리고 이런코드가 정말 있었다면 JDK8에서만 문제가 발생할리도 없었고, 개발할때 문제가 발생하지않을리도 없었다. 즉 한 스레드만 동작할때는 문제가 발생하지않기때문에 이는 필시 멀티스레드 문제일거라고 생각했다.


이 다음 확인한 부분은 "list가 공유자원인가" 이다. 하지만 list는 db에서 조회해온 결과를 담는 지극히 평범한 지역변수였다.


원인모를 증상에 이것저것 삽질을 하기시작했고 코드 변경이후에 예외 자체는 발생하지않게 만들 수 있었다.


Collections.sort(list); 를

List<Object> sortedList = list.stream().sorted().collect(toList());


로 변경하자 예외가 사라지는걸 확인할 수 있었다. 일단 문제는 해결했고, 이제 원인을 찾을 차례였다. 일단 저 두 방법의 차이는 Collections.sort() 같은경우 리스트를 가변적으로 변경시키는 메서드이고, Stream을 이용한 정렬은 정렬된 결과를 반환한다는 차이가 있다. 즉 sort() 일때 문제가 발생한다는건 같은 list에 접근하여 뭔가 작업을 한다는 의미이다. 그걸 상기하고 원인찾기를 시작했다. 몇가지 가정을 세우고 이를 증명해가는 형태로 원인을 찾으려했는데 우리가 세운 가정은 이렇다.


1. Collections.sort()가 비동기로 스펙이 변경됐다.

2. db에서 조회해올때 cache가 설정되어있었는데 이게 동일한 인스턴스를 반환한다.


첫번째 가정은 쉽게 아님을 증명할 수 있었다. 사실 가정을 세우면서도 만약 정말 이런걸 제공한다면 asynchronosSort() 같은걸 제공했으면 했지 기존 코드 스펙을 저렇게 바꿔버렸을까? 라는 의문을 갖기는했었다. 이후 두번째 가정을 증명했어야했는데 두번째 가정이 참임을 확인할 수 있었다.


해당 프로젝트는 오래된 프로젝트였기때문에 Persistence Framework로 myBatis도 아닌 iBatis를 사용하고있었는데 우리가 사용하고있던 iBatis 캐시가 "동일한 인스턴스"를 반환하고있었다. 즉 A 쓰레드에서 리스트를 반환받고 sort() 를 하고있을때 B 쓰레드에서 db를 실행한다면 iBatis는 sort() 를 하고있는 그 인스턴스를 그대로 반환하는 문제가 있던것이다. 개인적으로는 좀 충격이었는데 cache를 하는입장에서 내부데이터를 반환할때 복사된 객체를 주지않는것이 의아했다. 결과적으로 우리는 우리가 의도하지않은 상태로 공유자원을 사용하는 문제가 발생한것이다. 하지만 프로젝트의 기술스택을 올리면서 iBatis도 fade out 예정이기때문에 더 깊게는 들여다보지않고 원인을 찾은것에 의의를 뒀다.


하지만 이상태로 이슈가 해결되는것은 아니었다. iBatis가 동일 인스턴스를 주는 문제가 있다면 JDK6에서도 예외는 발생했어야했다. 이는 iBatis와는 별개로 JDK 버전에따른 문제가 분명히 있다는 뜻이다. 일단 공유자원을 사용한다는걸 알게됐으므로 공유자원을 사용하는 상태에서 JDK6와 JDK8의 문제를 찾는 샘플코드를 작성하고자했다. 이런저런 삽질끝에 샘플코드를 작성했다. (야호!)


public static void main(String[] args) {
final List<Integer> list = new ArrayList<Integer>();

for(int i = 0; i < 100000; i++){
list.add(0, i);
}

ExecutorService service = Executors.newCachedThreadPool();

service.execute(new Runnable() {
@Override
public void run() {
Collections.sort(list);
}
});

service.execute(new Runnable() {
@Override
public void run() {
for(Integer i : list){
System.out.println(i);
}
}
});

service.shutdown();
}


흥미가 있는 분이라면 위 코드를 JDK6와 JDK8에서 돌려보도록하자. 코드는 간단하다. 99999~0까지 역순으로 이루어진 리스트를 하나 만들고 두 스레드에서 동시에 정렬과 반복을 실행하는 것이다.


이코드는 재미있게도 JDK6에서는 예외가 발생하지않으며 JDK8에선 ConcurrentModificationException이 발생한다. 원인이 무엇일까? 정말 sort() 메서드의 구현이 달라진것일까?

사실 이 코드를 작성하고 출력된 결과를 유심히 봤으면 결과가 일찍 도출됐을텐데 예외발생유무에만 초점을 맞추다보니 출력결과를 대충넘겼고 이로인해 몇시간의 추가삽질을 하게됐다. 이부분에 있어서도 1가지 가정을 하고 증명하는 과정을 거쳤는데 내가 내렸던 정의는 이렇다.


JDK6에서는 sort() 메서드가 동기화되어있고, JDK8에서는 동기화가 되어있지않다.


가정이었지만 이것말고는 문제가 없다고 생각했고 거의 확신에 찬상태로 증명했는데 두 버전 모두 동기화는 되어있지않다. 정렬하는 알고리즘이 변경된 흔적은 있었지만 현재의 문제와는 별 상관없는 부분이었다. 그 이후 출력결과를 유심히 보기시작했고 그제서야 문제를 확인할 수 있었다. 정상작동한다고 생각했던 JDK6의 출력결과는 이렇다.


9

8

7

6

5

6

7

8

9


처음에는 역순으로 출력이 되다가 중간에 정렬이 다 된이후부터는 다시 정렬된 결과대로 출력을 하게된다. 지금 저 결과는 예제로 아주 간단하게 작성한거고 실제 출력결과는 숫자가 크기때문에 매우 길게 출력되므로 내가 눈치채지 못했던 것이다.

이 과정에서 6,7,8,9는 2번씩 출력되고 1,2,3,4는 데이터가 유실된걸 볼 수 있다. 즉 JDK6는 예외가 발생하지않았을뿐 문제가 없는게 아니었던 것이다. JDK8은 좀 더 단단한 로직으로 리스트의 변경을 체크하고 예외를 발생시키는 것이었다. 그리고 부끄럽게도 그동안 문제없이 돌던코드가 아니라 문제를 안고 돌고있던 코드였던 것이다.

Iterator 부분에 변경이 있는걸로 보이는데 여기까지 분석하고 그 이상은 더 파보지않기로했다.(이정도면 만족할정도로 분석했다고 판단했다.)


사실 공유자원을 사용하지않는다면 sort()와 Iterator를 동시에 사용할일도 없었기때문에 iBatis 캐시만 상식적으로 복사된 객체를 줬다면 이 증상은 영영 모른채로 있었을것이다. 이게 정확히 JDK7에서 변경된건지 JDK8에서 변경된건지는 모르겠지만 JDK6에서 버전업하는 다른분들에게 도움이 되길 바란다.


해당 이슈의 원인은 아니었지만 분석하는 과정에서 참고하고, 알아두면 좋을것같은 글을 공유한다.

* Java8의 ArrayList.subList()한후 Iterate시 ConcurrentModificationException 발생

* [Java 8] AtomicReference에서 만나는 ConcurrentModificationException

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함