본문 바로가기

내일배움캠프 안드로이드 3기

[TIL] 24.01.22

1. 알고리즘 문제 해결

 

부분수열의 합 2(백준 1208):

 저번에 풀었던 부분수열의 합 문제와 유사한데, N이 40개까지 주어질 수 있다는 점이 다르다. 순수하게 조합되는 경우의 수만 따져도 2^40이기 때문에 저번에 풀었던 방법으로는 풀 수 없다. 처음에는 dp를 이용해서 풀려고 했다. 주어진 수열에서 나올 수 있는 값들에 대한 카운트 배열을 두 개 만들어두고, 원소 1개를 선택할 때부터 시작해서 배열을 번갈아가며 선형 탐색 및 업데이트 하는 방식이었다. 근데 이때 나올 수 있는 값의 범위가 [-100,000 * 40, 100,000 * 40]이기 때문에 이 배열을 n번 탐색하는 것은 시간이 초과된다. 

 그래서 수열을 분할하여 풀었다. 수열을 절반으로 나누어 왼쪽 수열 에 대해 나올 수 있는 합 별로 갯수를 구해 Map에 저장해두고, 오른쪽 수열 에 대해 나올 수 있는 경우들에 대해 각각 Map[S-현재 경우의 합]만큼 카운트 해주는 것이다. 크기가 20개일 경우 모든 조합을 따지더라도 1,024*1,024 정도의 경우의 수로, 충분히 빠르게 구해낼 수 있기 때문에 두 개의 20개짜리 수열에 대해 조합을 구하는 것은 1초 안에 실행될 수 있다. 물론, 오른쪽 수열에서 부분수열을 따지는 모든 경우에 Map을 탐색하게 되지만, Map 자료구조의 탐색에 걸리는 시간은 O(log n)이기 때문에 문제 없다. 코드는 아래와 같다.

 

#include <iostream>
#include <vector>
#include <map>

using namespace std;

vector<int> nums;
map<int, int> m;
long long cnt = 0;
int s;

void combForMap(int prev, int current, int depth, int sum) {
    if(current > depth) {
        m[sum]++;
    }else {
        for(int i=prev+1; i<nums.size()/2; i++) {
            combForMap(i, current+1, depth, sum+nums[i]);
        }
    }
}

void comb(int prev, int current, int depth, int sum) {
    if(current > depth) {
        cnt += m[s-sum];
    }else {
        for(int i=prev+1; i<nums.size(); i++) {
            comb(i, current+1, depth, sum+nums[i]);
        }
    }
}

int main() {
    int n;
    cin >> n >> s;

    nums = vector<int> (n);

    for(int i=0; i<n; i++) {
        cin >> nums[i];
    }

    for(int i=0; i<=nums.size()/2; i++) {
        combForMap(-1, 1, i, 0);
    }
    for(int i=0; i<=nums.size()-(nums.size()/2); i++) {
        comb(nums.size()/2-1, 1, i, 0);
    }

    if(s==0) {
        cnt--;
    }

    cout << cnt;

}

 

 하나 특이할 것이라고 한다면 s(문제에서 말하는 S값)이 0일 경우, cnt값을 하나 빼주고 있다. s가 0인 경우에는, 탐색과정에서 왼쪽 수열에서 아무것도 고르지 않은 경우+오른쪽 수열에서 아무것도 고르지 않은 경우의 케이스가 하나 포함되기 때문에(문제조건에서 크기가 양수인 부분수열로 제한됨) 이를 제외해준 것이다.

 

 

 

암호 만들기(백준 1759): 

 주어지는 문자들을 받은 다음, 오름차순 정렬해두고 DFS로 조합을 찾으면 바로 해결 되는 문제이다. 조합의 수가 가장 많아지는 케이스는 C가 15이고, L이 7또는 8인 경우로, 시간은 충분하다. L개의 문자를 선택한 시점에, 자음 수와 모음 수의 조건만 추가로 확인해주면 된다.

 

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int l, c, cons=0, vow=0;
vector<char> cArr;
vector<char> ans;

bool isVowel(char ch) {
    return ch=='a' || ch=='e' || ch=='i' || ch=='o' || ch=='u';
}

void comb(int prev, int current) {
    if(current > l) {
        if(cons<2 || vow<1) {
            return;
        }
        for(auto ch: ans) {
            cout << ch;
        }
        cout << "\n";
        return;
    }else {
        for(int i=prev+1; i<c; i++) {
            ans.push_back(cArr[i]);
            if(isVowel(cArr[i])) {
                vow++;
            }else {
                cons++;
            }

            comb(i, current+1);

            if(isVowel(cArr[i])) {
                vow--;
            }else {
                cons--;
            }
            ans.pop_back();
        }
    }
}

int main() {

    cin >> l >> c;

     cArr = vector<char>(c);
   
    for(int i=0; i<c; i++) {
        cin >> cArr[i];
    }

    sort(cArr.begin(), cArr.end());

    comb(-1, 1);
}

 

 

 

