본문 바로가기
오늘은 뭘 배울까?/공식문서

[번역] 앱 아키텍처 가이드 - Android 공식 문서

by Kim Juhwan 2022. 11. 1.
1. 아키텍처
   1-1. 아키텍처란 무엇일까?
   1-2. 그렇다면 왜 아키텍처가 필요할까?

2. 아키텍처 설계 원칙
   2-1. 관심사 분리
   2-2. 데이터 모델에서 UI 도출하기
   2-3. 단일 소스 저장소

   2-4. 단방향 데이터 흐름
3. 권장 앱 아키텍처
   3-1. UI 레이어
   3-2. 데이터 레이어

   3-3. 도메인 레이어
4. 안드로이드 아키텍처를 위해 추천하는 방법들
   4-1. Layered architecture
   4-2. UI Layer
   4-3. ViewModel
   4-4. Lifecycle
   4-5. Handle dependencies
   4-6. Testing
   4-7. Models
   4-8. Naming conventions

 

 

 


 

 

이 글은 안드로이드 공식문서 중 "앱 아키텍처 가이드" 페이지를 공부하며 작성한 글입니다.
개인적인 생각이나 의견이 포함되어 있음을 알려드립니다.

1.  아키텍처

1-1. 아키텍처란 무엇일까?

나는 이렇게 표현하고 싶다.

돼지를 안창살, 삼겹살 등 부위별로 나누듯이

앱(프로젝트) 또한 UI, 데이터, 도메인 등 기능별로 나눌 필요가 있다.

그렇다면 어떻게 해야 잘 나누고 설계할 수 있을지 고민을 해야 하는데

우리는 이것을 이러한 설계를 아키텍처라고 부른다.

 

1-2. 그렇다면 왜 아키텍처가 필요할까?

공식문서에 따르면 스마트폰은 리소스가(사용할 수 있는 자원이) 제한되어 있으므로

운영체제나 사용자가 언제든지 앱 구성요소를 제거할 수 있다고 한다.

이러한 이벤트(상황)는 직접 제어할 수 있는 것이 아니기 때문에

앱 구성요소에 애플리케이션 데이터나 상태를 저장해서는 안 되고

앱 구성요소가 서로 종속되면 안 된다고 한다.

그렇다면 어떻게 해야 잘 설계할 수 있을까?

우선 공식문서에서는 잘 설계할 수 있는 원칙에 대해 설명하고 있다.

 

2. 아키텍처 설계 원칙

2-1. 관심사 분리

Activity나 Fragment에 모든 코드를 작성하는 것은 나 포함 초보들이 흔히 하는 실수다.

(나는 아직까지도 완전하게 분리하지 못하고 있으니…)

이러한 UI 기반 클래스는 정말 UI 및 운영체제와 상호작용을 처리하는 로직만을 가지고 있어야 한다.

최대한 클래스를 가볍게 유지해서 수명주기와 관련된 문제들을 피해야 한다.

→ 그렇게 하면 테스트 코드 작성도 편해진다.

 

Keep in mind that you don't own implementations of Activity and Fragment

한국어 해석이 애매해서 영문을 그대로 긁어왔다.

Activity나 Fragment의 구현체를 네가 소유하지 않는다는 것을 기억하란다.

그저 Android OS와 앱 사이의 계약을 나타내도록 이어주는 클래스일 뿐,

OS는 사용자나 메모리 부족에 의해서 언제든지 클래스를 제거할 수 있다.

그렇기 때문에 사용자 경험과 앱 관리를 위해서는 UI 클래스에 대한 의존성을 최소화하는 것이 좋다고 설명하고 있다.

 

라고 공식문서에 적혀있고 내 방식대로 설명해보자면

나는 관심사 분리를 도미노에 빗대어 설명하고 싶다.

 

 

도미노는 이전 블럭이 그다음 블록에 영향을 준다.

이전 블럭을 A, 다음 블록을 B라고 한다면

개발적인 관점에서 우리는 A가 B에 의존한다. A가 B에게 관심이 있다. 알고 있다. 등으로 표현하곤 한다.

이런 관계로 이어지면 생기는 문제가 뭐냐면 어느 한 구간에서 문제가 발생했을 때

마치 도미노가 끊임 없이 넘어지듯 문제가 전파된다는 것이다.

