안드로이드 프로그래밍 Next Step 11장
디자인 패턴은 복잡한 앱을 개발하면서 도움이 된다. 그렇다고 다 적용하려고 하지말고 필요할때에 쓰자.
이 챕터에서는 도움될 만한 패턴을 이야기해보자
앱에서 싱글톤 패턴은 메모리 누수 가능성을 고려해야하는 패턴이다. 가급적이면 반드시 필요한 곳에만 사용하자
실제로 support-v4에 포함된 LocalBroadcastManager 코드를 같이 봐보자
private final Context mAppContext;
private static final Object mLock = new Object();
private static LocalBroadcastManager mInstance;
public static LocalBroadcastManager getInstance(Context context) {
synchronized (mLock) {
if (mInstance == null) }
mInstance = new LocalBroadcastManager(
context.getApplicationContext());
}
return mInstance;
}
}
private LocalBroadcastManager(Context context) {
mAppContext = context;
...
}
물론 getInstance()
메서드에서 동기화 블록을 매번 타게되서 효율이 좋다곤 할 수 없다. 여러 스레드에서 동시에 호출할 일이 많지 않아서 저렇게 짠 거. ㅏㅌ다.
코드에서 실제로 context.getApplicationContext()
를 사용하여 하나뿐인 Application 인스턴스가 mAppContext
에 대입된다.
왜 굳이 context에서 해당 get메서드를 쓰는걸까? 특히 많은 문서에서 싱글톤을 만들때 Context를 그대로 전달하는 걸 볼 수 있는데 무작정 따라하면 안된다.
실제로 위에 코드를 비슷하게 짜되 Context를 넘기는 부분에서 2가지 케이스를 둬본다. (kotlin)
1) instance = TempManager(context)
2) instance = TempManager(context.applicationContext)
저렇게 2가지 코드 순으로 두고 IDE의 Android Monitor 탭에서 Dump Java Heap
을 클릭하면 Activity에서 TempManager
를 참조하는 걸 볼 수 있다.
그리고 Initiate GC
를 클릭해서 강제로 GC를 실행하게 되는데 1번 코드면 GC 대상이 되지 않아 Activity가 남아 있는데 2번 코드는 정리되는 것을 볼 수 있다.
즉, 싱글톤에서 Context를 그대로 멤버 변수에 대입해서 사용하면 Context가 참조로 남게 되어서 메모리 누수 문제가 발생할 수 있다.
따라서 Context보다는 Context의 getApplicationContext()가 싱글톤의 멤버 변수에 대입되어야 한다.
그렇다고 싱글톤을 사용하는 쪽에서 강제하도록 하지 말자.
ex)
TempManager.getInstance(this.applicationContext).someAction()
위와 같이 넘길 때는 this
로 넘기고 메서드에서 처리하도록 하자. 위와 같이 규칙을 지켜야하면 어디선가는 실수가 있을 수 있기 때문이다.
마커 인터페이스는 메서드 선언이 없는 인터페이스로 표식용이다. Serializable
도 마커 인터페이스의 예이다.
만약 쇼핑몰 앱을 운영하는데 상품에 여러 카테고리가 있어서 카테고리마다 구매액션이 다르다면 어떻게 코드를 짤 것인가?
조건문으로 액션들을 나눠서 코드를 짜는게 가장 먼저 떠올리게 될 것이다.
if (A || B) {
action1()
}
...
if (A || C || E) {
action2()
}
...
if (C || D || E) {
action3()
}
...
그런데 이런 코드라면 코드양도 길어질뿐만 아니라 카테고리가 추가되거나 action이 여러 개 더해진다면 주의를 기울여야 한다. 그리고 순서가 중요한 케이스일 때도 있다.
그래서 이럴 때 마커 인터페이스를 써보는 것이다.
마커는 1,2,3,4 인터페이스를 만들고 각 카테고리별로 자식 클래스를 만든다.
class A : Category, Marker1, Marker3 {}
class B : Category, Marker1, Marker4 {}
class C : Category, Marker2, Marker3 {}
...
그리고 부모 클래스에서는 인스턴스를 체크해서 해당 작업을 진행한다.
if (this instanceof Marker1) {
action1()
}
...
if (this instanceof Marker2) {
action2()
}
...
이렇게하면 부모 클래스에서 각 오퍼레이션 순서가 정해져서 순서 별 액션별 더 원활하게 로직을 짤 수가 있게 된다.
파라미터가 많은 메서드도 보완이 가능하다. 특정 상태에서 특정 액션을 하고 싶을때도 Marker Interface를 둬서 조건문으로 분기칠 수가 있게 된다.
마커 애너테이션으로도 대체 가능하다
@Retention(RetentionPolicy.RUNTIME)
@interface Marker1 {}
...
val clazz = this.class
if (clazz.isAnnotationPresent(Marker1.class)) {
action1()
}
...
if...
물론 이런 경우에는 인터페이스나 애너테이션이나 별 차이가 없어 보인다.
인터페이스를 구현하면 자식 클래스에도 영향이 있지만 애너테이션을 쓰는 경우 상속과 관련없이 각 클래스마다 개별적으로 애너테이션을 지정해야한다. 그러니 방식을 이해하고 상황에 맞게 선택하면 된다.
Fragment가 생성되면서 값이 전달될때가 많다. 그래서 보통 값을 전달할때 Bundle
을 써서 전달하는데 어디서도 왜 그래야하는지 설명이 없다.
또 다른 방법으로는 setter을 둬서 값을 전달해도 된다.
하지만 Bundle 방식을 권장하는데 구성 변경과 시스템에 의한 액티비티의 강제 종료에 대응할 수 있기 때문이다. 그리고 Bundle을 써서 값을 전달하면 변수명이 외부에 노출되지 않는 장점도 있다.
물론 setter을 값을 유지할수도 있는데 그것도 유사하게 변수 setting 하는게 아니라 setArguments()
에 Bundle로 담아서 전달하는 것이다.
혹은 Fragment에서 onSaveInstanceState()
를 오버라이드해서 해도 된다.
근데 전달된 값을 유지하려고 굳이 쓰려고는 말자. 프레임워크에서 이미 제공하는 기능을 피할 이유는 없으니까