2016년 8월 28일 일요일

[퍼온글]Inner class에서 local variable에 접근하려면 final이어야 하는 이유

안드로이드 개인 프로젝트를 진행하는 중에 옆에 있던 동기 누나가 생긴 오류로 코드를 들여다보았다.
?
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);      
        fragmentManager=getFragmentManager();
        fragmentTransaction=fragmentManager.beginTransaction();
        ContentsViewFragment contentsViewFragment=new ContentsViewFragment();
        fragmentTransaction.add(R.id.fragment_container,contentsViewFragment);
        fragmentTransaction.commit();
        // 1. Action bar에서 navigation drawer toggle버튼을 클릭하면 navigation list가 나옴
        //    -> navigation list에 eventlistener 설정
        //    -> selectCategory()
        upperToolbar=(Toolbar)findViewById(R.id.upper_toolbar);
        setSupportActionBar(upperToolbar);
        // 2. 하단에 Action bar로 home, search, write, mypage 버튼 구성 및 eventㅣistener로 각 fragment로 연결
        //    -> selectMenu()
        //    -> home : ContentsViewFragment
        //    -> search: before search; BeforeSearchFragment, after search; ContentsViewFragment, action bar 검색어 입력 모드로 연결
        //    -> write: WriteCategoryFragment
        //    -> mypage: MyPageFragment
        //bottomToolbar는 standalone으로 구현
        bottomToolbar=(Toolbar)findViewById(R.id.bottom_toolbar);
        bottomToolbar.inflateMenu(R.menu.menu_bottom_bar);
        bottomToolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener(){
@Override
public boolean onMenuItemClick(MenuItem item) {
        switch(item.getItemId()) {
        case R.id.home:
        ContentsViewFragment contentsViewFragment=new ContentsViewFragment();
        fragmentTransaction.replace(R.id.fragment_container,contentsViewFragment);
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
        break;
        case R.id.search:
        BeforeSearchFragment beforeSearchFragment=new BeforeSearchFragment();
        fragmentTransaction.replace(R.id.fragment_container,beforeSearchFragment);
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
        break;
        case R.id.write:
        WriteCategoryFragment writeCategoryFragment=new WriteCategoryFragment();
        fragmentTransaction.replace(R.id.fragment_container,writeCategoryFragment);
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
        break;
        case R.id.mypage:
        MyPageFragment myPageFragment=new MyPageFragment();
        fragmentTransaction.replace(R.id.fragment_container,myPageFragment);
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
        break;
        }
    return true;
  }
});
}
        });

 activity_main이 fragment로 화면을 바꾸어 주는데, 아래에 Toolbar로 변경할 수 있도록 구성했다. onMenuItemClick()에 switch문에서 id값에 따라 fragment를 바꿔준다. 코드를 보면 setOnMenuItemClickListener() 안에 new Toolbar.OnMenuItemClickListener() 로 Anonymous class가 선언되고 그 클래스의 onMenuItemClick() method 내에서 switch 구문을 구현한 것이다. 

코드를 실행하니 Inner class에서 local variable에 선언 된 변수에 접근하려니 에러가 발생했는데 그 내용이 'Inner class가 local variable에 접근하고 있다. 해결하려면 local variable을 final로 선언해야 한다' 라는 내용이었다.
 onMenuItemClick()의 switch 구문에서 onCreate()에서 선언된 contentsViewFragment 변수에 접근하면서 생긴 오류였다.

그 이유가 무엇일까 찾아보다가 깊이 있는 분석이 필요함을 느꼈다.

먼저 Inner Class의 개념에 대해 정확히 파악할 필요가 있었다. Java에는 Nested Class 가 있는데, 이 안에 Inner Class와 Lamda Expression이 포함된다. Inner Class는 다시 Local Class와 Anonymous Class가 있다. Java에서는 왜 Nested Class가 필요해졌으며, Inner Class에는 Local Class와 Anonymous Class가 분류되었을까? 이것에 대한 이해가 없이는 그 아래에 대한 이해가 힘들 것 같았다.

Nested Class가 왜 등장하게 되었는지 살펴보니
1. It is a way of logically grouping classes that are only used in one place
 개념적으로 하나의 클래스 B가 오직 다른 클래스 A에서만 유용하다면, B 클래스는 A 클래스 안에서 사용하는 것이 좋다는 것이다. 굳이 다른 곳에서 쓰는 일이 없는데 클래스로 빼낼 필요가 있을까라는 말인 듯 하다. 클래스로 만든 다는 것은 재사용성을 높이기 위함이 있으니까..

2. It increases encapsulation
 B 클래스가 A 클래스의 멤버에 접근해야 한다. 그러려면 public으로 선언하거나 protected로 선언해서 상속을 받아야 할 것이다. 하지만 inner class로 선언해두면 private으로 선언할 수 있다. 그리고 B class는 안에 있기 때문에 외부(outside world)로부터 숨을 수 있다.

3. It can lead to more readable and maintainable code
 좀 더 가독성이 좋고 유지보수하기 쉬운 코드가 된다고 한다. 

Nested Class는 2개의 카테고리로 나뉘는데 static nested class와 non-static nested class(inner class)로 나뉜다. Inner class는 enclosing class의 멤버가 private이더라도 접근할 수 있다.



Inner class의 경우 추가적으로 2가지 타입이 더 있다. Inner class는 method 안에서 선언할 수도 있는데, method 안에 선언하면 local class라고 부르고, class name 없이 method body 안에 선언하면 anonymous class라고 부른다.

Local class는 enclosing class의 멤버에 접근할 수 있다. method 내에 선언하는 class를 local class라고 하니 정확히는 method 내에 선언 된 변수를 의미하는 듯 하다. 그런데 Local class는 오직 final로 선언된 local variable에만 접근할 수 있다. 
"When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter."

Local Class가 local variable 또는 parameter에 접근(Java 8부터는 parameter에도 접근할 수 있다)하려고 하면 그 변수를 captured 한다는데 정확히 무슨 뜻인지 모르겠다. 복사한다는 뜻일까?

Anonymous Class도 Local Class와 마찬가지로 enclosing class의 member에 접근을 하려면 final로 선언이 되 있어야 한다고 했다.




이제 JVM 내부에서 어떻게 동작이 되길래 final에 접근하는지에 대한 고민이 필요했다.

http://www.slipp.net/questions/278 에서도 같은 고민의 글이 있어서 참고를 했다.

위에 읽은 글과 블로그의 내용을 바탕으로 이해한 것은 Anonymous Class나 Local Class에서 local variable에 접근을 하면 그 변수 값을 복사(captured)해서 사용하기 때문에 2개의 변수가 생겨서 값이 불일치 하는 상황이 생길 수 있다. 따라서 final로 선언해서 그런 일을 방지하려고 하는 것이다.
 위의 블로그에는 thread safe에 관해서도 얘기를 하고 있는데 거기까지는 이해가 되지 않고 있다. 좀 더 자세히 생각해봐야 할 것 같다.

[출처]http://ybin.tistory.com/8