Android 구현 패턴

woga·2024년 7월 21일
0

Android 공부

목록 보기
49/49
post-thumbnail

Reference

안드로이드 프로그래밍 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를 그대로 전달하는 걸 볼 수 있는데 무작정 따라하면 안된다.

  • 싱글톤에 그대로 넘긴 Activity는 GC에도 제거되지 않는가?
  • 싱글톤에서 Context에서 getApplicationContext로 대입되면 GC에 Activity가 제거되는가?

실제로 위에 코드를 비슷하게 짜되 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도 마커 인터페이스의 예이다.

만약 쇼핑몰 앱을 운영하는데 상품에 여러 카테고리가 있어서 카테고리마다 구매액션이 다르다면 어떻게 코드를 짤 것인가?

  • A/B는 action 1
  • A/C/E는 action2
  • C/D/E는 action3
  • B/D/E는 action 4

조건문으로 액션들을 나눠서 코드를 짜는게 가장 먼저 떠올리게 될 것이다.

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 정적 생성

Fragment가 생성되면서 값이 전달될때가 많다. 그래서 보통 값을 전달할때 Bundle을 써서 전달하는데 어디서도 왜 그래야하는지 설명이 없다.

또 다른 방법으로는 setter을 둬서 값을 전달해도 된다.

하지만 Bundle 방식을 권장하는데 구성 변경과 시스템에 의한 액티비티의 강제 종료에 대응할 수 있기 때문이다. 그리고 Bundle을 써서 값을 전달하면 변수명이 외부에 노출되지 않는 장점도 있다.

물론 setter을 값을 유지할수도 있는데 그것도 유사하게 변수 setting 하는게 아니라 setArguments()에 Bundle로 담아서 전달하는 것이다.

혹은 Fragment에서 onSaveInstanceState()를 오버라이드해서 해도 된다.
근데 전달된 값을 유지하려고 굳이 쓰려고는 말자. 프레임워크에서 이미 제공하는 기능을 피할 이유는 없으니까

profile
와니와니와니와니 당근당근

0개의 댓글