<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mi-fasol.log</title>
        <link>https://velog.io/</link>
        <description>정위블</description>
        <lastBuildDate>Thu, 25 Apr 2024 12:10:08 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. mi-fasol.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mi-fasol" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Algorithm] 백준 1520 내리막 길 - Java]]></title>
            <link>https://velog.io/@mi-fasol/Algorithm-%EB%B0%B1%EC%A4%80-1520-%EB%82%B4%EB%A6%AC%EB%A7%89-%EA%B8%B8-Java</link>
            <guid>https://velog.io/@mi-fasol/Algorithm-%EB%B0%B1%EC%A4%80-1520-%EB%82%B4%EB%A6%AC%EB%A7%89-%EA%B8%B8-Java</guid>
            <pubDate>Thu, 25 Apr 2024 12:10:08 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론</h3>
<p>요즘 꾸준히 취업 준비를 하던 중이라, 매일 코딩 테스트를 하고 있다.</p>
<p>하루하루 일정을 꽉 채우느라 블로그에 신경을 못 썼던 것 같아 오랜만에 오늘 푼 알고리즘을 포스팅 할까 한다.</p>
<p>사실 여태 한 코테 모두 노션에 문제와 코드, 요약한 내용이 정리되어 있지만 블로그에 따로 풀이를 하듯 적는 것도 좋을 것 같고.</p>
<hr>
<h3 id="문제">문제</h3>
<p><a href="https://www.acmicpc.net/problem/1520">내리막 길</a></p>
<p>문제는 위와 같고, 내리막 길만을 선택해서 목적지에 도달하는 경우의 수를 출력하면 되는 문제다.</p>
<p>모든 경우의 수를 찾아야 하니 알고리즘은 당연히 <strong>DFS</strong>를 생각했고,
다시 되돌아 가기 위해 <strong>백 트래킹</strong>을 적용시켰다.</p>
<p>결과는...
<img src="https://velog.velcdn.com/images/mi-fasol/post/85024781-1638-452f-8adf-3486f4dace4f/image.png" alt=""></p>
<p>처참했다.</p>
<h3 id="시간-초과-코드">시간 초과 코드</h3>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    static int N, M, CNT;
    static int[] dx = {0, 0, 1, -1};
    static int[] dy = {1, -1, 0, 0};
    static int[][] map;
    static boolean[][] visited;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        StringTokenizer st = new StringTokenizer(br.readLine());

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        map = new int[N][M];
        visited = new boolean[N][M];

        for(int i = 0; i &lt; N; i++){
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j &lt; M; j++){
                map[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        dfs(0,0);

        System.out.println(CNT);
    }

    public static void dfs(int x, int y){
        visited[x][y]  = true;

        if(x == N-1 &amp;&amp; y == M-1){
            CNT++;
            return;
        }

        for(int i = 0; i &lt; 4; i++){
            int nx = x + dx[i];
            int ny = y + dy[i];

            if(nx &gt;= 0 &amp;&amp; ny &gt;= 0 &amp;&amp; nx &lt; N &amp;&amp; ny &lt; M &amp;&amp; !visited[nx][ny]){
                if(map[nx][ny] &lt; map[x][y]){
                    visited[nx][ny] = true;
                    dfs(nx, ny);
                    visited[nx][ny] = false;
                }
            }
        }
    }
}</code></pre>
<p>위의 코드가 초기의 코드였는데 아무래도 시간 초과가 날 수밖에 없겠다, 싶어서 <strong>가지치기</strong>까지 모두 섞어 작성해 봤지만, 여전히 시간 초과가 발생했다.</p>
<p>조금 더 고민하다 든 생각이 하나 있었는데,</p>
<blockquote>
<p>이럴 때 쓰라고 만든 게 <strong>Dynamic Programming</strong> 기법이다.</p>
</blockquote>
<p>저번주 내내 DP 문제만 풀었더니 완전 탐색 코드의 감을 잃을까 봐 고른 문제였는데 결국 얘도 DP로 해결했다.</p>
<h3 id="최종-코드">최종 코드</h3>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    static int N, M;
    static int[] dx = {0, 0, 1, -1};
    static int[] dy = {1, -1, 0, 0};
    static int[][] map, dp;
    static boolean[][] visited;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        StringTokenizer st = new StringTokenizer(br.readLine());

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        map = new int[N][M];
        dp = new int[N + 1][M + 1];
        visited = new boolean[N][M];

        for (int i = 0; i &lt; N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; M; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        dp[N - 1][M - 1] = 1;

        System.out.println(dfs(0, 0));
    }

    public static int dfs(int x, int y) {

        if (visited[x][y]) return dp[x][y];

        visited[x][y] = true;

        for (int i = 0; i &lt; 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];

            if (nx &gt;= 0 &amp;&amp; ny &gt;= 0 &amp;&amp; nx &lt; N &amp;&amp; ny &lt; M &amp;&amp; map[nx][ny] &lt; map[x][y])
                dp[x][y] += dfs(nx, ny);
        }

        return dp[x][y];
    }
}</code></pre>
<p>이러나 저러나 핵심 아이디어는 아래와 같다.</p>
<blockquote>
<ul>
<li>한 번 방문한 곳은 <strong>다시 방문하지 않기</strong></li>
</ul>
</blockquote>
<ul>
<li><strong>최종 목적지의 DP 값은 미리 1로 설정</strong>하여 최종 목적지에 도착했을 시 경우의 수를 +1로 만들기</li>
</ul>
<p>시간 초과를 없애기 위해 <strong>한 번 구한 경우의 수는 저장했다 꺼내 쓰는 방식</strong>으로 바꿔야 했고, 매번 <strong>dfs가 실행될 때마다 x, y가 최종 목적지와 일치하는지 확인하는게 번거로워</strong> 미리 dp[N-1][M-1]에 1을 설정했다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/d91749f9-74af-48a9-9175-ff4103f36d82/image.png" alt=""></p>
<p>여섯 번의 시도만에 성공했다.</p>
<p>또, DP로 바꾸고 나서도 계속 시간 초과가 났었다.
DP를 쓰는데 시간 초과가 난다면 내가 코드를 아주 단단히 잘못 적고 있구나 싶어서 평소에 쓰던 DFS랑 구조를 좀 다르게 바꿨다.</p>
<p>원래는 DFS에 들어오자마자 visited 배열을 true로 할당하는데, 이건 DP를 활용한 DFS이므로 <strong>탐색을 시작하기 전에 방문 여부를 먼저 확인</strong>한다.</p>
<p>만약 visited가 true라면, 얘는 이미 본인 위치에서 목적지까지 가는 경우의 수가 구해져 있는 거라 DP 값을 가지고 있을 수밖에 없다.
그래서 바로 dp[x][y]를 리턴해준다.</p>
<p>for문 안에서 탐색하는 코드는 별반 다를 게 없는데, 하나 다른 거라면 dfs 값을 dp에 할당해주는 것 정도다.</p>
<p>내 코드에서 <strong>dp[x][y]</strong>가 뜻하는 건 <strong>x행 y열에서 목적지까지 갈 수 있는 경우의 수</strong>다.</p>
<p>알고리즘 흐름도는 아래와 같다!</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/b7aac63d-2ce7-4905-ad4a-69b0131b2c42/image.png" alt=""></p>
<hr>
<h3 id="결론">결론</h3>
<p>매번 만만한 골드 4, 5 문제만 찾아 풀다가 이대로는 성장할 수 없으니 오랜만에 골드 3 문제를 풀어봤다.</p>
<p>요즈음 들어 느끼는 건데, 작년부터 꾸준히 코테를 풀었던 효과가 슬슬 나타나는 것 같다.
코테를 처음 시작할 때만 해도 어떤 알고리즘을 써야 할지 모르고, 실버 문제도 어려워서 끙끙댔는데..이제는 골드 문제도 나름 할만 한 것 같다.</p>
<p>앞으로도 지금처럼 쭉 성장할 수 있었으면 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Jetpack Compose BLE 통신 - 블루투스 스캔]]></title>
            <link>https://velog.io/@mi-fasol/Android-Jetpack-Compose-BLE-%ED%86%B5%EC%8B%A0-%EB%B8%94%EB%A3%A8%ED%88%AC%EC%8A%A4-%EC%8A%A4%EC%BA%94</link>
            <guid>https://velog.io/@mi-fasol/Android-Jetpack-Compose-BLE-%ED%86%B5%EC%8B%A0-%EB%B8%94%EB%A3%A8%ED%88%AC%EC%8A%A4-%EC%8A%A4%EC%BA%94</guid>
            <pubDate>Sun, 17 Mar 2024 09:32:50 GMT</pubDate>
            <description><![CDATA[<p>전에 크몽에 서비스를 올려두고 간단한 앱 개발 의뢰가 들어오길 기다리던 중, 한 분께서 BLE 통신을 사용하는 앱을 의뢰하셨다.</p>
<p>BLE 통신을 한 번도 접해본 적이 없었기에 당연히 상황을 설명드린 후, 앱 개발을 거절했는데 이대로는 할 줄 아는 게 정말 UI 개발밖에 없는 사람이 될 것 같아 차근차근 범위를 넓혀 보기로 했다.</p>
<p>시작은 처음이니만큼 블루투스 스캔 기능을 구현하기로 했다.</p>
<p>우선 블루투스 기능을 사용하기 위해서는 <strong>AndroidManifest.xml</strong> 파일에 퍼미션을 추가해야 한다.</p>
<pre><code class="language-xml">&lt;uses-permission android:name=&quot;android.permission.BLUETOOTH_SCAN&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.BLUETOOTH_ADVERTISE&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.BLUETOOTH_CONNECT&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.BLUETOOTH&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.BLUETOOTH_ADMIN&quot; /&gt;
&lt;uses-feature android:name=&quot;android.hardware.bluetooth_le&quot; android:required=&quot;true&quot;/&gt;
&lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot; /&gt;</code></pre>
<p>다른 분들은 이렇게까지 많이 안 하시는 것 같던데, 나는 자꾸 오류가 나서 그냥 관련된 건 다 추가해줬다.</p>
<p>위의 코드는 Manifest 태그 안, Application 태그 밖에 넣어주면 된다.</p>
<p>퍼미션을 추가했으니 이제 유저가 해당 정보 사용을 허락했는지 안 했는지 확인해야 한다.</p>
<p>이 작업은 <strong>MainActivity.kt</strong> 파일에 추가해주면 된다.</p>
<pre><code class="language-kotlin">class MainActivity : ComponentActivity() {

    val scanViewModel by viewModels&lt;ScanViewModel&gt;()

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

        setContent {
            BLE_PracticeTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val navController = rememberNavController()
                    NavHost(navController = navController, startDestination = &quot;ScanScreen&quot;) {
                        composable(route = &quot;ScanScreen&quot;) { ScanScreen(
                            scanViewModel
                        ) }
//                        composable(route = &quot;ConnectScreen&quot;) { ConnectScreen() }
                    }
                }
            }
        }

        if(Build.VERSION.SDK_INT &gt;= 31){
            if(permissionArray.all{ ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED}){
                Toast.makeText(this, &quot;권한 확인&quot;, Toast.LENGTH_SHORT).show()
            }
            else{
                requestPermissionLauncher.launch(permissionArray)
            }
        }
    }

    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions -&gt;
        permissions.entries.forEach {
            Log.d(&quot;DEBUG&quot;, &quot;${it.key} = ${it.value}&quot;)
        }
    }
}</code></pre>
<p>위의 코드를 복사해서 MainActivity에 넣으면 아마 오류가 날 거다.</p>
<p>permissionArray가 없어서 그런데, 그건 Util에 따로 빼뒀다.</p>
<p><strong>PermissionArray.kt</strong></p>
<pre><code class="language-kotlin">val permissionArray = if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.S) {
    arrayOf(
        Manifest.permission.BLUETOOTH_SCAN,
        Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
    )
} else {
    arrayOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
    )
}</code></pre>
<p>이전 코드에서 여기저기 많이 쓰였어서 유틸리티로 빼뒀는데, 굳이 그러고 싶지 않다면 메인 액티비티에 추가하면 된다.</p>
<p>이제 뷰를 보자.</p>
<p><strong>ScanScreen.kt</strong></p>
<pre><code class="language-kotlin">@Composable
fun ScanScreen(scanViewModel: ScanViewModel = viewModel()) {
    val devices by scanViewModel.devices.observeAsState(initial = listOf())
    val isScanning by scanViewModel.isScanning.observeAsState(initial = false)

    Column {
        Button(onClick = {
            if (isScanning) {
                scanViewModel.stopScan()
            } else {
                scanViewModel.startScan()
            }
        }) {
            Text(if (isScanning) &quot;Stop Scan&quot; else &quot;Start Scan&quot;)
        }

        LazyColumn {
            items(devices) { device -&gt;
                ScanItem(device)
            }
        }
    }
}

@SuppressLint(&quot;MissingPermission&quot;)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScanItem(
    deviceData: BluetoothDevice
) {
    var expanded by remember { mutableStateOf(false) }
    val deviceName = deviceData.name ?: &quot;Unknown Device&quot;

    Card(
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFF569097)
        ),
        modifier = Modifier.padding(vertical = 4.dp),
        onClick = {
        }
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(3.dp)
                .padding(start = 2.dp)
        ) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = deviceName)

                if (expanded) {
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(text = &quot;UUID\n&gt;&gt; ${deviceData.uuids}&quot;)
                    Spacer(modifier = Modifier.height(2.dp))
                    Text(text = &quot;Address\n&gt;&gt; ${deviceData.address}&quot;)
                }
            }

            IconButton(onClick = { expanded = !expanded }) {
                Icon(
                    imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
                    contentDescription = &quot;&quot;
                )
            }
        }
    }
}</code></pre>
<p>뷰는 딱히 설명할 만한 부분이 없는 것 같은데, 우선 <strong>ScanScreen</strong>은 두 가지 컴포넌트로 구성된다.
스캔을 시작하고 멈출 <strong>Button</strong>과 블루투스 디바이스들을 보여줄 <strong>LazyColumn</strong>.</p>
<p>LazyColumn 안에는 스캔된 디바이스의 개수만큼 <strong>ScanItem</strong>이 생성된다.</p>
<p>ScanItem은 디바이스의 이름이 뜨고, 카드를 확장하면 그 안에 블루투스 기기의 UUID와 address가 보이게 된다.</p>
<p>이제 뷰모델 코드만 작성하면 코드 작성이 완료된다.</p>
<p><strong>ScanViewModel.kt</strong></p>
<pre><code class="language-kotlin">@Suppress(&quot;DEPRECATION&quot;)
class ScanViewModel(application: Application) : AndroidViewModel(application) {

    // 블루투스 기능을 위한 블루투스 어댑터
    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

    // 블루투스 기기 목록
    private val _devices = MutableLiveData&lt;List&lt;BluetoothDevice&gt;&gt;()
    val devices: LiveData&lt;List&lt;BluetoothDevice&gt;&gt; = _devices

    // 스캔 여부를 저장할 변수
    private val _isScanning = MutableLiveData&lt;Boolean&gt;()
    val isScanning: LiveData&lt;Boolean&gt; = _isScanning

