안드로이드 정적 팩토리 메서드 패턴 프래그먼트 생성

Kim JuYoung·2023년 10월 27일
0

Android Libraries

목록 보기
3/4
post-thumbnail

✏️ 공부하면서 잘못된 정보가 작성될 수 있습니다. 해당 부분은 댓글로 지적해주시면 감사하겠습니다.


Static Factory Method Pattern

companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) = BlankFragment().apply {
            arguments = Bundle().apply {
                putString(ARG_PARAM1, param1)
                putString(ARG_PARAM2, param2)
            }
        }
    }

안드로이드 스튜디오에서 Fragment를 생성하고 자동완성되는 코드를 보면 newInstance() 메서드가 생성되는 것을 볼 수 있다.

val fragment = BlankFragment()

일반적으로 사용하던 프래그먼트 생성 코드이다. 혹시 나처럼 프래그먼트를 생성하고 있지 않았는지

유튜브에서 안드로이드 앱 개발을 배울 때 거의 모든 유튜버들이 해당 코드를 제거해서 습관적으로 제거하면서 사용했는데, 문득 해당 코드가 왜 쓰이는지 궁금해졌고 이제는 해당 코드가 나름 중요하게 쓰이는 것을 알게되었다.



정적 팩토리 메서드 패턴

팩토리 패턴의 종류

일반적으로 팩토리 패턴이란, 대게 객체를 생성할 때 사용되는 new 키워드를 직접 사용하는 대신에,

객체의 생성을 담당하는 다른(일명 팩토리 객체)객체를 통해서 객체를 생성함으로써, 객체간의 의존 관계를 느슨하게 만드는 효과를 기대하는 디자인 패턴이다.

여기서 추상 팩토리 메서드를 갖는 추상 클래스를 상속 받아서 객체를 생성하는 서브 클래스를 통해서 객체의 생성을 위임하는 패턴이 바로 팩토리 메서드 패턴이다.

그리고 객체를 생성하는 생성자 대신 정적 메서드를 사용하여 객체의 생성을 제공하면 정적 팩토리 메서드 패턴이다. 팩토리 메서드 패턴과 얼추 비슷해보이지만 구현 방식에 있어서 조금 다르다.


정적 팩토리 메서드 패턴의 장점

정적 팩토리 메서드 패턴의 장점은 생성자 대신 정적 메서드를 사용하여 객체를 생성하므로, 가독성이 좋아지고 객체의 생성을 일관되게 유지할 수 있다는 장점이 있다.


프래그먼트에서의 정적 팩토리 메서드 패턴

정적 팩토리 메서드 패턴의 구현

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class BlankFragment : Fragment() {
    private var param1: String? = null
    private var param2: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        return inflater.inflate(R.layout.fragment_blank, container, false)
    }

    companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) = BlankFragment().apply {
            arguments = Bundle().apply {
                putString(ARG_PARAM1, param1)
                putString(ARG_PARAM2, param2)
            }
        }
    }
}

해당 코드의 newInstance 메서드의 구현부를 보자.

메서드의 매개변수로 인자값을 2개를 받고 프래그먼트 생성자를 프래그먼트를 생성하고 대입하는데, 생성을 하는 동시에 apply 키워드를 통해서 프래그먼트의 Bundle에 매개값을 넣고 있다.

companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String): BlankFragment {
            return BlankFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
        }
    }

본래 메서드를 위 메서드로 변경할 수 있다.

💡 @JvmStatic을 사용한 이유는 이 코드가 자바 코드에서 사용될 경우 자바에서 접근성을 위해 어노테이션을 설정한 것이다. 만약 자바에서 동작할 필요가 없으면 해당 코드를 제거해도 된다.


정적 팩토리 메서드 패턴의 프래그먼트 구현의 이유 및 장점

프래그먼트를 생성할 때 번들(Bundle)을 사용하여 데이터를 전달하거나 저장할 수 있는데,

이렇게 Bundle을 사용하면 화면이 회전되거나, 메모리 부족과 같은 환경에서 프래그먼트가 재생성되는 부분에서 데이터 유실을 방지할 수 있다.

