<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>upgrade and develop</title>
    <link>https://rungch.tistory.com/</link>
    <description>https://github.com/undcore</description>
    <language>ko</language>
    <pubDate>Tue, 11 Aug 2026 00:12:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>렁치</managingEditor>
    <image>
      <title>upgrade and develop</title>
      <url>https://tistory1.daumcdn.net/tistory/8871148/attach/139171449ca342d8a517a3016912e2ce</url>
      <link>https://rungch.tistory.com</link>
    </image>
    <item>
      <title>[C언어] 06. 함수 (Function)</title>
      <link>https://rungch.tistory.com/69</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 커지면 같은 코드가 여기저기 반복되고, 전체를 한눈에 파악하기 어려워진다. &lt;b&gt;함수&lt;/b&gt;는 특정 기능을 하나의 덩어리로 묶어 이름을 붙인 것으로, 이 문제를 푸는 핵심 도구다. 우리가 이미 써 온 &lt;code&gt;printf&lt;/code&gt;, &lt;code&gt;scanf&lt;/code&gt;도 모두 함수였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 함수란?&lt;/h2&gt;
&lt;div style=&quot;border-left: 4px solid #4f8cff; background: #f4f8ff; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;함수(function)&lt;/b&gt;&lt;br /&gt;특정 작업을 수행하는 명령어들의 묶음으로, 고유한 이름을 가지며 입력(인수)을 받아 결과(반환값)를 돌려줄 수 있다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 쓰면 좋은 점은 분명했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;중복 제거&lt;/b&gt;: 같은 코드를 반복하지 않고 한 번 정의해 여러 번 호출한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용&lt;/b&gt;: 한 번 잘 만든 함수는 다른 프로그램에서도 쓸 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모듈화&lt;/b&gt;: 프로그램을 기능 단위로 나눠, 개발과 유지보수가 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 크게 우리가 직접 만드는 &lt;b&gt;사용자 정의 함수&lt;/b&gt;와, 표준 라이브러리가 제공하는 &lt;b&gt;라이브러리 함수&lt;/b&gt;(&lt;code&gt;printf&lt;/code&gt;, &lt;code&gt;sqrt&lt;/code&gt; 등)로 나뉜다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 함수의 정의&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;909&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPqWEZ/dJMcadJs3bx/GrRiBnhfDKm2KMhtsvUTH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPqWEZ/dJMcadJs3bx/GrRiBnhfDKm2KMhtsvUTH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPqWEZ/dJMcadJs3bx/GrRiBnhfDKm2KMhtsvUTH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPqWEZ%2FdJMcadJs3bx%2FGrRiBnhfDKm2KMhtsvUTH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1560&quot; height=&quot;909&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;909&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 반환형, 이름, 매개변수, 몸체로 이뤄진다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void print_stars(void)   // 반환형 void / 이름 / 매개변수 없음
{
    for (int i = 0; i &amp;lt; 30; i++)
        printf(&quot;*&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;반환형&lt;/b&gt;: 함수가 일을 마치고 돌려주는 값의 자료형. 돌려줄 값이 없으면 &lt;code&gt;void&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수 이름&lt;/b&gt;: 식별자 규칙을 따르되, 기능을 드러내는 이름(동사+명사)이 좋다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수 몸체&lt;/b&gt;: 중괄호 안의 실제 수행 문장들.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 함수 원형 (prototype)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C는 코드를 위에서 아래로 읽으므로, &lt;code&gt;main&lt;/code&gt;보다 아래에 정의된 함수를 &lt;code&gt;main&lt;/code&gt;에서 호출하면 컴파일러가 그 함수를 아직 모른다. 그래서 &lt;b&gt;함수 원형&lt;/b&gt;으로 미리 알려준다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int add(int x, int y);   // 함수 원형: 이름&amp;middot;매개변수 타입&amp;middot;반환형만 미리 선언

int main(void) {
    printf(&quot;%d\n&quot;, add(3, 4));   // 정의는 아래에 있어도 호출 가능
    return 0;
}

int add(int x, int y) {   // 실제 정의
    return x + y;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원형은 함수 헤더에 세미콜론만 붙인 형태다. 매개변수는 타입만 적어도 되지만, 이름까지 적어두면 읽기 좋다. 함수를 main 아래에 정의했다가 컴파일 경고를 만난 적이 있는데, 원형을 위에 선언하니 깔끔히 풀렸다. C가 위에서 아래로 읽는다는 성질이 여기서 체감됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 호출과 반환, 인수와 매개변수&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b747eZ/dJMcafOdU3D/wbRnSq80kQolhkN8d0c0r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b747eZ/dJMcafOdU3D/wbRnSq80kQolhkN8d0c0r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b747eZ/dJMcafOdU3D/wbRnSq80kQolhkN8d0c0r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb747eZ%2FdJMcafOdU3D%2FwbRnSq80kQolhkN8d0c0r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1560&quot; height=&quot;1080&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 &lt;b&gt;호출(call)&lt;/b&gt;하면 현재 실행이 잠시 멈추고 그 함수로 넘어간다. 함수가 끝나거나 &lt;code&gt;return&lt;/code&gt;을 만나면, 호출한 자리로 되돌아와 멈췄던 곳부터 다시 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자주 헷갈리는 두 용어를 구분했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인수(argument)&lt;/b&gt;: 호출하는 쪽에서 &lt;b&gt;실제로 넘기는 값&lt;/b&gt; (&lt;code&gt;add(3, 4)&lt;/code&gt;의 3, 4).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매개변수(parameter)&lt;/b&gt;: 그 값을 &lt;b&gt;받는 변수&lt;/b&gt; (&lt;code&gt;int add(int x, int y)&lt;/code&gt;의 x, y).&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;border-left: 4px solid #4f8cff; background: #f4f8ff; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;인수는 여럿, 반환값은 하나&lt;/b&gt;&lt;br /&gt;함수에 넘기는 인수는 여러 개일 수 있지만, return으로 돌려주는 값은 단 하나다. 그리고 인수의 개수와 타입은 매개변수와 정확히 일치해야 한다. 여러 결과를 돌려주고 싶다면 포인터나 구조체 같은 방법이 따로 필요하다.&lt;/div&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;int max(int x, int y) {
    if (x &amp;gt; y) return x;
    else       return y;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 수업 코드로 보는 소수 판별 모듈화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;prime_no_prg.c&lt;/code&gt;는 &quot;입력 받기&quot;와 &quot;소수 판별하기&quot;를 함수로 나누는 예제였다. main은 전체 흐름만 담당하고, 실제 세부 작업은 함수가 맡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소수 판별 함수 내부는 약수 개수를 세는 반복으로 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int getIntNo(void);
void printPrimeResult(int iNo);

int main(void)
{
    int iNo;

    while (1)
    {
        iNo = getIntNo();

        if (iNo == 0)
        {
            printf(&quot;[EXIT]\n&quot;);
            break;
        }

        printPrimeResult(iNo);
    }

    return 0;
}

int getIntNo(void)
{
    int iNo;

    printf(&quot;정수 입력(0 종료): &quot;);
    scanf(&quot;%d&quot;, &amp;amp;iNo);

    return iNo;
}

void printPrimeResult(int iNo)
{
    int iIndex;
    int iDivisor;
    int iCount;

    iCount = 0;

    for (iIndex = 0; iIndex &amp;lt; iNo; iIndex++)
    {
        iDivisor = iIndex + 1;

        if (iNo % iDivisor == 0)
        {
            iCount = iCount + 1;
        }
    }

    if (iCount == 2)
    {
        printf(&quot;%d는 소수입니다.\n&quot;, iNo);
    }
    else
    {
        printf(&quot;%d는 소수가 아닙니다.\n&quot;, iNo);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제의 핵심은 main이 모든 계산을 직접 하지 않는다는 점이다. main은 &lt;b&gt;입력 &amp;rarr; 종료 판단 &amp;rarr; 함수 호출&lt;/b&gt; 흐름만 갖고, 소수 판별의 세부 반복은 &lt;code&gt;printPrimeResult&lt;/code&gt; 안으로 들어간다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 느낀 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수가 단순히 코드를 줄이는 게 아니라 &quot;기능 단위로 나눠 생각하는&quot; 도구라는 걸 알았다. 중복 제거보다 모듈화가 더 큰 가치였다.&lt;/li&gt;
&lt;li&gt;인수와 매개변수를 &quot;넘기는 값 vs 받는 변수&quot;로 구분하니, 그동안 뭉뚱그려 쓰던 용어가 또렷해졌다.&lt;/li&gt;
&lt;li&gt;return이 값을 돌려주면서 동시에 함수를 끝낸다는 점이, 조건마다 return을 둬 코드를 평평하게 만드는 방식으로 이어진다는 게 흥미로웠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 걸음 더&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수에 값을 넘길 때 C는 기본적으로 &lt;b&gt;값에 의한 호출(call by value)&lt;/b&gt;을 한다. 인수의 &quot;값을 복사해서&quot; 매개변수에 전달하므로, 함수 안에서 매개변수를 바꿔도 호출한 쪽의 원본 변수는 변하지 않는다. 원본을 바꾸고 싶으면 주소를 넘기는 포인터가 필요한데, 이게 포인터를 배우는 큰 동기가 된다.&lt;/li&gt;
&lt;li&gt;함수 안에서 선언한 변수(지역 변수)는 그 함수가 실행되는 동안만 존재하고 끝나면 사라진다. 그래서 다른 함수에서 같은 이름의 변수를 써도 서로 간섭하지 않는다. 이 &quot;각 함수가 자기만의 작업 공간을 갖는다&quot;는 성질이 모듈화의 토대다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return&lt;/code&gt;은 값을 돌려줄 뿐 아니라 함수를 &lt;b&gt;즉시 끝내는&lt;/b&gt; 효과도 있다. 그래서 &lt;code&gt;max&lt;/code&gt; 예처럼 조건마다 return을 두면, 굳이 else 없이도 &quot;조건에 맞으면 그 값을 돌려주고 함수 종료&quot;가 된다. 이를 활용하면 깊은 if 중첩을 평평하게 펼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>language/c</category>
      <category>callbyvalue</category>
      <category>c언어</category>
      <category>c프로그래밍</category>
      <category>function</category>
      <category>매개변수</category>
      <category>모듈화</category>
      <category>반환값</category>
      <category>함수</category>
      <category>함수원형</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/69</guid>
      <comments>https://rungch.tistory.com/69#entry69comment</comments>
      <pubDate>Mon, 10 Aug 2026 15:38:07 +0900</pubDate>
    </item>
    <item>
      <title>[인터넷활용] 03. JavaScript 기초</title>
      <link>https://rungch.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;HTML이 구조를, CSS가 표현을 맡는다면, 자바스크립트는 웹페이지에 동작과 논리를 불어넣는다. 버튼을 누르면 반응하고, 입력값을 계산하고, 화면을 바꾸는 일이 모두 자바스크립트의 영역이다. 오늘은 변수&amp;middot;자료형&amp;middot;연산자&amp;middot;제어문&amp;middot;함수 같은 언어의 기본기를 정리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 변수와 자료형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수를 선언하는 키워드는 세 가지이며, 각각 성격이 다르다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;키워드&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;var&lt;/td&gt;
&lt;td&gt;함수 스코프 (예전 방식, 지금은 권장하지 않음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;let&lt;/td&gt;
&lt;td&gt;블록 스코프, 재할당 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const&lt;/td&gt;
&lt;td&gt;블록 스코프, 재할당 불가 (상수)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let name = &quot;정주호&quot;;
const PI = 3.14;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료형은 숫자(Number), 문자열(String), 논리(Boolean), 객체(Object), 그리고 값이 없음을 나타내는 undefined와 null이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;border-left: 4px solid #4f8cff; background: #f4f8ff; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;기본은 const, 바뀌면 let&lt;/b&gt;&lt;br /&gt;실무에서는 일단 const로 선언하고, 값이 바뀌어야 할 때만 let으로 바꾸는 습관을 권한다. &quot;이 값이 중간에 바뀌지 않는다&quot;가 코드로 보장되면 읽는 사람이 추적하기 쉽고 실수도 줄어든다. var는 스코프 규칙이 헷갈리는 문제가 있어 거의 쓰지 않는다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C&amp;middot;자바에서 본 변수와 달리 선언 키워드가 셋이나 되는 게 처음엔 낯설었는데, &quot;기본 const, 바뀌면 let&quot;이라는 원칙을 잡으니 고민이 줄었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 연산자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 연산자는 다른 언어와 비슷하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;산술: &lt;code&gt;+ - * /&lt;/code&gt; 와 나머지를 구하는 &lt;code&gt;%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;비교: &lt;code&gt;==&lt;/code&gt;(값), &lt;code&gt;===&lt;/code&gt;(값+타입), &lt;code&gt;!=&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;논리: &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;(그리고), &lt;code&gt;||&lt;/code&gt;(또는), &lt;code&gt;!&lt;/code&gt;(부정)&lt;/li&gt;
&lt;li&gt;삼항 연산자: &lt;code&gt;조건 ? 참일_때_값 : 거짓일_때_값&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 특히 주의할 것이 비교 연산자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;border-left: 4px solid #ff9800; background: #fff7ec; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;== 와 === 의 차이&lt;/b&gt;&lt;br /&gt;&lt;code&gt;==&lt;/code&gt;는 타입이 다르면 자동으로 변환한 뒤 값을 비교한다. 그래서 &lt;code&gt;1 == &quot;1&quot;&lt;/code&gt;이 true가 된다. 반면 &lt;code&gt;===&lt;/code&gt;는 타입까지 같아야 true다(&lt;code&gt;1 === &quot;1&quot;&lt;/code&gt;은 false). 이 자동 변환이 예상치 못한 버그를 부르기 때문에, 실무에서는 거의 항상 &lt;code&gt;===&lt;/code&gt;를 쓴다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;1 == &quot;1&quot;&lt;/code&gt;이 true가 되는 걸 직접 확인하고 놀랐다. 자동 형 변환이 편의 같지만 버그의 씨앗이라, &lt;code&gt;===&lt;/code&gt;를 기본으로 쓰는 이유를 알았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 제어문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 제어하는 구문도 익숙한 형태다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if (조건) { } else if (조건) { } else { }
switch (값) { case 1: ...; break; default: ...; }
for (let i = 0; i &amp;lt; 10; i++) { }
while (조건) { }
do { } while (조건);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;switch&lt;/code&gt;에서 &lt;code&gt;break&lt;/code&gt;를 빠뜨리면 다음 &lt;code&gt;case&lt;/code&gt;로 그대로 흘러내려간다(fall-through). 실수의 원인이 되기도 하지만, 여러 case를 묶어 같은 처리를 할 때 일부러 활용하기도 한다. C&amp;middot;자바에서 본 fall-through가 여기서도 똑같았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 함수를 정의하는 방식은 세 가지다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function add(a, b) { return a + b; }      // 함수 선언
let f = function(a, b) { return a + b; };  // 함수 표현식
let g = (a, b) =&amp;gt; a + b;                    // 화살표 함수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 방식 모두 같은 일을 하지만, 화살표 함수는 짧고 간결해 최근 코드에서 자주 보인다. 함수를 변수에 담는다는 게 C에서는 없던 발상이라 신선했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 느낀 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제어문&amp;middot;연산자는 C&amp;middot;자바와 거의 같아 빠르게 적응했다. 대신 const/let/var, ==/===, 함수를 값으로 다루는 점 같은 JS만의 특징에 집중하니 효율적이었다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 == &quot;1&quot;&lt;/code&gt;이 true가 되는 자동 형 변환을 직접 보고, 편의가 곧 버그가 될 수 있다는 걸 체감했다. &lt;code&gt;===&lt;/code&gt;를 기본으로 삼기로 했다.&lt;/li&gt;
&lt;li&gt;함수를 변수에 담을 수 있다는 점이, 앞으로 배울 이벤트 처리(클릭 시 실행할 함수를 넘기는)의 토대라는 예감이 들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 걸음 더&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바스크립트에서 함수는 &quot;값&quot;처럼 다뤄진다. 위 예시처럼 함수를 변수에 담거나, 다른 함수의 인자로 넘기거나, 함수가 함수를 반환할 수 있다. 이를 일급 함수(first-class function)라 하며, 버튼 클릭 시 실행할 함수를 인자로 넘기는 이벤트 처리가 바로 이 성질 덕에 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;var&lt;/code&gt;를 권장하지 않는 결정적 이유 중 하나가 호이스팅(hoisting)이다. var로 선언한 변수는 코드상 선언 위치와 무관하게 함수 맨 위로 끌어올려진 것처럼 동작해, 선언 전에 써도 에러 대신 undefined가 나온다. let&amp;middot;const는 이 함정을 막아준다.&lt;/li&gt;
&lt;li&gt;화살표 함수는 단순히 문법만 짧은 게 아니라 &lt;code&gt;this&lt;/code&gt;의 동작이 일반 함수와 다르다. 일반 함수의 this는 호출 방식에 따라 바뀌지만, 화살표 함수는 자신을 둘러싼 바깥의 this를 그대로 쓴다. 이벤트 처리나 콜백에서 this가 엉뚱한 것을 가리키는 문제를 피하려고 화살표 함수를 택하는 경우가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>language/html</category>
      <category>Frontend</category>
      <category>HTML</category>
      <category>JavaScript</category>
      <category>인터넷활용</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/42</guid>
      <comments>https://rungch.tistory.com/42#entry42comment</comments>
      <pubDate>Sun, 9 Aug 2026 18:30:21 +0900</pubDate>
    </item>
    <item>
      <title>[Blazor Web  App] 11. 로그인 데이터 분리하기 - UserAccount와 TestUserRepository 만들기</title>
      <link>https://rungch.tistory.com/71</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover_11.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrzKE/dJMcahysQIK/ctx6dVahKSw6hK8EyObRLk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrzKE/dJMcahysQIK/ctx6dVahKSw6hK8EyObRLk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrzKE/dJMcahysQIK/ctx6dVahKSw6hK8EyObRLk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrzKE%2FdJMcahysQIK%2Fctx6dVahKSw6hK8EyObRLk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-filename=&quot;cover_11.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 작업에서는 로그인에 성공했을 때 사용자 정보를 &lt;code&gt;UserInfo&lt;/code&gt;에 담아 &lt;code&gt;LoginResult&lt;/code&gt;로 반환하도록 구조를 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 상태를 보관하는 &lt;code&gt;LoginStateService&lt;/code&gt;는 사용자 정보를 직접 만들지 않고, &lt;code&gt;LoginService&lt;/code&gt;가 만들어준 결과를 저장만 하도록 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조가 조금씩 정리되고 있었지만, &lt;code&gt;LoginService&lt;/code&gt;를 다시 보니 아직 테스트 사용자 정보가 직접 들어 있었다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private readonly string testUserId = &quot;sample-admin&quot;;
private readonly string testPassword = &quot;1111&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공했을 때 표시할 사용자 이름과 등급도 &lt;code&gt;LoginService&lt;/code&gt; 안에서 직접 만들고 있었다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;userInfo.UserId = loginRequest.UserId;
userInfo.UserName = &quot;테스트 관리자&quot;;
userInfo.UserGrade = &quot;관리자&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 한 명뿐일 때는 이 방식도 단순하고 이해하기 쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 사용자 계정이 두 명, 세 명으로 늘어나면 이야기가 달라진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sample-admin / 1111
sample-user / 2222
sample-manager / 3333
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 값이 모두 &lt;code&gt;LoginService&lt;/code&gt; 안에 들어가면 로그인 로직과 사용자 데이터가 한곳에 섞인다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;LoginService

├─ 테스트 사용자 데이터 보관
├─ 아이디 확인
├─ 비밀번호 확인
├─ 사용자 정보 생성
└─ 로그인 결과 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 &lt;b&gt;사용자 데이터를 보관하는 책임&lt;/b&gt;과 &lt;b&gt;로그인 여부를 판단하는 책임&lt;/b&gt;을 나눠보기로 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번에 만들 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;infographic_11_1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMH8YX/dJMcaiYk2rR/2TKUP5r4w1eNOE9Iwjjmr0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMH8YX/dJMcaiYk2rR/2TKUP5r4w1eNOE9Iwjjmr0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMH8YX/dJMcaiYk2rR/2TKUP5r4w1eNOE9Iwjjmr0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMH8YX%2FdJMcaiYk2rR%2F2TKUP5r4w1eNOE9Iwjjmr0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-filename=&quot;infographic_11_1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;LoginService
├─ 테스트 계정 보관
├─ 아이디 비교
├─ 비밀번호 비교
└─ 로그인 결과 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 다음과 같이 나눈다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;TestUserRepository
├─ 테스트 사용자 목록 보관
└─ 아이디로 사용자 검색

LoginService
├─ 입력값 검증
├─ Repository에 사용자 검색 요청
├─ 비밀번호 검증
└─ LoginResult 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 담당하는 &lt;code&gt;Home.razor&lt;/code&gt;는 계속 &lt;code&gt;LoginService&lt;/code&gt;만 호출한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Home.razor
&amp;darr;
LoginService
&amp;darr;
TestUserRepository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserAccount 모델 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 검증에 필요한 사용자 계정 데이터를 하나의 객체로 묶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Models/UserAccount.cs&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;namespace Study.Web.Models
{
    public class UserAccount
    {
        public string UserId { get; set; } = &quot;&quot;;
        public string Password { get; set; } = &quot;&quot;;
        public string UserName { get; set; } = &quot;&quot;;
        public string UserGrade { get; set; } = &quot;&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserAccount&lt;/code&gt;는 로그인 검증에 필요한 원본 계정 데이터다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;UserId
&amp;rarr; 로그인 아이디

Password
&amp;rarr; 로그인 검증에 사용할 비밀번호

UserName
&amp;rarr; 사용자 이름

UserGrade
&amp;rarr; 사용자 등급&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserAccount와 UserInfo는 무엇이 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에는 이미 &lt;code&gt;UserInfo&lt;/code&gt;가 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;namespace Study.Web.Models
{
    public class UserInfo
    {
        public string UserId { get; set; } = &quot;&quot;;
        public string UserName { get; set; } = &quot;&quot;;
        public string UserGrade { get; set; } = &quot;&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;UserAccount&lt;/code&gt;와 &lt;code&gt;UserInfo&lt;/code&gt;를 하나로 합쳐도 되지 않을까 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 모델 모두 아이디, 이름, 등급을 가지고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 두 모델은 사용하는 시점과 목적이 다르다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UserAccount
&amp;rarr; 로그인 검증을 위한 계정 데이터
&amp;rarr; Password 포함

UserInfo
&amp;rarr; 로그인 성공 후 화면과 상태에서 사용할 정보
&amp;rarr; Password 제외&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호는 로그인할 때만 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공한 뒤 회원 페이지나 상태 서비스에서 비밀번호를 계속 가지고 있을 이유는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 로그인에 성공하면 &lt;code&gt;UserAccount&lt;/code&gt; 전체를 넘기지 않고, 필요한 정보만 &lt;code&gt;UserInfo&lt;/code&gt;로 옮긴다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;userInfo.UserId = foundUser.UserId;
userInfo.UserName = foundUser.UserName;
userInfo.UserGrade = foundUser.UserGrade;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;UserAccount
&amp;darr; 필요한 정보만 복사
UserInfo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 예제에서는 학습을 위해 비밀번호 문자열을 코드 안에 직접 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스에서는 비밀번호를 평문으로 저장하거나 그대로 비교하면 안 되고, 해시 처리된 값을 검증하는 구조가 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문자열 프로퍼티 경고가 발생했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;UserAccount&lt;/code&gt;를 다음처럼 작성했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public string UserId { get; set; }
public string Password { get; set; }
public string UserName { get; set; }
public string UserGrade { get; set; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드는 성공했지만 &lt;code&gt;CS8618&lt;/code&gt; 경고가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;null을 허용하지 않는 속성이
생성자를 종료할 때 null이 아닌 값을 포함해야 합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러 입장에서는 다음 객체가 만들어진 직후를 생각한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;UserAccount userAccount;

userAccount = new UserAccount();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에는 &lt;code&gt;UserId&lt;/code&gt;, &lt;code&gt;Password&lt;/code&gt;, &lt;code&gt;UserName&lt;/code&gt;, &lt;code&gt;UserGrade&lt;/code&gt;에 값이 들어 있다는 보장이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 각 프로퍼티가 &lt;code&gt;null&lt;/code&gt;일 가능성이 있다고 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 다른 모델과 마찬가지로 빈 문자열을 초기값으로 넣었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public string UserId { get; set; } = &quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음 의미로 이해했다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;UserId는 null을 허용하지 않는다.
아직 값이 정해지지 않았다면 빈 문자열을 가진다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;string?&lt;/code&gt;으로 바꾸는 방법도 있지만, 그렇게 하면 해당 값이 &lt;code&gt;null&lt;/code&gt;이어도 정상적인 상태라는 뜻이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 계정의 아이디와 비밀번호가 &lt;code&gt;null&lt;/code&gt;인 상태를 정상으로 보고 싶지는 않았기 때문에 빈 문자열 초기화를 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TestUserRepository 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 사용자 목록을 보관할 클래스를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Repositories/TestUserRepository.cs&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;using Study.Web.Models;

namespace Study.Web.Repositories
{
    public class TestUserRepository
    {
        private readonly List&amp;lt;UserAccount&amp;gt; testUserAccounts;

        public TestUserRepository()
        {
            testUserAccounts = new List&amp;lt;UserAccount&amp;gt;();

            AddTestUsers();
        }

        public UserAccount? FindByUserId(string userId)
        {
            UserAccount? foundUser;
            UserAccount currentUser;

            foundUser = null;

            for (int index = 0; index &amp;lt; testUserAccounts.Count; index++)
            {
                currentUser = testUserAccounts[index];

                if (currentUser.UserId == userId)
                {
                    foundUser = currentUser;
                    break;
                }
            }

            return foundUser;
        }

        private void AddTestUsers()
        {
            UserAccount adminUser;
            UserAccount normalUser;

            adminUser = new UserAccount();
            adminUser.UserId = &quot;sample-admin&quot;;
            adminUser.Password = &quot;1111&quot;;
            adminUser.UserName = &quot;테스트 관리자&quot;;
            adminUser.UserGrade = &quot;관리자&quot;;

            normalUser = new UserAccount();
            normalUser.UserId = &quot;sample-user&quot;;
            normalUser.Password = &quot;2222&quot;;
            normalUser.UserName = &quot;테스트 사용자&quot;;
            normalUser.UserGrade = &quot;일반 사용자&quot;;

            testUserAccounts.Add(adminUser);
            testUserAccounts.Add(normalUser);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;List로 사용자 여러 명 보관하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 필드는 여러 사용자 계정을 보관한다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private readonly List&amp;lt;UserAccount&amp;gt; testUserAccounts;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;에서 &lt;code&gt;T&lt;/code&gt;는 목록에 들어갈 데이터 타입이다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;List&amp;lt;int&amp;gt;
&amp;rarr; 정수 목록

List&amp;lt;string&amp;gt;
&amp;rarr; 문자열 목록

List&amp;lt;UserAccount&amp;gt;
&amp;rarr; UserAccount 객체 목록&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 &lt;code&gt;List&amp;lt;UserAccount&amp;gt;&lt;/code&gt;나 &lt;code&gt;ArrayList&amp;lt;UserAccount&amp;gt;&lt;/code&gt;, C++의 &lt;code&gt;std::vector&amp;lt;UserAccount&amp;gt;&lt;/code&gt;와 비슷하게 생각할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;List&amp;lt;UserAccount&amp;gt; testUserAccounts;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;std::vector&amp;lt;UserAccount&amp;gt; testUserAccounts;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;readonly인데 Add는 왜 가능할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록 필드에는 &lt;code&gt;readonly&lt;/code&gt;가 붙어 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private readonly List&amp;lt;UserAccount&amp;gt; testUserAccounts;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;readonly&lt;/code&gt;라면 사용자 추가도 할 수 없는 것 아닌가 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;readonly&lt;/code&gt;는 필드가 참조하는 목록 객체 자체를 다른 목록으로 바꾸는 것을 제한한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;testUserAccounts = new List&amp;lt;UserAccount&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 재대입은 생성 이후 제한된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 기존 목록 안의 데이터를 변경하는 것은 가능하다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;testUserAccounts.Add(adminUser);
testUserAccounts.Add(normalUser);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;capnproto&quot;&gt;&lt;code&gt;목록 필드에 다른 List 객체를 다시 대입
&amp;rarr; 제한됨

현재 List 안에 사용자 추가
&amp;rarr; 가능함&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성자에서 사용자 목록 준비하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 객체가 만들어질 때 사용자 목록도 함께 초기화한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public TestUserRepository()
{
    testUserAccounts = new List&amp;lt;UserAccount&amp;gt;();

    AddTestUsers();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;TestUserRepository 객체 생성
&amp;darr;
빈 UserAccount 목록 생성
&amp;darr;
AddTestUsers() 실행
&amp;darr;
테스트 사용자 두 명 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자는 클래스 이름과 같고 반환형을 작성하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;public TestUserRepository()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 사용하던 생성자와 거의 같은 형태라서 이해하기 쉬웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아이디로 사용자 검색하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 검색은 &lt;code&gt;FindByUserId()&lt;/code&gt;에서 처리한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public UserAccount? FindByUserId(string userId)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환형 뒤의 &lt;code&gt;?&lt;/code&gt;는 사용자를 찾지 못했을 때 &lt;code&gt;null&lt;/code&gt;을 반환할 수 있다는 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;사용자를 찾음
&amp;rarr; UserAccount 반환

사용자를 찾지 못함
&amp;rarr; null 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색은 목록의 0번부터 차례대로 진행한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;for (int index = 0; index &amp;lt; testUserAccounts.Count; index++)
{
    currentUser = testUserAccounts[index];

    if (currentUser.UserId == userId)
    {
        foundUser = currentUser;
        break;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 손으로 풀어보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;0번 사용자 가져오기
&amp;darr;
입력한 아이디와 비교
&amp;darr;
같으면 foundUser에 저장
&amp;darr;
반복문 종료

다르면 다음 사용자 확인
&amp;darr;
끝까지 없으면 null 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LINQ를 사용하면 더 짧게 작성할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;return testUserAccounts.FirstOrDefault(
    user =&amp;gt; user.UserId == userId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번에는 검색이 실제로 어떤 순서로 진행되는지 확인하고 싶어서 &lt;code&gt;for&lt;/code&gt;문을 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LoginService에서 Repository 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LoginService&lt;/code&gt;는 더 이상 테스트 계정 문자열을 직접 비교하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Repository에 사용자를 찾아달라고 요청한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;foundUser = testUserRepository.FindByUserId(
    loginRequest.UserId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 결과가 &lt;code&gt;null&lt;/code&gt;이면 해당 아이디를 가진 사용자가 없다는 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;if (foundUser == null)
{
    loginResult.IsSuccess = false;
    loginResult.Message = &quot;아이디가 올바르지 않습니다.&quot;;
    return loginResult;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자를 찾았다면 비밀번호를 비교한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;isMatchedPassword =
    loginRequest.Password == foundUser.Password;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호까지 일치하면 필요한 사용자 정보만 &lt;code&gt;UserInfo&lt;/code&gt;로 옮긴다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;userInfo.UserId = foundUser.UserId;
userInfo.UserName = foundUser.UserName;
userInfo.UserGrade = foundUser.UserGrade;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변경 후 로그인 흐름&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;micro_11_2.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wqDAv/dJMcahebYPx/fx7lrf001Fm0dqvv2CXjy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wqDAv/dJMcahebYPx/fx7lrf001Fm0dqvv2CXjy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wqDAv/dJMcahebYPx/fx7lrf001Fm0dqvv2CXjy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwqDAv%2FdJMcahebYPx%2Ffx7lrf001Fm0dqvv2CXjy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;350&quot; data-filename=&quot;micro_11_2.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Home.razor
&amp;darr;
LoginService
&amp;darr;
TestUserRepository.FindByUserId()
&amp;darr;
UserAccount 또는 null 반환
&amp;darr;
비밀번호 비교
&amp;darr;
UserInfo 생성
&amp;darr;
LoginResult 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서 보이는 기능은 크게 바뀌지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용자 데이터를 보관하는 위치와 로그인 판단을 담당하는 위치가 분리됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 계정 두 개로 로그인 흐름을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;sample-admin / 1111
&amp;rarr; 로그인 성공
&amp;rarr; 테스트 관리자
&amp;rarr; 관리자

sample-user / 2222
&amp;rarr; 로그인 성공
&amp;rarr; 테스트 사용자
&amp;rarr; 일반 사용자

존재하지 않는 아이디
&amp;rarr; 아이디가 올바르지 않습니다.

존재하는 아이디와 틀린 비밀번호
&amp;rarr; 비밀번호가 올바르지 않습니다.

로그아웃
&amp;rarr; 홈 화면으로 이동
&amp;rarr; 로그아웃 안내 메시지 표시&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 배운 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 데이터 보관과 로그인 판단은 서로 다른 책임이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserAccount&lt;/code&gt;는 비밀번호를 포함한 로그인 검증용 모델이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserInfo&lt;/code&gt;는 로그인 이후 사용할 정보만 가진다.&lt;/li&gt;
&lt;li&gt;로그인 이후 상태에는 비밀번호를 포함하지 않는 편이 좋다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;UserAccount&amp;gt;&lt;/code&gt;로 여러 사용자 객체를 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;생성자는 객체가 만들어질 때 초기 상태를 준비한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserAccount?&lt;/code&gt;는 검색 결과가 없을 수 있다는 뜻이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readonly List&amp;lt;T&amp;gt;&lt;/code&gt;에서도 기존 목록에 값을 추가할 수 있다.&lt;/li&gt;
&lt;li&gt;Repository는 데이터 저장 방식과 서비스 로직 사이의 경계를 만든다.&lt;/li&gt;
&lt;li&gt;빌드 성공과 경고 없음은 서로 다른 문제이므로 경고도 확인해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 작업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 &lt;code&gt;LoginService&lt;/code&gt;가 &lt;code&gt;TestUserRepository&lt;/code&gt;라는 구체 클래스에 직접 의존하지 않도록 인터페이스를 추가할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 인터페이스, 생성자 주입, DI가 객체를 연결하는 흐름을 더 자세히 살펴볼 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>language/blazor</category>
      <category>blazor</category>
      <category>Csharp</category>
      <category>DOTNET</category>
      <category>loginservice</category>
      <category>Nullable</category>
      <category>Repository</category>
      <category>userinfo</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/71</guid>
      <comments>https://rungch.tistory.com/71#entry71comment</comments>
      <pubDate>Sat, 8 Aug 2026 14:22:11 +0900</pubDate>
    </item>
    <item>
      <title>[Blazor Web App] 10. UserInfo 생성 책임을 LoginService로 옮기기</title>
      <link>https://rungch.tistory.com/67</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover_10.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu7hyp/dJMcadbEItN/UMASXqSo2XGdDdoqPvNWK1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu7hyp/dJMcadbEItN/UMASXqSo2XGdDdoqPvNWK1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu7hyp/dJMcadbEItN/UMASXqSo2XGdDdoqPvNWK1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu7hyp%2FdJMcadbEItN%2FUMASXqSo2XGdDdoqPvNWK1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-filename=&quot;cover_10.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 작업에서는 &lt;code&gt;UserInfo&lt;/code&gt; 모델을 만들고, 로그인한 사용자 정보를 &lt;code&gt;LoginStateService&lt;/code&gt;에 저장하도록 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 구조도 괜찮아 보였다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;LoginStateService
 ├─ IsLoggedIn
 ├─ CurrentUser
 └─ StatusMessage&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 로그인 성공 시 &lt;code&gt;LoginStateService.Login(userId)&lt;/code&gt; 안에서 임시 사용자 정보를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;public void Login(string userId)
{
    UserInfo userInfo;

    userInfo = new UserInfo();

    userInfo.UserId = userId;
    userInfo.UserName = &quot;테스트 사용자&quot;;
    userInfo.UserGrade = &quot;테스트 회원&quot;;

    IsLoggedIn = true;
    CurrentUser = userInfo;
    StatusMessage = &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트도 정상적으로 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 페이지에서는 &lt;code&gt;CurrentUser&lt;/code&gt;를 통해 사용자 정보를 출력했다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;아이디: @LoginStateService.CurrentUser.UserId&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;이름: @LoginStateService.CurrentUser.UserName&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;회원 등급: @LoginStateService.CurrentUser.UserGrade&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 코드를 다시 보니 역할이 조금 애매해 보였다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;LoginStateService가 로그인 상태를 저장하는 건 자연스럽다.
그런데 UserInfo를 직접 만드는 것도 LoginStateService의 역할일까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에서 이번 리팩토링이 시작됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;infographic_10_1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JGl3o/dJMcag0xUEi/Wmus4bUgZGLF9vXvH6wBJ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JGl3o/dJMcag0xUEi/Wmus4bUgZGLF9vXvH6wBJ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JGl3o/dJMcag0xUEi/Wmus4bUgZGLF9vXvH6wBJ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJGl3o%2FdJMcag0xUEi%2FWmus4bUgZGLF9vXvH6wBJ0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-filename=&quot;infographic_10_1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LoginStateService의 역할을 다시 생각했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LoginStateService&lt;/code&gt;는 이름 그대로 로그인 상태를 관리하는 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 이 서비스가 들고 있는 값은 이런 것들이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public bool IsLoggedIn { get; private set; }
public UserInfo CurrentUser { get; private set; } = new();
public string StatusMessage { get; private set; } = &quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값들은 상태에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 로그인 상태인가?
현재 로그인한 사용자는 누구인가?
로그아웃 후 보여줄 메시지가 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;code&gt;LoginStateService&lt;/code&gt;는 다음 역할에 집중하는 게 자연스럽다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;로그인 상태 저장
현재 사용자 저장
상태 메시지 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 사용자 정보를 만드는 일은 조금 다르다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;아이디와 비밀번호가 맞는지 확인
로그인 성공 여부 판단
성공한 사용자의 정보 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 일은 &lt;code&gt;LoginService&lt;/code&gt; 쪽에 더 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 역할을 이렇게 바꾸기로 했다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;기존:
LoginStateService가 UserInfo 생성

변경:
LoginService가 UserInfo 생성
LoginResult에 담아 반환
LoginStateService는 전달받은 UserInfo를 저장&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LoginResult에 UserInfo 추가하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로그인 결과 모델인 &lt;code&gt;LoginResult&lt;/code&gt;를 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;LoginResult&lt;/code&gt;는 성공 여부와 메시지만 가지고 있었다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;namespace Study.Web.Models
{
    public class LoginResult
    {
        public bool IsSuccess { get; set; }
        public string Message { get; set; } = &quot;&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 로그인 성공 시 사용자 정보도 같이 반환해야 하므로 &lt;code&gt;UserInfo&lt;/code&gt;를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;namespace Study.Web.Models
{
    public class LoginResult
    {
        public bool IsSuccess { get; set; }
        public string Message { get; set; } = &quot;&quot;;
        public UserInfo UserInfo { get; set; } = new();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;LoginResult&lt;/code&gt;는 세 가지를 담는다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;IsSuccess
&amp;rarr; 로그인 성공 여부

Message
&amp;rarr; 로그인 결과 메시지

UserInfo
&amp;rarr; 로그인 성공 시 사용자 정보&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패했을 때는 &lt;code&gt;UserInfo&lt;/code&gt;가 빈 객체 상태로 남아 있어도 괜찮다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패 경로에서는 &lt;code&gt;UserInfo&lt;/code&gt;를 사용하지 않기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LoginService에서 UserInfo 만들기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;micro_10_2.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mKacN/dJMb99NSJbB/iox7WtawED0VdeAfSjIhck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mKacN/dJMb99NSJbB/iox7WtawED0VdeAfSjIhck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mKacN/dJMb99NSJbB/iox7WtawED0VdeAfSjIhck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmKacN%2FdJMb99NSJbB%2Fiox7WtawED0VdeAfSjIhck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;350&quot; data-filename=&quot;micro_10_2.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;LoginService&lt;/code&gt;에서 로그인 성공 시 &lt;code&gt;UserInfo&lt;/code&gt;를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 코드는 이런 형태다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;using Study.Web.Models;

namespace Study.Web.Services
{
    public class LoginService
    {
        private readonly string testUserId = &quot;admin&quot;;
        private readonly string testPassword = &quot;1234&quot;;

        public LoginResult Login(LoginRequest loginRequest)
        {
            LoginResult loginResult;
            UserInfo userInfo;

            bool isMatchedUserId;
            bool isMatchedPassword;

            isMatchedUserId = false;
            isMatchedPassword = false;

            loginResult = new LoginResult();
            userInfo = new UserInfo();

            if (loginRequest == null)
            {
                loginResult.IsSuccess = false;
                loginResult.Message = &quot;로그인 요청 정보가 없습니다.&quot;;
                return loginResult;
            }

            if (string.IsNullOrWhiteSpace(loginRequest.UserId))
            {
                loginResult.IsSuccess = false;
                loginResult.Message = &quot;아이디를 입력하세요.&quot;;
                return loginResult;
            }

            if (string.IsNullOrWhiteSpace(loginRequest.Password))
            {
                loginResult.IsSuccess = false;
                loginResult.Message = &quot;비밀번호를 입력하세요.&quot;;
                return loginResult;
            }

            isMatchedUserId = loginRequest.UserId == testUserId;
            isMatchedPassword = loginRequest.Password == testPassword;

            if (isMatchedUserId == false)
            {
                loginResult.IsSuccess = false;
                loginResult.Message = &quot;아이디가 올바르지 않습니다.&quot;;
                return loginResult;
            }

            if (isMatchedPassword == false)
            {
                loginResult.IsSuccess = false;
                loginResult.Message = &quot;비밀번호가 올바르지 않습니다.&quot;;
                return loginResult;
            }

            userInfo.UserId = loginRequest.UserId;
            userInfo.UserName = &quot;테스트 사용자&quot;;
            userInfo.UserGrade = &quot;테스트 회원&quot;;

            loginResult.IsSuccess = true;
            loginResult.Message = $&quot;{loginRequest.UserId}님 환영합니다. 로그인 성공.&quot;;
            loginResult.UserInfo = userInfo;

            return loginResult;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 이 부분이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;userInfo.UserId = loginRequest.UserId;
userInfo.UserName = &quot;테스트 사용자&quot;;
userInfo.UserGrade = &quot;테스트 회원&quot;;

loginResult.UserInfo = userInfo;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 테스트용 값을 직접 넣고 있지만, 나중에 DB를 붙이면 이 부분이 바뀔 것이다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;현재:
코드에서 테스트 사용자 정보 생성

나중:
DB에서 사용자 정보 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 지금 구조는 나중에 DB로 자연스럽게 이어질 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LoginStateService는 저장만 하게 바꾸기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;LoginStateService&lt;/code&gt;는 &lt;code&gt;UserInfo&lt;/code&gt;를 직접 만들지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 &lt;code&gt;string userId&lt;/code&gt;를 받았다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;public void Login(string userId)
{
    UserInfo userInfo;

    userInfo = new UserInfo();

    userInfo.UserId = userId;
    userInfo.UserName = &quot;테스트 사용자&quot;;
    userInfo.UserGrade = &quot;테스트 회원&quot;;

    IsLoggedIn = true;
    CurrentUser = userInfo;
    StatusMessage = &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 이미 만들어진 &lt;code&gt;UserInfo&lt;/code&gt;를 받는다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;using Study.Web.Models;

namespace Study.Web.Services
{
    // 로그인 성공 시 상태 저장
    public class LoginStateService
    {
        public bool IsLoggedIn { get; private set; }
        public UserInfo CurrentUser { get; private set; } = new();
        public string StatusMessage { get; private set; } = &quot;&quot;;

        public void Login(UserInfo userInfo)
        {
            IsLoggedIn = true;
            CurrentUser = userInfo;
            StatusMessage = &quot;&quot;;
        }

        public void Logout()
        {
            IsLoggedIn = false;
            CurrentUser = new UserInfo();
            StatusMessage = &quot;로그아웃되었습니다.&quot;;
        }

        public void ClearStatusMessage()
        {
            StatusMessage = &quot;&quot;;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 역할이 훨씬 단순해졌다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;LoginStateService
&amp;rarr; 전달받은 UserInfo를 CurrentUser에 저장
&amp;rarr; IsLoggedIn 상태 변경
&amp;rarr; StatusMessage 정리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 정보를 만드는 책임은 사라졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Home.razor에서 LoginResult.UserInfo 전달하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;Home.razor&lt;/code&gt;도 수정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 로그인 성공 시 &lt;code&gt;loginRequest.UserId&lt;/code&gt;를 넘겼다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;LoginStateService.Login(loginRequest.UserId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 &lt;code&gt;LoginService&lt;/code&gt;가 만들어준 사용자 정보를 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;LoginStateService.Login(loginResult.UserInfo);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 &lt;code&gt;Login()&lt;/code&gt; 함수는 이런 형태가 됐다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;private void Login()
{
    LoginResult loginResult;

    loginResult = LoginService.Login(loginRequest);

    if (loginResult.IsSuccess)
    {
        LoginStateService.Login(loginResult.UserInfo);

        NavigationManager.NavigateTo(&quot;/member&quot;);

        return;
    }

    loginMessage = loginResult.Message;
    messageClass = &quot;app-message error-message&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름이 더 자연스럽다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Home.razor
&amp;rarr; LoginService.Login(loginRequest)
&amp;rarr; LoginResult 받음
&amp;rarr; 성공하면 LoginResult.UserInfo를 LoginStateService에 저장
&amp;rarr; /member 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Home.razor&lt;/code&gt;는 사용자 정보를 직접 만들지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그저 로그인 결과에 담긴 정보를 상태 서비스에 넘긴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Member.razor는 그대로 둬도 됐다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Member.razor&lt;/code&gt;는 수정할 필요가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 정보 출력은 이미 &lt;code&gt;LoginStateService.CurrentUser&lt;/code&gt;를 보고 있었기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;아이디: @LoginStateService.CurrentUser.UserId&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;이름: @LoginStateService.CurrentUser.UserName&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;회원 등급: @LoginStateService.CurrentUser.UserGrade&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;접속상태: 로그인 완료&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀐 것은 화면 출력 방식이 아니라, &lt;code&gt;CurrentUser&lt;/code&gt;가 만들어지는 위치다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;기존:
LoginStateService가 UserInfo 생성 후 CurrentUser에 저장

변경:
LoginService가 UserInfo 생성
LoginResult에 담아 반환
LoginStateService가 CurrentUser로 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 변화는 화면에서 바로 티가 나지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 내부 책임은 더 명확해졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CurrentUser와 userInfo가 헷갈렸다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 하면서 변수 이름도 헷갈렸다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public UserInfo CurrentUser { get; private set; } = new();

public void Login(UserInfo userInfo)
{
    CurrentUser = userInfo;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;CurrentUser&lt;/code&gt;와 &lt;code&gt;userInfo&lt;/code&gt;가 둘 다 사용자 정보인데 왜 이름이 다른지 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;CurrentUser
&amp;rarr; LoginStateService가 계속 들고 있는 현재 로그인 사용자 정보
&amp;rarr; 프로퍼티

userInfo
&amp;rarr; Login() 메서드로 잠깐 들어온 사용자 정보
&amp;rarr; 매개변수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 둘 다 &lt;code&gt;UserInfo&lt;/code&gt; 타입이지만 역할이 다르다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;userInfo
&amp;rarr; 외부에서 전달받은 값

CurrentUser
&amp;rarr; 서비스 내부에 저장해두는 현재 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 코드는 &amp;ldquo;전달받은 사용자 정보를 현재 사용자 상태로 저장한다&amp;rdquo;는 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;CurrentUser = userInfo;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필드, 프로퍼티, 지역 변수도 다시 정리했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 대문자와 소문자 기준도 다시 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#에서는 대체로 이런 규칙을 따른다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;클래스
&amp;rarr; PascalCase
&amp;rarr; LoginService, UserInfo

메서드
&amp;rarr; PascalCase
&amp;rarr; Login, Logout

public 프로퍼티
&amp;rarr; PascalCase
&amp;rarr; IsLoggedIn, CurrentUser, UserInfo

지역 변수
&amp;rarr; camelCase
&amp;rarr; loginResult, userInfo

매개변수
&amp;rarr; camelCase
&amp;rarr; loginRequest, userInfo

private 필드
&amp;rarr; camelCase 또는 _camelCase
&amp;rarr; testUserId 또는 _testUserId&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++, Java에서는 클래스 안의 멤버 변수를 보통 소문자로 시작하는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#도 private 필드는 소문자로 시작할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private readonly string testUserId = &quot;admin&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 public 프로퍼티는 보통 대문자로 시작한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public string UserId { get; set; } = &quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에 헷갈렸던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#에서는 특히 필드와 프로퍼티를 구분해서 보는 게 중요하다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;필드
&amp;rarr; 클래스 내부에서 직접 쓰는 저장 변수
&amp;rarr; 보통 private

프로퍼티
&amp;rarr; 외부에서 읽거나 쓰게 하는 값 접근 통로
&amp;rarr; 보통 public&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;LoginService&lt;/code&gt;의 테스트 계정은 내부 비교용 값이므로 필드다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private readonly string testUserId = &quot;admin&quot;;
private readonly string testPassword = &quot;1234&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;UserInfo&lt;/code&gt;의 값들은 다른 클래스와 화면에서 읽어야 하므로 프로퍼티다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public string UserId { get; set; } = &quot;&quot;;
public string UserName { get; set; } = &quot;&quot;;
public string UserGrade { get; set; } = &quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주석 위치도 조금 정리했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 이런 코드가 있었다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;namespace Study.Web.Services
// 로그인 성공 시 상태 저장
{
    public class LoginStateService
    {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작에는 문제가 없지만, 주석 위치가 어색했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래처럼 namespace 안쪽, class 바로 위로 옮기는 편이 자연스럽다고 느꼈다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;namespace Study.Web.Services
{
    // 로그인 성공 시 상태 저장
    public class LoginStateService
    {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이전 방식으로 비교하던 주석 처리 코드도 남아 있었다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/*
if (loginRequest.UserId == testUserId)
{
    isMatchedUserId = true;
}
if (loginRequest.Password == testPassword)
{
    isMatchedPassword = true;
}
*/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 아래 코드로 대체됐기 때문에 삭제하는 게 맞다고 판단했다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;isMatchedUserId = loginRequest.UserId == testUserId;
isMatchedPassword = loginRequest.Password == testPassword;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git으로 기록이 남기 때문에, 더 이상 필요 없는 주석 처리 코드는 남겨두지 않는 게 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 기능 테스트를 다시 했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. / 접속
&amp;rarr; 로그인 화면 표시

2. 아이디 비움
&amp;rarr; 아이디를 입력하세요.

3. 비밀번호 비움
&amp;rarr; 비밀번호를 입력하세요.

4. 아이디 또는 비밀번호 틀림
&amp;rarr; 오류 메시지 표시

5. admin / 1234 로그인
&amp;rarr; /member 이동

6. 회원 정보 보기 클릭
&amp;rarr; 아이디, 이름, 회원 등급 정상 표시

7. 로그아웃 클릭
&amp;rarr; / 이동

8. 로그아웃되었습니다. 메시지 표시

9. /member 직접 입력
&amp;rarr; / 로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기 기능은 기존과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 내부 구조는 바뀌었다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;기존:
LoginStateService가 UserInfo 생성

변경:
LoginService가 UserInfo 생성
LoginResult가 UserInfo 전달
LoginStateService는 저장만 담당&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커밋 메시지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업은 기능 추가라기보다는 구조 정리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 커밋 메시지는 &lt;code&gt;feat&lt;/code&gt;보다 &lt;code&gt;refactor&lt;/code&gt;가 더 어울린다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;git add Skyz.Web/Models/LoginResult.cs `
        Skyz.Web/Services/LoginService.cs `
        Skyz.Web/Services/LoginStateService.cs `
        Skyz.Web/Components/Pages/Home.razor

git commit -m &quot;refactor: return user info from login result&quot;

git push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미는 이렇다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;로그인 결과가 사용자 정보를 반환하도록 구조를 정리했다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 작업 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 리팩토링으로 역할이 더 분명해졌다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;LoginService
&amp;rarr; 로그인 검증
&amp;rarr; 성공 시 UserInfo 생성
&amp;rarr; LoginResult 반환

LoginResult
&amp;rarr; 성공 여부
&amp;rarr; 메시지
&amp;rarr; 사용자 정보

Home.razor
&amp;rarr; LoginResult.UserInfo를 LoginStateService에 전달

LoginStateService
&amp;rarr; 로그인 상태 저장
&amp;rarr; CurrentUser 저장
&amp;rarr; StatusMessage 관리

Member.razor
&amp;rarr; CurrentUser를 화면에 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 나중에 DB로 넘어가기에도 더 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 &lt;code&gt;LoginService&lt;/code&gt; 안에서 테스트 사용자 정보를 직접 만들고 있지만, 나중에는 이 부분을 DB 조회로 바꾸면 된다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;현재:
userInfo.UserName = &quot;테스트 사용자&quot;;
userInfo.UserGrade = &quot;테스트 회원&quot;;

나중:
DB에서 사용자 조회
&amp;rarr; UserInfo에 담기
&amp;rarr; LoginResult.UserInfo로 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 배운 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태를 저장하는 서비스와 로그인 결과를 만드는 서비스는 역할이 다르다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LoginStateService&lt;/code&gt;는 상태 저장에 집중하는 편이 자연스럽다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LoginService&lt;/code&gt;는 로그인 검증과 성공 결과 생성을 담당하는 편이 자연스럽다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LoginResult&lt;/code&gt;에 &lt;code&gt;UserInfo&lt;/code&gt;를 담으면 로그인 성공 결과를 더 풍부하게 표현할 수 있다.&lt;/li&gt;
&lt;li&gt;화면이 바뀌지 않아도 내부 책임 구조가 좋아지는 리팩토링이 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CurrentUser&lt;/code&gt;는 서비스가 보관하는 현재 사용자 상태이고, &lt;code&gt;userInfo&lt;/code&gt;는 메서드로 전달된 매개변수다.&lt;/li&gt;
&lt;li&gt;C#에서는 public 프로퍼티는 대문자로 시작하고, 지역 변수와 매개변수는 소문자로 시작하는 경우가 일반적이다.&lt;/li&gt;
&lt;li&gt;필드는 클래스 내부 저장 변수이고, 프로퍼티는 외부에 공개되는 값 접근 통로에 가깝다.&lt;/li&gt;
&lt;li&gt;필요 없어진 주석 처리 코드는 Git 기록을 믿고 삭제하는 편이 좋다.&lt;/li&gt;
&lt;li&gt;기능 추가가 아니라 구조 정리라면 커밋 메시지에 &lt;code&gt;refactor&lt;/code&gt;를 쓰는 것이 자연스럽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 작업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 사용자 정보를 더 현실적인 테스트 데이터 구조로 분리해볼 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 &lt;code&gt;LoginService&lt;/code&gt; 안에 테스트 사용자 정보가 직접 들어가 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;userInfo.UserName = &quot;테스트 사용자&quot;;
userInfo.UserGrade = &quot;테스트 회원&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 테스트 사용자 목록을 별도 구조로 분리하거나, DB 연결 전에 임시 사용자 저장소를 만들어볼 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상 흐름은 이렇다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;LoginService 내부 하드코딩
&amp;darr;
테스트 사용자 목록 분리
&amp;darr;
아이디로 사용자 조회
&amp;darr;
비밀번호 확인
&amp;darr;
UserInfo 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 실제 DB로 넘어가기 전, Repository나 임시 저장소 구조를 가볍게 연습할 수 있을 것 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>language/blazor</category>
      <category>.NET</category>
      <category>blazor</category>
      <category>Csharp</category>
      <category>LoginResult</category>
      <category>loginservice</category>
      <category>NamingConvention</category>
      <category>Property</category>
      <category>Refactoring</category>
      <category>userinfo</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/67</guid>
      <comments>https://rungch.tistory.com/67#entry67comment</comments>
      <pubDate>Fri, 7 Aug 2026 21:32:04 +0900</pubDate>
    </item>
    <item>
      <title>[데이터베이스실습] 06. 다중행 함수와 그룹화</title>
      <link>https://rungch.tistory.com/62</link>
      <description>&lt;p&gt;지금까지의 함수는 한 행을 받아 한 결과를 내는 단일행 함수였다. 이번에 다룬 &lt;strong&gt;다중행 함수(집계 함수)&lt;/strong&gt;는 여러 행을 묶어 하나의 값으로 요약한다. &amp;quot;부서별 평균 급여&amp;quot;, &amp;quot;직책별 인원 수&amp;quot; 같은 통계가 모두 여기서 나온다.&lt;/p&gt;
&lt;p&gt;그리고 이 집계를 &amp;quot;무엇을 기준으로 묶을지&amp;quot; 정하는 것이 &lt;code&gt;GROUP BY&lt;/code&gt;, 묶은 결과에 조건을 거는 것이 &lt;code&gt;HAVING&lt;/code&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 다중행 함수 (집계 함수)&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SUM&lt;/td&gt;
&lt;td&gt;합계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COUNT&lt;/td&gt;
&lt;td&gt;개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAX&lt;/td&gt;
&lt;td&gt;최댓값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MIN&lt;/td&gt;
&lt;td&gt;최솟값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVG&lt;/td&gt;
&lt;td&gt;평균&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT SUM(sal)  FROM emp;                  -- 급여 합계
SELECT COUNT(*)  FROM emp WHERE deptno=30;  -- 30번 부서 인원
SELECT MAX(sal), MIN(sal) FROM emp WHERE deptno=10;
SELECT AVG(sal)  FROM emp WHERE deptno=30;&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;border-left:4px solid #ff9800; background:#fff7ec; padding:12px 16px; border-radius:6px; margin:16px 0;&quot;&gt;
&lt;strong&gt;집계 함수에서 꼭 기억할 세 가지&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;단일 값으로 줄어든 집계 결과(SUM(SAL))와 행마다 다른 일반 열(ENAME)은 함께 출력할 수 없다. 결과의 행 수가 다르기 때문이다.&lt;/li&gt;
&lt;li&gt;COUNT는 NULL을 세지 않는다. 그래서 COUNT(COMM)은 수당이 있는 행만 센다.&lt;/li&gt;
&lt;li&gt;COUNT(*)는 NULL 포함 전체 행을, COUNT(열)은 그 열이 NULL이 아닌 행만 센다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;특히 &amp;quot;COUNT(&lt;em&gt;)와 COUNT(열)의 차이&amp;quot;는 실무에서 인원 집계가 틀리는 단골 원인이라고 한다. NULL을 포함해 세려면 `COUNT(&lt;/em&gt;)&lt;code&gt;, 값이 있는 행만 세려면&lt;/code&gt;COUNT(열)`이다. COUNT(COMM)을 인원수로 착각하면 수당 없는 사원이 통째로 빠지니 주의해야겠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. GROUP BY 절&lt;/h2&gt;
&lt;p&gt;특정 열의 값을 기준으로 행을 묶고, 그룹마다 집계 결과를 낸다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT deptno, AVG(sal) FROM emp GROUP BY deptno ORDER BY deptno;
SELECT deptno, job, AVG(sal) FROM emp GROUP BY deptno, job ORDER BY deptno, job;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여러 열을 지정하면 &amp;quot;대그룹 안의 소그룹&amp;quot;으로 묶인다. 위 두 번째 쿼리는 부서로 먼저 묶고, 그 안에서 다시 직책으로 묶는다.&lt;/p&gt;
&lt;div style=&quot;border-left:4px solid #ff9800; background:#fff7ec; padding:12px 16px; border-radius:6px; margin:16px 0;&quot;&gt;
&lt;strong&gt;그룹화의 핵심 규칙&lt;/strong&gt;&lt;br&gt;
SELECT 절에는 GROUP BY에 명시한 열, 또는 집계 함수만 올 수 있다. 그룹으로 묶이지 않은 일반 열(예: ENAME)을 함께 쓰면 오류가 난다. &quot;한 그룹이 한 행으로 줄어드는데, 그 그룹 안의 서로 다른 이름들 중 무엇을 보여줄 것인가?&quot;라는 모순이 생기기 때문이다.
&lt;/div&gt;

&lt;p&gt;이 오류를 한 번 직접 내보고서야 규칙이 이해됐다. 외워서가 아니라 &amp;quot;한 행으로 줄어드는데 이름은 여럿&amp;quot;이라는 모순으로 받아들이니 잊히지 않았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. HAVING 절&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HAVING&lt;/code&gt;은 그룹으로 묶은 후, 그 그룹에 조건을 건다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 부서·직책별 평균 급여가 2000 이상인 그룹만
SELECT deptno, job, AVG(sal)
FROM emp
GROUP BY deptno, job
HAVING AVG(sal) &amp;gt;= 2000
ORDER BY deptno, job;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;WHERE vs HAVING&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;절&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;WHERE&lt;/td&gt;
&lt;td&gt;출력 대상 행을 제한&lt;/td&gt;
&lt;td&gt;그룹화 전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HAVING&lt;/td&gt;
&lt;td&gt;그룹화된 결과를 제한&lt;/td&gt;
&lt;td&gt;그룹화 후&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;집계 결과(&lt;code&gt;AVG(SAL)&lt;/code&gt; 등)는 &lt;strong&gt;WHERE에 쓸 수 없다.&lt;/strong&gt; WHERE가 평가되는 시점엔 아직 그룹이 만들어지지 않아 집계할 대상이 없기 때문이다. 그래서 &amp;quot;평균이 2000 이상&amp;quot; 같은 그룹 조건은 반드시 HAVING으로 건다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- WHERE로 행을 먼저 추린 뒤(급여 3000 이하), HAVING으로 그룹을 거른다
SELECT deptno, job, AVG(sal), COUNT(*)
FROM emp
WHERE sal &amp;lt;= 3000          -- 그룹화 전: 개별 행 제한
GROUP BY deptno, job
HAVING AVG(sal) &amp;gt;= 2000    -- 그룹화 후: 그룹 제한
ORDER BY deptno, job;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 쿼리 하나에 &amp;quot;행 거르기(WHERE) → 묶기(GROUP BY) → 그룹 거르기(HAVING) → 정렬(ORDER BY)&amp;quot;라는 처리 순서가 그대로 담겨 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 쿼리로 익히기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 부서별 평균·최대·최소 급여와 인원 (평균은 정수로 절삭)
SELECT DEPTNO, TRUNC(AVG(SAL)) AS AVG_SAL,
       MAX(SAL) AS MAX_SAL, MIN(SAL) AS MIN_SAL, COUNT(*) AS CNT
FROM EMP GROUP BY DEPTNO ORDER BY DEPTNO;

-- 인원이 3명 이상인 직책
SELECT JOB, COUNT(*) FROM EMP GROUP BY JOB HAVING COUNT(*) &amp;gt;= 3;

-- 입사 연도·부서별 인원 (TO_CHAR로 연도 추출해 그룹화)
SELECT TO_CHAR(HIREDATE, &amp;#39;YYYY&amp;#39;) AS HIRE_YEAR, DEPTNO, COUNT(*) AS CNT
FROM EMP GROUP BY TO_CHAR(HIREDATE, &amp;#39;YYYY&amp;#39;), DEPTNO;

-- 수당 유무별 인원 (NVL2로 가공한 값으로 그룹화)
SELECT NVL2(COMM, &amp;#39;O&amp;#39;, &amp;#39;X&amp;#39;) AS HAS_COMM, COUNT(*) AS CNT
FROM EMP GROUP BY NVL2(COMM, &amp;#39;O&amp;#39;, &amp;#39;X&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;마지막 두 예처럼 &lt;strong&gt;함수로 가공한 값을 기준으로 그룹화&lt;/strong&gt;할 수도 있다. &amp;quot;연도별&amp;quot;, &amp;quot;수당 유무별&amp;quot;처럼 원본 열에 없는 기준으로 묶고 싶을 때 쓰는 기법이다. 이걸 알고 나니 그룹화의 활용 폭이 확 넓어진 느낌이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;수업에서 직접 친 쿼리&lt;/h2&gt;
&lt;p&gt;수업에서는 부서별 평균 → 직책별 평균 → 인원수 순으로 GROUP BY를 단계적으로 쌓아갔다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 각 부서의 직책별 급여 평균
SELECT DEPTNO, JOB, AVG(SAL)
FROM EMP
GROUP BY DEPTNO, JOB
ORDER BY DEPTNO, JOB;

-- 부서별 사원 수가 5를 초과하는 부서만
SELECT DEPTNO AS &amp;quot;부서&amp;quot;, COUNT(*) AS &amp;quot;사원 수&amp;quot;
FROM EMP
GROUP BY DEPTNO
HAVING COUNT(*) &amp;gt; 5
ORDER BY DEPTNO;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WHERE와 HAVING을 한 쿼리에 같이 쓰는 문제도 풀었다. &amp;quot;급여 3000 이하 사원만 추린 뒤, 직책별 평균이 2000 이상인 그룹&amp;quot;을 구하면서 두 절의 시점 차이를 손으로 확인했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT DEPTNO AS &amp;quot;부서번호&amp;quot;, JOB AS &amp;quot;직책&amp;quot;,
       AVG(SAL) AS &amp;quot;평균급여&amp;quot;, COUNT(*) AS &amp;quot;사원수&amp;quot;
FROM EMP
WHERE SAL &amp;lt;= 3000          -- 그룹화 전: 개별 행
GROUP BY DEPTNO, JOB
HAVING AVG(SAL) &amp;gt;= 2000    -- 그룹화 후: 그룹
ORDER BY DEPTNO, JOB;

-- 매니저들의 평균 급여가 2500 이하인 부서
SELECT DEPTNO
FROM EMP
WHERE JOB = &amp;#39;MANAGER&amp;#39;
GROUP BY DEPTNO
HAVING AVG(SAL) &amp;lt;= 2500;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;연습문제에서는 가공한 값으로 그룹화하는 것도 다뤘다. 입사 연도(TO_CHAR), 수당 유무(NVL2)를 기준으로 묶었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT TO_CHAR(HIREDATE, &amp;#39;YYYY&amp;#39;) AS HIRE_YEAR, DEPTNO, COUNT(*) AS CNT
FROM EMP GROUP BY TO_CHAR(HIREDATE, &amp;#39;YYYY&amp;#39;), DEPTNO;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;오늘 느낀 점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;집계 함수, GROUP BY, HAVING이 따로가 아니라 &amp;quot;행 거르기 → 묶기 → 그룹 거르기&amp;quot;라는 한 처리 흐름의 단계라는 게 보였다. 처리 순서를 잡으니 WHERE와 HAVING을 더는 헷갈리지 않았다.&lt;/li&gt;
&lt;li&gt;그룹화 규칙(SELECT엔 묶인 열이나 집계만)을 오류로 직접 만나보니, 규칙이 자의적이 아니라 &amp;quot;한 행으로 줄어든다&amp;quot;는 사실에서 나온 필연이라는 게 이해됐다.&lt;/li&gt;
&lt;li&gt;함수로 가공한 값으로도 그룹화가 된다는 점에서, GROUP BY가 단순 열 묶기를 넘어 &amp;quot;원하는 기준을 만들어 묶는&amp;quot; 도구라는 걸 알았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;한 걸음 더&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;집계 함수는 NULL을 무시하므로 &lt;code&gt;AVG(COMM)&lt;/code&gt; 같은 평균이 직관과 다를 수 있다. &lt;code&gt;AVG(COMM)&lt;/code&gt;은 &amp;quot;수당이 있는 사원들만의 평균&amp;quot;이지 전체 사원 평균이 아니다. 전체 기준 평균을 원하면 &lt;code&gt;AVG(NVL(COMM,0))&lt;/code&gt;처럼 NULL을 0으로 채워야 한다. 집계 전에 NULL을 어떻게 다룰지가 결과를 크게 바꾼다.&lt;/li&gt;
&lt;li&gt;표준 SQL에서는 GROUP BY에 없는 열을 SELECT에 쓰면 오류지만, MySQL의 일부 모드는 이를 허용해 임의의 값을 내보낸다. 편해 보여도 결과가 예측 불가능해지는 함정이라, 오라클의 엄격한 규칙에 익숙해지는 편이 안전하다.&lt;/li&gt;
&lt;li&gt;부분합·총합을 한 번에 내고 싶을 땐 &lt;code&gt;ROLLUP&lt;/code&gt;, &lt;code&gt;CUBE&lt;/code&gt; 같은 확장 그룹 함수가 있다. 예를 들어 &lt;code&gt;GROUP BY ROLLUP(deptno)&lt;/code&gt;는 부서별 합계에 더해 전체 총합 행까지 함께 만들어준다. 보고서용 집계에서 요긴하니 이런 게 있다는 것만 알아두면 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;</description>
      <category>data/oracle</category>
      <category>AVG</category>
      <category>count</category>
      <category>GROUP BY</category>
      <category>Having</category>
      <category>oracle</category>
      <category>SQL</category>
      <category>SUM</category>
      <category>그룹화</category>
      <category>오라클</category>
      <category>집계함수</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/62</guid>
      <comments>https://rungch.tistory.com/62#entry62comment</comments>
      <pubDate>Thu, 6 Aug 2026 01:45:31 +0900</pubDate>
    </item>
    <item>
      <title>[컴퓨터네트워크] 05. 네트워크 계층 (IP)</title>
      <link>https://rungch.tistory.com/68</link>
      <description>&lt;p&gt;같은 네트워크 안에서는 MAC 주소만으로 통신할 수 있다. 하지만 다른 네트워크로 데이터를 보내려면 &lt;strong&gt;IP 주소&lt;/strong&gt;와 그 길을 찾는 &lt;strong&gt;라우팅&lt;/strong&gt;이 필요하다. 이 일을 담당하는 것이 네트워크 계층(3계층)이고, 그 핵심 프로토콜이 IP다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. TCP/IP와 주소 체계&lt;/h2&gt;
&lt;p&gt;인터넷의 표준 통신 규약이 TCP/IP다. 둘은 역할을 나눠 맡는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IP (Internet Protocol)&lt;/strong&gt;: 패킷을 목적지 호스트까지 전달(전송 담당).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP (Transmission Control Protocol)&lt;/strong&gt;: 오류 시 재전송, 흐름 제어 등 신뢰성 담당.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;통신에는 세 종류의 주소가 함께 쓰인다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주소&lt;/th&gt;
&lt;th&gt;길이&lt;/th&gt;
&lt;th&gt;식별 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;MAC 주소&lt;/td&gt;
&lt;td&gt;48비트&lt;/td&gt;
&lt;td&gt;네트워크 카드(기기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP 주소&lt;/td&gt;
&lt;td&gt;32비트(IPv4)&lt;/td&gt;
&lt;td&gt;호스트(컴퓨터)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포트 주소&lt;/td&gt;
&lt;td&gt;16비트&lt;/td&gt;
&lt;td&gt;프로세스(서비스)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&amp;quot;어느 기기(MAC)의, 어느 컴퓨터(IP)의, 어느 프로그램(포트)에 보낼지&amp;quot;가 이 셋으로 정해졌다. 세 주소가 각자 다른 대상을 식별한다는 구분이 머릿속을 정리해줬다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. IPv4 주소&lt;/h2&gt;
&lt;div style=&quot;border-left:4px solid #4f8cff; background:#f4f8ff; padding:12px 16px; border-radius:6px; margin:16px 0;&quot;&gt;
&lt;strong&gt;IPv4 주소&lt;/strong&gt;&lt;br&gt;
32비트(약 43억 개) 주소로, 8비트씩 네 부분을 10진수와 마침표로 표기한다(예: 192.168.2.124, 각 부분 0~255). 주소는 네트워크 ID와 호스트 ID로 나뉜다.
&lt;/div&gt;

&lt;p&gt;호스트 ID가 특별한 값일 때는 호스트에 할당할 수 없다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;호스트 ID가 모두 0 → 네트워크 자체를 가리키는 &lt;strong&gt;네트워크 주소&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;호스트 ID가 모두 1 → 그 네트워크 전체에 보내는 &lt;strong&gt;브로드캐스트 주소&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;또 공인 IP(전 세계 고유, 인터넷 통신용)와 사설 IP(내부망 전용, 192.168.x.x 등)로 나뉜다. 사설 IP는 부족한 공인 IP를 아끼기 위한 장치다. IP는 규모에 따라 A·B·C 클래스로 나뉘며, 자기 자신을 가리키는 127.x.x.x는 루프백 주소다.&lt;/p&gt;
&lt;div style=&quot;border-left:4px solid #4f8cff; background:#f4f8ff; padding:12px 16px; border-radius:6px; margin:16px 0;&quot;&gt;
&lt;strong&gt;서브넷팅과 서브넷 마스크&lt;/strong&gt;&lt;br&gt;
큰 네트워크를 작은 서브넷으로 쪼개는 것이 서브넷팅이다. 호스트 ID에서 비트를 빌려 서브넷을 구분한다. 서브넷 마스크는 &quot;어디까지가 네트워크 ID인가&quot;를 알려주는 32비트 값으로, 네트워크 부분은 1, 호스트 부분은 0이다. /24는 255.255.255.0을 뜻한다. 브로드캐스트 범위를 줄이고 IP를 효율적으로 쓰기 위한 기법이다.
&lt;/div&gt;

&lt;p&gt;집 공유기 주소가 192.168.x.x인 게 사설 IP였다는 걸 알고, 그동안 무심코 보던 숫자가 의미를 갖게 됐다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. IP 패킷과 TTL&lt;/h2&gt;
&lt;p&gt;IP 패킷의 헤더에는 전송에 필요한 정보가 담긴다. 그중 꼭 알아둘 필드가 &lt;strong&gt;TTL&lt;/strong&gt;이다.&lt;/p&gt;
&lt;div style=&quot;border-left:4px solid #4f8cff; background:#f4f8ff; padding:12px 16px; border-radius:6px; margin:16px 0;&quot;&gt;
&lt;strong&gt;TTL (Time To Live)&lt;/strong&gt;&lt;br&gt;
패킷이 거칠 수 있는 최대 라우터 수다. 라우터를 하나 지날 때마다 1씩 줄고, 0이 되면 그 패킷은 폐기된다. 잘못된 경로로 패킷이 네트워크를 영원히 떠도는 것을 막는 안전장치다.
&lt;/div&gt;

&lt;p&gt;IP의 성격도 중요하다. IP는 &lt;strong&gt;비연결형&lt;/strong&gt;이라 &amp;quot;최선을 다해 보내되 도착이나 순서를 보장하지 않는다.&amp;quot; 신뢰성은 상위 계층인 TCP가 책임진다. 이 단순함이 인터넷의 확장성을 만든 핵심이다. 앞 단원의 데이터그램 방식이 곧 IP의 이 비연결성이었다는 게 이어졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 라우터와 라우팅&lt;/h2&gt;
&lt;p&gt;다른 네트워크로 나가는 출입구가 &lt;strong&gt;기본 게이트웨이&lt;/strong&gt;(라우터의 IP)다. 라우터는 &lt;strong&gt;라우팅 테이블&lt;/strong&gt;을 보고 패킷을 어느 방향으로 보낼지 정한다.&lt;/p&gt;
&lt;p&gt;라우팅 과정을 간추리면 이렇다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수신지 IP와 서브넷 마스크를 AND 연산해 목적지 네트워크 주소를 구한다.&lt;/li&gt;
&lt;li&gt;라우팅 테이블에서 그 네트워크로 가는 경로를 찾는다.&lt;/li&gt;
&lt;li&gt;TTL을 1 줄이고 검사합을 다시 계산한 뒤, 다음 라우터(홉)로 넘긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 과정이 라우터마다 반복되며, 패킷은 여러 라우터를 거쳐 목적지에 도달한다. 경로 정보를 자동으로 주고받는 프로토콜로 RIP, OSPF가 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. IPv6&lt;/h2&gt;
&lt;div style=&quot;border-left:4px solid #4f8cff; background:#f4f8ff; padding:12px 16px; border-radius:6px; margin:16px 0;&quot;&gt;
&lt;strong&gt;IPv6: 128비트 주소&lt;/strong&gt;&lt;br&gt;
IPv4의 43억 개 주소가 고갈되어 등장했다. 128비트라 사실상 무한에 가까운 주소를 제공한다. 16비트씩 콜론(:)으로 나눠 16진수로 표기하며, 연속된 0은 ::로 한 번 줄일 수 있다.
&lt;/div&gt;

&lt;p&gt;IPv6는 주소 공간만 넓힌 게 아니다. 헤더를 단순화해 처리가 빠르고, 자동 주소 설정(플러그 앤 플레이)을 지원하며, 보안(IPSec)을 기본으로 갖춘다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 네트워크 계층 프로토콜&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;프로토콜&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;IP&lt;/td&gt;
&lt;td&gt;패킷 전송, 라우팅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARP&lt;/td&gt;
&lt;td&gt;IP 주소 → MAC 주소 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RARP&lt;/td&gt;
&lt;td&gt;MAC 주소 → IP 주소 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ICMP&lt;/td&gt;
&lt;td&gt;오류 보고·진단 (ping, traceroute가 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IGMP&lt;/td&gt;
&lt;td&gt;멀티캐스트 그룹 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;ping이 동작하는 게 바로 ICMP다. &amp;quot;거기 있니?&amp;quot;라는 에코 요청을 보내고 응답을 받아 연결과 속도를 진단한다. 평소 쓰던 ping이 ICMP라는 프로토콜이었다는 걸 알고 나니, 명령어와 이론이 연결됐다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;오늘 느낀 점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MAC(기기)·IP(컴퓨터)·포트(프로그램) 세 주소가 각자 다른 대상을 식별한다는 구분이, 통신이 &amp;quot;어디로 가는가&amp;quot;를 정하는 방식을 명확히 해줬다.&lt;/li&gt;
&lt;li&gt;집 공유기의 192.168.x.x가 사설 IP였다는 것처럼, 평소 보던 숫자들이 이론으로 설명되니 추상이 일상과 이어졌다.&lt;/li&gt;
&lt;li&gt;IP가 &amp;quot;최선을 다하되 보장은 안 한다&amp;quot;는 비연결성이, 앞 단원 데이터그램과 같은 개념이고 그 단순함이 인터넷 확장성의 비결이라는 흐름이 잡혔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;한 걸음 더&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;사설 IP가 어떻게 인터넷과 통신할까? 답은 &lt;strong&gt;NAT(Network Address Translation, 주소 변환)&lt;/strong&gt;다. 공유기가 내부의 사설 IP를 자기 공인 IP 하나로 바꿔 내보내고, 응답이 오면 다시 내부로 돌려준다. 덕분에 집 안 여러 기기가 공인 IP 하나로 모두 인터넷을 쓴다. IPv4 고갈을 늦춘 결정적 기술이다.&lt;/li&gt;
&lt;li&gt;TTL은 보안·진단에도 쓰인다. traceroute는 TTL을 1, 2, 3…으로 늘려가며 패킷을 보내, 각 단계에서 폐기 응답을 보낸 라우터를 추적해 &amp;quot;내 패킷이 어떤 경로로 가는지&amp;quot;를 그려낸다. 안전장치로 만든 필드가 진단 도구가 된 영리한 활용이다.&lt;/li&gt;
&lt;li&gt;IPv6가 나온 지 오래됐는데도 IPv4가 여전히 쓰이는 건, 위의 NAT가 IPv4의 수명을 크게 늘렸기 때문이다. 그래서 전환이 더디게 진행 중이고, 한동안 둘이 공존한다. &amp;quot;더 나은 기술이 있어도 기존 기술이 충분히 버티면 전환이 느리다&amp;quot;는 현실을 보여주는 사례다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;</description>
      <category>computer_science/network</category>
      <category>icmp</category>
      <category>IPv4</category>
      <category>IPv6</category>
      <category>ip주소</category>
      <category>TTL</category>
      <category>네트워크계층</category>
      <category>라우팅</category>
      <category>서브넷마스크</category>
      <category>컴퓨터네트워크</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/68</guid>
      <comments>https://rungch.tistory.com/68#entry68comment</comments>
      <pubDate>Wed, 5 Aug 2026 21:34:41 +0900</pubDate>
    </item>
    <item>
      <title>[Ubuntu] 08. 홈 서버 운영 - Unit Test와 Integration Test 분리</title>
      <link>https://rungch.tistory.com/66</link>
      <description>&lt;p&gt;서버 배포와 백업까지 정리한 뒤에는 테스트가 신경 쓰이기 시작했다.&lt;/p&gt;
&lt;p&gt;처음에는 로컬에서 &lt;code&gt;dotnet test&lt;/code&gt;가 통과하면 괜찮다고 생각했다. 그런데 Codex나 GitHub Actions 같은 환경에서 테스트를 돌리려고 하니 문제가 생겼다.&lt;/p&gt;
&lt;p&gt;오류는 대략 이런 식이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cannot create an automatic instance.
Unknown error&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 코드가 깨졌나 싶었는데, 자세히 보니 테스트 코드 자체의 실패가 아니라 LocalDB 자동 인스턴스를 만들지 못해서 생긴 문제였다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;문제 상황&lt;/h1&gt;
&lt;p&gt;현재 운영 DB는 Ubuntu 서버의 Docker MSSQL에 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ubuntu Server
    ↓
Docker
    ↓
mssql-db
    ↓
SurvivalGame DB&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 테스트가 운영 DB에 직접 붙으면 안 된다.&lt;/p&gt;
&lt;p&gt;테스트는 데이터를 만들고 지우고, 경우에 따라 Migration을 적용하거나 DB를 초기화할 수도 있다. 운영 DB에 붙이면 실제 데이터를 망가뜨릴 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 테스트용 DB가 따로 필요하다.&lt;/p&gt;
&lt;p&gt;처음에는 LocalDB를 사용할 수 있다고 생각했다. Windows 로컬에서는 흔한 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 실행
    ↓
LocalDB
    ↓
테스트용 DB 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;문제는 LocalDB가 Windows 환경에 강하게 묶여 있다는 점이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;LocalDB가 불편했던 이유&lt;/h1&gt;
&lt;p&gt;LocalDB는 로컬 Windows 개발 환경에서는 편하다. 하지만 아래 환경에서는 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GitHub Actions Linux Runner
Docker 컨테이너
Codex Sandbox
일부 제한된 Windows 세션&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제로 Codex 샌드박스에서는 LocalDB 자동 인스턴스를 만들지 못했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cannot create an automatic instance.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;나중에는 내 로컬 Windows의 LocalDB도 한 번 꼬였다. 결국 아래 명령으로 재생성해서 복구했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;sqllocaldb stop MSSQLLocalDB
sqllocaldb delete MSSQLLocalDB
sqllocaldb create MSSQLLocalDB
sqllocaldb start MSSQLLocalDB
sqllocaldb info MSSQLLocalDB&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정상 상태는 이렇게 나온다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;State: Running
Auto-create: Yes
Instance pipe name: ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이걸 고치고 나니 로컬 전체 테스트는 다시 통과했다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 중요한 건 LocalDB를 고친 것이 끝이 아니라는 점이다. 테스트 구조 자체를 환경에 덜 의존하게 만들어야 했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;테스트를 둘로 나누기로 했다&lt;/h1&gt;
&lt;p&gt;테스트를 크게 두 종류로 나눴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Unit Test
- 빠르게 실행
- DB 의존 없음
- 로컬 / Codex / CI에서 항상 실행 가능

Integration Test
- 실제 SQL Server 필요
- GitHub Actions의 MSSQL 컨테이너에서 실행
- main 또는 수동 검증용&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 모든 테스트를 한 덩어리로 봤다. 그런데 실제로는 성격이 다르다.&lt;/p&gt;
&lt;p&gt;데미지 계산, 권한 판정, 보상 계산 같은 로직은 DB 없이도 테스트할 수 있다.&lt;/p&gt;
&lt;p&gt;반대로 Migration, Repository, 실제 HTTP 요청과 DB 저장까지 확인하는 테스트는 SQL Server가 필요하다.&lt;/p&gt;
&lt;p&gt;두 테스트를 같은 방식으로 돌리면 빠른 테스트도 DB 환경 때문에 실패할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;Trait으로 Integration 표시&lt;/h1&gt;
&lt;p&gt;xUnit에서는 &lt;code&gt;Trait&lt;/code&gt;을 이용해 테스트를 분류할 수 있다.&lt;/p&gt;
&lt;p&gt;DB가 필요한 테스트 클래스에는 아래 특성을 붙였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[Trait(&amp;quot;Category&amp;quot;, &amp;quot;Integration&amp;quot;)]
public class MigrationFromScratchTests
{
    // 실제 SQL Server가 필요한 테스트
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;클래스 단위로 붙이는 편이 관리하기 쉬웠다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DB 연결 필요
Migration 필요
WebApplicationFactory로 실제 API 실행
SQL Server 상태 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 테스트는 Integration으로 분류했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;Unit Test만 실행하기&lt;/h1&gt;
&lt;p&gt;DB가 필요 없는 테스트만 실행할 때는 필터를 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;dotnet test .\SurvivalGame.sln --filter &amp;quot;Category!=Integration&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;빌드와 복원을 분리하면 더 명확하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;dotnet restore .\SurvivalGame.sln
dotnet build .\SurvivalGame.sln --no-restore
dotnet test .\SurvivalGame.sln --no-build --no-restore --filter &amp;quot;Category!=Integration&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 테스트 실행 중 restore/build를 반복하지 않는다.&lt;/p&gt;
&lt;p&gt;현재 Unit Test는 LocalDB 없이 정상 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Unit Test
    ↓
DB 연결 없음
    ↓
빠르게 통과&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h1&gt;Integration Test만 실행하기&lt;/h1&gt;
&lt;p&gt;Integration Test만 실행할 때는 반대로 필터를 건다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;dotnet test .\SurvivalGame.sln --filter &amp;quot;Category=Integration&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로컬에서는 LocalDB나 SQL Express가 정상이어야 돌릴 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 앞으로 기본 검증은 GitHub Actions의 MSSQL 컨테이너를 사용할 생각이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GitHub Actions
    ↓
MSSQL service container
    ↓
테스트 DB 생성
    ↓
Integration Test
    ↓
Runner 종료 후 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;운영 서버의 MSSQL에 붙이는 방식은 피했다.&lt;/p&gt;
&lt;p&gt;운영 DB는 실제 서비스용이고, 테스트가 건드리면 안 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;GitHub Actions에서 MSSQL 컨테이너 사용&lt;/h1&gt;
&lt;p&gt;Integration job에서는 MSSQL 컨테이너를 service로 띄운다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  mssql:
    image: mcr.microsoft.com/mssql/server:2022-CU20-ubuntu-22.04
    env:
      ACCEPT_EULA: Y
      MSSQL_SA_PASSWORD: Test_password123!
    ports:
      - 1433:1433
    options: &amp;gt;-
      --health-cmd &amp;quot;/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P Test_password123! -C -Q &amp;#39;SELECT 1&amp;#39;&amp;quot;
      --health-interval 10s
      --health-timeout 5s
      --health-retries 10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;테스트 연결 문자열은 환경변수로 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;env:
  SURVIVALGAME_TEST_SQLSERVER: &amp;quot;Server=localhost,1433;User Id=sa;Password=Test_password123!;Encrypt=False;TrustServerCertificate=True;MultipleActiveResultSets=true&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 Integration 테스트만 실행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Backend integration tests
  run: dotnet test ./SurvivalGame.sln --no-build --no-restore --filter &amp;quot;Category=Integration&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h1&gt;테스트 실행 정책&lt;/h1&gt;
&lt;p&gt;최종적으로 테스트 정책은 이렇게 잡았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;로컬 개발 중
    → Unit Test 위주

DB 기능 변경 시
    → 로컬 Integration Test 또는 수동 Actions Integration

main 반영 시
    → Unit + Frontend 자동 실행

중요한 DB 변경 전
    → Integration 수동 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 main에 들어갈 때마다 Integration까지 자동으로 돌리게 했지만, Actions 사용량이 너무 빨리 늘었다. 그래서 나중에는 Integration을 수동 실행으로 바꿨다.&lt;/p&gt;
&lt;p&gt;이 부분은 다음 글에서 따로 정리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;테스트 구조 다이어그램&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                   ┌──────────────────────┐
                   │       dotnet test      │
                   └───────────┬──────────┘
                               │
               ┌───────────────┴────────────────┐
               │                                │
       Category != Integration          Category = Integration
               │                                │
        Unit Test                         Integration Test
               │                                │
   DB 없이 빠르게 실행              MSSQL 컨테이너 필요
               │                                │
 로컬 / Codex / CI 가능             GitHub Actions에서 검증&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h1&gt;오늘 배운 점&lt;/h1&gt;
&lt;p&gt;테스트가 실패했다고 해서 항상 코드가 틀린 것은 아니었다.&lt;/p&gt;
&lt;p&gt;이번에는 LocalDB 자동 인스턴스 생성 실패, SQL Express 인증 문제, Codex 환경 제약 같은 것들이 섞여 있었다. 처음에는 전부 테스트 실패로만 보였는데, 하나씩 나눠보니 코드 실패와 환경 실패를 분리할 수 있었다.&lt;/p&gt;
&lt;p&gt;테스트도 서버 구조처럼 역할을 나눠야 한다는 걸 느꼈다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;빠른 검증은 가볍게
실제 DB 검증은 격리된 환경에서
운영 DB는 건드리지 않기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 원칙만 잡아도 테스트가 훨씬 덜 불안해진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;다음 작업&lt;/h1&gt;
&lt;p&gt;다음에는 GitHub Actions 사용 시간을 줄이는 작업을 정리한다.&lt;/p&gt;
&lt;p&gt;처음에는 feature 브랜치, main, Integration, Deploy가 자주 돌면서 Actions 분을 많이 사용했다. Private 저장소에서는 GitHub Actions 시간이 무료 포함량을 직접 소모하므로 운영 방식도 조정해야 했다.&lt;/p&gt;
&lt;hr&gt;</description>
      <category>study/server</category>
      <category>Csharp</category>
      <category>github actions</category>
      <category>Home Server</category>
      <category>IntegrationTest</category>
      <category>localDB</category>
      <category>SQLServer</category>
      <category>ubuntu</category>
      <category>unittest</category>
      <category>xUnit</category>
      <category>테스트 자동화</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/66</guid>
      <comments>https://rungch.tistory.com/66#entry66comment</comments>
      <pubDate>Tue, 4 Aug 2026 21:28:50 +0900</pubDate>
    </item>
    <item>
      <title>[C언어] 05. 반복문</title>
      <link>https://rungch.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;같은 작업을 여러 번 해야 할 때, 코드를 여러 번 복사하는 대신 &lt;b&gt;반복문&lt;/b&gt;을 쓴다. &quot;1부터 100까지 더하기&quot;를 덧셈 100줄로 쓰지 않고 몇 줄로 끝내는 것이다. C의 반복문에는 &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;do~while&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt; 세 가지가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQHga1/dJMcajo7yWl/FAtdwk4WXMSAYwkkz9EdzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQHga1/dJMcajo7yWl/FAtdwk4WXMSAYwkkz9EdzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQHga1/dJMcajo7yWl/FAtdwk4WXMSAYwkkz9EdzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQHga1%2FdJMcajo7yWl%2FFAtdwk4WXMSAYwkkz9EdzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1560&quot; height=&quot;893&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;while&lt;/td&gt;
&lt;td&gt;조건이 참인 동안 반복 (반복 횟수를 모를 때)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;do ~ while&lt;/td&gt;
&lt;td&gt;일단 한 번 실행한 뒤 조건 검사 (최소 1회 보장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;for&lt;/td&gt;
&lt;td&gt;정해진 횟수만큼 반복&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. while 문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 참인 동안 중괄호 안을 반복한다. 조건을 &lt;b&gt;먼저&lt;/b&gt; 검사하므로, 처음부터 거짓이면 &lt;b&gt;한 번도 실행되지 않는다&lt;/b&gt;(0번 이상).&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;while (조건) {
    수행 문장;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 횟수를 미리 알 수 없을 때(예: &quot;0이 입력될 때까지&quot;) 적합하다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int n;
while (1) {                 // 무한 루프
    scanf(&quot;%d&quot;, &amp;amp;n);
    if (n == 0) break;      // 0이 들어오면 탈출
    printf(&quot;%s\n&quot;, n % 2 ? &quot;홀수&quot; : &quot;짝수&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. do ~ while 문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;while&lt;/code&gt;과 비슷하지만, &lt;b&gt;문장을 먼저 실행한 뒤 조건을 검사&lt;/b&gt;한다. 그래서 조건과 상관없이 &lt;b&gt;최소 한 번은 실행&lt;/b&gt;된다(1번 이상).&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;do {
    수행 문장;
} while (조건);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;메뉴를 한 번은 보여주고, 계속할지 묻는다&quot; 같은 상황처럼 &quot;최소 한 번 실행&quot;이 필요할 때 어울린다. while과 do~while의 차이가 &quot;조건을 검사하는 시점&quot;이라는 걸, 처음 조건이 거짓인 경우로 비교해보고서야 분명히 잡았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. for 문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 횟수가 정해져 있을 때 가장 깔끔하다. 카운터 변수의 초기화&amp;middot;조건&amp;middot;증감을 한 줄에 모아 쓴다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2NYKS/dJMb99UqDBo/wHpll9EWNNmu7CPJYnDM81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2NYKS/dJMb99UqDBo/wHpll9EWNNmu7CPJYnDM81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2NYKS/dJMb99UqDBo/wHpll9EWNNmu7CPJYnDM81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2NYKS%2FdJMb99UqDBo%2FwHpll9EWNNmu7CPJYnDM81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1560&quot; height=&quot;847&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;847&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;for (초기식; 조건식; 증감식) {
    수행 문장;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초기식&lt;/b&gt; (&lt;code&gt;i = 0&lt;/code&gt;): 처음 한 번만 실행.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건식&lt;/b&gt; (&lt;code&gt;i &amp;lt; 5&lt;/code&gt;): 매 반복 전에 검사, 거짓이면 종료.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;증감식&lt;/b&gt; (&lt;code&gt;i++&lt;/code&gt;): 매 반복이 끝날 때 실행.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;for (int iIndex = 0; iIndex &amp;lt; 9; iIndex++) {
    int iMul = iIndex + 1;
    printf(&quot;5 x %d = %d\n&quot;, iMul, 5 * iMul);   // 구구단 5단
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이중 &lt;code&gt;for&lt;/code&gt;문을 쓰면 별 삼각형 같은 2차원 패턴도 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;for (int iLineIndex = 0; iLineIndex &amp;lt; 5; iLineIndex++) {       // 줄
    for (int iStarIndex = 0; iStarIndex &amp;lt; iLineIndex + 1; iStarIndex++)     // 각 줄의 별 개수
        printf(&quot;*&quot;);
    printf(&quot;\n&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바깥 루프가 &quot;줄&quot;을, 안쪽 루프가 &quot;그 줄에 찍을 별 개수&quot;를 담당한다는 구조를 잡으니 패턴 문제가 한결 쉬워졌다. 이중 for문이 처음엔 막막했는데, 두 루프의 역할을 분리해 보니 풀렸다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 분기문: break와 continue&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 도중 흐름을 바꾸는 두 명령이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;border-left: 4px solid #4f8cff; background: #f4f8ff; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;break vs continue&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;break&lt;/b&gt;: 반복문 자체를 즉시 종료하고 빠져나온다. 무한 루프 탈출에 자주 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;continue&lt;/b&gt;: 현재 반복의 남은 문장만 건너뛰고, 곧장 다음 반복으로 넘어간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 3의 배수는 제외하고 합산 (continue 활용)
int n, sum = 0;
while (1) {
    scanf(&quot;%d&quot;, &amp;amp;n);
    if (n &amp;lt; 0) break;         // 음수면 종료
    if (n % 3 == 0) continue; // 3의 배수면 합산 건너뛰고 다음 입력
    sum += n;
}
printf(&quot;3의 배수를 제외한 합: %d\n&quot;, sum);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;break&lt;/code&gt;는 &quot;루프를 끝낸다&quot;, &lt;code&gt;continue&lt;/code&gt;는 &quot;이번 회차만 건너뛴다&quot;는 차이를 분명히 구분하자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 수업 코드로 보는 카페 메뉴 반복&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;while&lt;/code&gt; 반복, &lt;code&gt;if&lt;/code&gt; 종료 조건, &lt;code&gt;switch&lt;/code&gt; 메뉴 처리가 한 번에 들어간 예제다. 메뉴를 계속 보여주다가 사용자가 &lt;code&gt;0&lt;/code&gt;을 입력하면 종료하고, 그동안 판매한 총액을 출력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eg6A5/dJMcacXVQTp/6W5UYQh1UgYKd2zrhvgPe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eg6A5/dJMcacXVQTp/6W5UYQh1UgYKd2zrhvgPe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eg6A5/dJMcacXVQTp/6W5UYQh1UgYKd2zrhvgPe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEg6A5%2FdJMcacXVQTp%2F6W5UYQh1UgYKd2zrhvgPe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;874&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int iMenuNo;
int iPriceAmericano;
int iPriceLatte;
int iPriceIceTea;
int iCountAmericano;
int iCountLatte;
int iCountIceTea;
int iTotal;

iMenuNo = 0;
iPriceAmericano = 1500;
iPriceLatte = 2000;
iPriceIceTea = 2500;
iCountAmericano = 0;
iCountLatte = 0;
iCountIceTea = 0;
iTotal = 0;

while (1)
{
    printf(&quot;1. 아메리카노 %d원\n&quot;, iPriceAmericano);
    printf(&quot;2. 카페라떼 %d원\n&quot;, iPriceLatte);
    printf(&quot;3. 아이스티 %d원\n&quot;, iPriceIceTea);
    printf(&quot;메뉴를 입력하세요(0 종료): &quot;);
    scanf(&quot;%d&quot;, &amp;amp;iMenuNo);

    if (iMenuNo == 0)
    {
        break;
    }

    switch (iMenuNo)
    {
    case 1:
        iCountAmericano = iCountAmericano + 1;
        break;
    case 2:
        iCountLatte = iCountLatte + 1;
        break;
    case 3:
        iCountIceTea = iCountIceTea + 1;
        break;
    default:
        printf(&quot;없는 메뉴입니다.\n&quot;);
        break;
    }

    iTotal = iPriceAmericano * iCountAmericano
           + iPriceLatte * iCountLatte
           + iPriceIceTea * iCountIceTea;
}

printf(&quot;판매한 총 금액: %d\n&quot;, iTotal);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서 반복의 흐름은 &lt;b&gt;출력 &amp;rarr; 입력 &amp;rarr; 종료 판단 &amp;rarr; 처리 &amp;rarr; 누적&lt;/b&gt; 순서다. 특히 &lt;code&gt;0&lt;/code&gt;은 메뉴가 아니라 종료 신호이므로, &lt;code&gt;switch&lt;/code&gt;로 보내기 전에 먼저 검사해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자릿수 뒤집기 유제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문 응용으로 끝자리부터 떼어 새 숫자를 만드는 문제다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3s92O/dJMcabdAsTo/EwptiACEhCTngmUdKSGRk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3s92O/dJMcabdAsTo/EwptiACEhCTngmUdKSGRk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3s92O/dJMcabdAsTo/EwptiACEhCTngmUdKSGRk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3s92O%2FdJMcabdAsTo%2FEwptiACEhCTngmUdKSGRk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;789&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;int iNo;
int iDigit;
int iReverse;

printf(&quot;정수 입력: &quot;);
scanf(&quot;%d&quot;, &amp;amp;iNo);

iReverse = 0;

while (iNo &amp;gt; 0)
{
    iDigit = iNo % 10;
    iReverse = iReverse * 10 + iDigit;
    iNo = iNo / 10;
}

printf(&quot;뒤집은 수: %d\n&quot;, iReverse);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;12345&lt;/code&gt;를 넣으면 &lt;code&gt;5 &amp;rarr; 54 &amp;rarr; 543 &amp;rarr; 5432 &amp;rarr; 54321&lt;/code&gt;처럼 결과가 한 자리씩 쌓인다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 느낀 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;while&amp;middot;do~while&amp;middot;for가 결국 같은 일을 하되, &quot;조건 검사 시점&quot;과 &quot;횟수를 아느냐&quot;로 쓰임이 갈린다는 걸 알았다. 셋을 서로 바꿔 짜보니 관례의 이유가 보였다.&lt;/li&gt;
&lt;li&gt;이중 for문은 &quot;바깥은 줄, 안쪽은 그 줄의 개수&quot;로 역할을 나눠 보니 패턴 문제의 벽이 낮아졌다.&lt;/li&gt;
&lt;li&gt;break와 continue를 같은 예제에서 한 번에 써보니, &quot;루프 종료 vs 회차 건너뛰기&quot;의 차이가 코드로 또렷하게 남았다.&lt;/li&gt;
&lt;li&gt;풀어서 하나씩 작성하다보면, 보이지 않던 반복 구조가 보인다.&lt;/li&gt;
&lt;li&gt;C언어에서 bool타입이 없기 때문에 기호상수로 1을 TRUE로, 0을 FALSE로 선언하고 사용하면 편하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 걸음 더&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세 반복문은 사실 서로 바꿔 쓸 수 있다. 같은 동작을 while로도 for로도 짤 수 있다. 다만 &quot;횟수가 정해졌으면 for, 조건으로만 도는 거면 while, 최소 한 번은 돌아야 하면 do~while&quot;이라는 관례를 따르면 코드의 의도가 잘 드러난다.&lt;/li&gt;
&lt;li&gt;무한 루프(&lt;code&gt;while(1)&lt;/code&gt;)는 잘못 쓰면 프로그램이 멈추지 않는다. 반드시 안에 &lt;code&gt;break&lt;/code&gt; 조건이나 종료 경로가 있어야 한다. 의도적인 무한 루프(서버의 요청 대기 등)가 아니라면, 루프가 &quot;언젠가는 끝나는지&quot;를 늘 점검하는 습관이 필요하다.&lt;/li&gt;
&lt;li&gt;난수가 필요하면 &lt;code&gt;rand()&lt;/code&gt;를 쓰는데, 그냥 쓰면 실행할 때마다 같은 수열이 나온다. &lt;code&gt;srand(time(0))&lt;/code&gt;으로 현재 시간을 시드로 한 번 심어줘야 매번 다른 난수가 나온다. &quot;숫자 맞히기&quot; 같은 프로그램에서 이 시드 설정을 빠뜨리면 매번 같은 답이 나오는 함정에 빠진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>language/c</category>
      <category>break</category>
      <category>continue</category>
      <category>c언어</category>
      <category>c프로그래밍</category>
      <category>for문</category>
      <category>while문</category>
      <category>반복문</category>
      <category>이중for문</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/41</guid>
      <comments>https://rungch.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 3 Aug 2026 16:00:14 +0900</pubDate>
    </item>
    <item>
      <title>[인터넷활용] 02. CSS 스타일링</title>
      <link>https://rungch.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;HTML이 문서의 뼈대라면, CSS는 그 위에 입히는 옷이다. 색&amp;middot;크기&amp;middot;여백&amp;middot;배치 같은 시각적 표현을 모두 CSS가 담당한다. 오늘은 CSS를 적용하는 방법, 어떤 요소를 꾸밀지 고르는 선택자, 그리고 자주 쓰는 속성을 정리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CSS를 적용하는 세 가지 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS는 HTML에 세 가지 방식으로 연결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;작성 위치&lt;/th&gt;
&lt;th&gt;예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인라인&lt;/td&gt;
&lt;td&gt;태그 안 style 속성&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;p style=&quot;color:red&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내부&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;의 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 블록&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;style&amp;gt; p { color:red; } &amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외부&lt;/td&gt;
&lt;td&gt;별도 .css 파일 연결&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 문법은 &quot;어떤 요소를(선택자) 어떻게(속성: 값)&quot; 꾸밀지 적는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scss&quot;&gt;&lt;code&gt;선택자 { 속성: 값; 속성: 값; }
body { font-family: Arial; background-color: #f4f4f4; padding: 20px; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;border-left: 4px solid #4f8cff; background: #f4f8ff; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;실무에선 외부 CSS를 쓴다&lt;/b&gt;&lt;br /&gt;인라인은 간편하지만 태그마다 스타일이 흩어져 관리가 어렵다. 외부 파일로 분리하면 여러 HTML이 하나의 CSS를 공유할 수 있고, 디자인을 한 곳에서 통째로 바꿀 수 있다. &quot;구조(HTML)와 표현(CSS)의 분리&quot;가 웹 개발의 기본 원칙이다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;구조와 표현의 분리&quot;가, 그동안 배운 &quot;관심사 분리&quot;&amp;middot;&quot;계층 분리&quot;와 같은 발상이라는 게 보였다. 분야가 웹으로 바뀌어도 복잡도를 다루는 원칙은 같았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 선택자: 무엇을 꾸밀지 고르기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택자는 &quot;스타일을 적용할 대상&quot;을 지정하는 부분이다. 가장 핵심적인 개념이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택자&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;태그 선택자&lt;/td&gt;
&lt;td&gt;태그 이름으로 선택&lt;/td&gt;
&lt;td&gt;&lt;code&gt;h1 { }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;클래스 선택자&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.클래스&lt;/code&gt; (여러 요소에 재사용)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.box { }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;아이디 선택자&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#아이디&lt;/code&gt; (문서에 하나)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#title { }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;후손 선택자&lt;/td&gt;
&lt;td&gt;공백&amp;middot;&lt;code&gt;&amp;gt;&lt;/code&gt;로 포함 관계 지정&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ul li { }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;border-left: 4px solid #4f8cff; background: #f4f8ff; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;클래스와 아이디, 언제 무엇을&lt;/b&gt;&lt;br /&gt;클래스(&lt;code&gt;.&lt;/code&gt;)는 여러 요소에 같은 스타일을 반복 적용할 때 쓴다. 아이디(&lt;code&gt;#&lt;/code&gt;)는 페이지에서 단 하나뿐인 요소를 가리킬 때 쓴다. 원칙적으로 한 페이지에 같은 아이디는 한 번만 등장해야 한다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 자주 쓰는 속성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃과 표현에 자주 쓰는 속성들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;속성&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;color / background-color&lt;/td&gt;
&lt;td&gt;글자색 / 배경색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;font-size / font-family&lt;/td&gt;
&lt;td&gt;글꼴 크기 / 종류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;width / height&lt;/td&gt;
&lt;td&gt;너비 / 높이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;padding / margin / border&lt;/td&gt;
&lt;td&gt;안쪽 여백 / 바깥 여백 / 테두리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;display&lt;/td&gt;
&lt;td&gt;block / inline / none(숨김)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;visibility&lt;/td&gt;
&lt;td&gt;visible / hidden(공간 유지하며 숨김)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여백을 다룰 때 padding과 margin을 헷갈리기 쉬웠다. padding은 테두리 안쪽(내용과 테두리 사이) 여백이고, margin은 테두리 바깥쪽(다른 요소와의 사이) 여백이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;border-left: 4px solid #ff9800; background: #fff7ec; padding: 12px 16px; border-radius: 6px; margin: 16px 0;&quot;&gt;&lt;b&gt;display:none vs visibility:hidden&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;display:none&lt;/code&gt;: 요소를 아예 없앤다. 차지하던 &lt;b&gt;공간까지 사라진다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;visibility:hidden&lt;/code&gt;: 보이지만 않을 뿐, 차지하던 &lt;b&gt;공간은 그대로 유지된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
같아 보여도 레이아웃에 미치는 영향이 전혀 다르다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;display:none과 visibility:hidden을 직접 둘 다 줘보니, 하나는 공간이 사라지고 하나는 빈 자리가 남아서 차이가 확실했다. 눈으로 보고서야 둘을 구분하게 됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 느낀 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;구조(HTML)와 표현(CSS) 분리&quot;가 그동안 배운 관심사&amp;middot;계층 분리와 같은 원칙이라, 웹에서도 복잡도를 다루는 법이 같다는 게 느껴졌다.&lt;/li&gt;
&lt;li&gt;선택자가 &quot;무엇을 꾸밀지 고르는&quot; 핵심이라, CSS 공부의 절반이 선택자라는 말이 와닿았다. 클래스(재사용)와 아이디(유일) 구분이 특히 중요했다.&lt;/li&gt;
&lt;li&gt;display:none과 visibility:hidden을 직접 비교해보고서야 둘의 차이가 박혔다. 비슷해 보이는 속성은 눈으로 확인하는 게 최고였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 걸음 더&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 선택자가 같은 요소를 가리키면 누구의 스타일이 이길까? CSS는 &quot;명시도(specificity)&quot;라는 우선순위 규칙으로 정한다. 대략 인라인 &amp;gt; 아이디 &amp;gt; 클래스 &amp;gt; 태그 순으로 더 구체적인 선택자가 이긴다. 스타일이 &quot;안 먹히는&quot; 문제의 상당수가 이 우선순위 때문이라, 알아두면 디버깅이 쉬워진다.&lt;/li&gt;
&lt;li&gt;화면에 보이는 모든 요소는 사실 사각형 상자(box)로 다뤄진다. 내용 &amp;rarr; padding &amp;rarr; border &amp;rarr; margin이 겹겹이 둘러싼 이 구조를 박스 모델이라 한다. 레이아웃이 의도와 다르게 어긋날 때, 이 박스 모델로 여백 계산을 따져보면 원인이 보인다.&lt;/li&gt;
&lt;li&gt;요즘은 화면 크기가 PC부터 스마트폰까지 제각각이다. 그래서 화면 너비에 따라 스타일을 바꾸는 미디어 쿼리(&lt;code&gt;@media&lt;/code&gt;)로 반응형 디자인을 구현한다. 같은 HTML이 기기마다 다른 모습으로 적응하는 것이, 현대 웹 CSS의 핵심 과제다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>language/html</category>
      <category>CSS</category>
      <category>Frontend</category>
      <category>stylesheet</category>
      <category>반응형</category>
      <category>선택자</category>
      <category>웹디자인</category>
      <category>인터넷활용</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/37</guid>
      <comments>https://rungch.tistory.com/37#entry37comment</comments>
      <pubDate>Sun, 2 Aug 2026 12:30:37 +0900</pubDate>
    </item>
    <item>
      <title>MaxUCB - Maximum Upper Confidence Bound</title>
      <link>https://rungch.tistory.com/97</link>
      <description>&lt;p&gt;AI 모델을 학습시킬 때, 손실(loss)이 큰 데이터를 우선적으로 학습시키는 것은 상식적인 접근이다. 오답 노트처럼 틀린 문제를 다시 풀어보는 원리다. 하지만 오차가 가장 큰 데이터만 탐욕적으로(greedy) 고르면 부작용이 생긴다.&lt;/p&gt;
&lt;p&gt;첫째, 예전에 학습해둔 쉬운 영역의 데이터를 까먹는 망각 현상이 발생한다.&lt;/p&gt;
&lt;p&gt;둘째, 데이터 자체에 오류가 있거나 노이즈가 심한 경우, 오차가 영원히 줄어들지 않아 모델이 그 데이터에만 갇혀버린다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하려면 &amp;#39;현재 오차가 큰 영역(활용)&amp;#39;과 &amp;#39;최근에 확인하지 않은 영역(탐색)&amp;#39; 사이의 균형이 필요하다. 이를 수학적으로 제어하는 방법이 UCB(Upper Confidence Bound) 알고리즘이다.&lt;/p&gt;
&lt;h3&gt;Max UCB의 개념&lt;/h3&gt;
&lt;p&gt;전체 데이터를 여러 구역으로 나눈 뒤, 각 구역마다 UCB 점수를 매긴다. 그리고 가장 점수가 높은(Max) 구역을 다음 학습 대상으로 고른다.&lt;/p&gt;
&lt;p&gt;점수 공식의 뼈대는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UCB 점수 = 현재까지의 평균 오차 + 불확실성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;평균 오차가 크면 점수가 높아지고, 해당 구역을 오랫동안 학습하지 않아 불확실성이 커져도 점수가 높아진다. UCB의 핵심은 이 불확실성을 수치화한 두 번째 항에 있다.&lt;/p&gt;
&lt;h3&gt;불확실성 수식의 수학적 도출&lt;/h3&gt;
&lt;p&gt;UCB의 불확실성 항은 통계학의 호에프딩 부등식(Hoeffding&amp;#39;s Inequality)에서 도출된다.&lt;/p&gt;
&lt;p&gt;어떤 구역의 실제 평균 오차를 $\mu$, 우리가 지금까지 관측한 표본 평균 오차를 $\bar{x}$라고 하자. 호에프딩 부등식은 우리가 관측한 $\bar{x}$가 실제 평균 $\mu$보다 특정 한계치 $u$ 이상 차이 날 확률이 지수적으로 감소한다는 것을 보여준다.&lt;/p&gt;
&lt;p&gt;$$P(\mu \geq \bar{x} + u) \leq \exp(-2n_i u^2)$$&lt;/p&gt;
&lt;p&gt;여기서 $n_i$는 해당 구역을 탐색(학습)한 횟수다.&lt;/p&gt;
&lt;p&gt;우리는 이 오차가 한계치를 벗어날 확률을 아주 작은 값인 $p$로 통제하고 싶다. 부등식의 우변을 $p$라고 두면 다음과 같다.&lt;/p&gt;
&lt;p&gt;$$\exp(-2n_i u^2) = p$$&lt;/p&gt;
&lt;p&gt;이 식을 우리가 구하고자 하는 한계치(불확실성) $u$에 대해 정리한다. 양변에 자연로그를 취하면,&lt;/p&gt;
&lt;p&gt;$$-2n_i u^2 = \ln p$$&lt;/p&gt;
&lt;p&gt;$$u = \sqrt{\frac{-\ln p}{2n_i}}$$&lt;/p&gt;
&lt;p&gt;여기서 알고리즘이 진행될수록 안 가본 구역에 대한 탐색 확률을 조금씩 높여주기 위해, $p$를 고정된 상수가 아니라 전체 탐색 횟수 $n$에 반비례하는 값으로 설정한다. 통상적으로 $p = n^{-4}$ 같은 다항식 붕괴 형태로 설정한다. $p$ 자리에 $n^{-4}$를 대입하면 식은 이렇게 변한다.&lt;/p&gt;
&lt;p&gt;$$u = \sqrt{\frac{4 \ln n}{2n_i}}$$&lt;/p&gt;
&lt;p&gt;여기서 상수 부분($\sqrt{4/2}$)을 하나의 조절 변수 $c$로 묶어내면 우리가 아는 최종 UCB 불확실성 항이 나온다.&lt;/p&gt;
&lt;p&gt;$$u = c \sqrt{\frac{\ln n}{n_i}}$$&lt;/p&gt;
&lt;h3&gt;고손실 영역 샘플링에의 적용&lt;/h3&gt;
&lt;p&gt;최종적으로 도출된 UCB 공식을 고손실 구역 샘플링 모델에 적용하면 다음과 같은 형태가 된다.&lt;/p&gt;
&lt;p&gt;$$UCB_i = L_i + c \sqrt{\frac{\ln n}{n_i}}$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;$L_i$: $i$번째 구역의 현재 평균 손실&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;$c$: 탐색 가중치&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;$n$: 전체 학습 루프 횟수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;$n_i$: $i$번째 구역을 학습한 횟수&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;학습 루프가 돌 때마다 이 점수가 가장 큰 구역을 골라 학습을 진행한다.&lt;/p&gt;
&lt;p&gt;특정 구역의 손실($L_i$)이 노이즈 때문에 줄어들지 않고 계속 높게 나오더라도, 그 구역을 반복해서 고르다 보면 분모인 $n_i$가 계속 커져 결국 UCB 점수가 낮아진다. 반면 그동안 방치되었던 다른 구역들은 분자의 $\ln n$이 커짐에 따라 불확실성 점수가 서서히 올라가서 결국 선택을 받게 된다.&lt;/p&gt;
&lt;p&gt;결과적으로 데이터의 노이즈에 갇히는 문제를 막고, 구역 간 학습 빈도를 적절히 분배하여 망각 현상 없이 효율적으로 모델의 오차를 줄일 수 있다.&lt;/p&gt;</description>
      <category>study/memo</category>
      <category>MaxUCB</category>
      <author>렁치</author>
      <guid isPermaLink="true">https://rungch.tistory.com/97</guid>
      <comments>https://rungch.tistory.com/97#entry97comment</comments>
      <pubDate>Sat, 1 Aug 2026 22:47:14 +0900</pubDate>
    </item>
  </channel>
</rss>