    // 기기가 스캔이 되면 addDevice() 실행
    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            result?.device?.let { device -&gt;
                addDevice(device)
            }
        }
    }

    // 스캔 시작 함수
    @SuppressLint(&quot;MissingPermission&quot;)
    fun startScan() {
        _isScanning.value = true
        bluetoothAdapter?.bluetoothLeScanner?.startScan(scanCallback)
    }

    // 스캔 정지 함수
    @SuppressLint(&quot;MissingPermission&quot;)
    fun stopScan() {
        _isScanning.value = false
        bluetoothAdapter?.bluetoothLeScanner?.stopScan(scanCallback)
    }

    // 기기를 _devices 변수에 추가
    private fun addDevice(device: BluetoothDevice) {
        val newList = _devices.value?.toMutableList() ?: mutableListOf()
        if (newList.none { it.address == device.address }) {
            newList.add(device)
            _devices.value = newList
        }
    }

    // 블루투스 스캔 중일 때 뷰모델 소멸 시 스캔 정지
    override fun onCleared() {
        super.onCleared()
        if (_isScanning.value == true) {
            stopScan()
        }
    }
}</code></pre>
<p>이렇게 작성하고 나면 </p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/d61f3682-e264-4b5b-8c21-3c718a034994/image.png" alt=""></p>
<p>사진처럼 디바이스 목록이 뜨게 된다.</p>
<p>왜인지 모르게 이름이 null인 디바이스가 참 많이 발견됐더라..
내 문제인지 모르겠어서 우선은 좀 더 공부해 봐야 할 것 같다.</p>
<p>아직은 BLE 통신의 초입에 들어온 거라 어떻게 활용할 수 있을지 감이 잘 안 온다.</p>
<p>다음은 블루투스 connect와 관련하여 포스팅을 해보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algorithm] 프로그래머스 - 표현 가능한 이진 트리 (Java)]]></title>
            <link>https://velog.io/@mi-fasol/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%91%9C%ED%98%84-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%A7%84-%ED%8A%B8%EB%A6%AC-Java</link>
            <guid>https://velog.io/@mi-fasol/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%91%9C%ED%98%84-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%A7%84-%ED%8A%B8%EB%A6%AC-Java</guid>
            <pubDate>Thu, 14 Mar 2024 10:36:37 GMT</pubDate>
            <description><![CDATA[<p>저번주 코테 스터디 문제인 2023 카카오 코테 중 하나인 표현 가능한 이진 트리의 풀이를 가지고 왔다.</p>
<p>문제는 아래 링크를 통해 확인해 주시면 될 것 같다.</p>
<h2 id="표현-가능한-이진-트리"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/150367">표현 가능한 이진 트리</a></h2>
<p>문제를 보고, 아 이건 이진 검색을 쓰면 되겠다 싶었다.</p>
<p>왼쪽 노드와 오른쪽 노드를 모두 검색해야 했기에 이진 검색 + 분할 정복 알고리즘을 사용하여 문제를 풀었다.</p>
<p>코드는 보고 싶지 않고 힌트만을 얻고 싶은 분들을 위해 설명부터 작성하겠다.</p>
<p><strong>풀이 흐름</strong></p>
<blockquote>
<ol>
<li>10진수를 2진수 문자열로 변경</li>
<li>만약 이진수가 모두 1일 경우는 1, 이진수가 0일 경우는 0을 result에 바로 삽입</li>
<li>위의 조건이 아닌 경우 makeTree 함수를 만들어 완전이진트리가 될 때까지 이진수 앞에 0 추가<ul>
<li>현재 이진수의 길이보다 첫 번째로 큰 길이가 될 때까지 추가</li>
</ul>
</li>
</ol>
</blockquote>
<ul>
<li>예를 들어 이진수가 11인 경우 완전이진트리로 만들려면 길이가 3이 되어야 하므로, 앞에 0 하나를 더 붙여 011로 만듦<ol start="4">
<li>isUnavailable 함수를 만들어 이진트리가 될 수 없으면 true, 될 수 있다면 false 출력</li>
</ol>
<ul>
<li><strong>이진 검색 알고리즘</strong>을 통해 root를 기준으로 양쪽을 검사</li>
<li><strong>분할 정복</strong>으로 모든 노드를 검사</li>
<li>재귀 사용</li>
<li>만약 루트가 0인데 양쪽 노드 중 하나라도 1이 있으면 이진트리가 될 수 없음</li>
</ul>
<ol start="5">
<li>true/false에 따라 result에 값 삽입</li>
</ol>
</li>
</ul>
<p>위와 같은 흐름으로 진행됐다.</p>
<p>처음 풀 때 놓쳤던 부분이 두 가지 있다.</p>
<ol>
<li>완전이진트리가 될 때까지 앞에 0을 추가해줘야 하는 점</li>
<li>단말노드가 아니고, 루트가 될 수 있는 노드 중 값이 0인 게 있다고 무조건 실패가 아닌 점</li>
</ol>
<p>1번을 보완하기 위해 makeTree 함수를 만들었고, 2번을 보완하기 위해 isUnavailable 함수 중간에 조건문을 하나 추가했다.</p>
<p>주석으로도 달아놨지만, 2번은 입력 값이 8일 때 이진법 1000으로 표현되는데 완전이진트리로 변경 후 왼쪽과 오른쪽 노드를 살펴보면 루트가 0이라 무조건 true가 반환 됐었다.
양쪽 부분 트리의 루트가 0이지만, 나머지도 모두 더미 노드이기 때문에 완전이진트리가 될 수 있기에 false가 출력됐어야 했다.</p>
<p>코드는 아래와 같다.</p>
<pre><code class="language-java">import java.io.*;
import java.util.*;

class Solution {
    static int[] result;

    public static int[] solution(long[] numbers) {

        result = new int[numbers.length];

        for (int i = 0; i &lt; numbers.length; i++) {
            StringBuilder sb = new StringBuilder();
            // 2진수 변환
            String binaryValue = Long.toBinaryString(numbers[i]);

            // 이진수가 모두 1로 구성되어 있다면 이진트리 가능
            if (!binaryValue.contains(&quot;0&quot;)) {
                result[i] = 1;
                continue;
            }
            // 만약 이진수 값이 0이라면 될 수 없으므로 바로 0 삽입
            if (binaryValue.length() == 1) {
                if (numbers[i] == 0) result[i] = 0;
                continue;
            } else{
                // 위의 조건이 모두 아닐 경우 완전이진트리로 만듦
                binaryValue = makeTree(binaryValue);
            }
            sb.append(binaryValue);

            binaryValue = sb.toString();

            // 완전이진트리로 만들 수 있는지 없는지 판별
            result[i] = isUnavailable(binaryValue, 0, binaryValue.length() - 1) ? 0 : 1;
        }

        return result;
    }

    public static boolean isUnavailable(String binaryValue, int start, int end) {
        // 이진 검색을 끝냈다면 false 반환, 만들 수 있음
        if (start &gt; end || start == end) {
            return false;
        }

        // 루트 설정
        int root = (start + end) / 2;

        // 루트가 0인 경우
        if (binaryValue.charAt(root) == &#39;0&#39;) {
            // start부터 end까지 1이 하나라도 있다면 완전이진트리 불가
            // 예를 들어, 입력값이 8인 경우 0001000로 표현됨
            // 0001000은 완전이진트리지만, 왼쪽과 오른쪽의 노드가 모두 0
            // 이런 경우를 방지하기 위한 for문
            for(int i = start; i &lt;= end; i++) {
                if(binaryValue.charAt(i) == &#39;1&#39;) return true;
            }
            // 모두 0이라면 false
            return false;
        }

        // 루트 기준 왼쪽 노드 이진 검색
        boolean left = isUnavailable(binaryValue, start, root - 1);

        // 루트 기준 오른쪽 노드 이진 검색
        boolean right = isUnavailable(binaryValue, root + 1, end);

        // 하나라도 true라면 true 반환
        return left || right;
    }

    public static String makeTree(String value){
        int treeLength = 1;
        // 완전이진트리의 길이 공식
        while(value.length() &gt; treeLength){
            treeLength = treeLength * 2 + 1;
        }

        // 완전이진트리 길이 - 이진수의 길이
        int zeroLength = treeLength - value.length();
        StringBuilder sb = new StringBuilder();
        // 빼준 길이만큼 앞에 0 추가
        sb.append(&quot;0&quot;.repeat(zeroLength));
        return sb.append(value).toString();
    }
}</code></pre>
<p>뭔가 조건을 제대로 생각하지 못한 것 같아서 다음에 풀 때는 조금 더 꼼꼼히 생각해 봐야 할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Security 기본]]></title>
            <link>https://velog.io/@mi-fasol/Spring-Boot-Security-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@mi-fasol/Spring-Boot-Security-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Thu, 14 Mar 2024 09:40:05 GMT</pubDate>
            <description><![CDATA[<p>요즘 스프링 부트의 <strong>Security</strong>에 관하여 공부 중이다.</p>
<p>지난주에 무작정 사람들의 글을 따라하며 Oauth2 로그인을 구현한 프로젝트를 만들었는데 Android와 연동을 하려니 이것저것 오류가 나는 게 많아 아직은 포스팅일 미뤄두고 있다.</p>
<p>무작정 따라하다 보니 이해도 안 되고, 필터 체인의 각 요소가 뭔지도 모르겠어서 우선 천천히 기초부터 시작하기로 했다.</p>
<h3 id="security를-적용할-프로젝트-준비">Security를 적용할 프로젝트 준비</h3>
<p>우선 시큐리티를 사용할 프로젝트를 만들어 줬다.</p>
<p>나는 <strong>java, gradle, jdk 17</strong>로 준비했고 추가한 의존성은 아래와 같다</p>
<pre><code class="language-java">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-thymeleaf&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity6&#39;
    compileOnly &#39;org.projectlombok:lombok&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testImplementation &#39;org.springframework.security:spring-security-test&#39;
}</code></pre>
<p>스프링부트를 처음 하시는 분이 시큐리티 포스팅을 읽을 것 같지는 않지만, 혹시 스프링부트 프로젝트를 어떻게 만들지 모르시는 분들이 계시다면 <a href="https://start.spring.io">해당 링크</a>에서 손쉽게 만들 수 있으니 참고하시길 바란다.</p>
<p>이렇게 작성하고 나면 시큐리티를 사용할 준비가 다 됐다.</p>
<h3 id="security-코드">Security 코드</h3>
<p>우선 java 코드를 작성하기 전에, 기능이 잘 작동하는지 확인할 html 파일을 작성한다.</p>
<p>src/main/resources/templates 이 경로에 hello.html 파일과, home.html, login.html 파일을 만들어 준다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/751405fb-d08b-4aff-b9da-097cdef24920/image.png" alt="">
위의 폴더 구조를 따르면 된다.</p>
<p><strong>home.html</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xmlns:th=&quot;https://www.thymeleaf.org&quot;&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;head&gt;
    &lt;title&gt;Spring Security Example&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome!&lt;/h1&gt;

&lt;p&gt;&lt;a th:href=&quot;@{/hello}&quot;&gt;로그인&lt;/a&gt;하러 가기&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/083cc45e-3b55-4188-bb2c-f3fc602e5db3/image.png" alt=""></p>
<p>이 화면은 위 사진처럼 간단한 헤더와, 로그인 버튼으로 구성되어 있다.</p>
<p>여기서 로그인을 누르면 이동하는 화면이</p>
<p><strong>login.html</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xmlns:th=&quot;https://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;title&gt;Spring Security Example &lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div th:if=&quot;${param.error}&quot;&gt;
    Invalid username and password.
&lt;/div&gt;
&lt;div th:if=&quot;${param.logout}&quot;&gt;
    You have been logged out.
&lt;/div&gt;
&lt;form th:action=&quot;@{/login}&quot; method=&quot;post&quot;&gt;
    &lt;div&gt;&lt;label&gt; User Name : &lt;input type=&quot;text&quot; name=&quot;username&quot;/&gt; &lt;/label&gt;&lt;/div&gt;
    &lt;div&gt;&lt;label&gt; Password: &lt;input type=&quot;password&quot; name=&quot;password&quot;/&gt; &lt;/label&gt;&lt;/div&gt;
    &lt;div&gt;&lt;input type=&quot;submit&quot; value=&quot;Sign In&quot;/&gt;&lt;/div&gt;
&lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>이 로그인 화면이다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/e1c0d9c8-8337-436b-8e6c-b6a975f77987/image.png" alt=""></p>
<p>위 사진처럼 나오면 성공!</p>
<p>마지막으로 <strong>hello.html</strong> 파일을 생성해주면 완성이다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xmlns:th=&quot;https://www.thymeleaf.org&quot;
      xmlns:sec=&quot;https://www.thymeleaf.org/thymeleaf-extras-springsecurity5&quot;&gt;
&lt;head&gt;
    &lt;title&gt;Hello World!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 현재 인증된 사용자의 이름을 표시 --&gt;
&lt;h1&gt;Hello, &lt;span sec:authentication=&quot;name&quot;&gt;&lt;/span&gt;!&lt;/h1&gt;
&lt;form th:action=&quot;@{/logout}&quot; method=&quot;post&quot;&gt;
    &lt;input type=&quot;submit&quot; value=&quot;Sign Out&quot;/&gt;
&lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/4ab6bb96-e69f-4810-9390-af39ccbcbc99/image.png" alt=""></p>
<p>마지막에 유저 이름과 로그아웃 버튼을 만들어준 화면이다.</p>
<p>이제 본격적으로 security를 사용할 java 파일을 만들어 보자.</p>
<p><strong>MvcConfig.java</strong></p>
<p>이 파일은, 어플리케이션에서 뷰를 연결해주기 위한 파일이다.</p>
<pre><code class="language-java">@Configuration
public class MvcConfig implements WebMvcConfigurer {

    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController(&quot;/home&quot;).setViewName(&quot;home&quot;);
        registry.addViewController(&quot;/&quot;).setViewName(&quot;home&quot;);
        registry.addViewController(&quot;/hello&quot;).setViewName(&quot;hello&quot;);
        registry.addViewController(&quot;/login&quot;).setViewName(&quot;login&quot;);
    }
}</code></pre>
<p>우리가 만들어둔 html 파일들을 매핑해 준 것인데, 간략히 설명을 해보자면 아래와 같다.</p>
<blockquote>
<p>registry.addViewController(<strong>&quot;/home&quot;</strong>).setViewName(<em><strong>&quot;home&quot;</strong></em>);</p>
</blockquote>
<ul>
<li><strong>/home</strong> 이라는 경로에 _<strong>home</strong>_이라는 이름의 뷰를 매핑<ul>
<li>즉, localhost:8080/home 으로 들어가면 home.html을 띄움</li>
</ul>
</li>
</ul>
<p>그리고 security를 사용하기 위해 여러가지 보안 필터를 걸어줘야 하는데, 그게 WebSecurityConfig.java 파일이다.</p>
<p>SecurityConfig 파일을 작성할 때, <strong>정말 오류가 많았다</strong>.</p>
<p>WebSecurityConfigurerAdapter을 사용해서 작성하려 했더니 이제 지원되지 않아서 필터 체인을 사용해야 했고, 필터 체인을 썼더니 온통 빨간 줄이라 더 찾아보니까 각 내용들을 람다식으로 변경해야 했다.</p>
<p>그렇게 완성된 오류 없는 버전이 아래의 코드.</p>
<p><strong>WebSecurityConfig.java</strong></p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig{
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                //crsf 보안을 꺼둠
                .csrf(AbstractHttpConfigurer::disable)
                //cors 보안을 꺼둠
                .cors(AbstractHttpConfigurer::disable)
                // restAPI 요청이 있을 시 해당 인가에 대한 필터링
                .authorizeHttpRequests(request -&gt; request
                        // 요청이 &quot;/&quot;이나 &quot;/home&quot;이면 로그인 없이 허용
                        .requestMatchers(&quot;/&quot;, &quot;/home&quot;).permitAll()
                        // 그 외는 어떠한 요청이라도 인가 필요
                        .anyRequest().authenticated()
                )

                .formLogin(login -&gt; login
                                // 로그인은 /login 페이지로 매핑
                                .loginPage(&quot;/login&quot;)
                                .permitAll()
                )
                .logout(LogoutConfigurer::permitAll);

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        // 유저 이름을 &quot;user&quot;, 비밀번호를 &quot;password&quot;로 설정
        // 실제로는 BCryptPasswordEncoder를 사용해서 비밀번호를 암호화
        //  - BCryptPasswordEncoder는 암호화할 때마다 다른 결과를 반환
        UserDetails user = User.withDefaultPasswordEncoder()
                .username(&quot;user&quot;)
                .password(&quot;password&quot;)
                .roles(&quot;USER&quot;)
                .build();

        return new InMemoryUserDetailsManager(user);
    }
}
</code></pre>
<p>나처럼 처음 <strong>SecurityFilterChain</strong>을 접하신 분들이 대체 무슨 내용인지 잘 이해가 가지 않을 것 같아서, 최대한 설명하기 위해 줄마다 주석을 달아봤다.</p>
<p>그래도 뭐가 뭔지 모르겠다 싶으면 아래의 설명을 참고해 주시면 된다.</p>
<blockquote>
<ul>
<li>crsf: 사이트 간 위조 요청 방지에 관한 필터링</li>
</ul>
</blockquote>
<ul>
<li>cors: 허용한 브라우저에서만 리소스 접근 가능</li>
<li>authorizeHttpRequests: http 요청 시 인가 여부 필터링</li>
<li>requestMatchers(&quot;주소&quot;): &quot;주소&quot;로 오는 요청 관련 인가 필터링<ul>
<li>permitAll(): 특별한 인증 없이 모두 허용</li>
<li>anyRequest: 모든 요청에 대한 인가 여부 필터링</li>
<li>authenticated: 무조건 인증 필요</li>
<li>formLogin: Spring Security에서 제공하는 폼 기반 로그인 사용</li>
<li>loginPage(&quot;/login&quot;): /login으로 매핑된 html을 로그인 화면으로 설정</li>
<li>logout: 로그아웃 관련 필터링</li>
</ul>
</li>
</ul>
<p>이 정도로 정리할 수 있을 것 같다.</p>
<p>이렇게 보안 규칙과 유저 정보를 모두 설정해주면 끝이 난다.</p>
<p>localhost:8080으로 들어가면 설정해둔 html 파일이 보이며 제대로 프로젝트가 작동된다.</p>
<p>또 열심히 Security 관련 공부를 하고 다음 포스팅을 이어가겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algorithm] 프로그래머스 미로 탐색 명령어 - Java]]></title>
            <link>https://velog.io/@mi-fasol/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AF%B8%EB%A1%9C-%ED%83%90%EC%83%89-%EB%AA%85%EB%A0%B9%EC%96%B4-Java</link>
            <guid>https://velog.io/@mi-fasol/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AF%B8%EB%A1%9C-%ED%83%90%EC%83%89-%EB%AA%85%EB%A0%B9%EC%96%B4-Java</guid>
            <pubDate>Sat, 02 Mar 2024 08:30:30 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/150365">https://school.programmers.co.kr/learn/courses/30/lessons/150365</a></p>
<h3 id="문제-설명">문제 설명</h3>
<blockquote>
<ul>
<li>N x M 크기의 미로가 존재한다.</li>
</ul>
</blockquote>
<ul>
<li>미로 안에서 움직일 수 있는 방향은 상하좌우 뿐이다.</li>
<li>제한사항<ul>
<li>미로의 벽 바깥으로는 움직이지 못 한다.</li>
<li>시작점에서 탈출 지점까지 K만큼 움직여야 하며, 왔던 경로를 재방문 할 수 있다.</li>
<li>탈출한 경로를 u(상), d(하), l(좌), r(우)로 나타냈을 때 사전 순으로 가장 빠른 경로로 탈출해야 한다.</li>
</ul>
</li>
</ul>
<h3 id="풀이-과정">풀이 과정</h3>
<p>문제를 보자마자 아 이건 완전 탐색이다 싶었다.</p>
<p>갈 수 있는 경로를 모두 체크해서 그 중 사전 순으로 가장 빠른 경로를 출력하면 된다고 생각해 DFS 알고리즘을 사용해 풀었었다.</p>
<pre><code class="language-java">import java.io.*;
import java.util.ArrayList;
import java.util.Collections;