A에서 문제가 발생하면 B도 고쳐야 하고 C도 고쳐야 하고... Z도 고쳐야 하고

 

우리는 그래서 관심사를 분리할 필요가 있다.

도미노에서는 이러한 일을 방지하기 위해 어느 일정 규모의 도미노를 세우고 나서

한 두 칸정도 띄어 또 새로운 도미노 군집을 만든다.

실수로 도미노를 쓰러트려도 어느 일정 구간에서 더 이상 쓰러지지 않도록 하는 것이다.

 

개발에서도 똑같다.

관심사를 분리하여 코드를 작성해서 문제가 발생하더라도 해당 부분만 고치면 되도록 해야 한다.

관심사 분리하는 이유가 이것만 있는 것은 아니지만, 가장 중요한 이유라고 할 수 있을 것이다.

 

2-2. 데이터 모델에서 UI 도출하기

사실 이 문단은 100% 이해하지 못했다.

우선, 이 문단에서 persistent models라는 단어가 나오는데

이게 대체 무슨 소린고 구글링 해보니 DB를 의미하는 듯하다.

(지속적으로 저장할 수 있는 데이터 = DB)

아무튼 이 문단에서 하고자 하는 말은 DB로부터 데이터를 받아와 UI를 구성하기를 권장한다는 의미인 듯하다.

그 이유는 다음과 같다.

  • Android OS가 리소스를 확보하려고 앱을 제거해도 사용자 데이터가 삭제되지 않아서
    (앱을 제거하는데 왜 데이터가 삭제되지 않지? 잘 이해가 가지 않는다)
  • 네트워크 연결이 취약하거나 연결되어 있지 않아도 앱이 계속 작동해서
    (이건 뭐, 맞는 말이지만 유틸 앱을 제외하고 DB로만 돌아가는 상용 서비스가 얼마나 될까?)

아무튼 이런 데이터 모델 클래스를 기반으로 앱 아키텍처를 구축하면 앱의 테스트 가능성과 견고성이 더 높아진다고 한다.

 

2-3. 단일 소스 저장소

단일 소스 저장소 or 단일 진실 공급원 or SSOT라고 불리는 이것은

음… 예시를 드는 것이 이해가 더 쉬울 것 같다.

내가 동아리에서 프로젝트를 하다가 특정 디자이너에게 전달해야 할 말이 있던 적이 있다.

갠톡으로 하려고 했으나 그때 한 시니어 개발자분이 모두가 모여있는 단톡에서 말하라고 하셨다.

나는 그 디자이너분에게만 전하면 되는 말인데 다른 분께 알람이 울리는 게 실례라고 생각했었다.

하지만 팀원 간의 정보 공유나 진행 상황 공유 등 측면에서 다 같이 있는 자리에서 말하는 게 좋다고 말씀하셨다.

이게 SSOT의 대략적인 큰 개념이다.

정보를 한 곳으로 모아 누구나 접근 가능하도록 하고, 의사 결정 시에 도움이 되도록 하는 것.

조금 더 개발적인 측면에서 바라보자면 데이터를 하나의 공간에 저장하여 데이터를 항상 최신으로 유지하는 것이다.

공식문서에서는 SSOT의 장점으로 다음 3가지를 언급하고 있다.

  • 특정 유형 데이터의 모든 변경사항을 한 곳으로 일원화할 수 있다.
  • 다른 유형이 조작할 수 없도록 데이터를 보호한다.
  • 데이터 변경사항을 더 쉽게 추적할 수 있도록 한다. → 버그 발견이 쉽다.

 

2-4. 단방향 데이터 흐름

공식문서에서는 SSOT를 viewModel로 쓰고 있는 듯했다(?)

데이터는 보통 Datasource에서 UI로 흐르며

(DB → datasource → repository → usecase → viewModel → UI …)

버튼 누르기와 같은 사용자 이벤트는 UI에서 SSOT로 흐른다고 설명하고 있다.

(UI → viewModel)

아무튼 그래서 상태가 한 방향으로만 흐르고

데이터 흐름을 수정하는 이벤트는 그 반대 방향으로 흐른다는 것!

약간 이 아래 사진 느낌

 

 

 

3. 권장 앱 아키텍처