2. 안드로이드 공부

 따로 듣고 있는 강의에선 안드로이드의 클린 아키텍처인 Data - Domain - Presentation 구조를 사용하고 있다. 이전 강의에서 한번 사용한 바 있고, 실제로 이를 활용해서 간단한 앱을 클린 아키텍처로 혼자 구현해보기도 했다. 하지만 강의에서 다루는 부분이 실무에 가까운 심화 파트라 그런지, 여태 알고 있던 것보다 훨씬 복잡한 형태로 앱이 구성되고 있다.
 모든 상세한 패키지나 레이어 구조를 이해할 수는 없어서 어느정도 핵심만 제대로 이해하려고 하고 있다. 개발자들 간의 디테일 차이도 어느정도 있는 것 같고.. 

 

 스스로 공부한 부분들을 간단하게 정리해보려고 한다. 큰 틀에서 보면, 구글에 클린 아키텍처를 검색했을 때 나오는 그 그림을 봐야한다.

 

 가장 기본적인 원칙은 의존성 규칙이다. 화살표가 가리키는 것처럼, 각 코드의 의존성은 바깥에서 안으로 향해야 한다. 안쪽에 위치한 고수준 정책이 바깥쪽에 위치한 저수준 정책의 변경에 영향을 받지 않아야 한다는 의미이다. 실제 데이터의 흐름은 바깥쪽 레이어에서 안쪽 레이어로, 또 다시 안쪽 레이어에서 바깥쪽 레이어로, 연속적인 흐름을 가지게 될 것이다.

 

 안드로이드에서는 다음의 구조를 통해 클린 아키텍처를 구현하고 있다.

-Presentation 레이어: Android SDK 및 Presenter 영역을 포함한다. 주로 유저와 상호작용 하게 되는 Interface Adapters 및 Frameworks&Drivers가 이 곳에 속하게 된다.

-Domain 레이어: 클린 아키텍처의 Entities와 Use Cases을 포함하는 레이어이다. 핵심 비즈니스 로직이 이 곳에 담기게 된다. 다만, 구현상 일부 로직은 Data 레이어에 담기게 될 수 있지만 후술할 내용을 통해 구조적으로 클린 아키텍처를 유지하게 된다.

-Data 레이어: Repository 및 Date Source(DB, Network) 영역을 포함한다. 실제 데이터를 활용하는 Interface Adapters 및 Frameworks&Drivers가 이 곳에 속하게 된다.

 

 그렇다면 Presentation -> Domain <- Data 의 방향으로 의존성을 가져야 하는 것은 명확하다. 하지만 단순하게 저 3개의 레이어를 구성하게 되면 Domain의 UseCase들이 Data의 Repository에 의존하게 되는 구조가 만들어 질 것이다(데이터를 처리하는 부분이 Repository에 담기기 때문에).

 그래서 이용하게 되는 것이 Interface이다. Repository Interface를 Domain 내에 생성해 Repository에서 이를 구현하도록 해서 의존성을 클린 아키텍처에 맞게 역전 시키는 것이다. I~~Repository니, ~~RepositoryImpl이니 하는 것들로 굳이 분리해서 사용하는게 이 때문이다.

 그리고 Data 레이어에선 사용되는 DTO나 Entity를 실제 Domain 레이어에서 이용되는 Entity(비즈니스 로직에서 이용되는 Model)로 매핑해서 제공해야 한다. 이는 일반적으로 Mapper를 따로 생성하여 사용하는 것 같다.

 이렇게 구성하고 나면 Domain 레이어는 Data 레이어에 대한 의존성을 더 이상 가지지 않게 된다. 

 

 Presentation 레이어의 경우, MVVM에 기반한 구조를 위해 ViewModel을 이용하게 된다. View를 담당하는 코드는 오로지 보여지는 UI만 신경쓰면 되고, 데이터 및 관련 로직은 ViewModel 내에서 구현한다. 이 때 xml 기반이라면 DataBinding을 활용하여 이를 효과적으로 결합할 수 있고, Compose 기반이라면 State 변경을 감지하여 효과적으로 결합할 수 있다. 

 

 종합해보면 Presentation 레이어의 View의 상호작용이 ViewModel에 의해 처리되는데, ViewModel은 Domain 레이어의 Use Case 클래스를 이용해 처리하게 된다. Use Case는 Domain 레이어 내부에 선언된 DataSource나 Repository의 Interface의 메소드들을 이용할 뿐이며, 이들의 구현체는 Data 레이어 내에 존재한다. Data 레이어에선 구현체 내의 로직을 따라 데이터를 Local, Remote(network) 등에서 처리하고, 이를 다시 비즈니스 로직에 맞는 Model로 매핑하여 리턴 값을 제공한다. 이 리턴 값은 다시금 역순으로 전달되고 View까지 도달하게 된다.

 

 여기까지가 내가 지금까지 안드로이드의 클린 아키텍처에 대해 이해한 부분이다. 틀린 부분이 있을 수 있지만 다른 강의를 들으면서, 또 직접 구조를 설계해보면서 조금씩 교정해나갈 생각이다. 아마 지금 듣고 있는 챕터가 모두 끝나면, 멀티 모듈 아키텍처에 대해서도 접하게 될 거 같다. 어렵겠지만 본 캠프 이전에 최대한 기량을 키워두고 싶다.

'내일배움캠프 안드로이드 3기' 카테고리의 다른 글

[TIL] 24.01.24  (1) 2024.01.24
[TIL] 24.01.23  (0) 2024.01.23
[TIL] 24.01.20  (0) 2024.01.20
[TIL] 24.01.19  (0) 2024.01.19
[TIL] 24.01.18  (0) 2024.01.18