class Solution {
    static Character[] location = {&#39;l&#39;, &#39;u&#39;, &#39;r&#39;, &#39;d&#39;};

    static char[][] campus;

    static String result = &quot;impossible&quot;;
    static ArrayList&lt;String&gt; resultList = new ArrayList&lt;&gt;();
    static int[] dy = {-1, 0, 1, 0};
    static int[] dx = {0, -1, 0, 1};

    static int N, M, X, Y, R, C, K;


    public static String solution(int n, int m, int x, int y, int r, int c, int k) {

        campus = new char[n][m];
        N = n;
        M = m;
        Y = y - 1;
        X = x - 1;
        R = r - 1;
        C = c - 1;
        K = k;

        if (y == c &amp;&amp; x == r) return &quot;&quot;;

        dfs(X, Y, 0, &quot;&quot;);

        // resultList가 비어있다면 impossible 출력
        if (resultList.isEmpty()) {
            return result;
        }
        // resultList를 사전 순으로 정렬 후 첫 번째 경로 출력
        else {
            Collections.sort(resultList);
            return resultList.get(0);
        }
    }

    public static void dfs(int x, int y, int cnt, String str) {

        // 남은 경로로 이동할 수 없으면 그냥 return
        int remainMoves = K - cnt;
        int distanceToGoal = Math.abs(R - x) + Math.abs(C - y);
        if (remainMoves &lt; distanceToGoal) return;

        // K번 이동했을 때 위치가 (R, C)면 resultList에 경로 추가
        if (cnt == K) {
            if (x == R &amp;&amp; y == C) {
                resultList.add(str);
            }
            return;
        }

        // 경로 탐색
        for (int i = 0; i &lt; 4; i++) {
            int fx = x + dx[i];
            int fy = y + dy[i];

            if (fy &lt; 0 || fx &lt; 0 || fx &gt;= N || fy &gt;= M) continue;

            dfs(fx, fy, cnt + 1, str + location[i]);
        }
    }
}</code></pre>
<p>이렇게 resultList를 만들어 경로를 모두 찾아준 후, Collections.sort로 사전 순 정렬을 해줬는데 결과는 대참패였다.</p>
<p>아마 7문제 빼고 전부 시간 초과나 메모리 초과였던 것 같다.</p>
<p><strong>이대로는 시간복잡도가 너무 크고 불필요한 경로까지 모두 찾아야 한다</strong>는 생각에 List 형태로 저장하지 않고, <strong>애초에 사전 순으로 빠른 경로부터 찾도록 변경</strong>해야 한다는 생각이 들었고 그게 아래의 코드다.</p>
<pre><code class="language-java">import java.io.*;
import java.util.ArrayList;
import java.util.Collections;

class Solution {
    // 경로를 찾을 때 움직이는 순서 변경
    static Character[] location = {&#39;d&#39;, &#39;l&#39;, &#39;r&#39;, &#39;u&#39;};
    static char[][] campus;
    static String result = &quot;impossible&quot;;
    static int[] dy = {0, -1, 1, 0};
    static int[] dx = {1, 0, 0, -1};
    static int N, M, X, Y, R, C, K;


    public static String solution(int n, int m, int x, int y, int r, int c, int k) {

        campus = new char[n][m];
        N = n;
        M = m;
        Y = y - 1;
        X = x - 1;
        R = r - 1;
        C = c - 1;
        K = k;

        if (y == c &amp;&amp; x == r) return &quot;&quot;;

        // 가지치기 - 애초에 갈 수 없는 경우
        if (!isAvailable(X, Y, 0)) return result;

        dfs(X, Y, 0, &quot;&quot;);

        return result;
    }

    // 목적지까지 갈 수 있는지 없는지 판별하는 함수
    public static boolean isAvailable(int x, int y, int cnt) {
        int remainMoves = K - cnt;
        int distanceToGoal = Math.abs(R - x) + Math.abs(C - y);

        return remainMoves &gt;= distanceToGoal &amp;&amp; ((remainMoves - distanceToGoal) % 2 == 0);
    }

    public static boolean dfs(int x, int y, int cnt, String str) {

        // 남은 횟수 안에 갈 수 없으면 바로 false 반환
        if(!isAvailable(x, y, cnt)) return false;

        // K만큼 이동하여 현재 위치가 (R, C)인가?
        if (cnt == K) {
            // 맞으면 result를 str로 변경 후 true 반환
            if (x == R &amp;&amp; y == C) {
                result = str;
                return true;
            }
            // 아닌 경우 false 반환
            return false;
        }

        for (int i = 0; i &lt; 4; i++) {
            int fx = x + dx[i];
            int fy = y + dy[i];

            if (fy &lt; 0 || fx &lt; 0 || fx &gt;= N || fy &gt;= M) continue;
            // 더 움직였을 때 도착 가능하면 true 반환
            if (dfs(fx, fy, cnt + 1, str + location[i])) {
                return true;
            }
        }

        // 기본적으로 false 반환
        return false;
    }
}</code></pre>
<p>변경사항은 아래와 같다.</p>
<blockquote>
<ol>
<li>움직이는 순서를 사전 순으로 설정<ul>
<li>l -&gt; u -&gt; r -&gt; d 순에서 d -&gt; l -&gt; r -&gt; u로 변경</li>
</ul>
</li>
<li>애초에 탈출이 불가능한 경우는 solution 함수 내에서 가지치기<ul>
<li>isAvailable 함수를 생성하여 true - false 반환</li>
</ul>
</li>
<li>경로 탐색 시작 시 남은 횟수 안에 갈 수 없다면 더이상 경로를 찾지 않고 false 반환</li>
<li>리스트로 경로를 모두 저장하지 않고 찾은 경로를 바로 result에 삽입</li>
</ol>
</blockquote>
<p>위와 같은 변경을 거치고 나니, 통과됐다.</p>
<p>뭔가 2023 카카오 공채 코테는 전체적으로 어떻게 풀어야 할지는 알겠지만 이런저런 조건을 생각하는 곳에서 많이 삐끗했던 것 같다.
그래도 점점 문제를 봤을 때 풀기 위해 코드를 구성하는 능력이 길러지고 있는 것 같다.</p>
<p>다음엔 2023 카카오 공채 코테 중 다른 lv.3 문제들을 가지고 오겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Android DI와 Hilt]]></title>
            <link>https://velog.io/@mi-fasol/Kotlin-Android-DI</link>
            <guid>https://velog.io/@mi-fasol/Kotlin-Android-DI</guid>
            <pubDate>Mon, 27 Nov 2023 11:08:42 GMT</pubDate>
            <description><![CDATA[<p>전에 스프링을 공부할 때 DI에 대해 작성한 적이 있었는데, 이번엔 안드로이드의 DI에 대한 이야기를 하려 한다.</p>
<h2 id="di">DI</h2>
<p><strong>DI</strong>는 Dependency Injection의 약자로 <strong>의존성 주입</strong>을 뜻한다.
특정 한 객체가 다른 객체를 필요로 할 때 이 의존성을 제공하는 기술이 DI다.</p>
<p>객체가 다른 객체를 필요로 하면 외부에서 해당하는 객체를 생성하여 필요한 객체에 넘겨주게 된다.</p>
<p>DI는 아래와 같은 장점이 있다.</p>
<blockquote>
<ol>
<li>코드 재사용성 향상<ul>
<li>결합도 감소</li>
</ul>
</li>
<li>테스트 용이성<ul>
<li>의존성을 가짜 객체나 Mock 객체로 대체하여 테스트 수행 가능</li>
</ul>
</li>
<li>코드의 유연성 및 확장성, 가독성 향상<ul>
<li>새로운 기능을 추가하거나, 기능을 수정할 때 미치는 영향 감소</li>
</ul>
</li>
<li>의존성 추적 용이성<ul>
<li>클래스가 사용하는 종속성의 명확한 파악 가능</li>
</ul>
</li>
</ol>
</blockquote>
<p>Android Developers에 있는 공식 코드를 활용해 이야기를 해 보겠다.</p>
<p>두 개의 코드가 있는데, 둘의 차이점은 무엇일까?</p>
<p>첫 번째 코드</p>
<pre><code class="language-kotlin">class Car {

    // 여기
    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}</code></pre>
<p>두 번째 코드</p>
<pre><code class="language-kotlin">class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}</code></pre>
<p>얼핏 보기에는 비슷해 보이지만 <strong>위의 코드는 DI를 사용하지 않은 코드</strong>고, <strong>두 번째 코드는 DI를 사용해 의존성을 주입</strong>한 예제 코드다.</p>
<p>첫 번째 코드의 주석 부분을 보면 Car 클래스가 자체적으로 Engine을 생성해 사용하고 있다.
이 코드는 둘 간의 의존성이 높아 결합도가 높아지고, Car 클래스가 Engine을 직접 인스턴스화 해서 Engine의 서브 클래스들을 사용할 수 없다.</p>
<p>반면 두 번째 코드는 Main 코드에서 Engine 인스턴스를 생성해 Car 클래스에 파라미터로 넘겨주고 있다.
그렇기에 Car 클래스의 재사용성이 높아지고, Engine의 구현이 수정돼도 Car 클래스에는 영향을 미치지 않는다.</p>
<p>안드로이드에서 의존성을 주입해 주는 방법은 두 가지가 있다.</p>
<blockquote>
<ul>
<li>생성자 삽입 (Constructor Injection)</li>
</ul>
</blockquote>
<ul>
<li>필드 삽입 (Field Injection)</li>
</ul>
<h4 id="생성자-삽입">생성자 삽입</h4>
<p>생성자 삽입은 위의 두 번째 코드에서 설명한 방법이다.</p>
<p>클래스의 종속 항목을 생성자에 전달한다.
코드는 앞서 말했으므로 생략하고 넘어가겠다.</p>
<h4 id="필드-삽입">필드 삽입</h4>
<p>Activity, Fragment 등 특정 안드로이드 프레임워크 클래스에서는 생성자 삽입이 불가한 경우가 있다.
이럴 때 필드 삽입을 통해 클래스가 생성된 후 종속 항목을 인스턴스화 해주는 방법이 있다.</p>
<pre><code class="language-kotlin">class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}</code></pre>
<p>위처럼 Car 클래스를 먼저 생성해주고, 그 후에 Engine을 인스턴스화 해주면 된다.</p>
<p>여기서 연계해서 설명할 개념이 하나 생긴다.</p>
<p>바로 Hilt다.</p>
<h2 id="hilt">Hilt</h2>
<p>Hilt란 Dagger를 기반으로 의존성을 주입하고, 수명 주기를 자동으로 관리하기 위한 안드로이드의 DI 라이브러리다.</p>
<p>라이브러리인만큼 build.gradle 파일에 플러그인을 추가해 줘야 한다.
또한, Java 8 기능을 사용하므로 프로젝트 내에서 Java 8을 사용하고자 한다면 아래와 같은 코드를 app/build.gradle에 추가해야 한다.</p>
<pre><code class="language-kotlin">android {
  ...
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
  }
}</code></pre>
<p>Hilt는 아래와 같은 특징이 있다.</p>
<blockquote>
<ol>
<li>AndroidX와 통합<ul>
<li>안드로이드 앱의 생명주기와 관련된 의존성 주입 지원</li>
</ul>
</li>
<li>안드로이드 컴포넌트와 자동 통합<ul>
<li>Application, Activity, Fragment, ViewModel과 자동으로 통합</li>
</ul>
</li>
<li>Daager의 간소화<ul>
<li>Dagger의 복잡성을 간소화 하고 Dagger 컴포넌트 및 모듈의 생성, 관리를 자동화</li>
</ul>
</li>
<li>코드 간소화 및 테스트 용이성<ul>
<li>DI 관련 코드를 간소화 하고, 테스트에서 필요한 의존성을 쉽게 대체 가능</li>
</ul>
</li>
</ol>
</blockquote>
<p>이 Hilt를 사용하기 위해서는, <strong>@HiltAndroidApp</strong> 어노테이션을 사용해야 한다.</p>
<pre><code class="language-kotlin">@HiltAndroidApp
class MainApplication : Application() { ... }</code></pre>
<p>매니페스트에도 아래와 같이 추가해야 한다.</p>
<pre><code class="language-kotlin"> &lt;application android:name=&quot;.ExampleApplication&quot;</code></pre>
<p>Hilt로 의존성을 주입하고 나면, <strong>@Injection</strong> 어노테이션을 사용하여 가져올 수 있다.</p>
<pre><code class="language-kotlin">@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}</code></pre>
<p><strong>@Binds</strong> 어노테이션도 있다.</p>
<p>@Binds 어노테이션은 <strong>인터페이스에 대해 종속성을 삽입</strong>할 때 사용하는 어노테이션이다.</p>
<p>예를 들어 한 인터페이스를 두 개의 레포지토리가 상속받는 경우, Hilt는 어느 레포지토리를 선택해야 할지 혼란을 겪게 된다. 이럴 때 @Binds로 상속받은 인터페이스를 구분지을 수 있게 된다.</p>
<pre><code class="language-kotlin">@Module
@InstallIn(ApplicationComponent.class)
public abstract class MyModule {

    @Binds
    @Singleton
    abstract MyInterface bindMyInterface(MyImplementation myImplementation);

}</code></pre>
<p>또한, <strong>@Provides</strong> 어노테이션으로 인스턴스를 삽입할 수도 있다.</p>
<p>@Provides 어노테이션은 <strong>Room이나 Retrofit 등</strong>의 외부 라이브러리에서 제공되는 클래스라 <strong>프로젝트 내에서 소유할 수 없는 경우</strong>나 <strong>Builder 패턴을 사용하여 인스턴스를 생성할 경우</strong> 주로 사용된다.</p>
<p>아래와 같은 규칙을 따라야 한다.</p>
<blockquote>
<ul>
<li>return 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알림</li>
</ul>
</blockquote>
<ul>
<li>파라미터는 해당 유형의 종속 항목을 Hilt에 알림</li>
<li>함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알림<ul>
<li>Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행</li>
</ul>
</li>
</ul>
<pre><code>@Module
@InstallIn(ApplicationComponent.class)
public class ExternalLibraryModule {