구글에서는 최소한 2개의 레이어는 포함해야 한다고 말하고 있다.

  • 화면에 데이터를 표시하는 UI 레이어
  • 비즈니스 로직을 포함하고 데이터를 노출하는 데이터 레이어
  • UI와 데이터 레이어 간의 상호작용을 간소화하고 재사용하기 위한 도메인 레이어

도메인 레이어는 필수는 아니지만 거의 다들 필수로 사용한다.

이제 각각 레이어에 대해 자세히 알아보자.

 

3-1. UI 레이어

UI 레이어는 쉽게 생각해서 받아온 데이터를 화면에 표시하는 역할을 한다.

사용자랑 상호작용을 하거나 외부 입력(버튼을 누르거나 네트워크 응답 등)으로 데이터가 변할 때마다

변경사항을 UI에 반영해야 한다.

UI 레이어는 크게 2가지로 나뉜다.

  • 화면에 데이터를 렌더링 하는 UI 요소
  • 데이터를 보유하고 이를 UI에 노출하여 로직을 처리하는 상태 홀더(viewModel)

UI 레이어에 대해서는 여기 링크에서 더 자세히 배울 수 있다. 나중에 읽어보자.

 

3-2. 데이터 레이어

데이터 레이어에는 비즈니스 로직이 포함되어 있다.

데이터 생성, 저장, 변경 방식을 결정하는 규칙이 여기에 들어간다.

(아마… 나는 repositoryImpl에 로직을 포함시키는 데 여기가 맞겠지?)

repository 클래스에서 담당하는 작업은 다음과 같다.

  • 앱의 나머지 부분에 데이터 노출
  • 데이터 변경사항을 한곳에 집중
  • 여러 데이터 소스 간의 충돌 해결
  • 앱의 나머지 부분에서 데이터 소스 추상화
  • 비즈니스 로직 포함

음, 저 나머지가 당최 무슨 의미로 쓰이는 건지 알 수가 없다.

나머지 담당 작업들에 대해서 이야기를 좀 더 풀어보자면

repository는 데이터가 어디서 오는 것인지 관심이 없다.

Local에서 오든 네트워크에서 오든 같은 비즈니스 로직을 수행할 뿐이다.

이것이 가능한 이유는 repository 이전 단계에 Datasource가 있기 때문이다.

Datasource가 Local에서 값을 가져오는지 Remote에서 값을 가져오는지는 관심이 없다.

이것을 아마 공식문서에서는 여러 데이터 소스 간의 충돌 해결이라고 작성한 것 같다.

데이터 변경사항을 한곳에 집중한다는 것은

repositoryImpl에 비즈니스 로직을 포함시키기 때문에

데이터가 UI 레이어에 도달하기까지 변경되는 곳은 repositoryImpl 딱 한 군데이기 때문에

한곳에 집중한다.라고 표현을 한 듯하다.

데이터 레이어에 대한 보다 더 자세한 내용은 여기에서 확인할 수 있다.

 

3-3. 도메인 레이어

도메인 레이어는 선택사항이다. 없어도 된다.

근데 클린 아키텍처를 사용한다는 앱들을 쭉 보면 항상 사용하는 듯하다.

도메인 레이어는 잡한 비즈니스 로직, 또는 여러 viewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당한다.

도메인 레이어에는 UseCase가 작성되는데, UseCase는 하나의 기능만을 담당해야 한다.

그리고 공식문서에는 적혀있지 않았지만, 도메인은 순수한 코틀린 코드만을 포함한다.

안드로이드 프레임워크나 외부 라이브러리에 의존하지 않아야 한다.

도메인 레이어에 대한 보다 더 자세한 내용은 여기에서 확인할 수 있다.

 

4. 안드로이드 아키텍처를 위해 추천하는 방법들

여기부터는 공식문서에서 한국어를 지원하지 않아 해석이 모호하거나 잘 이해하지 못한 부분이 있을 수도 있다.

 

이 페이지는 아키텍처를 연습하기 좋은 예제들을 다루고 있다.

앱의 퀄리티나 확장성, 테스트 등에 이점이 많으나 엄격하게 지켜야 하는 것은 아니고

원하는 만큼 채택해서 사용하라고 적혀있다.