만약 프래그먼트 생성자에 직접적으로 매개변수를 입력한다면 데이터가 유실되어 에러가 발생하지만 Bundle을 사용하면 에러가 생기는 것을 방지하고 데이터를 보존할 수 있다.

이렇게 Bundle을 구성하는데 있어서 일관된 생성 규칙을 사용하여 프래그먼트를 생성하면 가독성 및 일관성을 유지하는데 도움이 되는데,

정적 팩토리 메서드 패턴을 통해 메서드를 구현하면, 프래그먼트를 생성하는 내부적인 표준을 정함으로써 인스턴스를 생성할 때 잘못된 방식으로 초기화를 하는 경우를 방지하고 매개변수 상태를 저장하고 복원하기 위해서다.

게다가 프래그먼트의 초기화 코드가 변경되더라도 메서드만 변경하면 되기 때문에 기존에 사용되던 코드는 변경될 필요가 없다.

이러한 이유 때문에 프래그먼트를 생성할 때 정적 팩토리 메서트 패턴을 통해 Bundle 세팅을 해서 프래그먼트 재생성을 대비하는 것이다.


코드 구현의 주의점

해당 코드를 코틀린으로 구현하는데 있어서 주의해야할 부분이 존재하는데 바로 companion object 내부에 정적 상수를 구현하는 것이 아니라 최상위에 상수를 만드는 것이다.

해당 부분은 여기에서 자세히 찾을 수 있었으며 번역하면 다음과 같다.

Java에서는 모든 정적 필드와 메서드 선언을 클래스 안에 넣어야 합니다. 때로는 그 목적만을 위해 클래스를 생성해야 할 때도 있습니다. Kotlin으로 넘어오면서 많은 사용자들이 습관적으로 이와 동등한 기능을 찾게 되고, 결과적으로 컴패니언 오브젝트를 과도하게 사용하게 됩니다.

Kotlin은 파일과 클래스의 개념을 완전히 분리합니다. 하나의 파일에 여러 개의 public 클래스를 선언할 수 있습니다. 또한 private한 최상위 함수와 변수를 선언하면 해당 파일 내의 클래스에서만 접근할 수 있습니다. 이는 밀접하게 연관된 코드와 데이터를 구성하는 데 아주 유용합니다.

최상위 선언에 비해 컴패니언 오브젝트의 문법은 다소 번잡합니다. 특정한 public 정적 코드나 데이터를 클래스와 연결하고 싶을 때, 그리고 사용자에게 클래스 이름으로 그것에 접근하도록 하고 싶을 때만 사용해야 합니다. 이러한 사용 사례는 매우 드물며, 대부분의 경우 최상위 선언이 더 자연스럽습니다.

클래스와 연결하고 싶은 private 정적 코드/데이터가 있을 때는 private한 최상위 선언이 더 나을 것입니다.

마지막으로, 때로는 생성된 바이트코드에 대한 고려사항이 중요합니다. 어떤 이유로든 Kotlin 코드로 Java 클래스를 생성해야 하며, 그 클래스에 정적 멤버가 있어야 한다면, 컴패니언 오브젝트와 특별한 어노테이션을 사용해야 합니다.

설명을 통해 알 수 있는 부분은 일반적으로 자바에서 모든 정적 필드와 메서드 선언은 class 안에 존재해야하지만, 코틀린은 불필요한 객체 생성을 피하기 위해 파일과 클래스 개념이 완전히 분리되어 설계되었기 때문에 굳이 그럴 필요가 없다고 한다.

static String SOURCE = "study";

public class Fragment { }

실제로 자바에서 class 밖에 상수를 선언하면 에러가 생기지만,

const val SOURCE = "source"

class Fragment { }

코틀린에서는 에러가 생기지 않는다.

그래서 private 로 변수를 만들고 최상위에 존재하는 상수 부분과 연관된 코드와 데이터를 구성한다면 유용하게 사용될 수 있다고 한다.

또한, companion object 내부의 상수에 접근하여 키값이라던가 이러한 부분으로 사용되는 것은 바이트 코드의 생성부분에서 봐도 번잡하다고 한다.