    @Provides
    @Singleton
    ExternalLibraryClass provideExternalLibraryClass() {
        return new ExternalLibraryClass();
    }

}</code></pre><p>@Binds와 @Provides 모두 Dagger 모듈 내에 정의된다.</p>
<hr>
<p>오늘은 이렇게 안드로이드의 DI와 Hilt에 대해서 알아보았는데, 사실 아직 Hilt를 제대로 사용할 수 있다고 말하진 못할 것 같다.</p>
<p>Hilt를 사용해서 직접 프로젝트를 만들어서 의존성을 주입해 볼 생각이라, 추후 간단하게 코드를 작성하고 다시 포스팅을 하도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BAEKJOON] 2149 암호 해독 JAVA]]></title>
            <link>https://velog.io/@mi-fasol/BAEKJOON-2149-%EC%95%94%ED%98%B8-%ED%95%B4%EB%8F%85-JAVA</link>
            <guid>https://velog.io/@mi-fasol/BAEKJOON-2149-%EC%95%94%ED%98%B8-%ED%95%B4%EB%8F%85-JAVA</guid>
            <pubDate>Fri, 24 Nov 2023 13:04:24 GMT</pubDate>
            <description><![CDATA[<p>몇 주 전부터 자체적으로 코딩테스트 스터디를 시작했다.</p>
<p>내가 푼 문제들을 작성해보면 좋을 것 같아서, 이번 주 과제 중 하나인 백준 암호 해독 문제를 가져왔다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/27ed8122-da30-430e-9cc3-f1af08b1d6bd/image.png" alt=""></p>
<p>문제에서는 평서문을 암호문으로 만들 수 있는 조건을 알려준다.
입력 값으로 암호 해독 키와 암호문을 주면, 이걸 평서문으로 바꿔서 출력하면 되는 문제다.</p>
<p>이 문제를 풀기 위해 역으로 암호문을 평서문으로 만들기 위한 과정을 생각했다.</p>
<blockquote>
</blockquote>
<ol>
<li>암호 해독 키를 알파벳 순으로 정렬한다.</li>
<li>암호문의 길이를 암호 해독 키의 길이로 나눈 길이만큼의 부분 문자열을, 1열부터 마지막 열까지 차례대로 넣어준다.<ul>
<li>암호문의 길이는 언제나 암호 해독 키의 배수라는 조건이 존재</li>
</ul>
</li>
<li>정렬된 암호 해독 키를 원래의 형태로 만든다.<ul>
<li>이때, 중복된 문자가 존재하면 먼저 들어온 순서대로 생성</li>
<li>부분 문자열을 담은 열(Column)의 순서도 알파벳 위치대로 변경</li>
</ul>
</li>
<li>완성된 행렬 테이블을 1행부터 마지막 행까지 순서대로 읽어준다.</li>
</ol>
<p>크게 말하자면 위의 네 단계로 볼 수 있다.</p>
<p>맨 처음에 생각했던 방법은, 이차원 배열을 사용하는 방법이었다.</p>
<p>그러나 원래 코딩 테스트를 파이썬으로 공부하다가 언어를 자바로 바꾼 거라 자바를 완전히 활용하지 못하고, 이차원 배열을 활용한 로직이 잘 생각나지 않아서 <strong>해시맵</strong>을 활용하는 코드로 변경했다.</p>
<p>총 두 개의 해시맵을 사용했는데, 하나는 <strong>암호 해독 키의 문자와 문자가 들어온 순서를 기억</strong>하기 위한 <strong>&lt;Integer - Character&gt; 쌍의 해시맵</strong>이다.
즉, 암호 해독 키의 들어온 순서와 문자를 기억한다.</p>
<p>다른 해시맵은 <strong>문자가 들어온 순서와 부분 문자열을 저장</strong>하는 <strong>&lt;Integer - String&gt; 쌍의 해시맵</strong>이다.
두 번째 해시맵을 통해 정답을 출력한다.</p>
<p>원래는 부분 문자열을 저장할 때 &lt;Character - String&gt; 쌍의 해시맵을 하나 추가해 사용했었는데, Character를 사용하니 들어온 순서를 기억 못 하는 걸 생각 못 하고 제출했다가 틀렸었다.</p>
<p>코드에 사용한 알고리즘은 아래와 같다.</p>
<blockquote>
</blockquote>
<ol>
<li>&lt;Integer - Character&gt; 해시맵에 들어온 순서와 글자 저장</li>
<li>암호 해독 키의 글자를 알파벳 순으로 해시맵 정렬<ul>
<li>Value 값으로 해시맵 정렬</li>
</ul>
</li>
<li>List를 생성해 해시맵의 KeySet 저장<ul>
<li>그냥 해시맵을 사용하면 0부터 차례대로 저장되기 때문에, LinkedHashMap을 사용해 Value 값으로 정렬되어 뒤죽박죽인 Key의 순서 그대로 저장</li>
</ul>
</li>
<li>&lt;Integer - String&gt; 해시맵에 해독 키의 문자가 들어왔던 순서와 부분 암호문 저장<ul>
<li>들어온 순서는 정렬되어 있지 않음</li>
<li>부분 암호문의 길이 = 암호문의 길이 / 암호 해독 키의 길이</li>
<li>암호문의 첫 번째 글자부터 순서대로 저장</li>
</ul>
</li>
<li>&lt;Integer - String&gt; 해시맵의 Key 값들을 들어온 순서대로 정렬</li>
<li>각 Value 값의 첫 번째 글자를 모두 출력 후 두 번째 글자, 세 번째 글자, ..., 마지막 글자 순서대로 출력<ul>
<li>하나의 Value 값을 한 번에 모두 출력하는 게 아님</li>
<li>모든 Value의 첫 글자를 순서대로 출력 후, 그 다음엔 모든 Value의 두 번째 글자를 순서대로 출력하여 마지막 글자까지 출력하는 과정</li>
<li>Value 값이 &#39;Spring&#39;, &#39;Python&#39;이라면 출력은 SPpyrtihnogn</li>
</ul>
</li>
</ol>
<p>코드에 대 한 설명은 모두 끝났으니, 어디 보여주긴 부끄러운 코드지만 코드를 띄우고 마무리하겠다.</p>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        String pw = br.readLine();
        String sentence = br.readLine();

        int len = sentence.length() / pw.length();

        HashMap&lt;Integer, Character&gt; sequence = new HashMap&lt;&gt;();

        for (int i = 0; i &lt; pw.length(); i++) {
            sequence.put(i, pw.charAt(i));
        }

        List&lt;Map.Entry&lt;Integer, Character&gt;&gt; list = new ArrayList&lt;&gt;(sequence.entrySet());

        Collections.sort(list, new Comparator&lt;Map.Entry&lt;Integer, Character&gt;&gt;() {
            @Override
            public int compare(Map.Entry&lt;Integer, Character&gt; o1, Map.Entry&lt;Integer, Character&gt; o2) {
                return Character.compare(o1.getValue(), o2.getValue());
            }
        });

        LinkedHashMap&lt;Integer, Character&gt; sortedSequence = new LinkedHashMap&lt;&gt;();
        for (Map.Entry&lt;Integer, Character&gt; entry : list) {
            sortedSequence.put(entry.getKey(), entry.getValue());
        }

        List&lt;Integer&gt; keySets = new ArrayList&lt;&gt;(sortedSequence.keySet());

        HashMap&lt;Integer, String&gt; amo = new HashMap&lt;&gt;();



        int idx = 0;
        for(int i = 0; i &lt; pw.length(); i++){
            int key = keySets.get(i);
            String subString = sentence.substring(idx * len, len * (idx + 1));

            amo.put(key, subString);
            idx++;
        }

        Collections.sort(keySets);


        int maxLength = amo.values().stream().mapToInt(String::length).max().orElse(0);

        for (int i = 0; i &lt; maxLength; i++) {
            for (Integer key : amo.keySet()) {
                String value = amo.get(key);
                if (i &lt; value.length()) {
                    bw.write(value.charAt(i));
                }
            }
        }

        br.close();
        bw.close();

    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] MVVM]]></title>
            <link>https://velog.io/@mi-fasol/Kotlin-MVVM</link>
            <guid>https://velog.io/@mi-fasol/Kotlin-MVVM</guid>
            <pubDate>Mon, 20 Nov 2023 10:08:26 GMT</pubDate>
            <description><![CDATA[<p>오늘은 안드로이드의 <strong>MVVM</strong>에 관하여 포스팅을 할까 한다.</p>
<p>아마 개발을 하다가 한 번쯤은 들어보았을 것 같은데, MVVM이란 <strong>Model, View, ViewModel</strong>의 줄임으로 뷰가 특정 모델에 의존하지 않도록 하는 소프트웨어 디자인 패턴이다.</p>
<p>MVVM은 아래와 같은 장점을 가지고 있다.</p>
<blockquote>
<ul>
<li>관심사 분리<ul>
<li>각 컴포넌트가 각자의 역할에만 집중</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li>유연한 UI 개발</li>
<li>테스트 용이성</li>
<li>유지보수 및 재사용 용이</li>
<li>데이터의 일관성 유지</li>
</ul>
<p>MVVM은 모듈화와 분리가 목적인 패턴인만큼, UI 중심의 어플리케이션에서 유용하게 사용된다.
UI와 비즈니스 로직이 독립적으로 존재하여 변경 사항의 빠른 적용과 테스트가 가능해진다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/2d761e1c-4b73-4f64-8eb2-057c8684503d/image.png" alt=""></p>
<p>코틀린에서는 위의 사진과 같은 앱 아키텍처를 권장하고 있다.</p>
<p>액티비티나 프래그먼트가 뷰모델에만 종속된 걸 볼 수 있는데, 각 구성요소가 한 수준 아래의 구성 요소에만 종속되어 있는 구조다.</p>
<p>그렇다면 본격적으로 Model, View, ViewModel에 대해 알아보기로 하자.</p>
<h3 id="model">Model</h3>
<p><strong>Model</strong>은 어플리케이션의 데이터와 비즈니스 로직을 담당하는 요소다.</p>
<p>쉽게 말해 만약 우리가 User라는 데이터 클래스를 생성하게 된다면, 이게 MVVM의 Model이 되는 거다.</p>
<p>네트워크 요청이나 데이터베이스 접근, 데이터 가공 등의 작업을 수행할 수 있다.</p>
<pre><code class="language-kotlin">data class User(
    val id: Int,
    val name: String
)</code></pre>
<p>위의 코드가 Model의 코드다.
뷰와 뷰모델 관련한 정보는 아무것도 없도록 작성해야 한다.</p>
<h3 id="view">View</h3>
<p>그렇다면 <strong>View</strong>는 무엇일까?</p>
<p>View는 말 그대로 UI와 사용자의 입력을 처리하는 요소다.
사용자와 상호작용하고, 화면의 데이터를 표시하며 입력 등으로 인해 발생하는 이벤트를 ViewModel에 전달해야 한다.</p>
<p>View는 화면에 보여지기 위한 코드만 작성하도록 최소한의 로직을 가지고 있어야 한다.</p>
<pre><code class="language-kotlin">import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.Observer
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: UserViewModel

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

        viewModel = ViewModelProvider(this).get(UserViewModel::class.java)

        viewModel.user.observe(this, Observer { user -&gt;
            displayUserInfo(user)
        })

        updateButton.setOnClickListener {
            val newUser = User(1, &quot;Mi-fasol&quot;)
            viewModel.updateUser(newUser)
        }
    }

    private fun displayUserInfo(user: User) {
        userIdTextView.text = &quot;Id: ${user.id}&quot;
        userNameTextView.text = &quot;Name: ${user.name}&quot;
    }
}
</code></pre>
<p>이렇게 사용자 입력을 처리하고, 뷰모델의 로직을 가져와 쓰는 게 뷰다.</p>
<h3 id="viewmodel">ViewModel</h3>
<p>마지막으로 <strong>ViewModel</strong>은 View와 Model의 중간 매개체로, UI와 관련된 로직을 처리하는 요소다.</p>
<p>View에서 입력을 받은 값을 Model에 전달하고, Model의 데이터를 받아와 View에 전달한다.
ViewModel에서는 View의 상태를 관리하고 비즈니스 로직을 처리해야 한다.</p>
<pre><code class="language-kotlin">import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class UserViewModel : ViewModel() {

    private val _user = MutableLiveData&lt;User&gt;()

    val user: LiveData&lt;User&gt;
        get() = _user

    fun updateUser(newUser: User) {
        _user.value = newUser
    }
}</code></pre>
<p>아까 있던 View에서 LiveData를 활용하고 있기 때문에, 뷰모델의 _user 변수도 MutableLiveData로 생성해야 한다.</p>
<p>이렇게 하면, 이벤트가 발생할 때 즉각적으로 View에 반영해 줄 수 있다.</p>
<p>보이는 것처럼 User 입력 이벤트에 대한 처리만 담당하는 게 ViewModel이다.</p>
<hr>
<p>오늘은 간단하게 MVVM에 대해서 알아봤다.</p>
<p>오늘 GDSC Android Study가 있는 날인데, 추가적으로 알게 되는 정보가 있으면 포스트를 수정하며 다시 개념을 잡아야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] @Controller, @Service, @Component, @Repository의 차이]]></title>
            <link>https://velog.io/@mi-fasol/Spring-Controller-Service-Component-Repository%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@mi-fasol/Spring-Controller-Service-Component-Repository%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Thu, 16 Nov 2023 15:04:23 GMT</pubDate>
            <description><![CDATA[<p>요즘 스프링 부트를 공부해 보고 싶다는 생각이 많이 들어서, 기본 개념부터 차근차근 다시 짚고 넘어가려고 한다.</p>
<p>Controller와 Service, Component, Repository는 스프링을 사용하면서 가장 많이 사용하는 어노테이션이다.
코드를 작성하며 사용하기는 했지만, 어떻게 작동되고 어떤 기능을 하는지는 정확히 알지 못해서 공부할 겸 정리하고자 포스팅을 한다.</p>
<p>우선 설명에 앞서, 위의 네 가지를 <strong>어노테이션</strong>이라고 하는데 스프링 코드에 <strong>메타데이터를 제공</strong>하여 스프링 컨테이너가 해당 클래스나 메서드를 어떻게 처리해야 하는지를 알려주는,<strong>기능을 수행하게 하는 주석</strong>이다.</p>
<p>우리는 어노테이션을 사용하여 코드를 더 짧고 효율적으로, 유지보수에 용이하게 작성할 수 있다.</p>
<p>가장 기본적인 <strong>@Repository</strong> 어노테이션부터 살펴보자.</p>
<h2 id="repository">@Repository</h2>
<p><strong>Repository</strong> 어노테이션은 데이터 액세스 계층의 구성 요소를 다루는 어노테이션이다.</p>
<p>아래와 같은 특징을 가지고 있다.</p>
<blockquote>
<ul>
<li>데이터베이스와의 상호작용을 담당</li>
</ul>
</blockquote>
<ul>
<li>Persistence Layer에 존재</li>
<li>주로 DAO(Data Access Object) 클래스에 사용되는 어노테이션</li>
<li>스프링에서 지원하지 않는 특정 예외를 Spring Exception으로 전환</li>
<li>예외 발생 시 Unchecked Exception을 DataAccessException으로 변경하여 처리</li>
</ul>
<p>Repository 어노테이션의 개념과 특징에 대해서 간단히 설명을 했으니 사용법에 대해 알아보자.</p>
<pre><code class="language-java">import org.springframework.stereotype.Repository;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;

@Repository
public class ExampleRepository {

    private final JdbcTemplate jdbcTemplate;


     @Autowired
    public ExampleRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public String getNameById(Long id) {
        String sql = &quot;SELECT name FROM my_table WHERE id = ?&quot;;

        try {

            // JDBC로 id를 통해 name 필드 가져오기
            return jdbcTemplate.query(sql, new Object[]{id}, (ResultSetExtractor&lt;String&gt;) rs -&gt; {
                if (rs.next()) {
                    return rs.getString(&quot;name&quot;);
                }
                return null;
            });
        } catch (DataAccessException ex) {
            // 예외 처리
            throw ex;
        }
    }
}</code></pre>
<p>위의 코드처럼 class 위에 어노테이션을 붙여주면 끝이다.</p>
<p> @Autowired로 의존성을 주입해준 코드는 아래와 같이 변경할 수 있으나, 어노테이션으로 주입해주는 걸 추천한다.</p>
<pre><code class="language-java">    public ExampleRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
</code></pre>
<p>SpringBoot에서 JPA를 사용하여 Repository를 사용하는 건 더 간단하다.
어노테이션을 붙일 필요 없이 아래와 같이 작성해주면 된다.</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
    User findByNickname(String nickname);

    Boolean existsByNickname(String Nickname);
}</code></pre>
<p>어노테이션을 사용할 필요 없이 JPARepository를 사용해주면 된다.</p>
<p>그럼 다음으로 Service 어노테이션에 대해 알아보자.</p>
<h2 id="service">@Service</h2>
<p><strong>Service</strong> 어노테이션은 해당 클래스가 비즈니스 로직을 담당하는 서비스 계층 구성 요소임을 알려주는 어노테이션이다.</p>
<p>아래와 같은 특징을 가지고 있다.</p>
<blockquote>
<ul>
<li>주로 Service Layer 클래스에 사용</li>
</ul>
</blockquote>
<ul>
<li>스프링 컨테이너에 해당 클래스를 빈으로 등록</li>
<li>스프링의 트랜잭션 관리를 받음</li>
<li>비즈니스 로직을 가지고 있음</li>
</ul>
<p>개념과 특징을 알았으니, 코드로 살펴보자.</p>
<pre><code class="language-java">@Service
public class UserService {

    @Autowired
    UserRepository userRepository;

    public User saveUser(User user) {
        return userRepository.save(user);
    }
}</code></pre>
<p>위에 보이는 것처럼 클래스 위에 어노테이션을 사용해 주면 된다.</p>
<p>서비스 클래스는 기능의 처리와 관련된 부분을 작성해야 한다.
서비스 클래스에서 비즈니스 요구사항을 구현하면, 후에 나올 컨트롤러에서 서비스 클래스의 함수를 호출해주는 형식으로 작성이 이루어져야 한다.</p>
<h2 id="component">@Component</h2>
<p><strong>Component</strong> 어노테이션은 스프링 IoC 컨테이너가 해당 클래스를 자동으로 빈으로 인식하고, 필요한 곳에 주입하거나 관리할 수 있게 하는 어노테이션이다.</p>
<p>아래의 특징이 있다.</p>
<blockquote>
<ul>
<li>빈을 등록하고 자동으로 검색 및 등록 가능</li>
</ul>
</blockquote>
<ul>
<li>스프링이 클래스의 의존성을 관리하며 필요한 곳에 자동으로 의존성 주입</li>
<li>@Repository, @Service. @Controller는 모두 @Component를 확장한 어노테이션</li>
</ul>
<p>설명은 이 정도면 끝일 것 같고, 앞서 했던 것처럼 코드로 확인해보자.</p>
<pre><code class="language-java">@Component
public class User {
    public User() {
        System.out.println(&quot;Component Example&quot;);
    }
}</code></pre>
<p>이렇게 사용해주면 User 클래스를 스프링 컨테이너가 빈으로 인식하게 되고, 의존성을 주입한다.</p>
<h2 id="controller">@Controller</h2>
<p><strong>Controller</strong> 어노테이션은 해당 클래스가 웹 어플리케이션 요청을 처리하는 컨트롤러임을 나타내는 어노테이션이다.</p>
<p>아래의 특징이 있다.</p>
<blockquote>
<ul>
<li>웹 어플리케이션에서 클라이언트의 요청을 처리하는 컨트롤러 역할</li>
</ul>
</blockquote>
<ul>
<li>@RequestMapping 어노테이션으로 컨트롤러에서 처리하는 요청의 기본 경로 지정 가능</li>
<li>메서드 수준에서 어떤 경로의 요청을 처리할지 지정 가능</li>
<li>@Component 어노테이션을 구체화 한 것으로, 해당 클래스를 IoC 컨테이너에 빈 등록</li>
</ul>
<p>컨트롤러에는 자주 사용되는 어노테이션이 있는데, 대표적으로 몇 가지만 알아보자.</p>
<table>
<thead>
<tr>
<th align="center">어노테이션</th>
<th align="center">목적</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">@RequestMapping</td>
<td align="center">URI로 요청이 오면, 어떤 메서드로 처리할지 매핑</td>
<td>헤더 등 지정 가능</td>
</tr>
<tr>
<td align="center">@GetMapping</td>
<td align="center">HTTP GET 메서드에 대한 요청을 처리</td>
<td>CRUD의 Read</td>
</tr>
<tr>
<td align="center">@PostMapping</td>
<td align="center">HTTP POST 메서드에 대한 요청을 처리</td>
<td>CRUD의 Create</td>
</tr>
<tr>
<td align="center">@PutMapping</td>
<td align="center">HTTP PUT 메서드에 대한 요청을 처리</td>
<td>CRUD의 Update</td>
</tr>
<tr>
<td align="center">@DeleteMapping</td>
<td align="center">HTTP DELETE 메서드에 대한 요청을 처리</td>
<td>CRUD의 Delete</td>
</tr>
</tbody></table>
<p>그럼, 마지막으로 코드로 살펴보자.</p>
<pre><code class="language-java">@Controller
public class ExampleController {

    @RequestMapping(&quot;/example&quot;)
    public String example() {
        return &quot;example&quot;;
    }
}</code></pre>
<p>이런 식으로 사용해주면 된다.</p>
<p>Controller 말고도 <strong>@RestController</strong>라는 어노테이션이 존재하는데, @Controller는 JSP 등의 View 템플릿 엔진을 사용하여 HTML 페이지를 넘겨주지만, @RestController는 RESTful API를 제공하는 컨트롤러에서 사용된다는 차이점이 있다.</p>
<hr>
<p>오늘은 이렇게 Spring의 MVC를 위한 기초 어노테이션에 대해서 알아봤다.
원래는 <strong>Bean</strong>에 대해서도 작성할까 헀지만, 너무 길어질 것 같아 다음 포스팅에서 다루기로 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] StateFlow vs. SharedFlow]]></title>
            <link>https://velog.io/@mi-fasol/Kotlin-StateFlow-vs.-SharedFlow</link>
            <guid>https://velog.io/@mi-fasol/Kotlin-StateFlow-vs.-SharedFlow</guid>
            <pubDate>Tue, 14 Nov 2023 13:32:54 GMT</pubDate>
            <description><![CDATA[<p>GDSC에서 안드로이드 스터디를 진행하며 Flow에 관해 공부했었다.</p>
<p>그 중, StateFlow와 SharedFlow의 차이점에 대해 조금 더 공부하고자 포스팅을 한다.</p>
<p>그 전에 짚고 넘어갈 점이 있다면, StateFlow와 SharedFlow는 모두 핫 스트림이란 점이다.</p>
<p>콜드 스트림과 핫 스트림의 차이는 아래와 같다.</p>
<blockquote>
<ul>
<li>콜드 스트림: 구독자가 스트림을 시작할 때마다 데이터 재생성 및 방출<ul>
<li>각 구독자는 스트림을 <strong>독립적으로</strong> 소비</li>
<li>각자의 시작점부터 데이터 수신</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li>핫 스트림: 데이터를 생성하고 방출하는 주체가 구독자와 독립적으로 동작<ul>
<li>스트림이 이미 존재하고 있음</li>
<li>구독자가 현재의 스트림 상태를 얻거나 중간부터 데이터 수신 가능</li>
<li>이벤트 발생 시 <strong>모든 구독자에게</strong> 동시에 데이터 <strong>전달</strong></li>
</ul>
</li>
</ul>
<p>우선 SharedFlow부터 살펴 보기로 하자.</p>
<h2 id="sharedflow">SharedFlow</h2>
<p><strong>SharedFlow</strong>는 무엇일까?</p>
<blockquote>
<ul>
<li>SharedFlow: 수집하는 모든 소비자에게 값을 내보내는 핫 스트림 플로우<ul>
<li>여러 구독자 간에 데이터 공유 가능</li>
<li>내부적으로 상태를 유지하고 있기에 현재 상태를 새로운 구독자에게 전달하지만 과거에 있던 정보를 보여주는 것</li>
<li>Flow를 상속받고 있음</li>
</ul>
</li>
</ul>
</blockquote>
<p>SharedFlow는 Flow를 상속받고 있으며, 여러 구독자가 데이터를 공유할 수 있는 플로우다.</p>
<p><strong>emit</strong> 함수를 통해 새로운 <strong>이벤트를 방출</strong>하고, 구독 중인 모든 구독자에게 이를 전달하게 된다.</p>
<p>SharedFlow는 아래와 같이 생성하고, 방출할 수 있다.</p>
<pre><code class="language-kotlin">// 생성
 val sharedFlow = MutableSharedFlow&lt;Int&gt;()

 // 방출
 sharedFlow.emit(1)
 sharedFlow.emit(2)
 sharedFlow.emit(3)</code></pre>
<p>SharedFlow는 <strong>중복 값을 필터링</strong> 하여 emit 된 값이 중복된다면 collect 하지 않는 특성을 가지고 있으며, 만약 Flow를 SharedFlow로 변경하고 싶다면, <strong>SharedIn</strong>을 사용할 수 있다.</p>
<pre><code class="language-kotlin">public fun &lt;T&gt; Flow&lt;T&gt;.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow&lt;T&gt; </code></pre>
<p>위의 코드를 작성하면 Flow가 주어진 코루틴 스코프의 스트림을 SharedFlow로 변경할 수 있다.</p>
<h2 id="stateflow">StateFlow</h2>
<p>그렇다면 <strong>StateFlow</strong>란 무엇일까?</p>
<blockquote>
<ul>
<li>StateFlow: 상태를 가지고 있는 Flow<ul>
<li>상태를 가지고 있기 때문에 새로운 수신자가 등록되면 현재 상태를 즉시 전달 가능</li>
<li>SharedFlow를 상속 받고 있음</li>
</ul>
</li>
</ul>
</blockquote>
<p>이렇게 간단하게 정의할 수 있다. 단어 그대로 상태를 갖는 Flow다.</p>
<p>StateFlow는 현재 상태와 새로 변경된 상태를 계속 내보내게 된다.
상태를 변경하기 위해서는 MutableStateFlow의 value 값에 할당해주면 된다.</p>
<pre><code class="language-kotlin"> private val stateFlow = MutableStateFlow(&quot;First State&quot;)</code></pre>
<p>위의 코드가 <strong>StateFlow를 생성한 코드</strong>다.
이 코드에서 <strong>상태를 변경</strong>하고자 한다면 아래와 같이 작성하면 된다.</p>
<pre><code class="language-kotlin">exmaple.value = &quot;Seconde Flow&quot;</code></pre>
<p>앞서 말한 것처럼 value를 사용해 상태를 변경해준 코드다.</p>
<p>StateFlow는 상태를 가지고 있는 Flow이기 때문에 주로 UI처럼 상태 변화에 민감한 애플리케이션에서 사용된다.</p>
<p>만약 Flow를 SharedFlow처럼 핫 스트림으로 변경하고 싶다면, 또다른 방법으로 <strong>StateIn</strong>을 사용할 수 있다.</p>
<pre><code class="language-kotlin">fun &lt;T&gt; Flow&lt;T&gt;.stateIn(
    scope: CoroutineScope,
    started: SharingStarted,
    initialValue: T
): StateFlow&lt;T&gt;</code></pre>
<p>위의 코드를 작성하면 Flow가 주어진 코루틴 스코프의 스트림을 StateFlow로 변경할 수 있다.</p>
<hr>
<p>오늘은 간단하게 SharedFlow와 StateFlow의 차이에 대해서 알아봤다.</p>
<p>사실 이걸 작성하면서도 완전히 이해된 것은 아니라 앞으로도 계속 Flow에 대해서 공부를 해봐야 할 것 같다.</p>
<p>다음에는 이해도를 높이기 위해 Flow 자체에 대한 포스팅을 올려보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 닉네임 중복 확인 함수]]></title>
            <link>https://velog.io/@mi-fasol/Spring-%EB%8B%89%EB%84%A4%EC%9E%84-%EC%A4%91%EB%B3%B5-%ED%99%95%EC%9D%B8-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@mi-fasol/Spring-%EB%8B%89%EB%84%A4%EC%9E%84-%EC%A4%91%EB%B3%B5-%ED%99%95%EC%9D%B8-%ED%95%A8%EC%88%98</guid>
            <pubDate>Tue, 22 Aug 2023 14:25:27 GMT</pubDate>
            <description><![CDATA[<p>이래저래 시간을 보내다 굉장히 오랜만에 글을 작성한다.</p>
<p>그동안 정보처리기사 필기 시험을 준비하고 토이 프로젝트를 진행하고 있었다.
필기 시험은 공부한 범위 내에서 나왔고 80점을 넘기며 넉넉하게 합격했다.</p>
<p>이제 실기 시험만 남았는데, 실기 시험도 열심히 공부를 해야지.</p>
<p>사담은 여기까지 하고 오늘은 <strong>스프링부트</strong>와 <strong>플러터</strong>에서 회원가입 시 닉네임을 확인하는 기능을 포스팅 할 예정이다.</p>
<p>현재 진행하고 있는 토이 프로젝트가 어느정도는 틀이 잡혔는데 생각보다 세세한 부분을 놓친 게 많더라.
오늘은 그 중 하나인 <strong>닉네임 중복 확인 기능</strong>이다.</p>
<p>우선 스프링의 코드부터 확인하겠다.</p>
<p><strong>UserController.java</strong></p>
<pre><code class="language-java">    @GetMapping(&quot;isExist/{nickname}&quot;)
    public ResponseEntity&lt;Boolean&gt; checkNickname(@PathVariable String nickname)
    {
        return ResponseEntity.ok(userService.checkNickname(nickname));
    }</code></pre>
<p>굉장히 간단한 한 줄짜리 함수다.</p>
<p>userService에 분리해둬서 그렇다.</p>
<p><strong>UserService.java</strong></p>
<pre><code class="language-java">public boolean checkNickname(String nickname) {
        return userRepository.existsByNickname(nickname);
    }</code></pre>
<p>사실 얘도 한 줄짜리다. ㅋㅋㅋㅋ.</p>
<p>뭔가 복잡한 로직이 아니라 해당 닉네임이 존재하는지 안 하는지만 확인해 주면 되기 때문이다.</p>
<p>그 다음은 레포지토리에도 딱 한 줄만 선언해주면 된다.</p>
<p><strong>UserRepository.java</strong></p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
    User findByNickname(String nickname);

    // 여기!
    Boolean existsByNickname(String Nickname);
}</code></pre>
<p>이렇게 작성을 마쳐주고 나면, JPA가 스스로 대문자를 필드 이름으로 인식해 매개변수로 들어온 Nickname이 테이블의 nickname 필드에 존재하고 있는지를 확인해 준다.</p>
<p>나는 있는지 없는지 여부만 확인하면 됐기에 Boolean으로 리턴했다.</p>
<p>그럼 이제 Flutter의 코드를 보자.</p>
<p><strong>DBService.dart</strong></p>
<pre><code class="language-dart">Future&lt;bool&gt; checkNicknameDuplicate(String nickname) async {
    final response = await http
        .get(Uri.parse(&quot;uri&quot;));

    if (response.statusCode == 200) {
      final data = json.decode(response.body) as bool;
      return data;
    } else {
      throw Exception(&#39;Failed to check nickname availability&#39;);
    }
  }</code></pre>
<p>위와 같이 함수를 작성했다. uri 주소에 Get 형식의 요청을 보내서 돌아온 json 값을 bool 형식으로 받아 와 반환해줬다.</p>
<p>활용하는 코드는 아래와 같다.</p>
<p><strong>UserController.dart</strong></p>
<pre><code class="language-dart">  Future&lt;bool&gt; checkNickname(String nickname) async {
    DBService db = DBService();
    bool isUsed = await db.checkNicknameDuplicate(nickname);
    if (isUsed == false) {
      return true;
    } else {
      return false;
    }
  }</code></pre>
<p>Future를 활용하여 통신이 가능하게 해줬고 bool 값을 반환하여 존재하는지 안 하는지 여부를 알 수 있게 해줬다.</p>
<p>서버와 통신하는 함수를 분리해서 사용할 때는 꼭 async-await을 사용하는 걸 잊지 말자.</p>
<p>이렇게 controller에 함수를 작성해주면, 마지막으로 아래와 같이 활용해주면 된다.</p>
<p>나는 모든 회원 필드가 입력됐다는 가정 하에 버튼이 활성화 되도록 구현했는데,</p>
<blockquote>
<ol>
<li>버튼을 클릭하면 닉네임 중복을 확인하고,
2-1. 닉네임 중복 시 다이얼로그를 띄운다.
2-2. 닉네임이 중복되지 않을 시 모든 회원 정보를 저장한다.</li>
</ol>
</blockquote>
<p>대충 이런 알고리즘으로 간단하게 작성했다.</p>
<p><strong>RegisterPage.dart</strong></p>
<pre><code class="language-dart">onPressed: () async {
          bool isAvailableNickname = await _userController
                          .checkNickname(_textController.text);
          if (isAvailableNickname) {
                 _userController.saveInfo(
                 _textController.text,
                 _selectedMajor,
                 _selectedGender,
                 userImage[_index],
                        );
          } else {
                 showNicknameUnavailable(
                       context,
                       &quot;닉네임 사용 불가&quot;,
                       &quot;이미 사용 중인 닉네임입니다.&quot;);
                  }
         },</code></pre>
<p>이렇게 작성해주면 아래와 같이 작동이 된다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/9eab9cb3-a543-4e42-83a0-0f0547c61706/image.png" alt=""></p>
<p>이렇게 이미 존재하는 닉네임을 입력을 해주고, 등록을 누르면</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/9dc906aa-b869-42c3-94d4-408ec2530829/image.png" alt=""></p>
<p>이렇게 간단하게 다이얼로그가 생성되며 회원 저장이 되지 않는다.</p>
<p>어렵지 않은 기능인데도 말로 풀어서 설명하려니 글이 생각보다 길어진 느낌이다.</p>
<p>다음에 구현할 기능은 신고하기 기능인데, 자동으로 이메일이 전송되도록 할 예정이다.</p>
<p>블로그를 돌아보니 내 눈에는 잘 읽히는 코드들이 다른 사람들에게는 잘 이해되지 않을 수도 있을 거라는 게 문득 떠올랐다.
다음에는 처음 본 사람도 이해하기 쉽게 글을 작성하는 걸 우선적으로 생각해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[정보처리기사 필기] 네트워크 IP 주소 범위]]></title>
            <link>https://velog.io/@mi-fasol/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%ED%95%84%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP-%EC%A3%BC%EC%86%8C-%EB%B2%94%EC%9C%84</link>
            <guid>https://velog.io/@mi-fasol/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%ED%95%84%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP-%EC%A3%BC%EC%86%8C-%EB%B2%94%EC%9C%84</guid>
            <pubDate>Fri, 14 Jul 2023 09:29:11 GMT</pubDate>
            <description><![CDATA[<p>오늘도 필기 시험을 준비하다가, 기출 몇 개를 풀어도 헷갈리는 부분이 또 생겨서 글을 작성한다.</p>
<p>프로그래밍 파트의 IP 주소의 단계에 관한 개념이다.
분명 컴퓨터네트워크라는 과목에서 배웠던 개념인데, 네트워크의 길이에 따라서 클래스가 나뉜다는 것을 알고 있는데 이상하게 문제를 풀 때마다 헷갈린다.</p>
<blockquote>
<p>Q. C Class에 속하는 IP address는?</p>
</blockquote>
<ol>
<li>200.168.30.1</li>
<li>10.3.2.1</li>
<li>225.2.4.1</li>
<li>172.16.98.3</li>
</ol>
<p>정답은 1번 200.168.30.1이었다.</p>
<h3 id="ip-address">IP Address</h3>
<p><strong>IP 주소</strong>란, 인터넷에 연결된 모든 컴퓨터 자원을 구분하기 위한 고유한 인터넷 주소다.
8비트 씩 네 개로 구성되어 있어 총 32비트의 길이를 가지고 있고, 네트워크 주소 길이에 따라 <strong>A 클래스부터 E 클래스까지 총 다섯 개로 분류</strong>된다.</p>
<p>보통은 A, B, C의 세 가지가 가장 많이 활용된다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/b2a773ba-78c2-425c-8999-6e3450cc0863/image.png" alt=""></p>
<h4 id="a-class">A Class</h4>
<p>A 클래스는 국가 및 대형 통신망에 이용되는 주소로, 0 ~ 127 사이의 숫자로 시작한다.</p>
<h4 id="b-class">B Class</h4>
<p>B 클래스는 중대형 통신망에 이용되는 주소로, 128 ~ 191 사이의 숫자로 시작한다.</p>
<h4 id="c-class">C Class</h4>
<p>C 클래스는 그보다 더 작은 소규모 통신망에 이용되는 주소로, 192 ~ 223 사이의 숫자로 시작한다.</p>
<h4 id="d-class">D Class</h4>
<p>D 클래스는 멀티캐스트에 이용되는 주소로, 224 ~ 239 사이의 숫자로 시작한다.</p>
<h4 id="e-class">E Class</h4>
<p>E 클래스는 실험적인 주소로, 공용되는 주소가 아니다.</p>
<hr>
<p>시험이 얼마 남지 않았음에도, 틀린 문제를 계속 틀리고 있다.
이번 정리를 계기로 IP 주소에 관련된 문제가 스스로 잘 이해됐기를 바랄 뿐이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[정보처리기사 필기] UML (Unified Modeling Language)]]></title>
            <link>https://velog.io/@mi-fasol/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%ED%95%84%EA%B8%B0-UML-Unified-Modeling-Language</link>
            <guid>https://velog.io/@mi-fasol/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%ED%95%84%EA%B8%B0-UML-Unified-Modeling-Language</guid>
            <pubDate>Wed, 12 Jul 2023 12:57:05 GMT</pubDate>
            <description><![CDATA[<p>7월 20일에 있을 정보처리기사 필기 시험을 위해서 공부하던 중 매번 비슷하게 틀리는 문제가 있어서 제대로 공부할 겸 포스팅 해보기로 했다.</p>
<p>정보시스템이랑 DB, 프로그래밍 쪽은 어느정도 완성이 됐는데 소프트웨어 계획과 개발 쪽이 계속 오답이 나온다.</p>
<p>우선 내가 틀린 문제는 시나공 2023년도 3회 대비 모의고사의 17번 문제였다.</p>
<blockquote>
</blockquote>
<p>Q. UML에서 활용되는 다이어그램 중 시스템의 동작을 표현하는 행위 다이어그램에 해당하지 않는 것은?</p>
<ol>
<li>유스케이스 다이어그램</li>
<li>시퀀스 다이어그램</li>
<li>활동 다이어그램</li>
<li>배치 다이어그램</li>
</ol>
<p>1번과 3번은 알고 있었는데, 뭔가 배치 다이어그램도 행위 다이어그램에서 본 기억이 있던 것 같아서 바보같이 2번을 선택했었다.</p>
<p>정답은 4번 배치 다이어그램이었다.
워낙에 다이어그램의 종류가 많기도 하고 이름이 헷갈려서 매번 풀 때마다 오답률이 꽤 높았기에 이번에 다시 정리해보려 한다.</p>
<h3 id="umlunified-modeling-language">UML(Unified Modeling Language)</h3>
<p>UML은 소프트웨어 모델을 생성하기 위한 표준 언어다. 
<strong>소프트웨어 개발 과정 전반에서의 원활한 의사소통을 위한 통합 모델링 언어</strong>라고 정의하면 될 것 같다.</p>
<p>UML은 각 목적에 따라 크게 두 가지로 분류될 수 있다.</p>
<h4 id="구조-다이어그램">구조 다이어그램</h4>
<blockquote>
<ul>
<li>클래스 다이어그램:클래스의 속성, 함수, 변수 타입으로 구성된 다이어그램</li>
</ul>
</blockquote>
<ul>
<li>객체 다이어그램: 클래스의 인스턴스, 객체와 객체 간의 관계로 구성된 다이어그램</li>
<li>복합체 구조 다이어그램: 클래스 혹은 컴포넌트가 복합 구조인 경우 해당 내부 구조를 표현하는 다이어그램</li>
<li>배치 다이어그램: 물리적 요소들의 위치를 표현하는 다이어그램</li>
<li>컴포넌트 다이어그램: 컴포넌트 간의 관계나 컴포넌트 사이의 인터페이스를 표현하는 다이어그램</li>
<li>패키지 다이어그램: 모델 요소를 그룹화 한 패키지 간의 관계를 표현하는 다이어그램</li>
</ul>
<h4 id="행위-다이어그램">행위 다이어그램</h4>
<blockquote>
<ul>
<li>활동 다이어그램: 시스템이 수행하는 기능에 대한 로직 혹은 조건에 따른 처리 흐름을 순서에 따라 표현하는 다이어그램</li>
</ul>
</blockquote>
<ul>
<li>상태 다이어그램: 객체 내부의 자세한 동작이나 시스템 전체의 동작을 표현하는 다이어그램</li>
<li>유스케이스 다이어그램: 사용자 요구를 분석하여 시스템과 사용자 간의 상호작용을 표현하는 다이어그램</li>
<li>순차 다이어그램: 상호작용하는 시스템 혹은 객체 간에 주고 받는 메시지를 표현하는 다이어그램</li>
<li>상호작용 개요 다이어그램: 상호작용 다이어그램 사이의 제어 흐름을 표현하는 다이어그램</li>
<li>통신 다이어그램: 상호작용에 참여하는 요소 간의 관계를 명확하게 표현하는 다이어그램</li>
<li>타이밍 다이어그램: 객체 상태 변화 및 시간 제약을 명시적으로 표현하는 다이어그램</li>
</ul>
<p>이 정도로 정리할 수 있을 것 같다.
이번 정리를 계기로 UML 문제의 오답률이 조금 줄어들기를 바란다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Troubleshooting] port is already in use. (for Mac)]]></title>
            <link>https://velog.io/@mi-fasol/Bug-Review-port-is-already-in-use.-for-Mac</link>
            <guid>https://velog.io/@mi-fasol/Bug-Review-port-is-already-in-use.-for-Mac</guid>
            <pubDate>Mon, 22 May 2023 15:06:11 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 개발을 위해서 서버를 돌렸는데, 저번부터 자꾸 <strong>&quot;port  0000 is already in use.&quot;</strong>라는 에러가 발생했다.</p>