아래 예제들을 쭉 나열할 건데, 예제마다 중요도를 다음과 같이 나타냈다.

표에는 맨 앞 문자를 따서 S, R, O로 표기할 것이다.

  • Strongly recommanded: 강력하게 추천
  • Recommended: 하는 것을 추천
  • Optional: 선택 사항

 

4-1. Layered architecture

권고사항 중요 설명 비고
Data 레이어를 사용해라 S Data 레이어는 대부분의
비즈니스 로직을 포함한다.
- 1개의 data source를 사용하더라도
repository를 만들어주세요.

- 규모가 작은 앱에서는 data 패키지를 써도
되고 모듈을 써도 됩니다.
UI 레이어를 사용해라 S UI 레이어는 데이터를 화면에 표시하며
사용자 상호 작요의 기본 지점 역할을 한다.
- 규모가 작은 앱에서는 ui 패키지를 써도 되고 모듈을 써도 됩니다.
Data 레이어는 repository를
사용해 데이터를 노출해라
S Composable, activity 혹은 viewModel과
같은 UI 레이어에 속한 컴포넌트는
data source와 직접 상호작용하면 안된다.
data source의 예시
: Database, DataStore, SharedPrefereneces, Firebase APIs, GPS location providers, Bluetooth data providers, Network connectivity status provider
coroutine과 flows를 사용해라 S 레이어 간의 커뮤니케이션을 위해 coroutine과 flow를 사용하라
Domain 레이어를 사용해라 R 다음과 같은 경우에 Domain 레이어를 사용하라

1. 여러 viewModel에서 데이터 레이어와 상호 작용하는 비즈니스 로직을 재사용 하는 경우
2. 특정 viewModel의 비즈니스 로직을 단순화 하려는 경우

Clean Architecture를 적용할 때 패키지로 나눠도 문제없다는 점을 이번에 새로 알았다.

거의 모든 예제가 모듈로 나눠져 있어서 약간 필수라고 생각했는데

구글 피셜 작은 앱은 패키지로 나눠도 상관없는 걸로...!

(하긴.. 앱이 커져서 + 다양한 이유 때문에 모듈로 나누는 거니까 레이어를 나누려면 꼭 모듈을 써야 해!라는 법은 없는 듯)

 

4-2. UI layer

권고사항 중요 설명
단방향 데이터 흐름
(UDF)을 따라라
S UDF 원칙을 따라
viewModel은 observer 패턴을 사용해 UI에게 상태를 전달하고
method call을 통해서 UI로 부터 action을 받는다.
앱에 이점을 가져다 줄 수 있다면
AAC viewModel을 사용해라
S AAC viewModel로 비즈니스 로직을 처리하고
UI에게 상태를 전달한다.
생명주기를 알고 있는
상태관리 collection을 사용해라
S coroutine의 repeatOnLifecycle이나
compose의 collectAsStateWithLifecycle을 사용해라
viewModel에서 UI로
이벤트를 보내지 말아라
S viewModel에서 바로 이벤트를 처리하고
이벤트 처리 결과와 함께 상태 업데이트를 수행해라
하나의 activity를 사용해라 R 2개 이상의 화면이 있다면
Navigation Fragments나 Navigation Compose를 사용해라
Jetpack Compose를 사용해라 R 음.. compose 써보라고 권장함
이유는 딱히 써있지 않음

Activity와 Fragment가 어떤 장단점이 있고 하는 부분은 알고 있었지만

구글이 공식적으로 하나의 Activity를 사용하라고 권장하고 있는지는 이번에 처음 알았다. (Strong Recommend는 아니지만)

Google I/O 2018에서 이미 Single Activity Achitecture(SAA)에 대해 언급을 했었다고 한다.

SAA 구조로 코드를 작성해왔지만 뭔가 확실한 이유 없이 써서 찜찜한 기분이었는데

구글에서 확정 땅땅땅 해준걸 알았으니 이제 맘편히 사용해야겠다.

 

4-3. viewModel