public 정적 데이터를 클래스와 연결하고 싶을 때, 그리고 사용자에게 클래스 이름으로 데이터에 접근하고자 할 때 companion object를 사용하는 것이 자연스럽다고 한다.


실제 프래그먼트 사용

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                .replace(R.id.fragment, BlankFragment.newInstance(1, 1))
                .commit()
        }
    }
}
BlankFragment.kt
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class BlankFragment : Fragment(), View.OnClickListener {

    private var param1: Int = 0
    private var param2: Int = 0

    private var text1: TextView? = null
    private var text2: TextView? = null
    private var button: Button? = null


    companion object {
        @JvmStatic
        fun newInstance(param1: Int, param2: Int): BlankFragment {
            return BlankFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_PARAM1, param1)
                    putInt(ARG_PARAM2, param2)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getInt(ARG_PARAM1)
            param2 = it.getInt(ARG_PARAM2)
        }

        savedInstanceState?.let {
            param1 = it.getInt(ARG_PARAM1)
            param2 = it.getInt(ARG_PARAM2)
        }

    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(ARG_PARAM1, param1)
        outState.putInt(ARG_PARAM2, param2)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        val view = inflater.inflate(R.layout.fragment_blank, container, false)

        text1 = view.findViewById(R.id.param1)
        text2 = view.findViewById(R.id.param2)

        button = view.findViewById(R.id.button)
        button?.setOnClickListener(this)

        changeText()

        return view
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.button -> {
                param1 += 1
                param2 += 1
                changeText()
            }
        }
    }

    private fun changeText() {
        text1?.text = param1.toString()
        text2?.text = param2.toString()
    }
}

이렇게 코드를 작성하면 화면을 회전하더라도 데이터가 사라지지 않는다.

정적 팩토리 메서드 패턴을 사용하여 Fragment 생성에 new 키워드 (코틀린에는 없지만 객체 생성자라고 보자)를 직접 사용하지 않고 객체 생성의 일관성을 유지하면서,

Bundle에 데이터를 저장하고 복구하면 화면을 회전하더라도 데이터 유실을 방지할 수 있고 데이터를 복원하여 에러가 생기지 않는다.


BlankFragment.kt
class BlankFragment(param1: Int, param2: Int) : Fragment(), View.OnClickListener {

    private var param1: Int = 0
    private var param2: Int = 0

    private var text1: TextView? = null
    private var text2: TextView? = null
    private var button: Button? = null

    init {
        this.param1 = param1
        this.param2 = param2

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        val view = inflater.inflate(R.layout.fragment_blank, container, false)

        text1 = view.findViewById(R.id.param1)
        text2 = view.findViewById(R.id.param2)

        button = view.findViewById(R.id.button)
        button?.setOnClickListener(this)

        changeText()

        return view
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.button -> {
                param1 += 1
                param2 += 1
                changeText()
            }
        }
    }

    private fun changeText() {
        text1?.text = param1.toString()
        text2?.text = param2.toString()
    }
}

그러나 이렇게 프래그먼트 생성자에 매개변수를 넣는 경우 화면 회전을 하면 에러가 발생하고 앱이 종료된다.

그렇기 때문에 프래그먼트 생성자에 매개변수를 넣는것은 허용되지 않으며 값을 보존하기 위해서는 Bundle 혹은 ViewModel과 같은 방법을 사용해야한다.


MainActivity.kt (다른 파일)
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navView: BottomNavigationView = binding.navView

        val navController = findNavController(R.id.nav_host_fragment_activity_main) // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
            )
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)
    }
}

그리고 이건 네비게이션을 이용하는 코드인데 생성자를 이용해서 프래그먼트를 생성하는 부분이 없기도 하고 값을 보존하려면 Bundle을 이용하는 메서드 패턴을 이용하여 구현하는 것은 거의 필수라고 봐도 된다.

아니면 ViewModel을 구현하거나



profile
안드로이드와 인공지능에서 살아남기

0개의 댓글

관련 채용 정보