<p>저번에 발생했었을 때는 포트 번호를 8080으로 사용하고 있었기 때문에 그러려니 하고 프로퍼티의 포트 번호를 변경하고 사용했는데, 절대 사용하지 않을 번호가 계속 사용 중이라는 거다.</p>
<p>말하기는 조금 부끄럽지만, 복잡한 번호가 귀찮아서 1004번을 사용하고 있는데 내가 이걸 다른 곳에서 사용할 일이 뭐가 있나..</p>
<p>그래서 이곳저곳을 찾아본 결과, 서버를 종료할 때 비정상적으로 종료가 되면 계속 사용되고 있다고 나오는 경우도 종종 발생한다고 한다.</p>
<p>해결하기 위해서는 아래의 두 가지 방법이 있다.</p>
<h4 id="1-포트-번호를-변경한다">1. 포트 번호를 변경한다.</h4>
<h4 id="2-현재-실행-중인-포트를-강제-종료하고-서버를-재시작한다">2. 현재 실행 중인 포트를 강제 종료하고 서버를 재시작한다.</h4>
<p>포트 번호를 바꾸는 거야, 나처럼 application.properties 파일에서 번호만 바꿔주면 되는 건데 계속 이런 식으로 반복되는 오류라면 바꾸지 않고 해결하는 방법을 찾고 싶었다.</p>
<p>필자는 mac을 사용 중이기에 mac 기준으로 해결 방법을 작성하겠다.</p>
<p>우선 terminal을 켜고 정말 간단하게 명령을 작성하면 된다.</p>
<p><strong>lsof -i tcp:포트번호</strong></p>
<p>이렇게 작성하면 아래와 같은 정보가 나온다.
<img src="https://velog.velcdn.com/images/mi-fasol/post/3da145bf-16a0-42c8-83a5-c55d8ea6636a/image.png" alt=""></p>
<p>java에서 해당 포트를 사용하고 있다는 건데, 이 정보에서 우리가 필요한 건 PID다.</p>
<p>해당 PID를 확인한 후 <strong>sudo kill -9 PID</strong> 명령어를 작성해주면 해당 포트를 사용 중인 작업이 종료된다.</p>
<p>그 후 다시 서버를 돌려보면 멀쩡하게 돌아가는 모습을 확인할 수 있다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] MapBox 활용]]></title>
            <link>https://velog.io/@mi-fasol/React-MapBox-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@mi-fasol/React-MapBox-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Wed, 17 May 2023 13:30:34 GMT</pubDate>
            <description><![CDATA[<p>시험기간이다 뭐다 해서 바빠서 블로그를 못 올린지 벌써 한 달이나 됐다.</p>
<p>다행히 시험 점수는 괜찮게 나온 듯 싶어서 기분 좋게 포스팅을 하려고 한다.</p>
<p>현재 4학년으로 재학 중이라 졸업작품을 진행하고 있는데, 이번 졸업작품에서 리액트로 웹 서비스를 해야 할 일이 생겼다.</p>
<p>기본적인 틀은 다 MapBox에서 기본으로 제공하는 튜토리얼 소스를 활용했고, 서울시의 지도를 보여줘야 했기 때문에 구글에서 찾은 서울시 geojson 데이터를 활용했다.</p>
<p>우선 <strong>맵 박스</strong>는, 카카오 맵이나 구글 맵처럼 지도를 제공하는 서비스 중 하나다.</p>
<p>자유롭게 디자인을 변경할 수 있어 자유도가 굉장히 높은 편이라, 디자인에 따라 수정할 게 많지만 그 대가로 확실히 예쁜 지도를 얻을 수 있다.</p>
<p>나는 아직 디자인 단계가 아니기 때문에 기본 스타일을 그대로 활용했다.</p>
<p>나는 노트북에 원래 리액트 관련 소프트웨어가 설치되어 있어서 이 포스팅에서 굳이 언급하지 않겠지만, 혹시 깔려있지 않으신 분들은 미리 설치하고 이 포스팅을 참고하시길 바란다.</p>
<p><a href="https://github.com/mapbox/mapbox-react-examples/tree/c9ebecb50510aa4b2aa88180354fe71f3131cbe8/data-overlay">맵박스 예제 코드</a></p>
<p>위의 코드를 참고하여 진행했었다.</p>
<p>귀찮은 분들을 위해 코드의 전문을 올려보자면</p>
<p><strong>Legend.js</strong></p>
<pre><code class="language-javascript">import React from &quot;react&quot;;

const Legend = (props) =&gt; {
  const renderLegendKeys = (stop, i) =&gt; {
    return (
      &lt;div key={i} className=&quot;txt-s&quot;&gt;
        &lt;span
          className=&quot;mr6 round-full w12 h12 inline-block align-middle&quot;
          style={{ backgroundColor: stop[1] }}
        /&gt;
        &lt;span&gt;{`${stop[0].toLocaleString()}`}&lt;/span&gt;
      &lt;/div&gt;
    );
  };

  return (
    &lt;&gt;
      &lt;div className=&quot;bg-white absolute bottom right mr12 mb24 py12 px12 shadow-darken10 round z1 wmax180&quot;&gt;
        &lt;div className=&quot;mb6&quot;&gt;
          &lt;h2 className=&quot;txt-bold txt-s block&quot;&gt;{props.active.name}&lt;/h2&gt;
          &lt;p className=&quot;txt-s color-gray&quot;&gt;{props.active.description}&lt;/p&gt;
        &lt;/div&gt;
        {props.stops.map(renderLegendKeys)}
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default Legend;
</code></pre>
<p><strong>Optionfield.js</strong></p>
<pre><code class="language-javascript">import React from &quot;react&quot;;

const Optionsfield = (props) =&gt; {
  const renderOptions = (option, i) =&gt; {
    return (
      &lt;label key={i} className=&quot;toggle-container&quot;&gt;
        &lt;input
          onChange={() =&gt; props.changeState(i)}
          checked={option.property === props.property}
          name=&quot;toggle&quot;
          type=&quot;radio&quot;
        /&gt;
        &lt;div className=&quot;toggle txt-s py3 toggle--active-white&quot;&gt;
          {option.name}
        &lt;/div&gt;
      &lt;/label&gt;
    );
  };
  return (
    &lt;div className=&quot;toggle-group absolute top left ml12 mt12 border border--2 border--white bg-white shadow-darken10 z1&quot;&gt;
      {props.options.map(renderOptions)}
    &lt;/div&gt;
  );
};

export default Optionsfield;</code></pre>
<p>이 두 코드를 src 디렉토리에 하위 디렉토리 components를 생성해 넣어줬다.</p>
<p>data.json 파일도 하나 생성해서 원하는 geojson 데이터를 넣어주면 된다. 이건 각자 원하는 데이터가 다를 테니까 생략하고 넘어가도록 하겠다.</p>
<p>App.js 파일도 변경해줘야 하는데, 정말 간단하다. Map을 리턴해주기만 하면 된다.</p>
<p><strong>App.js</strong></p>
<pre><code class="language-javascript">import Map from &#39;./Map&#39;;
import React from &#39;react&#39;

function App() {
  return (
    &lt;div&gt;
      &lt;Map/&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<p>그 다음으로는 index.css파일을 설정해줘야 한다.</p>
<p><strong>index.css</strong></p>
<pre><code class="language-css">body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, &#39;Roboto&#39;, &#39;Oxygen&#39;,
    &#39;Ubuntu&#39;, &#39;Cantarell&#39;, &#39;Fira Sans&#39;, &#39;Droid Sans&#39;, &#39;Helvetica Neue&#39;,
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, &#39;Courier New&#39;,
    monospace;
}</code></pre>
<p>css 파일을 설정해줬으니 js 파일도 설정해주자.</p>
<p><strong>index.js</strong></p>
<pre><code class="language-javascript">import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import &#39;./index.css&#39;;
import App from &#39;./App&#39;;

const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;));
root.render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;
);</code></pre>
<p>그 다음은 정말 별 거 없는 Map.css 파일이다.</p>
<p><strong>Map.css</strong></p>
<pre><code class="language-css">.map-container {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}</code></pre>
<p>이제 마지막으로 가장 중요한 코드인데, MapBox에서 기본으로 제공하는 코드인 Map.js 코드다.</p>
<p><strong>Map.js</strong></p>
<pre><code class="language-javascript">import React, { useRef, useEffect, useState } from &#39;react&#39;;
import mapboxgl from &#39;mapbox-gl&#39;;
import Legend from &#39;./components/Legend&#39;;
import Optionsfield from &#39;./components/Optionsfield&#39;;
import &#39;./Map.css&#39;;
import data from &#39;./data.json&#39;;

mapboxgl.accessToken =
  &#39;pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4M29iazA2Z2gycXA4N2pmbDZmangifQ.-g_vE53SD2WrJ6tFX7QHmA&#39;;

const Map = () =&gt; {
  const options = [
    {
      name: &#39;Population&#39;,
      description: &#39;Estimated total population&#39;,
      property: &#39;pop_est&#39;,
      stops: [
        [0, &#39;#f8d5cc&#39;],
        [1000000, &#39;#f4bfb6&#39;],
        [5000000, &#39;#f1a8a5&#39;],
        [10000000, &#39;#ee8f9a&#39;],
        [50000000, &#39;#ec739b&#39;],
        [100000000, &#39;#dd5ca8&#39;],
        [250000000, &#39;#c44cc0&#39;],
        [500000000, &#39;#9f43d7&#39;],
        [1000000000, &#39;#6e40e6&#39;]
      ]
    },
    {
      name: &#39;GDP&#39;,
      description: &#39;Estimate total GDP in millions of dollars&#39;,
      property: &#39;gdp_md_est&#39;,
      stops: [
        [0, &#39;#f8d5cc&#39;],
        [1000, &#39;#f4bfb6&#39;],
        [5000, &#39;#f1a8a5&#39;],
        [10000, &#39;#ee8f9a&#39;],
        [50000, &#39;#ec739b&#39;],
        [100000, &#39;#dd5ca8&#39;],
        [250000, &#39;#c44cc0&#39;],
        [5000000, &#39;#9f43d7&#39;],
        [10000000, &#39;#6e40e6&#39;]
      ]
    }
  ];
  const mapContainerRef = useRef(null);
  const [active, setActive] = useState(options[0]);
  const [map, setMap] = useState(null);

  // Initialize map when component mounts
  useEffect(() =&gt; {
    const map = new mapboxgl.Map({
      container: mapContainerRef.current,
      style: &#39;mapbox://styles/mapbox/streets-v11&#39;,
      center: [5, 34],
      zoom: 1.5
    });

    map.on(&#39;load&#39;, () =&gt; {
      map.addSource(&#39;countries&#39;, {
        type: &#39;geojson&#39;,
        data
      });

      map.setLayoutProperty(&#39;country-label&#39;, &#39;text-field&#39;, [
        &#39;format&#39;,
        [&#39;get&#39;, &#39;name_en&#39;],
        { &#39;font-scale&#39;: 1.2 },
        &#39;\n&#39;,
        {},
        [&#39;get&#39;, &#39;name&#39;],
        {
          &#39;font-scale&#39;: 0.8,
          &#39;text-font&#39;: [
            &#39;literal&#39;,
            [&#39;DIN Offc Pro Italic&#39;, &#39;Arial Unicode MS Regular&#39;]
          ]
        }
      ]);

      map.addLayer(
        {
          id: &#39;countries&#39;,
          type: &#39;fill&#39;,
          source: &#39;countries&#39;
        },
        &#39;country-label&#39;
      );

      map.setPaintProperty(&#39;countries&#39;, &#39;fill-color&#39;, {
        property: active.property,
        stops: active.stops
      });

      setMap(map);
    });

    // Clean up on unmount
    return () =&gt; map.remove();
  }, []);

  useEffect(() =&gt; {
    paint();
  }, [active]);

  const paint = () =&gt; {
    if (map) {
      map.setPaintProperty(&#39;countries&#39;, &#39;fill-color&#39;, {
        property: active.property,
        stops: active.stops
      });
    }
  };

  const changeState = i =&gt; {
    setActive(options[i]);
    map.setPaintProperty(&#39;countries&#39;, &#39;fill-color&#39;, {
      property: active.property,
      stops: active.stops
    });
  };

  return (
    &lt;div&gt;
      &lt;div ref={mapContainerRef} className=&#39;map-container&#39; /&gt;
      &lt;Legend active={active} stops={active.stops} /&gt;
      &lt;Optionsfield
        options={options}
        property={active.property}
        changeState={changeState}
      /&gt;
    &lt;/div&gt;
  );
};

export default Map;</code></pre>
<p>위의 코드는 각 나라별 인구분포도와 GDP에 따라 세계지도에 색을 입혀 보여주는 코드다.</p>
<blockquote>
<ul>
<li>options: 해당하는 property 값에 따라 정해진 구간 별로 색을 설정</li>
</ul>
</blockquote>
<ul>
<li>useEffect의 center: 지도를 띄울 때 중앙에 보일 지역<ul>
<li>본인은 서울 지도가 필요해서 뒤에 나올 코드에 보이 듯 서울의 위경도로 설정</li>
</ul>
</li>
<li>useEffect의 zoom: 지도 줌 비율</li>
<li>map.on(&#39;load&#39; ...): 지도가 로딩 될 때 설정<ul>
<li>countries라는 id를 가진 속성에 geojson 타입의 데이터를 삽입</li>
<li>data.json 파일을 따로 만들어뒀기에 data를 import 함</li>
</ul>
</li>
<li>map.addLayer(...): MapBox 스타일의 레이어 추가</li>
<li>map.setPaintProperty(...): 지정된 스타일 레이어의 페인트 속성 값 변경</li>
</ul>
<p>대충 이정도로 간단하게 설명할 수 있겠다. 실행시키면 아래의 화면이 나온다.
<img src="https://velog.velcdn.com/images/mi-fasol/post/c46a9bf7-59bd-48af-bb98-238dd1d320d8/image.png" alt=""></p>
<p>나는 여기서 사소하게 몇 부분만 건드렸는데, 우리 졸업작품에선 <strong>포트홀에 따라 색</strong>이 바뀌어야 했기 때문에 <strong>서울시</strong> 행정구역 geojson 파일에 pothole이라는 이름의 <strong>프로퍼티를 임의로 추가</strong>해줬고, <strong>색의 구간도 변경</strong>해줬으며 <strong>각 구역에 클릭 이벤트</strong>를 주기 위해 코드를 추가했었다.</p>
<pre><code class="language-javascript">  const options = [
    {
      name: &#39;도로 파손 정도&#39;,
      description: &#39;도로 파손 개수에 따른 지표&#39;,
      // data.json에 있는 포트홀 프로퍼티로 구간 사용
      property: &#39;pothole&#39;,
      // 내 데이터에 맞도록 각 구간 변경
      stops: [
        [0, &#39;#f8d5cc&#39;],
        [3, &#39;#f4bfb6&#39;],
        [5, &#39;#f1a8a5&#39;],
        [7, &#39;#ee8f9a&#39;],
        [9, &#39;#ec739b&#39;],
        [12, &#39;#dd5ca8&#39;],
        [15, &#39;#c44cc0&#39;],
        [18, &#39;#9f43d7&#39;],
        [20, &#39;#6e40e6&#39;]
      ]
    },
  ];</code></pre>
<p>위의 코드가 구간을 정하는 프로퍼티 값을 포트홀로 바꾸고, 구간을 재설정한 코드다. 또, 아까 살짝 언급했지만 나는 서울 지도가 필요해서 아래와 같이 지도의 설정을 바꿔줬다.</p>
<pre><code class="language-javascript">useEffect(() =&gt; {
    const map = new mapboxgl.Map({
      container: mapContainerRef.current,
      style: &#39;mapbox://styles/mapbox/streets-v11&#39;,
      // 센터는 서울의 위경도
      center: [127.0016958, 37.5642135],
      // 줌도 모자라서 더 확대되도록 변경
      zoom: 10.0
    });</code></pre>
<p>위처럼 구간을 나눴고, addLayer 부분을 변경했다.</p>
<pre><code class="language-javascript">map.addLayer({
        // countries라는 id는 맞지 않다고 판단하여 변경
      id: &#39;region&#39;,
      type: &#39;fill&#39;,
      source: &#39;region&#39;,
      paint: {
        &#39;fill-color&#39;: {
          // 아까 구간 설정한 것처럼 pothole로 변경
          property: &#39;pothole&#39;,
          stops: active.stops
        }
      }
    });</code></pre>
<p>레이어의 id 값을 region으로 변경한 후 색이 입혀질 프로퍼티를 pothole로 변경해줬다.</p>
<p>마지막으로 지도의 각 구역을 클릭했을 때의 클릭 이벤트 리스너를 등록하는 코드를 추가적으로 넣어줬다.</p>
<pre><code class="language-javascript">map.on(&#39;click&#39;, &#39;region&#39;, (e) =&gt; {
      const features = map.queryRenderedFeatures(e.point, { layers: [&#39;region&#39;] });

      if (features.length &gt; 0) {
        const clickedFeature = features[0];
        // 클릭된 레이어(구역)의 포트홀 데이터를 가져옴
        const potholeValue = clickedFeature.properties.pothole;
        // 콘솔창에 로그로 출력
        console.log(`해당 구역의 포트홀 개수: ${potholeValue}`);
      }
    });</code></pre>
<p>이렇게 하면 아래처럼 간단한 결과를 얻을 수 있다. 
<img src="https://velog.velcdn.com/images/mi-fasol/post/4c2091bd-4c13-43da-af9e-91a5e7b19819/image.png" alt=""></p>
<p>왼쪽의 웹 화면에 있는 각 구역을 클릭하면 해당하는 포트홀 값을 아래 콘솔창에서 로그 값으로 보여준다.</p>
<p>지금은 아직 이렇게 간단한 기능밖에 없지만, 추후에 onClick 이벤트를 다이얼로그로 바꾸고 주변 지도도 좀 더 예쁘게 꾸며볼 생각이다.</p>
<p>맨 처음 맵박스를 활용할 때 정말 어떻게 해야 할지 감도 안 잡혔었고, 생각보다 자료도 많이 안 나와서 막막했었는데 나와 같은 경험을 한 분들이 조금이나마 수월하게 만드시기를 바라는 마음으로 올려본다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Flutter로 데이터 가져오기]]></title>
            <link>https://velog.io/@mi-fasol/Spring-Boot-Flutter%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</link>
            <guid>https://velog.io/@mi-fasol/Spring-Boot-Flutter%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</guid>
            <pubDate>Sat, 15 Apr 2023 09:15:39 GMT</pubDate>
            <description><![CDATA[<p>지난 게시물에서는 Flutter와 Spring boot를 연결하여 데이터를 전송하고 DB에 저장하는 코드를 작성했었다.</p>
<p>이제 Flutter에서 DB에 저장된 게시물 목록을 가져와 보여주고, 게시물 리스트의 아이템을 클릭하면 게시물의 상세 정보를 보여주는 기능을 생성해야 했는데, 어처구니 없는 이유로 오류가 발생한 걸 꼬박 하루만에 깨달아서 나와 같이 헤매고 있는 분들이 계실까 싶어 게시물로 작성한다.</p>
<p>우선, <strong>Flutter</strong>의 코드부터 살펴보자.</p>
<p>데이터를 전송하는 코드와 마찬가지로 <strong>db_service.dart</strong> 페이지에 API 통신을 위한 코드를 작성해줬다.</p>
<pre><code class="language-dart">  Future&lt;List&lt;PostResponse&gt;&gt; getAllPost() async {
    final response = await http.get(Uri.parse(&quot;api 통신 uri&quot;));
    if (response.statusCode == 200) {
      final data = json.decode(response.body) as List&lt;dynamic&gt;;
      return data
          .map&lt;PostResponse&gt;((json) =&gt; PostResponse.fromJson(json))
          .toList();
    } else {
      throw Exception(&#39;Failed to load post list&#39;);
    }
  }

  Future&lt;PostResponse&gt; getPostById(int id) async {
    final response =
        await http.get(Uri.parse(&quot;api 통신 uri&quot;));
    if (response.statusCode == 200) {
      final jsonData = json.decode(response.body) as Map&lt;String, dynamic&gt;;
      return PostResponse.fromJson(jsonData);
    } else if (response.statusCode == 404) {
      throw Exception(&quot;Post not found&quot;);
    } else {
      throw Exception(&quot;Failed to fetch Post by Id&quot;);
    }
  }</code></pre>
<p>간단히 함수에 대해서 설명하겠다.</p>
<blockquote>
<ul>
<li>getAllPost(): 모든 게시물 데이터를 list 형태로 불러오는 함수</li>
</ul>
</blockquote>
<ul>
<li>getPostById(int id): pId를 사용하여 특정 게시물을 불러오는 함수</li>
</ul>
<p>getAllPost() 함수는 PostResponse 모델을 리스트로 저장해 모든 게시물을 불러오고, getPostById() 함수는 단일 PostResponse 객체를 불러와 특정 게시물의 정보를 받아온다.</p>
<p>나는 pId를 Auto Increase로 설정했기 때문에 Flutter에서 전송을 할 때는 pId가 없는 Post 모델을, 스프링을 통해 데이터를 받아올 때는 pId까지 포함된 PostResponse 모델을 사용했다.</p>
<p>받아올 때 Post 모델을 사용하면 pId 필드가 없기 때문에 pId를 통해 게시물을 상세조회할 수 없고, PostResponse 모델을 통해 서버에 데이터를 전송하면 pId필드가 null로 들어가서 Auto Increase가 아니라 직접 pId를 입력해야 했기 때문에 위와 같은 방법을 선택했다.</p>
<p>Flutter에서는 더 설명할 부분이 없으니 바로 <strong>Spring Boot</strong>로 넘어가자.</p>
<p>우선 getAllPost() 함수부터 설명하겠다.</p>
<p><strong>PostContoller.java</strong></p>
<pre><code class="language-java">@GetMapping(produces = &quot;application/json&quot;)
@ResponseBody
public List&lt;PostDto&gt; getAllPost(){
    return postService.allCashBoardEntity();
}
</code></pre>
<blockquote>
<ul>
<li>@GetMapping: Get 방식을 통해서 매핑해주는 어노테이션</li>
</ul>
</blockquote>
<p>이 정도면 간단히 설명이 끝난다.
return에 있는 postService.allCashBoardEntity() 함수는 아래와 같다.</p>
<pre><code class="language-java">public List&lt;PostDto&gt; allCashBoardEntity(){
        return exec();
    }

public List&lt;PostDto&gt; exec(){
    List&lt;Post&gt; postList = new ArrayList&lt;&gt;(postRepository.findAll());

    List&lt;PostDto&gt; postDtoList = new ArrayList&lt;&gt;();

    for(Post post: postList){
        PostDto requestDetailAll = new PostDto();
        requestDetailAll.setPId(post.getPId());
        requestDetailAll.setTitle(post.getTitle());
        requestDetailAll.setContent(post.getContent());
        requestDetailAll.setCategory(post.getCategory());
        requestDetailAll.setPerson(post.getPerson());
        requestDetailAll.setNickname(post.getNickname());
        requestDetailAll.setCreatedAt(post.getCreatedAt());
        requestDetailAll.setType(post.getType());
        postDtoList.add(requestDetailAll);
    }
    return postDtoList;
}</code></pre>
<p>유지보수를 위해 함수를 분리해줬고, exec() 함수에서는 혹시나 pId가 누락되는 상황을 막기 위해 모든 필드에 set을 직접 해줬다.</p>
<p>왜 이렇게 하지?라는 의문이 생기는 것도 당연하나, 나는 코드를 다 작성하고 플러터에서 앱을 실행했을 때 자꾸 pId가 null로 받아와지는 오류가 발생했다.</p>
<p>게시물 리스트는 보이지도 않고 자꾸 아래 사진처럼 &quot;<strong>type &#39;NULL&#39; is not a subtype of type &#39;int&#39;</strong>&quot;라는 오류가 발생하는 거다.
<img src="https://velog.velcdn.com/images/mi-fasol/post/dc8637d4-269d-44c3-8f70-67a5f235a1c5/image.png" alt=""></p>
<p>스프링 코드에도 pId가 있고, 데이터베이스에도 있고. 혹시 PostDto에 pId가 안 받아와지나 싶어서 하나하나 set을 해줘도 계속 같은 오류가 발생했었다.</p>
<p>웹에서 api를 사용하면 제대로 Json 데이터가 나오고, pId 필드도 보이는데 이상하게 Flutter에서 게시물 조회가 안 됐다.</p>
<p>그러다가 찾은 것이 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/d34d2d83-abc5-426c-b9d2-69e4f6cbac9a/image.png" alt=""></p>
<p>글씨가 작아서 잘 안 보이겠지만, 자세히 보면 pId 필드가 &quot;pid&quot;로 모두 소문자다. Auto Increase로 자동 생성되는 필드기 때문에 DB에 들어갈 때 그냥 pid로 들어간 거다. 이름이 pid인데, Flutter에서는 자꾸 pId인 속성을 찾으라 하니 당연히 못 찾고 pId 필드가 null로 받아와진 거다.</p>
<p>Flutter의 toJson, fromJson 함수의 pId를 pid로 바꿔주니 아래처럼 정상적으로 작동했다.</p>
<p>나처럼 바보같은 실수를 하지 않았다면, Dto를 따로 안 만들고 그냥</p>
<pre><code>@GetMapping(produces = &quot;application/json&quot;)
@ResponseBody
public List&lt;Post&gt; getAllPost(){
    return postRepository.findAll();
}</code></pre><p>이렇게만 해줘도 정상적으로 작동한다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/107b7368-92b2-47be-a571-0f7039411020/image.png" alt=""></p>
<p>이렇게 게시물을 리스트로 받아오게 했으면 이제 게시물을 상세 조회해줄 차례다.</p>
<p>Flutter의 코드는 위에서 함수를 설명하며 작성해뒀으니 스프링만 보겠다.</p>
<p><strong>PostController.java</strong></p>
<pre><code class="language-java">@GetMapping(&quot;/{id}&quot;)
@ResponseBody
public ResponseEntity&lt;Post&gt; getPostById(@PathVariable Long id) {
     Optional&lt;Post&gt; postOptional = postRepository.findById(id);
     return postOptional.map(ResponseEntity::ok).orElseGet(() -&gt; ResponseEntity.notFound().build());
}</code></pre>
<p>모든 게시물을 가져올 때와 마찬가지로 get 방식을 사용하며, 하나의 게시물만 가져오면 되기에 Post를 반환하도록 했다.</p>
<p>스프링의 findById() 함수를 사용하면 데이터 중에 해당 id 값을 가진 데이터 하나만 반환해준다.</p>
<p>스프링에서 이렇게 작성을 마치고 플러터에서 리스트의 아이템을 클릭해주면</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/8fd1698f-ae80-4157-af5b-6fd2f60978fa/image.png" alt=""></p>
<p>이렇게 게시물의 타이틀과 내용을 보여준다. 아직 페이지 디자인이 완료되지 않았기 때문에 우선은 이렇게 간단하게 보여주도록 작성했다.</p>
<p>스프링을 공부한지는 얼마 되지 않았지만, 지금 내가 작성한 코드들은 어디 가서 &quot;스프링 알아요&quot; 할 정도도 못된다는 것을 안다.
앞으로 좀 더 공부해서 CRUD 말고 다른 기능들도 작성해보고 싶다.</p>
<p>다음 게시물은 Spring의 개념에 대해서 조금 더 알아볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] IoC와 DI]]></title>
            <link>https://velog.io/@mi-fasol/Spring-Boot-IoC%EC%99%80-DI</link>
            <guid>https://velog.io/@mi-fasol/Spring-Boot-IoC%EC%99%80-DI</guid>
            <pubDate>Thu, 06 Apr 2023 09:16:47 GMT</pubDate>
            <description><![CDATA[<p>스프링 부트 하면 빠질 수 없는 개념이 있다. 바로 <strong>DI</strong>(의존성 주입)과 <strong>IoC</strong>(제어의 역전)이다.</p>
<p>오늘은 이 두 가지 개념에 대하여 알아보도록 하겠다.</p>
<h2 id="ioc">IoC</h2>
<p><strong>IoC</strong>부터 살펴보도록 하자.</p>
<p>IoC는 <strong>제어의 역전</strong>을 의미한다.</p>
<p>객체와 메소드를 호출하는 작업은 보통 개발자가 하게 되는데, IoC를 사용하면 스프링 컨테이너가 이에 대한 제어권을 가지게 되어 제어가 역전되는 것이다.</p>
<p>우리가 보통 객체를 제어하려면 어떤 코드가 필요할까?</p>
<pre><code class="language-java">class Example{

    private Test test;

    Example(){
        this.test = new Test();
    }
}</code></pre>
<p>이렇게 new를 통해 직접 객체를 생성한 후 객체를 할당해야 한다.
이제 IoC를 활용하여 적은 코드의 예시를 보자.</p>
<pre><code class="language-java">public class Example{

    @Autowired
    private Test test;

}</code></pre>
<p>위의 코드를 살펴 보면, 클래스에서 @Autowired 어노테이션을 통해서 객체를 주입 받았다.</p>
<p>Test 객체가 스프링 컨테이너(IoC 컨테이너)에게 관리받는 대상인 빈(Bean)일 경우, 이 @Autowired 어노테이션을 사용해 객체 주입이 가능해진다.</p>
<p>컨테이너가 객체를 생성해서 해당 객체에 주입시킨 것인데, 이것이 제어 역전의 예시다.</p>
<h3 id="ioc의-장점">IoC의 장점</h3>
<blockquote>
<ul>
<li>객체 간 결합도 감소</li>
</ul>
</blockquote>
<ul>
<li>코드의 유연성</li>
<li>코드 해석의 용이성</li>
<li>개발 및 유지보수의 용이성</li>
</ul>
<p>IoC를 사용할 경우, 객체를 일일이 클래스에 생성하고 사용할 필요 없이 그냥 주입받아서 사용하면 된다.</p>
<p>그럼 객체를 주입받는 것은 무엇인가?</p>
<h2 id="di">DI</h2>
<p>여기서 필요한 개념이 DI다.</p>
<p><strong>의존성</strong>이란 무엇인가?
스프링에서 생성되는 <strong>객체 간의 관계</strong>를 뜻한다.
한 객체가 다른 객체에 의존한다는 것은, 의존대상인 객체가 변할 때 그것이 다른 객체에도 영향을 미친다는 것을 의미하는 것이다.</p>
<h3 id="di의-장점">DI의 장점</h3>
<blockquote>
<ul>
<li>재사용성 향상</li>
</ul>
</blockquote>
<ul>
<li>테스트의 용이성</li>
<li>유지보수의 용이성</li>
<li>코드 해석의 용이성</li>
<li>코드의 단순화</li>
<li>종속성 감소</li>
<li>결합도는 감소시키면서 유연성 및 확장성 향상</li>
<li>객체 간 의존 관계 설정</li>
</ul>
<p>DI는 위처럼 개발 및 유지보수가 편해지고, 결합도가 낮아지며 유연해진다는 장점이 존재한다.</p>
<p>그렇다면 의존성을 주입하는 방법은 어떤 것이 있을까?</p>
<h3 id="의존성-주입의-세-가지-방법">의존성 주입의 세 가지 방법</h3>
<blockquote>
<ul>
<li>생성자 주입: 가장 권장되는 방법</li>
</ul>
</blockquote>
<ul>
<li>필요한 의존성을 포함한 클래스 생성자를 만든 후, 이 생성자를 통해 의존성 주입<ul>
<li>필드 주입: 필드에 의존 관계를 주입하는 방법</li>
</ul>
</li>
<li>프레임워크에 의존적이고, final 선언이 불가하므로 권장되지 않는 방법<ul>
<li>수정자 주입: setter 메소드를 통해서 의존성을 주입</li>
</ul>
</li>
<li>현재는 주입 받는 객체가 변경될 수 있는 경우에 사용</li>
</ul>
<p>위의 세 가지 방법으로 의존성을 주입한다고 한다.</p>
<p>예시 코드를 확인해보자.</p>
<p><strong>생성자 주입</strong></p>
<pre><code class="language-java">@Component
public class TestController {

   private TestRepository testRepository;

   public TestController(TestRepository testRepository) {
       this.testRepository = testRepository;
   }
}</code></pre>
<p><strong>필드 주입</strong></p>
<pre><code class="language-java">@Component
public class TestController {
    @Autowired            // 필드에 직접 @Autowired 어노테이션 선언
    private TestRepository testRepository;
}</code></pre>
<p><strong>수정자 주입</strong></p>
<pre><code class="language-java">@Component
public class TestController {

   private TestRepository testRepository;

   @Autowired         // setTestRepository로 setter 생성하여 의존성 주입
   public void setTestRepository(TestRepository testRepository) {
       this.testRepository = testRepository;
   }
}</code></pre>
<p>오늘은 이렇게 간단하게 DI와 IoC에 대하여 알아보았다.</p>
<p>전에 학교에서 수업을 들을 때 잠깐 다뤄본 기억이 있는데, 시간이 지나고 다시 글로 작성해보니 기억하고 있던 개념을 직접 코드로 볼 수 있어서 좋은 것 같다.</p>
<p>다음 게시물도 스프링에 관한 글을 적어보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Flutter와 연동하기]]></title>
            <link>https://velog.io/@mi-fasol/Spring-Boot-Flutter%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@mi-fasol/Spring-Boot-Flutter%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 04 Apr 2023 14:00:56 GMT</pubDate>
            <description><![CDATA[<p>토이 프로젝트를 진행하면서 DB를 사용할 일이 생겼다.</p>
<p>백 개발자를 따로 구하지 않았기 때문에 이번에 한 번 해보자고 생각하며 처음 스프링에 대해서 알아가게 되었다.</p>
<p>우선 Flutter에 화면 구성은 웬만큼 되어 있는 상태였기 때문에, DB를 생성해서 연결하는 작업이 필요했다.</p>
<p>처음 접해보는 스프링이다 보니, 어떻게 해야 할지 막막했는데 찾아서 하다 보니 되긴 되더라. 백을 공부하고 있던 애인의 도움도 많이 받았다.</p>
<p>일단 스프링을 처음 해본 후기는 딱 한 줄이다.
설정이 정말 힘들다.</p>
<p>설정하면서 이런저런 오류가 계속 발생해서 서버를 새로 만들었다가 프로젝트를 새로 만들었다가 몇 번을 반복했는지 모르겠다.
다들 설정이 더럽다고 하는 데는 이유가 있었다.</p>
<p>그래도 하는 김에 제대로 해보려고 Spring Study도 신청했다.
현재 GDSC에 소속되어 있어서, 우리 학교 GDSC에서 열리는 스프링 스터디 분야에서 공부해보기로 했다. 조금 걱정되긴 하지만 뭐, 잘 할 수 있겠지.</p>
<p>이 포스팅은 서버와 스프링 연결이 완료되었고, 플러터에 프론트가 어느정도 완성되었다는 것을 기반으로 한다는 점을 미리 알린다.
참고로 나는 MariaDB와 MySQL 워크벤치를 사용했다.</p>
<p>Flutter의 코드부터 살펴보겠다.</p>
<p>나는 user를 스프링 부트 프로젝트에 전송해서 DB에 저장하는 코드가 필요했다.</p>
<p>그러기 위해서 <strong>user_model.dart</strong> 파일에 User 데이터 모델부터 정의해보자.</p>
<pre><code class="language-dart">class User {
  final String? nickname;
  final String? major;
  final String? gender;

  User({required this.nickname, required this.major, required this.gender});

  factory User.fromJson(Map&lt;String, dynamic&gt; json) {
    return User(
        nickname: json[&#39;nickname&#39;],
        major: json[&#39;major&#39;],
        gender: json[&#39;gender&#39;]);
  }
  Map&lt;String, dynamic&gt; toJson() =&gt; {
        &#39;nickname&#39;: nickname,
        &#39;major&#39;: major,
        &#39;gender&#39;: gender,
      };
}</code></pre>
<p>위와 같이 fromJson, toJson를 통해 Json으로의 변환이 가능하게 해줬다.
DB에는 uId라는 애트리뷰트가 하나 더 있지만, autoIncrease로 생성해둬서 굳이 플러터에서 설정할 필요가 없다.</p>
<p>데이터 클래스를 만들었으니 서비스 해줄 차례다.</p>
<p>나는 용이한 사용을 위해 service 디렉토리에 <strong>db_server.dart</strong> 파일을 만들어 분리했다.</p>
<pre><code class="language-dart">Future&lt;void&gt; saveUser(User user) async {
    try {
      final response = await http.post(
        Uri.parse(&quot;http://server-uri/user&quot;),
        headers: &lt;String, String&gt;{
          &#39;Content-Type&#39;: &#39;application/json; charset=UTF-8&#39;,
        },
        body: jsonEncode(user.toJson()),
      );
      if (response.statusCode != 201) {
        throw Exception(&quot;Failed to send data&quot;);
      } else {
        print(&quot;User Data sent successfully&quot;);
        Get.to(const HomePage());
      }
    } catch (e) {
      print(&quot;Failed to send user data: ${e}&quot;);
    }
  }</code></pre>
<blockquote>
<ul>
<li>Future<void> saveUser(User user) async){...}: 비동기 처리로 진행</li>
</ul>
</blockquote>
<ul>
<li><p>try-catch: 예외 처리</p>
<ul>
<li>Uri.parse(&quot;&quot;): 서버와 연결</li>
<li>&#39;Content-Type&#39;: &#39;application/json; charset=UTF-8&#39;:
http 메시지를 json으로 변환해서 전송, charset을 UTF-8로 사용</li>
<li>jsonEncode(user.toJson): 인코딩</li>
</ul>
<p>이 정도로 간단히 설명할 수 있을 것 같다.
server-uri에는 본인의 서버 주소를 넣으면 된다.</p>
<p>위의 saveUser 함수는 아래와 같이 사용할 수 있다.</p>
</li>
</ul>
<pre><code class="language-dart"> DBService dbService = DBService();
      dbService
          .saveUser(User(nickname: nickname, major: major, gender: gender));</code></pre>
<p>그럼 이제 스프링 부트에 플러터를 연결할 준비가 끝났으니, <strong>스프링 코드</strong>를 작성해줘야 한다.</p>
<p>나는 vscode에서 스프링 작업을 하다가 중간에 뭘 잘못 건드렸는지 온통 빨간줄이라 IntelliJ로 갈아탔다.</p>
<p>연결을 해주기 위해선 스프링에서 테이블을 생성해야 하므로 플러터와 마찬가지로 <strong>User</strong> 데이터 클래스를 만들어준다.</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;user&quot;)
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;uId&quot;)
    private Long uId;

    @Column(name = &quot;nickname&quot;)
    private String nickname;

    @Column(name = &quot;major&quot;)
    private String major;

    @Column(name = &quot;gender&quot;)
    private String gender;
}</code></pre>
<p>필요한 애트리뷰트들을 선언해줬다.
uId는 앞서 말했던 것처럼 AI 속성이므로 @GeneratedValue 어노테이션을 붙여줬다.</p>
<p>그 다음은 <strong>UserRepository</strong> 클래스로 레포지토리를 만들어준다. 한 줄이면 끝난다.</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {}</code></pre>
<p>그 다음에는 매핑을 위한 <strong>UserController</strong> 파일을 만들어준다.</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/user&quot;)
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping(produces = &quot;application/json&quot;)
    public ResponseEntity&lt;User&gt; saveUser(@RequestBody User user) {
        User savedUser = userService.saveUser(user);
        return new ResponseEntity&lt;&gt;(savedUser, HttpStatus.CREATED);
    }
}</code></pre>
<p>플러터에서 스프링 부트로 데이터를 전송할 때 필요한 코드다.</p>
<blockquote>
<ul>
<li>@RequestMapping: 매핑 요청을 /user로 받음</li>
</ul>
</blockquote>
<ul>
<li>@PostMapping(produces = &quot;application/json&quot;): Post 타입으로 매핑하며 json 형태로 받아옴</li>
</ul>
<p>위의 두 가지 어노테이션만 알아두면 될 것 같다.
사실 스프링 한지 얼마 안 돼서 나도 잘 모른다. 추후에 더 공부해서 스프링에 대한 게시물도 포스팅할 예정이다.</p>
<p>그리고 마지막으로 데이터를 서비스 해주기 위한 <strong>UserService</strong> 파일을 만들어준다.</p>
<pre><code class="language-java">@Service
public class UserService {

    @Autowired
    UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;

    }

    public User saveUser(User user) {
        return userRepository.save(user);
    }
}</code></pre>
<blockquote>
<ul>
<li>@Autowired: 의존성 주입 어노테이션</li>
</ul>
</blockquote>
<ul>
<li>save(): 데이터 업데이트</li>
</ul>
<p>이 정도로 간단하게 설명이 끝나겠다.</p>
<p>이렇게 코드 작성을 모두 완료하고 서버를 돌린 후 여기에 정보를 모두 입력하고
<img src="https://velog.velcdn.com/images/mi-fasol/post/e9a65ca2-c343-4a73-94ff-b6d44a23e085/image.png" alt="">
가입 버튼을 눌러주면
<img src="https://velog.velcdn.com/images/mi-fasol/post/7025b881-e104-491e-ad9d-35574ccb906a/image.png" alt="">
이렇게 전송이 완료되었다는 로그와 함께
<img src="https://velog.velcdn.com/images/mi-fasol/post/a90ff1bf-b6c8-4b5f-84ef-50ca4e3e194b/image.png" alt="">
전송이 완료된 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/mi-fasol/post/3fdcb749-fb9a-4bf1-9657-587bd153cca2/image.png" alt=""></p>
<p>이 간단한 코드를 짜는데 꽤나 시간을 잡아먹었지만, 하고 나니 괜히 뿌듯해진다.</p>
<p>다음 게시물도 스프링에 관해 올려보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] getX 활용법 - 의존성 주입]]></title>
            <link>https://velog.io/@mi-fasol/Flutter-getX-%ED%99%9C%EC%9A%A9%EB%B2%95-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85</link>
            <guid>https://velog.io/@mi-fasol/Flutter-getX-%ED%99%9C%EC%9A%A9%EB%B2%95-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85</guid>
            <pubDate>Tue, 28 Mar 2023 12:40:33 GMT</pubDate>
            <description><![CDATA[<p>오늘은 <strong>GetX의 의존성 주입</strong>에 대해 알아보도록 하겠다.</p>
<p>의존성 주입이란 컨트롤러를 인스턴스화 시키는 것을 말하는데, GetX에는 총 네 가지의 방법이 있다.</p>
<blockquote>
<ul>
<li>Get.put: 페이지로 이동할 때 생성(create), 초기화(initialized)되며 페이지에서 나갈 때 delete</li>
</ul>
</blockquote>
<ul>
<li>Get.lazyPut: 페이지로 이동할 때가 아닌, 무언가 변화가 생겼을 때 create, inintalized</li>
<li>Get.putAsync: 데이터에 가공 처리 등을 한 후 인스턴스화 해야 할 때 사용</li>
<li>Get.create: 동작이 일어날 때마다 인스턴스 생성</li>
</ul>
<p>개념은 위와 같고, 특징이라고 하자면 put, lazyPut, putAsync는 싱글톤이기 때문에 인스턴스를 단 한 번 생성하지만, create는 동작/변화가 발생할 때마다 인스턴스를 생성한다는 점을 들 수 있겠다.</p>
<p>코드 예시로 살펴보자.</p>
<pre><code class="language-dart">import &#39;package:flutter/foundation.dart&#39;;
import &#39;package:flutter/material.dart&#39;;
import &#39;package:get/get.dart&#39;;
import &#39;package:getx/controller/dependency_controller.dart&#39;;
import &#39;package:getx/page/get_lazy_put.dart&#39;;
import &#39;get_put.dart&#39;;

class DependencyManagePage extends StatelessWidget {
  const DependencyManagePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
        ElevatedButton(
            onPressed: () {
              Get.to(const GetPut(), binding: BindingsBuilder(() {
                Get.put(DependencyController());
              }));
            },
            child: Text(&quot;Get.put&quot;)),
        ElevatedButton(
            onPressed: () {
              Get.to(const GetLazyPut(), binding: BindingsBuilder(() {
                Get.lazyPut&lt;DependencyController&gt;(() =&gt; DependencyController());
              }));
            },
            // 컨트롤러에 접근하려 할 때 메모리에 올림
            // 접근을 하려 할때 그제서야 intialized
            child: Text(&quot;Get.lazyPut&quot;)),
        // 바로 intialized
        // 페이지에 접근할 때 데이터에 가공을 하고 해야 할 때
        // 아래 코드는 5초 후에 initialized 해줌
        ElevatedButton(
            onPressed: () {
              Get.to(const GetPut(), binding: BindingsBuilder(() {
                Get.putAsync&lt;DependencyController&gt;(() async {
                  await Future.delayed(Duration(seconds: 5));
                  return DependencyController();
                });
              }));
            },
            child: Text(&quot;Get.putAsync&quot;)),
        // 위의 세 개는 싱글톤이었음, 한 번 생성하면 그 뒤로 생성 X
        // 버튼을 누를 때마다 인스턴스를 생성해줌
        ElevatedButton(
            onPressed: () {
              Get.to(const GetPut(), binding: BindingsBuilder(() {
                Get.create&lt;DependencyController&gt;(() =&gt; DependencyController());
              }));
            },
            child: Text(&quot;Get.create&quot;))
      ]),
    ));
  }
}</code></pre>
<p>내 GetX 게시물은 모두 유튜버 &#39;개발하는 남자&#39;님의 강의를 보고 요약한 것인데, 코드만 보고서는 이해가 잘 안 되는 분들을 위해 코드의 주석까지 그대로 올린다.</p>
<p>화면의 구성은 간단하게 버튼 네 개로 되어있고, 각각 onPressed 시 순서대로 put, lazyPut, putAsync, create가 동작된다.</p>
<p>버튼들을 누르면 각각의 페이지로 이동하게 되는데, 아래는 put, putAsync, create를 눌렀을 때 이동하는 <strong>get_put.dart</strong> 코드다.</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:get/get.dart&#39;;
import &#39;../controller/dependency_controller.dart&#39;;

class GetPut extends StatelessWidget {
  const GetPut({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: ElevatedButton(
          child: Text(&quot;&quot;),
          onPressed: () {
            print(Get.find&lt;DependencyController&gt;().hashCode);
            Get.find&lt;DependencyController&gt;().increase;
          },
        ));
  }
}</code></pre>
<blockquote>
<ul>
<li>print(...): 버튼 클릭 시 인스턴스 값을 콘솔에 띄워주는 코드</li>
</ul>
</blockquote>
<p>복잡한 코드는 아니라서 위의 한 줄로 설명이 될 것 같다.</p>
<p>컨트롤러의 increase 함수에는 기능이 없어서, 코드를 보여주지 않고 넘어가겠다.</p>
<p>그 다음은 putLazy 버튼을 눌렀을 때 이동하는 <strong>get_lazy_put.dart</strong> 코드다.</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:get/get.dart&#39;;
import &#39;package:getx/controller/dependency_controller.dart&#39;;

class GetLazyPut extends StatelessWidget {
  const GetLazyPut({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: ElevatedButton(
          child: Text(&quot;&quot;),
          onPressed: () {
            Get.find&lt;DependencyController&gt;().increase;
          },
        ));
  }
}</code></pre>
<p>역시 특별한 기능은 없다.
동작이 발생해야 initialized 되는 것을 확인하기 위해 버튼만 하나 만들어뒀다.</p>
<p>게시물 초반부에 create만 싱글톤이 아니라고 했었는데, 그 차이를 확인해보자.</p>
<p>Get.put 버튼을 누르면 콘솔창이 아래와 같이 나온다.
<img src="https://velog.velcdn.com/images/mi-fasol/post/5c68a2f4-c06c-434e-82a8-832221533180/image.png" alt=""></p>
<p>보이는 바와 같이 여러 번 버튼을 눌러도, create와 initialized가 한 번만 출력되고, 해시코드도 749971821로 하나만 출력된다.
하나의 인스턴스만 생성된다는 뜻이다.</p>
<p>Get.create의 콘솔도 확인해보자.
<img src="https://velog.velcdn.com/images/mi-fasol/post/126c225d-cb31-4203-ad90-7518cba9304b/image.png" alt=""></p>
<p>put과는 다르게 클릭할 때마다 initialized가 실행되고, 각 인스턴스의 해시코드도 다르게 출력되는 걸 볼 수 있다.
클릭할 때마다 새로운 인스턴스가 생성되는 것이다.</p>
<p>플러터를 처음 접해보시는 분이라면 개발하는 남자 님의 유튜브에 자세하게 설명이 되어 있으니 다들 한 번씩 참고햅길 추천드린다.</p>
<p>다음에는 GetX의 바인딩에 대해서 알아보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] getX 활용법 - 반응형 상태 관리]]></title>
            <link>https://velog.io/@mi-fasol/Flutter-getX-%ED%99%9C%EC%9A%A9%EB%B2%95-%EB%B0%98%EC%9D%91%ED%98%95-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@mi-fasol/Flutter-getX-%ED%99%9C%EC%9A%A9%EB%B2%95-%EB%B0%98%EC%9D%91%ED%98%95-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 26 Mar 2023 17:28:04 GMT</pubDate>
            <description><![CDATA[<p>저번에는 간단한 예시와 함께 단순 상태 관리에 대해 알아보았다.</p>
<p>지난 게시물에서 얘기한대로, 오늘은 반응형 상태 관리에 대해 알아보겠다.</p>
<p>반응형 상태 관리와 단순 상태 관리의 차이점은 지난 게시글에서 다뤘으니, 오늘은 예시 코드와 코드 해석만 간단히 짚고 넘어갈 예정이다.</p>
<p>반응형 상태 관리는 변수를 선언하는 방법이 조금 독특하다.
<strong>reactive_controller.dart</strong>의 코드를 보며 확인해보자.</p>
<pre><code class="language-dart">import &quot;package:get/get.dart&quot;;

class ReactiveController extends GetxController {
  RxInt count = 0.obs;
  void increase() {
    count++;
  }
}</code></pre>
<p>반응형 상태 관리를 사용하려면, 변수 타입 앞에 Rx를 추가하고 변수 뒤에 .obs를 붙이면 된다.</p>
<blockquote>
<ul>
<li>obs: 따로 타입을 지정하지 않고, 컨트롤러에서 선언한 변수의 변화를 감지했을 때 바로 update 가능</li>
</ul>
</blockquote>
<ul>
<li>RxInt 변수명 = 0.obs; // 정수형</li>
<li>RxDouble 변수명 = 0.0.obs; // 실수형</li>
<li>RxString 변수명 = &quot;&quot;.obs;    // 문자열</li>
<li>Rx&lt;클래스명&gt; 변수명 = 클래스(인자).obs;
= RxList&lt;배열 타입&gt; 변수명 = [].obs;</li>
</ul>
<p>increase 함수는 별반 다를 게 없다.
이전 게시물에서 말했듯이 반응형 상태 관리는 provider의 notifyListener()나, 단순 상태 관리의 update() 등 상태 변경을 알리는 함수를 사용하지 않아도 된다는 차이점 정도가 있겠다.</p>
<p>그 다음은 <strong>reactive_getx.dart</strong>의 코드다.</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:get/get.dart&#39;;
import &#39;package:getx/controller/reactive_controller.dart&#39;;

class ReactiveGetX extends StatelessWidget {
  const ReactiveGetX({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final controller = Get.put(ReactiveController());

    return Center(
        child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        GetBuilder&lt;ReactiveController&gt;(builder: (controller) {
          return Obx(() =&gt; Text(
                &quot;${controller.count}&quot;,
                style: const TextStyle(
                  fontSize: 50,
                ),
              ));
        }),
        ElevatedButton(
            onPressed: controller.increase,
            child: const Text(
              &quot;+&quot;,
              style: TextStyle(
                fontSize: 30,
              ),
            ))
      ],
    ));
  }
}</code></pre>
<p>이전 게시글의 with_getx.dart 파일과 동일한 폼이지만, 다른 코드가 하나 있다.
바로 Obx() =&gt;를 통해 텍스트를 선언해줬다는 것이다.</p>
<blockquote>
<ul>
<li>Obx(): 컨트롤러를 명시하지 않아도 위젯을 반환해주는 함수</li>
</ul>
</blockquote>
<p>이전 with_getx.dart 코드에서는</p>
<pre><code class="language-dart">GetBuilder&lt;IncreaseController&gt;(builder: (controller) {
          return Text(
            &quot;${controller.count}&quot;,
            style: const TextStyle(
              fontSize: 50,
            ),
          );
        }),</code></pre>
<p>이랬던 코드가, 반응형 상태 관리를 적용하면</p>
<pre><code>Obx(() =&gt; Text(
              &quot;${controller.count}&quot;,
              style: const TextStyle(
                fontSize: 50,
              ),
            )),</code></pre><p>이렇게 변한다.
Obx()를 사용하여 빌더를 통해 컨트롤러를 명시해줘야 했던 불편함을 덜어줄 수 있다.</p>
<p>또는</p>
<pre><code>GetX(builder: (_) {
          return Text(
            &quot;${Get.find&lt;ReactiveController&gt;().count.value}&quot;,
            style: const TextStyle(
              fontSize: 50,
            ),
          );
        })</code></pre><p>이런 식으로 코드를 변경할 수도 있다. 보여지는 결과는 차이가 없고, 코드의 로직만 살짝 바뀐 것이다.
무엇을 쓸지는 본인 취향이지만, Obx()가 조금 더 깔끔해 보이기는 한다.</p>
<p>main.dart의 코드는 이전 코드의 WithGetx()를 ReactiveGetx()로 바꾸면 되므로 생략하고 넘어가도록 하겠다.</p>
<p>반응형 상태 변경의 또다른 장점으로는, 값이 변경이 되어야만 리렌더링이 된다는 것이다.
이를 통해 불필요한 랜더링을 확실하게 줄일 수 있다.</p>
<p>예를 들어, <strong>reactive_getx.dart</strong>에 아래와 같은 버튼을 하나 더 추가했다고 가정해보자.</p>
<pre><code class="language-dart">ElevatedButton(
            onPressed: () {
              controller.makeIt();
            },
            child: const Text(
              &quot;make it 0&quot;,
              style: TextStyle(
                fontSize: 30,
              ),
            ))</code></pre>
<p>onPressed에 선언된 makeIt 함수를 사용하기 위해 <strong>reactive_controller.dart</strong>에 아래와 같은 함수를 추가해야 한다.</p>
<pre><code class="language-dart">void makeIt() {
    count(0);
  }</code></pre>
<blockquote>
<ul>
<li>count(0): count라는 변수에 0 삽입</li>
</ul>
</blockquote>
<p>그리고 생긴 make it 0 버튼을 눌러도 값은 이미 0이기 때문에 화면이 랜더링 되지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/mi-fasol/post/8c95211b-e852-4548-8346-fe5dff22ad6b/image.png" alt=""></p>
<p>혹시 정말 그런지 궁금하신 분이 계시다면, Text 위젯에 print(&quot;rendering&quot;);을 추가해서 버튼을 눌렀을 때 콘솔에 나타나는지 확인해보시면 된다.</p>
<p>이렇게 간단한 반응형 상태 관리의 코드를 확인해 보았는데, getX는 알면 알수록 참 편리한 것 같다.</p>
<p>다음에는 getX의 의존성 주입에 대해 알아보겠다.</p>
]]></description>
        </item>
    </channel>
</rss>