권고사항 중요 설명
viewModel은 생명주기에
영향받지 않아야 한다
S viewModel은 생명주기와 관련된 타입을 참조해서는 안된다.
예를 들어 activity, fragment, context, resource와 같은 애들을 의존하면 안된다는 것이다.
만약 viewModel에서 context가 필요하다면,
과연 내가 올바른 레이어에 코드를 작성하고 있는 것인지 진지하게 다시 생각해보아라
(한마디로 잘못 짰다는 소리다)
coroutine과 flows를 사용해라 S viewModel은 flow, suspend, viewModelScope를 이용해
데이터 레이어와 상호작용 한다.
화면 수준에서
viewModel을 사용해라(?)
S viewModel을 재사용 가능한 UI 조각에 사용하지 말아라 (?)

다음과 같은 용도로 사용해야 한다.
- 화면 수준의 composable
- View 안의 activity, fragment
- graph의 destination
순수한 state holder
클래스를 사용해라...?
S 공부 필요
AndroidViewModel을
사용하지 마라
R AndroidViewModel대신 viewModel을 사용해라
Application 클래스는 viewModel에서 사용하면 안된다.
대신에 UI나 데이터 레이어를 의존하도록 해라
UI state를 노출해라 R 공부 필요

이번 목차는 모르는 부분이 많은 듯...

application context가 필요하면 AndroidViewModel을 사용하고

그 외의 경우는 viewModel을 사용하라고 알고 있긴 했는데

AndroidViewModel 쓰지 마!라고 할 줄은 몰랐다. (그럴 거면 왜 만들어 놓은 건데...)

AndroidViewModel이 필요한 경우는 아직까지 없었으나 기억해둬야겠다.

 

4-4. Lifecycle

권고사항 중요 설명
생명주기 메서드를
오버라이딩 하지 말아라
S onResume 같은 거 오버라이딩 하면 안된다
대신 LifecycleObserver를 사용해라
만약 어떤 라이프 상태에 도달했을 때 해야하는 작업이 있다면
repeatOnLifecycle API를 사용해라

 

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

예를 들어 이런 느낌.

근데 도대체 쓰지 말라고 할 거면 왜 오버라이딩 가능하게 해 둔 걸까? 흠...

사용하면 안 되는 이유는 대체 뭐고? 이유까지 알려줘야 납득을 할거 아닌가 😢

구글링을 해봤지만 관련 내용을 찾을 수 없었다.

 

4-5. Handle dependencies

권고사항 중요 설명
DI(의존성 주입)을 사용하라 S 가능한 경우 주로 생성자 주입을 사용함
필요한 경우
컴포넌트의 범위를 지정하라
S 공유되거나 초기화 하기에 비용이 많이들고 앱 전반적으로 사용되는
mutable한 데이터 타입이라면
dependency container로 범위를 지정해라
Hilt를 사용해라 R 간단한 앱이면 메뉴얼 의존성 주입 혹은 hilt를
복잡한 앱이면 hilt를 써라

컴포넌트 범위를 지정하라는 말이 이해가 가지 않아서 관련 문서를 읽어보니

내가 이해한 게 맞다면, 이건 hilt와 같은 DI 라이브러리를 사용하지 않았을 때의 이야기다.

DI 라이브러리 없이 의존성 관리를 하는 방법 중 하나의 노하우 정도로 보면 될 것 같다.

(앱 전반적으로 사용되는 놈을 어떻게 관리해야 할까에 대한 이야기)

 

보통 다들 라이브러리를 사용하기 때문에

이 부분은 관련 문서에 있는 코드를 보고

라이브러리가 없을 때는 이렇게 했구나 정도로 생각하고 넘어갔다.

 

4-6. Testing

권고사항 중요 설명 비고
무엇을 테스트해야
하는지 알아야 한다
S Hello world 급 앱이 아닌이상 테스트해라
(사실상 하란 소리)
최소한 이정도는 테스트 해라

- flow를 포함한 viewModel
- Data layer entity
- UI Navigation
mock보다 fake를 선호해라 S 참고
StateFlow를 테스트해라 S StateFlow를 테스트 할때
- 가능하다면 언제든 프로퍼티 값을 검증하라
- 만약 WhileSubscribed를 사용한다면 collectJob을 생성해라

테스트 코드 작성은 하면 당연히 좋지만

현실적으로 회사에서 빠듯한 개발 일정에 치여 진행하기가 어렵다고 들었다.

그래서 옵션이나 권장 정도겠거니... 했는데 중요도가 높아서 놀랐다.

테스트에 대한 지식이 없다 보니 이 목차는 제대로 이해하지 못했다. 언젠가 날 잡아서 공부해야겠다.

 

4-7. Models

권고사항 중요 설명 비고
복잡한 앱에서는
레이어별로 모델을 만들어라
R 복잡한 앱에서는 여러 계층이나
컴포넌트에 모델을 만들어라
- remote data source는
네트워크를 통해 받은 모델을
앱이 필요로 하는
간단한 클래스로 맵핑(변환)할 수 있다.

- Repository는 DAO 모델을
UI 레이어가 필요로 하는 정보를 가진
간단한 데이터 클래스로 맵핑할 수 있다.

- viewModel은 UiState 클래스에
Data 레이어 모델을 포함할 수 있다.

레이어마다 모델이 있는 구조는 유지 보수를 용이하게 해 준다.

만약 모델이 하나라면 앱은 서버가 정의한 모델 구조에 의존적일 수밖에 없다.

서버에서 변동사항이 생길 때마다 영향을 받게 된다.

하지만 레이어 별로 모델이 있다면 의존하지 않게 되므로 유지 보수 상황에 유리하다.

그렇기 때문에 공식 문서에서는 레이어별로 모델을 만들 것을 권장하고 있는 것 같다.

 

4-8. Naming conventions

권고사항 중요 설명 비고
메서드 네이밍 O 메서드는 동사 구문이여야 한다. 예) makePayment()
프로퍼티 네이밍 O 프로퍼티는 명사 구문이여야 한다. 예) inProgressTopicSelection
데이터 스트림 네이밍 O 클래스가 Flow, LiveData 혹은 다른 스트림을 노출할 때
네이밍은 get{model}Stream()으로 하면 된다.
interface implement 네이밍 O 인터페이스 구현체 네이밍은 의미가 있어야 한다.
더 나은 이름을 찾을 수 없다면
접두사로 Default를 사용하라
예를 들어 NewsRepository의 구현체를 만들땐
OfflineFirstNewsRepository,
InMemoryNewsRepository
와 같은 네이밍을 사용할 수 있다.

만약 좋은 이름을 찾지 못하겠다면
DefaultNewsRepository를 사용하면 된다.

Fake 구현체는 접두사로 Fake를 붙이면 된다.
예를 들어
FakeAuthorsRepository 처럼 말이다.

이번 목차에서는 다른 것 보다도 구현체 네이밍에 대한 것이 나에겐 새로운 내용이었다.

NewsRepository의 구현체를 만들 때는 항상 NewsRepositoryImpl 이렇게 만들어 왔기에...

그리고 DataSource는 로컬이나 네트워크를 구분하니까 접두사로 Local, Remote 따위를 붙인다지만

Repository는 하나만 존재하는 구조로 짜 와서 새로웠다. 🤔

 

그리고 좋은 이름을 찾지 못하면 DefaultNewsRepository처럼 접두사에 Default를 붙이라는 것도..

어떻게 보면 Default라는 게 그렇게 큰 의미를 담고 있나? 굳이 넣어야 하나? 싶기도 하다.

차라리 Impl을 붙여서 이건 구현체야 라는 정보를 주는 게 나은 것 같은데...

평소에 다른 레포를 볼 때 코드 잘 짜는 사람들은 어떻게 하는지 보고 결정해야겠다.

 


💡 느낀 점

  • 공식문서를 이렇게 통으로 번역하면서 공부한 적은 처음인데, 역시 공식문서에서 배울 점이 많은 것 같다.
  • 내가 구조적으로 잘 짜고 있는 건가? 고민이 될 때가 많았는데 [목차 4]가 이러한 고민에 많은 도움이 된 것 같다.
  • 문서를 읽으면서 질문이 왕창 생겼다. 가장 궁금한 건...
    • [목차 4-4] 왜 생명주기 관련 메서드를 오버라이딩 하면 안 되는 걸까?
    • [목차 3-2], [목차 3-3]을 보면 비즈니스 로직이 데이터 레이어에도 들어가고 도메인에도 들어간다.
      둘의 차이는 뭐고 어떤 비즈니스 로직을 어디에 넣어야 하는 걸까?

📘 참고한 자료


 

 

반응형

댓글