<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>choitree_.log</title>
        <link>https://velog.io/</link>
        <description>개발자가 되기 위해서 공부중입니다 :ㅡ)</description>
        <lastBuildDate>Thu, 07 Nov 2024 05:32:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>choitree_.log</title>
            <url>https://images.velog.io/images/choitree_/profile/5f261f03-c77c-4292-a202-82b458840ae0/social.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. choitree_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/choitree_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[로드밸런싱된 서버에 배포하기]]></title>
            <link>https://velog.io/@choitree_/%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1%EB%90%9C-%EC%84%9C%EB%B2%84%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choitree_/%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1%EB%90%9C-%EC%84%9C%EB%B2%84%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 07 Nov 2024 05:32:49 GMT</pubDate>
            <description><![CDATA[<p>내가 작업하는 프로젝트의 구조는, 위챗 내부에서 회사의 독자적인 기술의 라벨을 스캔하면 프로모션 백엔드로 프로모션 여부를 검증한 후 프로모션이라면 프로모션 로직에서 response하는 값으로 웹뷰의 내용이 보여지고, 프로모션 라벨이 아닌 일반 라벨이라면 일반 라벨의 웹뷰 내의 랜딩페이지가 보여지는 방식으로 동작하고 있다.</p>
<p>그리고 작업을 한 내용은 프로모션 검증하는 내용을 위챗 내부에 추가해야 해서
프로모션 백엔드를 호출하는 post method를 추가해야 했기에,
프로모션 백엔드를 위챗에 추가된 도메인 내부에 배포해야 했다!</p>
<p>단순히 ec2에 배포하면 되겠거니 하고, docker-compose로 배포한 후 
postman으로 헬스체크를 해보니 Error: connect ECONNREFUSED 71.xxx.xx.x:8080 라는 응답을 받았다. 근데 내가 배포한 ec2 ip는 71로 시작하지 않는걸????????????????</p>
<p>AWS에 접속해서 확인하니, 로드밸런서와 타겟 그룹이 보인다🙃🙃🙃</p>
<hr>
<h3 id="로드-밸런서-alb---application-load-balancer">로드 밸런서 (ALB - Application Load Balancer)</h3>
<blockquote>
<p>로드 밸런서는 여러 서버(인스턴스) 간에 네트워크 트래픽을 자동으로 분산하여 서버의 부하를 분산하고 애플리케이션의 가용성과 성능을 높이는 장치 또는 서비스입니다. 주로 사용자의 요청을 서버 여러 대에 고르게 분산해 주며, 단일 서버에 과부하가 걸리는 상황을 방지해줍니다.</p>
</blockquote>
<h3 id="역할">역할</h3>
<ul>
<li>트래픽 분산: 여러 서버로 요청을 분산하여 각 서버의 부하를 줄이고 시스템의 안정성을 유지합니다.</li>
<li>고가용성 제공: 서버 중 하나가 고장 나더라도 다른 서버가 트래픽을 처리하여 서비스가 중단되지 않도록 합니다.
장애 감지 및 헬스 체크: 로드 밸런서는 각 서버의 상태를 정기적으로 확인(헬스 체크)하여, 비정상적인 서버로는 트래픽을 보내지 않습니다.</li>
<li>보안 강화: HTTPS를 통해 암호화된 트래픽을 처리하여 클라이언트와 서버 간의 보안을 강화할 수 있습니다.</li>
</ul>
<h4 id="로드-밸런서의-종류">로드 밸런서의 종류</h4>
<p>AWS에서는 여러 유형의 로드 밸런서를 제공합니다:</p>
<h5 id="1-application-load-balancer-alb">1. Application Load Balancer (ALB)</h5>
<p>HTTP/HTTPS 트래픽을 분산하는 데 최적화되어 있습니다.
경로 기반 또는 호스트 기반 라우팅이 가능하며, URL의 경로나 도메인 이름에 따라 트래픽을 특정 서버로 라우팅할 수 있습니다.
주로 웹 애플리케이션이나 API 트래픽을 처리할 때 사용됩니다.</p>
<h5 id="2-network-load-balancer-nlb">2. Network Load Balancer (NLB)</h5>
<p>TCP/UDP 트래픽을 빠르게 처리하며, 초당 수백만 개의 요청을 처리할 수 있는 고성능 로드 밸런서입니다.
주로 초저지연과 고성능이 필요한 트래픽에 적합합니다.</p>
<h5 id="3-classic-load-balancer-clb">3. Classic Load Balancer (CLB)</h5>
<p>과거에 사용되던 기본적인 로드 밸런서입니다. 현재는 새로운 프로젝트에 ALB 또는 NLB가 권장됩니다.
애플리케이션 및 네트워크 계층에서의 간단한 로드 밸런싱이 가능합니다.</p>
<h4 id="로드-밸런서의-구성-요소">로드 밸런서의 구성 요소</h4>
<h5 id="1-리스너-listener">1. 리스너 (Listener)</h5>
<p>리스너는 로드 밸런서가 요청을 수신하는 프로토콜과 포트를 정의합니다. 예를 들어, HTTP:80, HTTPS:443과 같이 특정 포트와 프로토콜을 설정하여 클라이언트의 요청을 받아들입니다.</p>
<h5 id="2-대상-그룹-target-group">2. 대상 그룹 (Target Group)</h5>
<p>로드 밸런서가 트래픽을 전달할 서버 또는 서비스 그룹을 정의합니다. 대상 그룹에는 EC2 인스턴스, Lambda 함수, 컨테이너 등 다양한 대상이 포함될 수 있습니다.
헬스 체크 설정을 통해 각 서버의 상태를 모니터링하고, 비정상 서버로는 트래픽을 전달하지 않도록 합니다.</p>
<h5 id="3-헬스-체크-health-check">3. 헬스 체크 (Health Check)</h5>
<p>로드 밸런서는 대상 서버의 상태를 주기적으로 확인하여, 정상적인 서버에만 트래픽을 분산합니다. 헬스 체크가 실패하면 해당 서버는 Unhealthy 상태로 표시되어 트래픽이 전달되지 않습니다.</p>
<h4 id="로드-밸런서를-설정하려면">로드 밸런서를 설정하려면?</h4>
<ol>
<li><p>로드 밸런서 생성: AWS Management Console에서 Application, Network, 또는 Classic Load Balancer를 생성합니다.</p>
</li>
<li><p>리스너 설정: 로드 밸런서가 사용할 프로토콜과 포트를 지정하고 리스너를 설정합니다.
대상 그룹 생성 및 등록: 트래픽을 분산할 서버(대상)를 지정하여 대상 그룹을 생성하고 로드 밸런서와 연결합니다.</p>
</li>
<li><p>헬스 체크 설정: 서버가 정상 상태인지 확인하기 위해 헬스 체크 경로와 주기를 설정합니다.</p>
</li>
<li><p>예시: ALB로 웹 애플리케이션 트래픽 분산</p>
<pre><code>클라이언트가 ALB의 도메인(예: example.com)으로 요청을 보냅니다.
HTTP:80 리스너가 요청을 수신하고, HTTPS로 리다이렉션합니다.
HTTPS:443 리스너가 요청을 처리하며, /api/*로 들어오는 트래픽은 Spring Boot 서비스로, 
나머지 트래픽은 Tomcat 서버로 라우팅하는 규칙에 따라 요청을 분배합니다.
로드 밸런서의 헬스 체크가 서버 상태를 주기적으로 확인하여, 
Healthy 상태인 서버에만 트래픽을 전달합니다.</code></pre><h3 id="리스너">리스너</h3>
<blockquote>
<p><strong>로드 밸런서에서 리스너(Listener)</strong>는 클라이언트 요청을 수신하고, 이를 설정된 대상 그룹(Target Group)으로 전달하는 역할을 하는 구성 요소입니다. 리스너는 특정 프로토콜과 포트에 대한 트래픽을 관리하며, 이를 통해 로드 밸런서가 어떻게 요청을 처리할지 결정합니다.</p>
</blockquote>
</li>
</ol>
<h4 id="역할-1">역할</h4>
<ol>
<li>요청 수신: 리스너는 지정된 프로토콜(예: HTTP, HTTPS)과 포트(예: 80, 443)에서 들어오는 요청을 수신합니다.</li>
<li>규칙 평가: 리스너에는 여러 개의 규칙을 설정할 수 있습니다. 각 규칙은 URL 경로, 요청 헤더, IP 주소 등 특정 조건에 따라 요청을 필터링하여 각기 다른 대상 그룹(Target Group)으로 트래픽을 라우팅합니다.</li>
<li>대상 그룹으로 트래픽 포워딩: 규칙에 따라 요청을 특정 대상 그룹으로 포워딩합니다. 대상 그룹은 로드 밸런서가 분산해야 할 실제 서버(예: EC2 인스턴스) 또는 컨테이너 그룹입니다.</li>
<li>SSL 종료: HTTPS 리스너의 경우, SSL 인증서를 사용하여 SSL/TLS 연결을 종료하고 클라이언트와 보안 통신을 설정합니다.</li>
</ol>
<h4 id="리스너-구성-요소">리스너 구성 요소</h4>
<ul>
<li>프로토콜: 리스너가 수신할 요청의 프로토콜 (예: HTTP 또는 HTTPS).</li>
<li>포트: 리스너가 요청을 수신할 포트 (예: 80, 443).</li>
<li>규칙: 트래픽 라우팅 조건을 설정할 수 있는 규칙입니다. 예를 들어, /api/*로 시작하는 모든 요청을 특정 대상 그룹으로 보내도록 규칙을 설정할 수 있습니다.</li>
<li>대상 그룹: 리스너가 요청을 전달할 대상 그룹을 지정합니다.<h4 id="리스너-설정-방법">리스너 설정 방법</h4>
<h5 id="1-로드-밸런서-생성-시-리스너-설정">1. 로드 밸런서 생성 시 리스너 설정</h5>
</li>
<li>로드 밸런서를 처음 생성할 때 리스너를 설정할 수 있습니다. 이때, 기본 프로토콜과 포트를 지정합니다.</li>
</ul>
<h5 id="2-기존-로드-밸런서에-리스너-추가">2. 기존 로드 밸런서에 리스너 추가</h5>
<ul>
<li>AWS Management Console에서 로드 밸런서의 Listeners 탭에서 리스너를 추가할 수 있습니다.<h5 id="3-필요한-구성-설정">3. 필요한 구성 설정</h5>
</li>
<li>프로토콜 및 포트 설정: 트래픽을 처리할 프로토콜 (HTTP, HTTPS)과 포트를 설정합니다.</li>
<li>규칙 추가: 경로 기반 라우팅을 설정하려면 Add rule 기능을 사용하여 요청 경로에 따라 트래픽을 라우팅하는 규칙을 추가합니다.</li>
<li>SSL 인증서 (HTTPS의 경우): HTTPS 리스너를 사용할 때는 SSL 인증서를 설정해야 합니다. 이 설정을 통해 클라이언트와 로드 밸런서 간의 트래픽을 암호화할 수 있습니다.</li>
</ul>
<h3 id="타겟-그룹">타겟 그룹</h3>
<blockquote>
<p><strong>타겟 그룹(Target Group)</strong>은 로드 밸런서가 트래픽을 전달하는 서버 그룹입니다. 로드 밸런서는 타겟 그룹을 통해 여러 서버(예: EC2 인스턴스)로 트래픽을 분산하고, 헬스 체크를 통해 서버 상태를 관리합니다. 각 타겟 그룹은 로드 밸런서가 요청을 분산할 서버나 서비스들을 정의하며, 특정 조건에 맞춰 선택적으로 요청을 전달할 수 있습니다.</p>
</blockquote>
<h4 id="타겟-그룹의-역할과-기능">타겟 그룹의 역할과 기능</h4>
<h5 id="1-트래픽-분산-대상-지정">1. 트래픽 분산 대상 지정</h5>
<ul>
<li>타겟 그룹은 로드 밸런서가 트래픽을 보낼 서버(인스턴스, 컨테이너, IP 등)를 지정합니다.</li>
<li>각 타겟 그룹에는 하나 이상의 서버가 포함될 수 있으며, 로드 밸런서는 타겟 그룹의 서버들로 요청을 자동으로 분산합니다.</li>
</ul>
<h5 id="2-헬스-체크health-check-관리">2. 헬스 체크(Health Check) 관리</h5>
<ul>
<li>타겟 그룹 내 서버의 상태를 주기적으로 확인하여, 정상(Healthy) 상태가 아닌 서버로는 트래픽을 보내지 않습니다.</li>
<li>헬스 체크 설정(경로, 주기, 응답 시간)을 통해 서버가 정상인지 여부를 판단합니다.</li>
</ul>
<h5 id="3-라우팅-규칙과-연계">3. 라우팅 규칙과 연계</h5>
<ul>
<li>로드 밸런서의 리스너에서 조건(경로, 헤더, 도메인 등)에 따라 요청을 특정 타겟 그룹으로 라우팅할 수 있습니다. 예를 들어, /api/* 경로는 API 서버로, /web/* 경로는 웹 서버로 분산할 수 있습니다.</li>
</ul>
<h5 id="4-특정-애플리케이션-설정">4. 특정 애플리케이션 설정</h5>
<ul>
<li>타겟 그룹을 통해 로드 밸런서가 다양한 서비스(예: 마이크로서비스 아키텍처에서 API 서버, 웹 서버 등)로 요청을 분산할 수 있습니다. 각 서비스마다 별도의 타겟 그룹을 생성하고 라우팅 규칙을 설정해주면 됩니다.</li>
</ul>
<h4 id="타겟-그룹의-구성-요소">타겟 그룹의 구성 요소</h4>
<h5 id="1-타겟target">1. 타겟(Target)</h5>
<ul>
<li>트래픽을 전달할 서버나 인스턴스입니다. 타겟은 EC2 인스턴스, Lambda 함수, IP 주소, 또는 ECS 컨테이너 등으로 지정할 수 있습니다.</li>
</ul>
<h5 id="2-타겟-그룹-유형target-type">2. 타겟 그룹 유형(Target Type)</h5>
<ul>
<li>Instance: EC2 인스턴스를 타겟으로 지정.</li>
<li>IP: 특정 IP 주소(프라이빗 IP 또는 퍼블릭 IP)를 타겟으로 지정.</li>
<li>Lambda: Lambda 함수를 타겟으로 지정하여, 요청을 Lambda 함수로 분산.</li>
</ul>
<h5 id="3-포트와-프로토콜">3. 포트와 프로토콜</h5>
<ul>
<li>타겟 그룹 내의 타겟이 요청을 수신할 포트와 프로토콜을 정의합니다. 예를 들어, HTTP 프로토콜과 포트 8080을 사용하여 웹 서버로 요청을 전달할 수 있습니다.</li>
</ul>
<h5 id="4헬스-체크-설정">4.헬스 체크 설정</h5>
<ul>
<li>헬스 체크 프로토콜 및 경로: 서버 상태를 확인하기 위해 HTTP, HTTPS와 같은 프로토콜과 경로(예: /health 또는 /status)를 지정합니다.</li>
<li>헬스 체크 주기 및 타임아웃: 헬스 체크 요청을 보내는 주기와 응답 대기 시간을 설정합니다. 서버가 헬스 체크를 통과하지 못하면 Unhealthy로 표시됩니다.</li>
<li>정상/비정상 임계값: 특정 횟수 이상의 헬스 체크를 통과해야 Healthy 상태로 표시되며, 연속 실패 시 Unhealthy로 전환됩니다.</li>
</ul>
<h4 id="타겟-그룹의-작동-예시">타겟 그룹의 작동 예시</h4>
<h5 id="1-http-요청-분산">1. HTTP 요청 분산</h5>
<ul>
<li><p>ALB가 HTTP 요청을 수신한 후, 요청 경로에 따라 특정 타겟 그룹으로 분산합니다.</p>
</li>
<li><p>예를 들어, /api/* 경로는 타겟 그룹 API-Backend로, /web/* 경로는 타겟 그룹 Web-Frontend로 라우팅할 수 있습니다.</p>
<h5 id="2-헬스-체크에-따른-트래픽-분산">2. 헬스 체크에 따른 트래픽 분산</h5>
</li>
<li><p>타겟 그룹은 각 타겟의 헬스 체크를 통해 상태를 주기적으로 확인하며, Healthy 상태의 타겟에게만 트래픽을 전달합니다.</p>
</li>
<li><p>만약 서버가 Unhealthy 상태로 표시되면 로드 밸런서는 자동으로 다른 타겟에 트래픽을 전달합니다.</p>
<h4 id="타겟-그룹-설정-방법">타겟 그룹 설정 방법</h4>
<h5 id="1-로드-밸런서와-연결할-타겟-그룹-생성">1. 로드 밸런서와 연결할 타겟 그룹 생성</h5>
</li>
<li><p>AWS Management Console에서 EC2 서비스 &gt; Target Groups 메뉴에서 새 타겟 그룹을 생성합니다.</p>
</li>
<li><p>타겟 유형, 프로토콜 및 포트 등을 설정합니다.</p>
</li>
</ul>
<h5 id="2-타겟-등록">2. 타겟 등록</h5>
<ul>
<li>트래픽을 수신할 타겟을 선택하여 타겟 그룹에 등록합니다. 예를 들어, EC2 인스턴스, IP 주소 또는 Lambda 함수를 추가할 수 있습니다.</li>
</ul>
<h5 id="3-헬스-체크-설정">3. 헬스 체크 설정</h5>
<ul>
<li>헬스 체크 경로, 프로토콜, 주기, 타임아웃 등의 설정을 지정하여 서버 상태를 모니터링할 수 있습니다.</li>
</ul>
<h5 id="4-로드-밸런서에-타겟-그룹-연결">4. 로드 밸런서에 타겟 그룹 연결</h5>
<ul>
<li>로드 밸런서의 리스너 규칙에 따라 트래픽이 특정 타겟 그룹으로 전달되도록 설정합니다. 예를 들어, HTTPS 리스너에서 /api/* 요청이 API 서버 타겟 그룹으로 라우팅되도록 규칙을 추가할 수 있습니다.</li>
</ul>
<hr>
<h3 id="실제-구성-예시">실제 구성 예시</h3>
<pre><code>클라이언트 요청
    ↓
도메인 (wechat.xxx.cn)
    ↓
ALB (HTTPS:443)
    ↓
리스너 규칙에 따라 분기
    ├── /api/promotion/* → WECHAT-PROMOTION-8080-BACKEND (Spring Boot)
    └── 그 외 → WECHAT-8090-TOMCAT (Tomcat)</code></pre><h3 id="보안-그룹">보안 그룹</h3>
<ul>
<li><p>ALB의 보안 그룹</p>
<ul>
<li>인바운드: HTTPS(443) 허용</li>
<li>아웃바운드: 타겟 그룹의 포트(8080, 8090) 허용</li>
</ul>
</li>
<li><p>EC2의 보안 그룹</p>
<ul>
<li>인바운드: ALB로부터의 트래픽 허용</li>
<li>포트: 애플리케이션 포트(8080, 8090)</li>
</ul>
</li>
</ul>
<h3 id="헬스-체크">헬스 체크</h3>
<ul>
<li>각 타겟 그룹별로 설정</li>
<li>주기적으로 지정된 엔드포인트로 요청</li>
<li>성공/실패에 따라 타겟의 상태 결정</li>
<li>비정상 타겟은 자동으로 트래픽에서 제외</li>
</ul>
<hr>
<h3 id="실제-서버-적용">실제 서버 적용</h3>
<p><img src="https://velog.velcdn.com/images/choitree_/post/ad7e2b5d-c13d-4e41-8a20-94c1c4eca09b/image.png" alt=""></p>
<ul>
<li>위의 도식화처럼, 로드밸런서에는 2개의 리스너가 존재</li>
<li>HTTP:80은 HTTPS:443으로 리다이렉트해주는 기능을 하는 리스너</li>
<li>HTTPS:443 리스너는 다시 2개의 규칙이 존재<ul>
<li>기존 규칙은 52.xx.xxx.x:8090로  포워딩되고 있음</li>
<li>여기에 새로운 규칙을 추가하여, /api/promotion/* 의 path 패턴의 경우 52.xx.xxx.x:8080 으로 포워딩 되도록 설정 ( 우선순위를 10으로 둬서 default보다 우선적으로 처리)</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[nglok 설정]]></title>
            <link>https://velog.io/@choitree_/nglok-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@choitree_/nglok-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 11 Oct 2024 08:56:37 GMT</pubDate>
            <description><![CDATA[<p>위챗 미니 프로그램에 개발을 하는데, 위챗은 https가 아니면 접속이 불가능하다.
<del>디버깅 모드 내에 설정을 하면 https 접속이 가능하다고 하지만, 그거는 ide 내부의 시뮬레이터에 한정된다.</del>
카메라로 촬영을 하는 로직을 추가하는데, 시뮬레이터로는 도저히 실행이 불가능하다!!</p>
<p>로컬호스트를 https를 붙여서 접속할 방법이 없을까 찾아보다가, localtunnel 및 다른 방법도 시도했지만, ngrok이 가장 쉬운 방법이다!</p>
<blockquote>
<p>ngrok은 로컬 개발 환경의 서버를 안전하게 인터넷에 노출시키는 터널링 도구다. ngrok은 로컬호스트에서 실행 중인 웹 서버에 대해 보안 터널을 생성하고, 공개적으로 접근 가능한 URL을 제공한다. 이를 통해 개발자는 방화벽 제한이나 NAT(Network Address Translation) 문제 없이 외부에서 로컬 서버에 접근할 수 있게 되어, 웹훅 테스트, 모바일 앱 개발, 원격 데모 등 다양한 개발 시나리오를 용이하게 한다.</p>
</blockquote>
<h3 id="사용방법">사용방법</h3>
<ol>
<li><a href="https://ngrok.com/download">ngrok 사이트</a>에 접속하여, os에 맞는 다운로드를 실행한다.</li>
<li>다운로드 받은 ngrok 프로그램을 실행한다.</li>
<li>특정 포트를 터널링한다.<pre><code> ngrok http 80</code></pre></li>
</ol>
<h4 id="2개-포트는-안돼">2개 포트는 안돼?</h4>
<p>공식 사이트에서는 분명!! pricing에 애매모호하지만, 2개의 포트에 대해 터널링이 가능하다고 하지만, 프로그램을 2개 실행해서 명령어를 입력하는 방법도, 한 번에 2개의 명령어를 입력하는 방법도 되지 않는다.
그치만 나는 총 3개의 프로젝트를 함께 변경중으로, 위챗에서는 2개의 프로젝트를 로컬호스트로 붙어야 한다! 약 3일간의 삽질을 하다가(3일 내내 한 것은 아니고 시간이 날 때마다,,,) 방법을 찾았다. 그건 바로 ngrok의 실행 파일 내부에 포워딩할 포트를 지정하고, 실행하는 것!</p>
<h3 id="무료로-2개-포트를-포워딩하는-방법">무료로 2개 포트를 포워딩하는 방법</h3>
<ol>
<li>ngrok 프로그램을 실행한다.</li>
<li>ngrok config edit 명령어를 입력한다.
설정 파일을 수정하는 명령어로, 해당 파일은 C:\Users\AppData\Local\ngrok 경로의 ngrok.yml이다.</li>
<li>ngrok.yml 파일 내부에 터널링할 포트 정보를 입력한다.
<del>authtoken값도 같이 입력해두면 편리하다.</del><pre><code>version: &quot;2&quot;
authtoken: 내 authToken값
</code></pre></li>
</ol>
<p>tunnels:
  tunnel1:
    addr: 8082
    proto: http
  tunnel2:
    addr: 8080
    proto: http</p>
<pre><code>4. ngrok 프로그램에 설정 파일을 실행한다는 명령어 입력</code></pre><p>ngrok start --all</p>
<pre><code>
무료로 사용하면 ngrok가 꺼질 때마다 또는 24시간 단위로 url 주소가 변경되는 불편함이 있지만 돈을 내지 않는다는 장점이 있다!

---
#### 번외
 localtunnel은 더 자주 url이 변경되는 불편함이 있어, ngrok를 사용했다.

```bash
ngrok는 1개 서버만 접속 가능해서, localtunnel 사용
1. node.js 설치
2. npm install -g localtunnel

lt --port 8080 --subdomain backend
your url is: https://itchy-monkey-95.loca.lt

lt --port 8082 --subdomain webview
your url is: https://webview.loca.lt</code></pre><p><del>ssh 인증서를 pc에 발급받고 이를 설정하는 방법도 있는데, 제일 복잡하고
코드에 내용을 추가하는 부분이 발생해서 포기했다.</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트는 1개, 백엔드는 2개?]]></title>
            <link>https://velog.io/@choitree_/%ED%94%84%EB%A1%A0%ED%8A%B8%EB%8A%94-1%EA%B0%9C-%EB%B0%B1%EC%97%94%EB%93%9C%EB%8A%94-2%EA%B0%9C</link>
            <guid>https://velog.io/@choitree_/%ED%94%84%EB%A1%A0%ED%8A%B8%EB%8A%94-1%EA%B0%9C-%EB%B0%B1%EC%97%94%EB%93%9C%EB%8A%94-2%EA%B0%9C</guid>
            <pubDate>Fri, 11 Oct 2024 08:24:44 GMT</pubDate>
            <description><![CDATA[<p>회사 내 서비스의 어드민 프로젝트가 있는데, 어드민도 서비스 단위로 생성되있지 않고 
2개 서비스를 한개의 프로젝트로 개발해놓았다.</p>
<p>그래서 추가 개발이 있을 때, 사이드 이펙트도 터지고 불필요한 검수도 하게 되고
여러모로 번거로움이 있어 프로젝트를 분리하게 되었다.</p>
<p>정말 간단한 어드민 페이지로, 프론트는 그대로 1개
백엔드는 서비스 단위로 2개로 분리했다. 어떻게?</p>
<hr>
<h2 id="1-백엔드-분리">1. 백엔드 분리</h2>
<ol>
<li>jwt 로 로그인 검증을 하는 프로젝트로 security에 대한 로직은 두개 프로젝트에 모두 존재하고, 
대신 로그인에 관련된 컨트롤러는 기존 프로젝트에 두었다.</li>
<li>그리고 충돌되지 않도록 새로운 미사용 포트를 잡아주면 끝!</li>
</ol>
<h2 id="2-프론트-연결">2. 프론트 연결</h2>
<p>프론트는 vuejs를 사용중이고, 기존에는 서버로 프록시하는 주소가 다 하나의 port로 요청되고, 
url 주소의 첫 경로가 /api로 시작되었기 때문에 1개의 프록시만 설정하면 되었는데
포트가 분리되면서 이에 대한 정보를 추가해야 한다.</p>
<pre><code>//vite.config.ts
...
server: {
    proxy: {
    //1번 서비스
      &quot;/api/admin/aaa&quot;: {
        target: &#39;http://localhost:8050&#39;, 
        changeOrigin: true,
        secure: false,
        // rewrite: (path) =&gt; path.replace(/^\/api/, &#39;&#39;)
      },


      //2번 서비스
      &quot;/api/admin/bbb&quot;: {
        target: &#39;http://localhost:8060&#39;,
        changeOrigin: true,
        secure: false,
        rewrite: (path) =&gt; path.replace(/^\/api\/admin\/bbb/, &#39;/api/admin/bbb&#39;),
      }
    }
  },
  ...</code></pre><p><del>rewrite는 생략해도 되는 내용</del>
생각보다 쉽당!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swagger API in Spring Boot]]></title>
            <link>https://velog.io/@choitree_/Swagger-API-in-Spring-Boot</link>
            <guid>https://velog.io/@choitree_/Swagger-API-in-Spring-Boot</guid>
            <pubDate>Wed, 02 Oct 2024 01:29:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>개발을 하다보면, 프로젝트에 대한 문서가 필요한 경우가 있다.
 프론트, 앱 개발자와 소통하는 목적일 수도 있고, 
어떤 API가 구현되있는지 확인하는 목적일 수도 있다.</p>
</blockquote>
<p>Spring Boot에서 가장 흔하게 사용하는 DOCS는 Spring Docs와 Swagger가 있다.
Spring Docs는 Controller Test를 만들어야 Docs가 생성되기 때문에 1석 2조로 활용할 수 있지만, 그만큼 개발 비용이 많이 든다.
Swagger API 는 간단하게 만들 수 있지만, Controller 메소드 상단에 annotation으로 추가해서, 실제 코드의 가독성을 떨어트리는 단점이 있다.</p>
<p><strong>만약, Controller에 작성하지 않고 간단하게 Swagger API를 사용할 수 있다면?!!</strong></p>
<p>Swagger를 활용해서 example로 샘플 데이터를 보여주는 경우가 있는데
샘플 데이터가 너무 길어지면, swagger에 대한 설정만 10줄은 쉽게 넘기는 경우가 있었다.
이런 경우, example를 yml 또는 properties 파일로 분리하고 해당 내용을 호출해서 사용할 수 있을까?하고 확인해보았다.</p>
<ol>
<li>example.yml 파일 생성
spring boot에서는 application.yml 파일 이외의 값은 import되지 않아서
swagger 내용에만 활용할 yml 파일을 만든다면, application.yml에 해당 파일을import한다는 설정이 필요하다.<pre><code>//application.yml
</code></pre></li>
</ol>
<p>spring:
  config:
    import: classpath:docs/example.yml, classpath:docs/totalDashboard.yml //swagger에 example이 될만한 코드를 import</p>
<h2 id="스웨거-설정spring-doc">스웨거 설정(spring doc)</h2>
<p>springdoc:
  swagger-ui:
    path: /api/promotion/swagger-ui
    defaultModelsExpandDepth: -1  # 모델(Schemas) 영역을 최소화 / -1은 아예 화면에서 사라짐, 0은 최소화</p>
<pre><code>classpath까지는 src/resources의 경로고, 그 하위에 디렉토리 및 파일 위치를 명시하면 된다.만약 추가하는 파일이 2개 이상이라면, 콤마(,)로 이어서 작성하면 된다.

swagger api는 api영역과 schemas(dto)영역이 존재하는데
schemas는 결국 api 하위에 포함된 내용으로 해당 내용을 최소화하고 싶다면, 위와 같은 설정을 추가하면 된다.


2. example.yml 파일 
</code></pre><p>landingPageThemeByPromotionType:
        response:
                example: |
                        {
                          &quot;statusCode&quot;: 200,
                          &quot;status&quot;: &quot;정상&quot;,
                          &quot;data&quot;: [
                            {
                              &quot;idx&quot;: 8,
                              &quot;themeName&quot;: &quot;캐릭터&quot;,
                              &quot;sampleImagePath&quot;: &quot;S3 버킷 주소&quot;
                            },
                            ...
                          ]
                        }</p>
<pre><code>S3 버킷 주소는 회사명이 담겨서 string 값으로 변경하고, 이런 식으로 값을 추가하면 된다.
| 하위의 내용은 알아서 줄바꿈이 yml 파일과 동일하게 적용되서, swagger api에서 보인다.

3. Controller 내 example에 추가 (실패한 방법)</code></pre><p>@Value(&quot;${landingPageThemeByPromotionType.response.example}&quot;)
private String themeResponseExample;</p>
<pre><code>@Operation(summary = &quot;프로모션 타입별 랜딩 페이지 테마 조회 API&quot;, description = &quot;프로모션 등록 시, 업체에 해당하는 테마가 보이게 하는 조회 API&quot;)
@ApiResponses(value = {
        @ApiResponse(responseCode = &quot;200&quot;,
            content = {@Content(schema = @Schema(implementation = ResponseThemeDto.class),
            mediaType = &quot;application/json&quot;,
            examples = @ExampleObject(value = themeResponseExample))})
})</code></pre><pre><code>@Value 어노테이션은 Java 상수나 필드에서만 사용 가능해서, @ExampleObject의 value 속성에 직접 선언하는 방식은 지원하지 않는다. 
그래서 @Value로 선언하고, 이를 다시 호출하는 방식으로 사용해야 한다.

**그런데, Attribute value must be constant 라고 컴파일이 되지 않는다!!**
@Value는 어노테이션의 속성으로 사용할 수 없어서, 선언 후 호출하더라도 결국 에러가 발생하는 것이다.
이 문제를 해결하려면 미리 데이터를 읽어와서 주입한 후에 사용하는 방법을 사용해야 한다고 한다.</code></pre><p>import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;</p>
<p>@Configuration
public class SwaggerConfig {</p>
<pre><code>@Value(&quot;classpath:docs/example.yml&quot;)
private String themeResponseExample;

@Bean
public OpenApiCustomizer customizeOpenApi() {
    return openApi -&gt; {
        openApi.getPaths().get(&quot;/theme&quot;).getGet().getResponses().get(&quot;200&quot;)
            .getContent().get(&quot;application/json&quot;).getExamples().put(&quot;example-1&quot;,
            new io.swagger.v3.oas.models.examples.Example().value(themeResponseExample));
    };
}</code></pre><p>}</p>
<pre><code>이런 식으로 SwaggerConfig 클래스에 동적으로 데이터를 주입하는 방법을 사용해야 한다고 한다.
&gt; Swagger API를 Controller와 SwaggerConfig 2개 클래스에서 관리한다면, 유지보수의 번거로움도 있고 가독성이 역시 좋지 않다고 생각이 들었다.
**그러면 SwaggerConfig에 합쳐서 작성하는 방법은 없을까?**

4. SwaggerConfig 클래스에 api 명세에 대한 모든 내용을 추가하자!</code></pre><p>import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;</p>
<p>import java.io.IOException;
import java.util.Arrays;
import java.util.List;</p>
<p>@Configuration
@PropertySource(value = &quot;classpath:docs/example.yml&quot;, encoding = &quot;UTF-8&quot;)  // UTF-8로 인코딩 설정
public class SwaggerConfig {
    @Value(&quot;${server.name}&quot;)
    private String serverName;</p>
<pre><code>@Value(&quot;${landingPageThemeByPromotionType.response.example}&quot;)
private String themeResponseExample;

@Bean
public OpenAPI customOpenAPI() {
    return new OpenAPI()
            .info(new Info()
                    .title(&quot;promotion API document - &quot; + serverName)
                    .description(&quot;[&quot; + serverName + &quot;] 프로모션 백엔드 api 문서 - &quot;)
                    .version(&quot;1.0.0&quot;))
            .components(new Components()
                    .addSecuritySchemes(&quot;bearerAuth&quot;,
                            new SecurityScheme()
                                    .type(SecurityScheme.Type.HTTP)
                                    .scheme(&quot;bearer&quot;)
                                    .bearerFormat(&quot;JWT&quot;)))
            .addSecurityItem(new SecurityRequirement().addList(&quot;bearerAuth&quot;));
}


@Bean
public OpenApiCustomizer customizeLandingPageThemeApi() throws IOException {
    // JSON 문자열을 객체로 변환
    ObjectMapper objectMapper = new ObjectMapper();
    Object themeResponseExampleObject = objectMapper.readValue(themeResponseExample, Object.class);

    return openApi -&gt; {
        openApi.path(&quot;/api/promotion/landingPage/theme&quot;, new PathItem()
                .get(new Operation()
                        .summary(&quot;프로모션 타입별 랜딩 페이지 테마 조회 API&quot;)
                        .description(&quot;프로모션 등록 시, 업체에 해당하는 테마가 보이게 하는 조회 API&quot;)
                        .addTagsItem(&quot;LandingImageController&quot;) // Tag 추가
                        .responses(new ApiResponses()
                                .addApiResponse(&quot;200&quot;, new ApiResponse()
                                        .description(&quot;정상 응답&quot;)
                                        .content(new Content()
                                                .addMediaType(&quot;application/json&quot;, new MediaType()
                                                        .schema(new Schema().$ref(&quot;#/components/schemas/ResponseThemeDto&quot;))
                                                        .example(new Example().value(themeResponseExampleObject)))))
                        )
                        .addParametersItem(new Parameter()
                                .name(&quot;customerCd&quot;)
                                .in(&quot;query&quot;) //파라미터가 어디에 포함되는지 나타냄
                                .required(true)
                                .schema(new Schema&lt;Integer&gt;().type(&quot;integer&quot;))
                                .description(&quot;고객 코드&quot;)
                                .example(121438))
                        .addParametersItem(new Parameter()
                                .name(&quot;promotionType&quot;)
                                .in(&quot;query&quot;)
                                .required(true)
                                .schema(new Schema&lt;Integer&gt;().type(&quot;integer&quot;))
                                .description(&quot;프로모션 타입&quot;)
                                .example(1))
                ));
    };
}</code></pre><p>}</p>
<p>```</p>
<p>이렇게 하면, swagger 코드가 길어지면서 controller의 가독성을 해치는 문제도 해결되고
모든 api의 내용을 한 파일에서 관리할 수 있게 된다.</p>
<p>메소드 내부 내용</p>
<ol>
<li>.in .는 OpenAPI 명세에서 파라미터가 어디 위치하는지 나타내는 속성이다.<ol>
<li>query: URL의 쿼리 문자열에 파라미터가 포함됩니다.
예: /api/users?id=123
path: URL 경로의 일부로 파라미터가 포함됩니다.
예: /api/users/{id}
header: HTTP 헤더에 파라미터가 포함됩니다.
cookie: 쿠키에 파라미터가 포함됩니다.
body: HTTP 요청 본문에 파라미터가 포함됩니다 (주로 POST, PUT 요청에서 사용).</li>
</ol>
</li>
</ol>
<p>만약 컨트롤러가 있지만, 당장 사용하고 있지 않는 내용이라면?
@Hidden을 Controller layer에 붙이면, 해당 컨트롤러에 대한 api는 swagger에 포함되지 않는다.
method layer에도 물론 추가할 수 있는 어노테이션이다.</p>
<p><del>오히려 너무 많은 메소드가 담기면, 가독성이 더 떨어지려나?</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[다국어 작업(jsp, vue, ts, i18n)]]></title>
            <link>https://velog.io/@choitree_/%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%9E%91%EC%97%85vue-ts-i18n</link>
            <guid>https://velog.io/@choitree_/%EB%8B%A4%EA%B5%AD%EC%96%B4-%EC%9E%91%EC%97%85vue-ts-i18n</guid>
            <pubDate>Mon, 27 May 2024 00:43:13 GMT</pubDate>
            <description><![CDATA[<h1 id="jsp">jsp</h1>
<ol>
<li><p>WebMvcConfigurer를 implements한 config 클래스를 만들고, 내부에 messageSource 빈 등록을 해야함</p>
</li>
<li><p>resource 내에 properties를 국가별로 별도로 생성한다.</p>
</li>
</ol>
<pre><code>//3. jsp파일에서 태그로 값을 읽는다.
&lt;spring:message code=&quot;sample.hello&quot;&gt;
</code></pre><hr>
<h2 id="properties-개행">properties 개행</h2>
<p>properties 파일 내에 텍스트가 jsp 내에서 읽는데, 개행이 필요한 경우
\n이 아닌, &lt; br/&gt; 태그를 사용하면 된다.</p>
<hr>
<h2 id="vuejs에서-다국어-버전-적용">vue.js에서 다국어 버전 적용</h2>
<p>jsp와 유사하지만, 프로퍼티 파일이 아닌 제이슨으로 설정해서 더 간단한 것 같다.</p>
<ol>
<li><p>src 하위에 locale 폴더 생성 후, 하위에 언어별 폴더와 폴더명과 동일한 이름의 json 파일 생성
폴더명은 무관하지만, 해당 언어를 인지하기 좋게 작성하는 것이 좋음
언어별 같은 키값에 번역 내용을 value로 설정한 json 파일 저장.</p>
</li>
<li><p>src 하위에 i18n.js, main.js 생성
i18n.js</p>
<pre><code>import Vue from &#39;vue&#39;
import VueI18n from &#39;vue-i18n&#39;
</code></pre></li>
</ol>
<p>// json 파일을 읽어들이기 위한 function
const requireLang = require.context(
  &#39;@/locales&#39;,    // 폴더명 입니다.
  true,
  /.json$/       // 폴더 아래 json 찾기용
)</p>
<p>const messages = {}</p>
<p>// json file read
for (const file of requireLang.keys()) {
  const path = file.replace(/(./|.json$)/g, &#39;&#39;).split(&#39;/&#39;)  // 폴더 패스</p>
<p>  path.reduce((o, s, i) =&gt; {
    if (o[s]) return o[s]</p>
<pre><code>o[s] = i + 1 === path.length
  ? requireLang(file)
  : {}

return o[s]</code></pre><p>  }, messages)</p>
<p>}</p>
<p>Vue.use(VueI18n);</p>
<p>const i18n = new VueI18n({
  locale: &#39;ko&#39;, // 기본 locale
  fallbackLocale: &#39;ko&#39;, // locale 설정 실패시 사용할 locale
  messages, // 다국어 메시지
  silentTranslationWarn: true // 메시지 코드가 없을때 나오는 console 경고 off
})</p>
<p>export default i18n</p>
<pre><code>main.js</code></pre><p>import Vue from &#39;vue&#39;
import App from &#39;./App.vue&#39;
import i18n from &#39;./i18n&#39;</p>
<p>Vue.config.productionTip = false</p>
<p>new Vue({
  i18n,
  render: h =&gt; h(App),
  data: {
    foo:&quot;test&quot;
  }
}).$mount(&#39;#app&#39;)</p>
<pre><code>

3. {{ $t(&#39;  json파일명.변수명 &#39;) }}으로 태그 내부에 작성 &lt;&gt;요기에&lt;&gt;


---
---
# Vue.js / typescript
vue.js 환경에서 다국어 작업 (typescript)
vue도 typescript도 써본 적이 없어(사실 프론트 자체를 거의 안해본,,,) chatGPT의 도움을 많이 받아서 다국어 작업을 진행했다.

다국어 작업으로 화면에서 처리가 추가적으로 필요하지만
세팅하고 어떻게 하면 다국어가 적용된 값을 불러올 수 있는지 정리해본다.

1. main.ts
//spring boot에서는 gradle 격인 느낌이다.</code></pre><p>//main.ts
import { createApp } from &#39;vue&#39;;
import { createI18n } from &#39;vue-i18n&#39;;</p>
<p>import en from &#39;@/utils/locales/en.json&#39;;
import ko from &#39;@/utils/locales/ko.json&#39;;
import vi from &#39;@/utils/locales/vi.json&#39;;
import jp from &#39;@/utils/locales/jp.json&#39;;
import cn from &#39;@/utils/locales/cn.json&#39;;</p>
<p>type MessageSchema = typeof en;
const i18n = createI18n&lt;[MessageSchema], &#39;ko&#39; | &#39;en&#39; | &#39;vi&#39; | &#39;jp&#39; | &#39;cn&#39;&gt;({
  // options
  locale: &#39;ko&#39;,
  fallbackLocale: &#39;en&#39;,
  legacy: false,
  messages: {
    en: en,
    ko: ko,
    vi: vi,
    jp: jp,
    cn: cn
  }
  // ,
  // silentTranslationWarn: true,
  // silentFallbackWarn: true
});</p>
<p>const app = createApp(App);
app.use(i18n);
app.use(vuetify).mount(&#39;#app&#39;); // vuetify는 생략 가능하다.</p>
<pre><code>&gt; main.ts 에서 vue-i18n을  import해주고, 각 언어별 json 파일을 import해준다.
&gt; MessageSchema는 한 개의 json 파일을 type으로 지정해주는 것 같다.
&gt; 그래서 type 지정된 json 내부에 있는 단어가 다른 json 파일에는 없다면 에러가 발생한다.
&gt;
&gt; legacy는 false로 해두어야 Composition API에서 사용 가능하다고 한다.
&gt; 내가 사용하려는 언어별 파일을 messages에 추가한다.(import되있어야 함)


2. vite.config.ts</code></pre><p>//vite.config.ts
...
import path from &#39;node:path&#39;;
// import vueI18n from &#39;@intlify/vite-plugin-vue-i18n&#39; // 설치
import VueI18nPlugin from &#39;@intlify/unplugin-vue-i18n/vite&#39;; // vite ver4 이상</p>
<p>// <a href="https://vitejs.dev/config/">https://vitejs.dev/config/</a>
export default defineConfig({
  plugins: [</p>
<p>   //plugins에 추가
   VueI18nPlugin({
      include: [path.resolve(__dirname, &#39;./src/utils/locales/**&#39;)],
    })
  ],
  resolve: {
    alias: {
      ...
      &#39;vue-i18n&#39;: &#39;vue-i18n/dist/vue-i18n.esm-bundler.js&#39;
    }
  },
  ...
});</p>
<pre><code>
3. LanguageSelector.vue
</code></pre><p>// /src/components/LanguageSelector.vue
<template>
  <div class="lang-selector dropdown">
    &lt;div class=&quot;selected-lang&quot; @click=&quot;toggleDropdown&quot; aria-expanded=&quot;false&quot;&gt;
      <img :src="currentFlag" style="width: 45px; height: 45px" />
      <span style="display: block; width: 30%; margin-left: 5px; cursor: pointer">
        <img src="/src/assets/images/allow.png" />
      </span>
    </div>
    <ul :class="['dropdown-menu', 'lang-list', { show: showDropdown }]" aria-labelledby="dropdownMenuButton" style="width: 30px">
      &lt;li v-for=&quot;lang in languages&quot; :key=&quot;lang.id&quot; @click=&quot;selectLang(lang.id)&quot; class=&quot;dropdown-item&quot;&gt;
        <img :src="lang.src" style="width: 30px; height: 30px" />
      </li>
    </ul>
  </div>
</template></p>
<script lang="ts">
import { defineComponent } from 'vue';
import { useLanguageSelector } from '@/composables/useLanguageSelector';

export default defineComponent({
  name: 'LanguageSelector',
  setup() {
    return useLanguageSelector();
  }
});
</script>

<style scoped>
.lang-selector {
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  position: relative;
}

.selected-lang {
  display: flex;
  align-items: center;
}

.lang-list {
  position: fixed; /* Change to fixed to keep it above all other content */
  top: 60px; /* Adjust this value to position it correctly below the header */
  right: 20px; /* Adjust this value to position it correctly aligned with the button */
  display: flex;
  flex-direction: column;
  background-color: white;
  z-index: 300000000; /* Ensure the dropdown is above other content */
  box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
  display: none; /* Initially hidden */
}

.lang-list.show {
  display: flex; /* Show the dropdown when it has the 'show' class */
}

.lang-list li {
  list-style: none;
}

.lang-list li img {
  width: 30px;
  height: 30px;
}
</style>

<pre><code>

4. useLanguageSelector.ts</code></pre><p>// /src/composables/useLanguageSelector.ts
// LanguageSelectorScript.ts
import { ref, onMounted, onBeforeUnmount } from &#39;vue&#39;;
import { useI18n } from &#39;vue-i18n&#39;;</p>
<p>export function useLanguageSelector() {
  const showDropdown = ref(false);
  const currentLang = ref(&#39;ko&#39;); // default language
  const languages = [
    { id: &#39;en&#39;, src: &#39;/src/assets/images/lang/en.png&#39; },
    { id: &#39;cn&#39;, src: &#39;/src/assets/images/lang/cn.png&#39; },
    { id: &#39;jp&#39;, src: &#39;/src/assets/images/lang/jp.png&#39; },
    { id: &#39;vi&#39;, src: &#39;/src/assets/images/lang/vi.png&#39; },
    { id: &#39;ko&#39;, src: &#39;/src/assets/images/lang/ko.png&#39; }
  ];</p>
<p>  const currentFlag = ref(<code>/src/assets/images/lang/${currentLang.value}.png</code>);
  const { locale } = useI18n();</p>
<p>  const toggleDropdown = () =&gt; {
    showDropdown.value = !showDropdown.value;
  };</p>
<p>  const selectLang = (lang: string) =&gt; {
    currentLang.value = lang;
    currentFlag.value = <code>/src/assets/images/lang/${lang}.png</code>;
    showDropdown.value = false;
    // Save selected language in localStorage or make API call to update language preference
    localStorage.setItem(&#39;lang&#39;, lang);
    locale.value = lang;
  };</p>
<p>  const handleClickOutside = (event: MouseEvent) =&gt; {
    const target = event.target as HTMLElement;
    if (!target.closest(&#39;.lang-selector&#39;)) {
      showDropdown.value = false;
    }
  };</p>
<p>  onMounted(() =&gt; {
    document.addEventListener(&#39;click&#39;, handleClickOutside);
  });</p>
<p>  onBeforeUnmount(() =&gt; {
    document.removeEventListener(&#39;click&#39;, handleClickOutside);
  });</p>
<p>  return {
    showDropdown,
    languages,
    currentFlag,
    toggleDropdown,
    selectLang
  };
}</p>
<pre><code>
처음에는 vue 파일 내부에 script 코드도 포함해서 만들었지만, 그러면 다국어가 반영되어야 하는 코드(script만 필요한 코드) 내부에도 국기를 선택하는 화면이 추가되기 때문에 script와 화면을 구성하는 vue로 나눴다. (vue 파일에 들어간 script 코드는 국기를 선택하면 되어야 하는 동작이 들어가있다.)

locale 디렉토리 내부에 각 국가별 json 파일을 추가하고, images에도 국기 모양을 선택하고(국기 모양으로 헤더에 보여지기 위함)한다.

5. header.vue</code></pre><p>//header.vue</p>
<script>
...
import LanguageSelector from '@/components/LanguageSelector.vue';
import { useLanguageSelector } from '@/composables/useLanguageSelector';
...
const { currentFlag, selectLang } = useLanguageSelector();
</script>
<template>
...
      <LanguageSelector />
...
</template>

<pre><code>헤더에서는 국기를 선택하는 화면과 선택 후 헤더에 포함된 글자도 다국어 처리되어야 하기 때문에 두가지 모두 import한다.

6. xxxx.vue</code></pre><script setup lang="ts">
...
import { ref, computed, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';

...

const { t } = useI18n();

watchEffect(() => {
  customizer.setCurrentMenu(t('xxxx')); // Dynamically update based on language
});

const headers = computed(() => [
 //변수 내부에서는 computed 함수 호출하고, t('')로 사용 가능
  { title: t('startDt'), key: 'startDt', align: 'center' },
  { title: t('endDt'), key: 'endDt', align: 'center' },
 ...
]);
</script>

<template>
  ...
            <v-col sm="1" style="margin-right: -22px">
              <v-switch
                color="facebook"
                value=""
                false-value=""
                :label="$t('overall')"
                hide-details
                inset
              />
            </v-col>
...
              <v-btn variant="outlined" color="facebook" width="80" @click="promotionManagement.promotionInfoList()">
                <SearchOutlined :style="{ fontSize: '18px', color: 'darkprimary', marginLeft: '-6px' }" />
                <span color="darkprimary" style="padding-left: 2px; margin-right: -3px">{{ $t('search') }}</span></v-btn
              >
...
</template>

<pre><code>
vue에서 watchEffect, computed 를 import한다.
watchEffect는 콜백 함수 안에 반응성 데이터 변화가 감지되면 자동으로 실행
script의 변수 내부에서는 computed 함수 호출하고, t(&#39;startDt&#39;)로 사용 가능
template 내부에서는 &lt;&gt; 안에 있는 attribute라면, :label=&quot;$t(&#39;overall&#39;)&quot; 으로 사용 가능(:으로 무조건 시작해야함), &lt;&gt;&lt;&gt;사이에 있는 값이라면 {{ $t(&#39;search&#39;) }} 이런 식으로 사용 가능

## json 개행
properties 파일은 &lt;br.&gt; 태그로 개행을 해야하지만(태그 보이기 위해 점찍음), 
json의 경우 \n으로 개행이 읽힌다. 오히려 br 태그를 붙이는 경우 프론트 페이지 에러가 난다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Entity가 할 일? Service가 할 일?]]></title>
            <link>https://velog.io/@choitree_/Entity%EA%B0%80-%ED%95%A0-%EC%9D%BC-Service%EA%B0%80-%ED%95%A0-%EC%9D%BC</link>
            <guid>https://velog.io/@choitree_/Entity%EA%B0%80-%ED%95%A0-%EC%9D%BC-Service%EA%B0%80-%ED%95%A0-%EC%9D%BC</guid>
            <pubDate>Wed, 30 Nov 2022 10:09:33 GMT</pubDate>
            <description><![CDATA[<ul>
<li>특정 Entity의 컬럼을 확인하고, 검증하는 일은 해당 Entity 내부에서 해야할 일이다.</li>
<li>쉽게 표현하면, Entity 내부에 구현된 메소드를 호출하는 것은 Entity에게 물어보는 느낌이라면</li>
<li>Service단에서 Entity가 해야할 일을 직접 구현하는 느낌은, Entity의 의사를 묻지 않고 내 마음대로 확인하는 느낌이다.
(Entity 주머니 뒤적뒤적....)</li>
<li>Entity에서 검증하는 코드를 구현하는 경우, 여러 서비스단에서 호출해서 사용하면 되지만, 만약 서비스단에서 직접 구현했다면 다른 서비스가 추가되는 경우</li>
<li>다른 서비스에서도 동일한 코드를 추가하거나 이동해야하는 경우가 발생할 수 있다. -&gt; 의존성의 문제가 발생할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[클린코드 5, 6장]]></title>
            <link>https://velog.io/@choitree_/%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-5-6%EC%9E%A5</link>
            <guid>https://velog.io/@choitree_/%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-5-6%EC%9E%A5</guid>
            <pubDate>Mon, 31 Oct 2022 10:36:07 GMT</pubDate>
            <description><![CDATA[<h1 id="5-형식-맞추기">5. 형식 맞추기</h1>
<ul>
<li>프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다.</li>
<li>코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다.</li>
<li>팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다.</li>
<li>필요하다면 규칙을 자동으로 적용하는 도구를 활용한다.</li>
</ul>
<h2 id="형식을-맞추는-목적">형식을 맞추는 목적</h2>
<ul>
<li>코드 형식은 의사소통의 일환이다. 돌아가는 코드가 전문 개발자의 일차적인 의무라고 생각할 수 있지만, 코드 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.</li>
</ul>
<h3 id="1-적절한-행-길이를-유자하라">1. 적절한 행 길이를 유자하라</h3>
<h4 id="신문-기사처럼-작성하라">신문 기사처럼 작성하라</h4>
<ul>
<li>이름은 간단하면서 설명이 가능하게 짓고, 이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지 판단할 정도로 신경 써서 짓는다.</li>
<li>첫 부분은 고차원 개념과 알고리즘을 설명하고, 아래로 내려올수록 의도를 세세하게 묘사한다. 마지막에는 가장 저차원 함수와 세부 내역이 나온다.<h4 id="개념은-빈-행으로-분리하라">개념은 빈 행으로 분리하라</h4>
</li>
<li>거의 모든 코드는 왼쪽에서 오른쪽, 위에서 아래로 읽힌다.</li>
<li>각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다. 생각 사이에는 빈 행을 넣어 분리해야 마땅하다.</li>
</ul>
<pre><code>package fitnesse.wikitext.widgets;

import java.uitl.regex.*;

public class BoldWidget extends ParentWidget {
    public static final String REGXP = &quot;&#39;&#39;&#39;.+?&#39;&#39;&#39;&quot;;
    private static final Patten patten = Patten.compile(&quot;&#39;&#39;&#39;(.+?)&#39;&#39;&#39;&quot;,
        Patten.MULTILINE + Patten.DOTALL
    );

    public BoldWidget(ParentWidget parent, String text) throws Exception {
        super(parent);
        Matcher match = patten.matcher(text);
        match.find();
        addChildWidgets(match.group(1));
    }

    public String render() throws Exception {
        StringBuffer html = new StringBuffer(&quot;&lt;b&gt;&quot;);
        html.append(childHtml().append(&quot;&lt;b&gt;&quot;);
        return html.toString();
    }
}</code></pre><pre><code>package fitnesse.wikitext.widgets;
import java.uitl.regex.*;
public class BoldWidget extends ParentWidget {
    public static final String REGXP = &quot;&#39;&#39;&#39;.+?&#39;&#39;&#39;&quot;;
    private static final Patten patten = Patten.compile(&quot;&#39;&#39;&#39;(.+?)&#39;&#39;&#39;&quot;,
        Patten.MULTILINE + Patten.DOTALL
    );
    public BoldWidget(ParentWidget parent, String text) throws Exception {
        super(parent);
        Matcher match = patten.matcher(text);
        match.find();
        addChildWidgets(match.group(1));
    }
    public String render() throws Exception {
        StringBuffer html = new StringBuffer(&quot;&lt;b&gt;&quot;);
        html.append(childHtml().append(&quot;&lt;b&gt;&quot;);
        return html.toString();
    }
}</code></pre><p>위와 아래는 동일한 코드인데, 빈 행 유무의 차이만 있다.
빈 행이 없어지면, 코드 가독성이 현저하게 떨어져 암호처럼 보인다.</p>
<h4 id="세로-밀집도">세로 밀집도</h4>
<ul>
<li><p>줄바꿈이 개념을 분리한다면, 세로 밀집도는 연관성을 의미한다.</p>
<pre><code>public class ReporterConfig {
  /**
  * 리포터 리스너의 클래스 이름
  */
  private String m_className;

  /**
  * 리포터 리스너의 속성
  */
  private List&lt;Property&gt; m_properties = new ArrayList&lt;Property&gt;();
  public void addProperty(Property property) {
      m_properties.add(property);
  }
}</code></pre><pre><code>public class ReporterConfig 
  private String m_className;
  private List&lt;Property&gt; m_properties = new ArrayList&lt;Property&gt;();

  public void addProperty(Property property) {
      m_properties.add(property);
  }
}</code></pre><p>위에 주석이 있는 코드보다, 아래 코드가 &#39;한 눈&#39;에 들어온다.</p>
</li>
</ul>
<h4 id="수직-거리">수직 거리</h4>
<ul>
<li>서로 밀접한 개념은 세로로 가까이 둬야 한다. 물론 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않지만, 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다. 이 것이 바로 protected 변수를 피해야 하는 이유 중 하나다.</li>
<li>밀접한 두 개념은 세로 거리로 연관성을 표현한다.</li>
<li>연관성이란 한 개념을 이해하는 데 다른 개념이 중요한 정도로, 연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스 파일과 클래스를 여기저기 뒤지게 된다.<h5 id="변수-선언">변수 선언</h5>
<blockquote>
<p>변수는 사용하는 위치에 최대한 가까이 선언한다.
루프를 제어하는 변수는 흔히 루프 문 내부에 선언한다.</p>
</blockquote>
</li>
</ul>
<h5 id="인스턴스-변수">인스턴스 변수</h5>
<blockquote>
<p>인스턴스 변수는 클래스 맨 처음에 선언한다. 또한, 변수 간에 세로로 거리를 두지 않는다.</p>
</blockquote>
<h5 id="종속-함수">종속 함수</h5>
<blockquote>
<p>한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 
호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽힌다.</p>
</blockquote>
<h5 id="개념적-유사성">개념적 유사성</h5>
<blockquote>
<p>친화도가 높을수록 코드를 가까이 배치한다. 친화도가 높은 요인은 여러 가지다. 한 함수가 다른 함수를 호출해 생기는 직접적인 종속성이 한 예이고, 변수와 그 변수를 사용하는 함수도 한 예이다.
아래 예시는 명명법이 같고 기본 기능이 유사하고 간단하다. 종속적인 관계가 없더라도 가까이 배치해야할 함수들이다.</p>
</blockquote>
<pre><code>public class Assert {
    static public void assertTrue(String message, boolean condition) {
        if(!condition) 
            fail(message);
    }

    static public void assertTrue(boolean condition) {
        assertTrue(null, condition);
    }

    static public void assertFalse(String message, boolean condition) {
        assertTrue(message, !condition);
    }

    static public void assertFalse(boolean condition) {
        assertFalse(null, condition);
    }
}</code></pre><h4 id="세로-순서">세로 순서</h4>
<ul>
<li>일반적으로 함수 호출 종속성은 아래 방향으로 유지한다.</li>
<li>호출되는 함수를 호출하는 함수보다 나중에 배치하면, 소스 코드 모듈이 고차우너에서 저차원으로 자연스럽게 내려간다.</li>
</ul>
<h4 id="가로-형식-맞추기">가로 형식 맞추기</h4>
<ul>
<li>가급적 120자 이상을 넘지 않는 것이 좋다.</li>
</ul>
<h4 id="가로-공백과-밀집도">가로 공백과 밀집도</h4>
<pre><code>private void measureLine(String line) {
    lineCount++;
    int lineSize = line.length();
    totalChars += lineSize;
    lineWithHistogram.addLine(lineSize, lineCount);
    recordWidgetLine(lineSize);
}</code></pre><ul>
<li><p>할당 연산자를 강조하려고 앞뒤에 공백을 줬다. 할당문은 왼쪽 요소와 오른쪽 요소가 분명히 나뉜다. 공백을 넣으면 두 가지 주요 요소가 확실히 나뉜다는 사실이 더욱 분명해진다.</p>
</li>
<li><p>함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않았다. 함수와 인수는 서로 밀접하기 때문이다. 공백을 넣으면 한 개념이 아닌 별개로 보인다.</p>
</li>
<li><p>함수를 호출하는 인수는 쉼표를 강조해 인수가 별개라는 사실을 보여주기 위해 가로 공백을 준다.</p>
</li>
<li><p>연산자 우선 순위를 강조하기 위해서도 공백을 사용한다.</p>
</li>
</ul>
<pre><code>public class Quadratic {
    public static double root1(double a, double b, double c) {
        double determinant = determinant(a, b, c);
        return (-b + Math.sqrt(determinant)) / (2*a);
    }

    public static double root2(int a, int b, int c) {
        double determinant = determinant(a, b, c);
        return (-b - Math.sqrt(determinant)) / (2*a);
    }
    private static double determinant(double a, double b, double c) {
        return b*b - 4*a*c;
    }
}</code></pre><ul>
<li>곱셈은 우선 순위가 가장 높아서, 승수 사이에는 공백이 없다. ex)2<em>a, 4</em>a*c</li>
<li>덧셈과 뺄셈은 곱셈보다 우선 순위가 낮기 때문에, 항 사이에는 공백이 들어간다.</li>
</ul>
<h4 id="가로-정렬">가로 정렬</h4>
<ul>
<li>이전에는 변수를 선언할 때, 가로 공백을 맞춰서 나란히 정렬했으나, 이제는 선언문과 할당문을 별도로 정렬하지 않는다. 정렬하지 않으면 오히려 중대한 결함을 찾기가 쉽다.</li>
</ul>
<h4 id="들여쓰기">들여쓰기</h4>
<ul>
<li>코드는 윤곽도(outline) 계층과 비슷하며, 이러한 범위로 이루어진 계층을 표현하기 위해 우리는 코드를 들여쓴다.</li>
<li>들여쓰는 정도는 계층에서 코드가 자리잡은 수준에 비례한다.</li>
<li>클래스 정의처럼 파일 수준의 문장은 들여쓰지 않고, 클래스 내 메서드는 클래스보다 한 수준 들여쓴다.</li>
<li>메소드 코드 내부는 메소드 선언보다 한 수준 들여쓰고, 블로코드는 블록을 포함하는 코드보다 한 수준 들여쓴다.</li>
</ul>
<h5 id="들여쓰기-무시하기">들여쓰기 무시하기</h5>
<pre><code>public class CommentWidget extends TextWidget 
{
    public static final String REGEXP = &quot;^#[^\r\n]*(?:(?:\r\n)|\n|\r?&quot;;

    public CommentWidget(ParentWidget parent, String text) {super(parent, text);}
    public String render() throws Exception{ return &quot;&quot;;}
}</code></pre><pre><code>public class CommentWidget extends TextWidget {
    public static final String REGEXP = &quot;^#[^\r\n]*(?:(?:\r\n)|\n|\r?&quot;;

    public CommentWidget(ParentWidget parent, String text) {
    super(parent, text);
    }
    public String render() throws Exception{ 
        return &quot;&quot;;
    }
}</code></pre><h4 id="가짜-범위">가짜 범위</h4>
<ul>
<li>비어있는 while문이나 for문을 사용하지 않는 것이 좋지만, 만약 사용하게 되는 경우 새 행에 세미콜론을 들여써서 넣어준다.</li>
</ul>
<h3 id="팀-규칙">팀 규칙</h3>
<ul>
<li>프로그래머는 각자 선호하는 규칙이 있지만, 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다.</li>
<li>팀은 한 가지 규칙에 합의해야 하고, 모든 팀원은 그 규칙을 따라야 한다.</li>
<li>좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다는 사실을 기억해야 한다.</li>
<li>스타일은 일관적이고 매끄러워야 하며, 한 형식이 다른 소스 파일에도 쓰이라는 신뢰가 필요하다.</li>
</ul>
<pre><code>public class CodeAnalyzer implements JavaFileAnalysis { 
    private int lineCount;
    private int maxLineWidth;
    private int widestLineNumber;
    private LineWidthHistogram lineWidthHistogram; 
    private int totalChars;

    public CodeAnalyzer() {
        lineWidthHistogram = new LineWidthHistogram();
    }

    public static List&lt;File&gt; findJavaFiles(File parentDirectory) { 
        List&lt;File&gt; files = new ArrayList&lt;File&gt;(); 
        findJavaFiles(parentDirectory, files);
        return files;
    }

    private static void findJavaFiles(File parentDirectory, List&lt;File&gt; files) {
        for (File file : parentDirectory.listFiles()) {
            if (file.getName().endsWith(&quot;.java&quot;)) 
                files.add(file);
            else if (file.isDirectory()) 
                findJavaFiles(file, files);
        } 
    }

    public void analyzeFile(File javaFile) throws Exception { 
        BufferedReader br = new BufferedReader(new FileReader(javaFile)); 
        String line;
        while ((line = br.readLine()) != null)
            measureLine(line); 
    }

    private void measureLine(String line) { 
        lineCount++;
        int lineSize = line.length();
        totalChars += lineSize; 
        lineWidthHistogram.addLine(lineSize, lineCount);
        recordWidestLine(lineSize);
    }

    private void recordWidestLine(int lineSize) { 
        if (lineSize &gt; maxLineWidth) {
            maxLineWidth = lineSize;
            widestLineNumber = lineCount; 
        }
    }

    public int getLineCount() { 
        return lineCount;
    }

    public int getMaxLineWidth() { 
        return maxLineWidth;
    }

    public int getWidestLineNumber() { 
        return widestLineNumber;
    }

    public LineWidthHistogram getLineWidthHistogram() {
        return lineWidthHistogram;
    }

    public double getMeanLineWidth() { 
        return (double)totalChars/lineCount;
    }

    public int getMedianLineWidth() {
        Integer[] sortedWidths = getSortedWidths(); 
        int cumulativeLineCount = 0;
        for (int width : sortedWidths) {
            cumulativeLineCount += lineCountForWidth(width); 
            if (cumulativeLineCount &gt; lineCount/2)
                return width;
        }
        throw new Error(&quot;Cannot get here&quot;); 
    }

    private int lineCountForWidth(int width) {
        return lineWidthHistogram.getLinesforWidth(width).size();
    }

    private Integer[] getSortedWidths() {
        Set&lt;Integer&gt; widths = lineWidthHistogram.getWidths(); 
        Integer[] sortedWidths = (widths.toArray(new Integer[0])); 
        Arrays.sort(sortedWidths);
        return sortedWidths;
    } 
}</code></pre><hr>
<h1 id="6-객체와-자료-구조">6. 객체와 자료 구조</h1>
<blockquote>
<p>변수를 비공개로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶어서다. 하지만 수많은 프로그래머가 조회함수와 설정함수(getter, setter)를 당연하게 공개해 비공개 변수를 외부에 노출할까?</p>
</blockquote>
<h3 id="자료-추상화">자료 추상화</h3>
<pre><code>public class Point {
    public double x;
    public double y;
}</code></pre><pre><code>public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}</code></pre><ul>
<li>두 클래스 모두 2차원 점을 표현하지만, 위에 클래스는 구현을 외부로 노출하고 밑에 클래스는 구현을 완전히 숨긴다.</li>
<li>변수를 private으로 설정하더라도, getter/setter를 제공한다면 구현을 외부로 노출하는 셈이다. 변수 사이에 함수라는 계층을 넣는다고 구현이 저절로 감춰지지 않는다. </li>
<li>구현을 감추려면 추상화가 필요하다. &gt; 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.</li>
</ul>
<pre><code>public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}    </code></pre><pre><code>public interface Vehicle {
    double getPercentFuelRemaining();
}</code></pre><p>상위 인터페이스는 자동차 연료 상태를 구체적인 숫자로 알려주지만, 하위 인터페이스는 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려준다. 
상위 인터페이스는 변수값을 읽어 반환할 뿐이라는 사실이 거의 확실하지만, 하위 인터페이스는 정보가 어디서 오는지 전혀 드러나지 않는다.</p>
<h4 id="자료객체-비대칭">자료/객체 비대칭</h4>
<ul>
<li>객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.</li>
<li>자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.</li>
<li>객체와 자료구조의 정의는 본질적으로 상반된다. </li>
</ul>
<pre><code>절차적인 도형
public class Square {
    public Point topLeft;
    public double side;
}

public class Rectangle {
    public Point topLeft;
    public double height;
    public double width;
}

public class Circle {
    public Point center;
    public double radius;
    public double width;
}

public class Geometry {
    public final double PI = 3.141592653585793;

    public double area(Object shape) throws NoSuchShapeException {
        if(shape instanceOf Square) {
            Square s = (Square)shape;
            return s.side * s.side;
        }
    else if(shape instanceOf Rectangle) {
            Rectangle r = (Rectangle)shape;
            return r.height * r.width;
        }
    else if(shape instanceOf Circle) {
            Circle c = (Circle)shape;
            return PI * c.radius * c.radius
        }
    }
    throw new NoSuchShapeException();
}</code></pre><blockquote>
<p>Geometry 클래스는 세 가지 도형 클래스를 다루고, 각 도형 클래스는 간단한 자료 구조이다. 즉, 자료구조인 각 도형 클래스에서는 아무 메소드를 제공하지 않고, 도형이 동작하는 방식은 Geometry라는 객체에서 구현한다.</p>
</blockquote>
<ul>
<li>클래스가 절차적이지만, 만약 둘레 길이를 구하는 함수를 추가하고 싶다면, 각 도형 클래스에는 아무런 영향을 미치지 않는다.</li>
<li>반대로 새 도형을 추가하고 싶다면, Geometry 클래스에 속한 함수를 모두 고쳐야 한다.</li>
</ul>
<pre><code>다형적인 도형(객체지향적 도형)
public class Square implements Shape {
    public Point topLeft;
    public double side;

    public double area() {
        return side*side;
    }
}

public class Rectangle implements Shape {
    public Point topLeft;
    public double height;
    public double width;

    public double area() {
        return height*width;
    }
}

public class Circle implements Shape {
    public Point center;
    public double radius;
    public double width;

    public double area() {
        return PI*radius*radius;
    }
}</code></pre><p>area 메소드는 다형 메소드로, Geometry 클래스는 필요하지 않다. 새 도형을 추가하고 싶더라도 기존 함수에 아무런 영향을 주지 않지만, 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.</p>
<ul>
<li>절차 지향과 객체 지향은 상호 보완적인 특질이 있어, 객체와 자료구조는 근본적으로 양분된다.</li>
</ul>
<blockquote>
<p>(자료 구조를 사용하는)절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다. 반면, 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.</p>
</blockquote>
<blockquote>
<p>절차적인 코든느 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다. 객체 지향 코든느 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.</p>
</blockquote>
<p>객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉽고, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.</p>
<h4 id="디미터-법칙">디미터 법칙</h4>
<ul>
<li>디미터 법칙은 잘 알려진 휴리스틱(발견법)으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다. 객체는 자료를 숨기고 함수를 공개한다. </li>
<li>즉, 객체는 getter로 내부 구조를 공개하면 안된다는 의미다.<pre><code>클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.
</code></pre></li>
</ul>
<p>클래스 C
f가 생성한 객체
f 인수로 넘어온 객체
C 인스턴스 변수에 저장된 객체</p>
<pre><code>쉽게 말해, 낯선 사람은 경계하고 친구랑만 놀라는 의미다.</code></pre><p>final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();</p>
<pre><code>#### 기차 충돌
- 여러 객차가 한 줄로 이어진 기차처럼 보여서, 위와 같은 코드를 기차 충돌이라고 부른다.
- 일반적으로 조잡하다고 여겨지는 방식으로 피하는 것이 좋다.</code></pre><p>Options opts = ctxt.getOption();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();</p>
<pre><code>기차 충돌 코드는 이처럼 수정하는 것이 좋지만, getter를 사용하는 바람에 디미터 법칙을 위반한 것인지 혼란을 일으킨다.</code></pre><p>final String outputDir = ctxt.options.scratchDir.absolutPath;</p>
<pre><code>위 코드처럼 구현한다면, 디미터 법칙을 거론할 필요가 없어진다.

#### 잡종 구조
- 이런 혼란으로 절반은 객체, 절반은 자료 구조인 잡종 구조가 나온다. 
- 잡종 구조는 중요한 기능을 수행하는 함수도 있고, 공개 변수나 공개 getter/setter도 있으며, getter/setter는 비공개 변수를 그대로 노출한다.
- 이 때문에 다른 함수가 절차적인 프로그래밍의 자료 구조 접근 방식처럼 비공개 변수를 사용하고픈 유혹에 빠지기 쉽다.
- 잡종 구조는 객체, 자료 구조의 단점만 모아놓는 구조로, 되도록 잡종 구조는 피하는 것이 좋다.

#### 구조체 감추기
만약 ctxt, options, scratchDir이 진짜 객체라면, 기차 충돌 코드를 사용하면 안된다. </code></pre><p>// 1번
ctxt.getAbsolutePathOfScratchDirectoryOption();</p>
<p>// 2번
ctxt.getScratchDirectoryOption().getAbsolutePath()</p>
<p>// 3번
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);</p>
<pre><code>첫 번째 방법은 ctxt 객체에 공개해야 하는 메서드가 너무 많아지고,
두 번째 방법은 getScratchDirectoryOption()이 객체가 아닌 자료 구조를 반환한다고 가정한다.
세 번째 방법은 ctxt 객체에 임시 파일을 생성하도록 하는 것으로, 객체에서 맡기기 적당한 임무로 보인다.
또한, ctxt 내부 구조를 드러내지 않으며, 모듈에서 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없기에 디미터 법칙을 위반하지 않는다.

#### 자료 전달 객체
- 자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스로, 이런 자료 구조체를 자료 전달 객체(Data Transfer Object, DTO)라고 한다.
- DTO는 데이터베이스와 통신하거나 소켓에서 받은 메세지의 구문을 분석할 때 유용하다.
- DTO는 데이터베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체다.
- 좀 더 일반적인 형태는 빈(bean) 구조다. 빈은 비공개 변수를 getter/setter로 조작한다. 일종의 사이비 캡슐화로 별다른 이익을 제공하진 않는다.</code></pre><p>class Address {</p>
<pre><code>private final String postalCode;

private final String city;

private final String street;

private final String streetNumber;

private final String apartmentNumber;

public Address(String postalCode, String city, String street, String streetNumber, String apartmentNumber) {
    this.postalCode = postalCode;
    this.city = city;
    this.street = street;
    this.streetNumber = streetNumber;
    this.apartmentNumber = apartmentNumber;
}

public String getPostalCode() {
    return postalCode;
}

public String getCity() {
    return city;
}

public String street() {
    return street;
}

public String streetNumber() {
    return streetNumber;
}

public String apartmentNumber() {
    return apartmentNumber;
}</code></pre><p>}</p>
<pre><code>
#### 활성 레코드(active record)
- 활성 레코드는 DTO의 특수한 형태이다. 공개 변수가 있거나 비공개 변수에 getter/setter 함수가 있는 자료 구조지만, 여기에 save, find와 같은 탐색 함수도 제공한다.
- 활성 레코드는 테이터 베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과다.
- 활성 레코드에 비지니스 규칙 메소드를 추가해 이런 자료 구조를 객체로 취급하는 개발자가 흔하지만, 이는 자료 구조도 객체도 아닌 잡종 구조가 나오기 때문에 바람직하지 않다.
- 활성 레코드는 자료 구조로 취급하고, 비지니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다.

---
### 결론
- 객체는 동작을 공개하고 자료를 숨긴다. 그래서 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.
- 자료구조는 별다른 동작없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기는 어렵다.
- 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하다면, 객체가 적합하다.
- 새로운 동작을 추가하는 유연성이 필요하면, 자료 구조와 절차적인 코드가 적합하다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[어쩌다 프론트....]]></title>
            <link>https://velog.io/@choitree_/%EC%96%B4%EC%A9%8C%EB%8B%A4-%ED%94%84%EB%A1%A0%ED%8A%B8</link>
            <guid>https://velog.io/@choitree_/%EC%96%B4%EC%A9%8C%EB%8B%A4-%ED%94%84%EB%A1%A0%ED%8A%B8</guid>
            <pubDate>Thu, 23 Jun 2022 03:34:38 GMT</pubDate>
            <description><![CDATA[<p>date range picker를 사용해서, 날짜를 검색하는 기능에 대해서 만족하고
해당 기능을 응용해서 하루 날짜에 대한 매출 검색, 월별/년별 매출 검색을 구현하려고 했다.</p>
<p>우선 하루 매출 검색을 구현하는데, 내가 사용한 코드의 경우 dateRange 타입으로 attribute를 받아서 처리하는 방식이고 이를 응용해서 사용하려면 하루 날짜를 검색함에도 시작날짜와 끝날짜가 필요했다.
시작 날짜와 끝 날짜를 같은 날로 지정하고 검색하면 당연히 되겠거니 하고 구현을 하니, 
input으로 받은 date는 시작 날짜에만 value를 전달하고 끝 날짜는 값이 전달되지 않았다.
hidden으로 시작 value를 옮겨주는 작업도 해봤지만, 되지 않았다.
jquery와 태그 내부에서 모두 시도했지만, 빈값으로 받아짐ㅠㅠ
신기하게도 검색은 되는데!ㅋㅋㅋㅋㅋㅋㅋ url상 param이 비어있는 상황이었다.</p>
<p>안되는데 작동은 하는 상황인데, 이대로 계속하다가는 나중에 막 꼬일 것 같아서
다른 date range picker를 찾아봤다.
<a href="https://preview.keenthemes.com/metronic/demo2/crud/forms/widgets/bootstrap-daterangepicker.html#">https://preview.keenthemes.com/metronic/demo2/crud/forms/widgets/bootstrap-daterangepicker.html#</a>
여기는 bootstrap daterangepicker라고 하는데, html과 javascript 코드만 복붙해서 만든 샘플 페이지에서는 아이콘이나 클릭했을 때 달력이 보이지 않았다.
<del>css 링크나 javascript를 사용할 코드의 링크를 안써준 문제</del>
아이콘을 받을 수 있는 css 링크를 붙여주고 나니, 아이콘은 보이지만 정렬이나 디자인은 빠져있었다.</p>
<p>개발자 페이지에서 css 파일을 그대로 긁어와서 붙여주니, 모달을 제외한 기능은 들어왔는데
파일이 8만줄이 넘었곸ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 나는 모달을 사용하려고 하고 있기 때문에 또 확인을 해봐야 한다...</p>
<p>조그만 팝업의 이름이 모달인 것도 오늘 처음 알았닿ㅎㅎㅎㅎㅎ
야매로 프론트를 보면서 모달로 붙여주는 작업을 시도해야한다.
생각보다 시간이 오래 걸리겠는걸,,,,?ㅎㅎㅎㅎㅎㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[thymeleaf 기간별 검색 기능]]></title>
            <link>https://velog.io/@choitree_/thymeleaf-%EA%B8%B0%EA%B0%84%EB%B3%84-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@choitree_/thymeleaf-%EA%B8%B0%EA%B0%84%EB%B3%84-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Tue, 21 Jun 2022 09:43:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choitree_/post/7c4e7215-fd1a-452a-9283-de6a6e150cf3/image.gif" alt="기간별 검색 기능"></p>
<p>와 하루종일 걸려서 너무 뿌듯해서 영상까지 업로드!ㅋㅋㅋㅋㅋㅋㅋㅋ
날짜나 월별 검색은 input으로 바로 받아서 가능한데, 기간별은 어떻게 해야하나 구글링하던 중에
<a href="https://github.com/martinwojtus/tutorials/tree/master/thymeleaf/thymeleaf-bootstrap-date-range-picker">date range picker</a>라는 오픈소스를 찾았다!!!
<a href="https://frontbackend.com/thymeleaf/spring-boot-bootstrap-thymeleaf-date-range-picker">설명</a>은 frontbackend라는 페이지에서 볼 수 있다.</p>
<p>해당 페이지를 보면서 차근차근 따라해봤는데
gradle에 추가해주면서,버전때문에도 애먹고 빌드가 안되서도 애먹었다ㅠㅠ</p>
<pre><code>//for date range picker
    implementation group: &#39;org.webjars&#39;, name: &#39;jquery&#39;, version: &#39;3.0.0&#39;
    implementation group: &#39;org.webjars&#39;, name: &#39;bootstrap&#39;, version: &#39;4.0.0-2&#39;
    implementation &#39;org.webjars:popper.js:1.12.9-1&#39;
    implementation &#39;org.webjars:webjars-locator:0.45&#39;
    implementation &#39;org.webjars:font-awesome:5.11.2&#39;</code></pre><p>총 다섯개의 디펜던시를 추가해줬는데, bootstrap이 5버전 이하라면 jquery를 추가해줘야 한다고 한다.
<del>빌드를 한 열번 하면서 정상적으로 불러와질 때까지 해봤다.</del></p>
<p>그리고, controller와 index페이지를 똑같이 생성해서 실행해봤는데, 달력 모양이 나오지 않았다🙄
깃허브에 들어가서 css, js 파일을 복붙해주니 샘플 페이지는 정상적으로 작동되었다.</p>
<p>내가 사용하는 페이지에 적용하기 위해서 검색에 해당하는 div태그를 전체적으로 옮겨주고
script 부분과 css를 옮겨줬는데, 또 달력모양이 안나온다!
파일이 이제 다 static 내부에 있는데, 뭐가 문제인지 한참을 삽질하다가 경로 문제인 것을 확인했다.
나는 templates에 하위 디렉토리를 두고서 작업중인데, 그러면 static에 접근하기 위해 한 번 더 상위로 (../)나갔어야 했다! 너무 당연한 소리지만 css를 지이이이이잉이인짜 오랜만에 봐서 아예 생각도 못하던 부분이다.</p>
<p>이제 검색 껍데기는 보이는데, 날짜를 선택하고 검색할 때 검색 결과가 보일 수 있도록 변경해야 한다.
미리 만들어둔 period에 해당하는 메소드와 연결을 해야하는데, 나는 LocalDate를 사용했고, 오픈 소스에서는 Date를 사용한다.</p>
<pre><code>public LocalDate convertFromDateToLocalDate(Date date) {
        return date.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDate();
}</code></pre><p>Date를 LocalDate로 타입 변환을 할 수 있도록 메소드를 만들어주고, 기간별 매출조회에 해당하는 메소드와 연결해줬다.
그랬더니 계속 아래와 같은 오류가 발생했다....
Failed to convert from type [java.lang.String] to type [@org.springframework.format.annotation.DateTimeFormat java.util.Date]
에러 코드를 보면, 날짜가 DD/MM/YYYY 형식으로 보이는데 내가 Format으로 맞춰준 날짜의 형식은 모두
YYYY-MM-DD 타입이다. 보이는 모든 부분은 수정했는데도 문제가 안나와서 command + shift + F로 (dd/mm/yyyy)를 검색했다ㅋㅋㅋㅋㅋ
그랬더니 script부분에서 picker.startDate.format 내부가 DD/MM/YYYY로 적혀있었다.
startDate, endDate 부분을 모두 수정하고 나니 정상적으로 검색된다!!</p>
<pre><code>    var $dateRange = $(&#39;#dateRange&#39;);

    $dateRange.daterangepicker();
    $dateRange.on(&#39;apply.daterangepicker&#39;, function (ev, picker) {
        $(&#39;input[name=&quot;dateFrom&quot;]&#39;).val(picker.startDate.format(&#39;YYYY-MM-DD&#39;));
        $(&#39;input[name=&quot;dateTo&quot;]&#39;).val(picker.endDate.format(&#39;YYYY-MM-DD&#39;));
    });</code></pre><p><del>java에서는 dateFormat을 YYYY-MM-dd라고 적어주지만, js에서 동일하게 적었더니 
dd에서 일자가 아닌 요일이 불러와졌다. js에서는 YYYY-MM-DD로 적어야 하는 것 같다.</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Thymeleaf 문법 정리]]></title>
            <link>https://velog.io/@choitree_/Thymeleaf-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@choitree_/Thymeleaf-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 25 May 2022 08:19:11 GMT</pubDate>
            <description><![CDATA[<p>오랜만에 또 블로그 업로드다!
백엔드를 마무리하고, 프론트를 시작하고 나니 결국 백엔드도 리턴을 바꾸면서 같이 수정을 하면서 작업중이다.
이제 수정 form과 수정만 연결하면, 환자와 매입의 기능적인 부분은 마무리가 되는데 완성이 되면 메뉴바를 만들고 그리드 모양도 좀 잡아서 보여지도록 수정해야 한다.
각설하고, 프론트를 너무너무 오랜만에 작업하는 바람에 정말 기본적인 문법이나 만들 때 어떤걸 사용해야지 원하는대로 보여지고, 값이 전달되는지 더듬더듬 배우면서 만들고 있다.</p>
<p>프론트는 정말,,,, 어려운것
여튼 타임리프를 처음 써보면서 사실 문법에 대한 학습도 없이 우선 만들면서 필요한 부분을 찾으면서 만들다 보니, 정리하지 않는다면 다 까먹을 것 같다는 생각이 들어서 글을 작성한다!</p>
<hr>
<h3 id="1-form태그">1. form태그</h3>
<p>Thymeleaf에서는 백엔드에서 값을 받고 전달하려면, th:를 써줘야한다.
form태그는 submit(제출)하는 값이 있을 때 사용하는 태그인데, @PathVarible을 쓰면 Long타입이 String 타입으로 전달되서 타입이 맞지 않는다는 예외가 나왔다. 그러고 겨우 타입을 맞춰놓으니, ${patient.id}라는 내용이 그대로 전달이 되고 해당하는 값이 전달되지 않았다.</p>
<p>th:action=&quot;@{&#39;/&#39; + ${patient.id} + &#39;/income/create&#39;}&quot;<br>처음에는 위에와 같이 작성했는데, 읽혀지지가 않았고 구글링을 한참하면서 해결해본 결과, 아래와 같은 방식으로 작성하면 정상적으로 읽히는 것을 확인했다.</p>
<p>th:action=&quot;@{/{pId}/income/create(pId=${patient.id})}</p>
<p>그런데 웃긴?신기한 것은 href는 위에와 같이 작성해도 정상적으로 읽힌다.......!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[진행 상황ㅎㅎㅎㅎ]]></title>
            <link>https://velog.io/@choitree_/%EC%A7%84%ED%96%89-%EC%83%81%ED%99%A9%E3%85%8E%E3%85%8E%E3%85%8E%E3%85%8E</link>
            <guid>https://velog.io/@choitree_/%EC%A7%84%ED%96%89-%EC%83%81%ED%99%A9%E3%85%8E%E3%85%8E%E3%85%8E%E3%85%8E</guid>
            <pubDate>Tue, 26 Apr 2022 00:47:26 GMT</pubDate>
            <description><![CDATA[<p>기능 개발에 박차를 가하던 중에 코로나에 걸렸당ㅎㅎㅎㅎㅎ
일주일 동안 아무것도 못하고 푹 쉬다가, 겨우겨우 매입 기능을 추가했다.
추가하면서, 문제가 발생한 부분은 거래처와 매입의 관계가 ManyToOne인데, 매입과 재고의 관계도 ManyToOne이라서 조회할 때, 각 DTO에서 어느 선까지 조회를 해줘야하는 것인지 생각이 필요하다.</p>
<p>또한, 재고는 로우가 계속 생기는 방식으로 만들겠지만, 실질적으로 재고를 조회할 때는 한 품목당 현재 재고가 어떻게 되는지 조회를 해야하는데, 값을 + -로 애초에 받아서 계산을 해서 뿌려줘야할지, 아니면 처음 설계한 대로 isBuy에 값을 통해서 +-를 곱해서 계산해야하는지도 고민이 필요하다.</p>
<p>그리고, 클라이언트의 요구사항이 발생했닿ㅎㅎㅎㅎㅎ
한의원 이전으로 이제 예약 시스템을 도입하려고 하는데, 20분에 1명씩 예약을 원하고
점심시간 전 1시간과 마감 1시간 전부터는 예약을 받지 않도록 원하는 상황이다.
우선 네이버 예약을 찾아보니, 네이버의 경우 30분/1시간 단위로만 예약을 할 수 있게 되어있다.</p>
<p>내가 만들고 있는 기능을 실제로 사용할지는 모르겠지만, 프로그램이 완성되는 경우
프로그램을 구매하겠다는 의사를 밝혀주셔서(얼마나 주실랑가)
해당 기능을 추가해보기로 했다!</p>
<p>이제 백엔드 로직은 거의 마무리되는 단계인데, 프론트를 시작해야할지
OAuth와 문자 발송 기능을 만들어야할지 배포를 진행하는지, 우선 순위를 정하기가 어렵다...</p>
<p>이 것이 현재까지의 나의 상황!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트2]]></title>
            <link>https://velog.io/@choitree_/%ED%85%8C%EC%8A%A4%ED%8A%B82</link>
            <guid>https://velog.io/@choitree_/%ED%85%8C%EC%8A%A4%ED%8A%B82</guid>
            <pubDate>Sat, 16 Apr 2022 09:28:40 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-테스트-적용">스프링 테스트 적용</h2>
<p>빈이 많아지고 복잡해지면 ApplicationContext 생성에 적지 않은 시간이 걸릴 수 있다. ApplicationContext가 만들어질 때 모든 싱글톤 빈 오브젝트를 초기화하는데, 이 작업이 많은 시간이 소요된다.
이 문제를 해결하기 위해서 ApplicationContext를 static 레벨에 저장해두는 방법도 있지만, 스프링이 직접 제공하는 ApplicationContext 테스트 지원 기능을 사용하는 것이 편리하다.</p>
<h3 id="테스트를-위한-애플리케이션-컨텍스트-관리">테스트를 위한 애플리케이션 컨텍스트 관리</h3>
<h4 id="스프링-테스트-컨텍스트-프레임워크-적용">스프링 테스트 컨텍스트 프레임워크 적용</h4>
<pre><code>@Runwith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations=&quot;/applicationContext.xml&quot;)
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;

    @Before
    public void setUp() {
        this.dao = this.context.getBean(&quot;userDao&quot;, UserDao.class);
    }
}</code></pre><p>context를 초기화해주는 코드는 없어서, 메소드에서 context를 사용하려면 NullPointerException이 발생하지만, 테스트에서는 아무런 문제가 발생하지 않는다.</p>
<p>여기서 더 간단하게는, UserDao를 주입받아서 사용할 수도 있다.</p>
<pre><code>public class UserDaoTest {
    @Autowired
    UserDao dao;
}</code></pre><h4 id="di와-테스트">DI와 테스트</h4>
<p>DataSource의 구현 클래스를 바꾸지 않고, DB 커넥션의 정보를 바꾸 일이 없는데도, 굳이 인터페이스를 사용해서 DI를 주입해주는 방식을 사용해야 할까?
(매일 고민하는 부분...)</p>
<ol>
<li>소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문에, 클래스 대신 인터페이스를 사용하고 new를 이용해서 생성하는 대신 DI를 통해 주입받게 하는 것은 아주 단순하고 쉬운 작업이다. 당장에는 클래스를 바꿔서 사용할 계획이 없더라도 언젠가 변경이 필요한 상황이 닥친다면 수정에 들어가는 시간, 비용을 줄일 수 있다.</li>
<li>클래스의 구현 방식은 바뀌지 않더라도 인터페이스를 두고 DI를 적용해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다. 스프링의 AOP 기법을 적용하기 위해선 기본적으로 DI 적용이 필요하다.</li>
<li>효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야 한다.
DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되기 하는데 중요한 역할을 한다.</li>
</ol>
<h5 id="테스트-코드에-의한-di">테스트 코드에 의한 DI</h5>
<p>테스트를 하면서 실제 운영되는 DB에 사용자 정보를 수정하거나 삭제하는 일이 벌어진다면???
그러지 않으려면, @DirtiesContext로 ApplicationContext 구성이나 상태를 변경한다는 것을 알리고, DataSource의 정보를 다시 생성해서 사용하는 방법이 있다. 하지만, 이미 주입받은 것을 다시 덮어쓰기?하는 느낌으로
별도의 DI를 설정하는 방법이 있다.</p>
<h5 id="테스트를-위한-별도의-di-설정">테스트를 위한 별도의 DI 설정</h5>
<p>책에서는 xml 파일을 별도로 두고, @ContextConfiguration으로 설정 파일의 정보를 알려주는 방식을 알려주고 있지만, 나는 properties 파일을 적용하고 있어서 찾아보았다.</p>
<p><a href="https://stackoverflow.com/questions/32974432/spring-junit-testing-properties-file">https://stackoverflow.com/questions/32974432/spring-junit-testing-properties-file</a></p>
<p>배포용과 로컬 DB 정보를 별도로 두고, 설정해줬던 방법처럼 
application-test.properties 파일을 만들고 경로 지정하려고 하며 검색해보니, 스택오버플로우에 좋은 답변이 있다.</p>
<p>test에서는 굳이 파일명을 다르게 할 필요가 없이, 경로를 main, test 위치만 다르게 하여서 @PropertySource(&quot;classpath:application.properties &quot;)로 경로를 알려주는 방법이 있었다.</p>
<h5 id="컨테이너-없는-di-테스트">컨테이너 없는 DI 테스트</h5>
<pre><code>public class UserDaoTest {
    UserDao dao;

    @Before
    public void setUp() {
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource(&quot;jdbc:mysql://localhost/testdb&quot;, &quot;spring&quot;, &quot;book&quot;, true);
        dao.setDataSource(dataSource);
    }

}</code></pre><p>@Autowired를 하지 않아도 되고, 직접 오브젝트 생성, 관계 설정을 해준다.
Autowired는 하나만 주입 시에는 생략해도, 필드 주입이 가능하고,,,
@Before 어노테이션으로 무조건 해당 메소드가 실행되니까 메소드 내부에서 생성을 해주는 방법이 가능한 것 같다.</p>
<hr>
<h2 id="학습-테스트">학습 테스트</h2>
<h3 id="학습-테스트의-장점">학습 테스트의 장점</h3>
<ol>
<li>다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.</li>
<li>학습 테스트 코드를 개발 중에 참고할 수 있다</li>
<li>프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.</li>
<li>테스트 작성에 대한 좋은 훈련이 된다.</li>
<li>새로운 기술을 공부하는 과정이 즐거워진다.ㅎㅎㅎㅎ</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[querydsl 에서 count, sum 함수 사용하기]]></title>
            <link>https://velog.io/@choitree_/querydsl-%EC%97%90%EC%84%9C-count-sum-%ED%95%A8%EC%88%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0ing</link>
            <guid>https://velog.io/@choitree_/querydsl-%EC%97%90%EC%84%9C-count-sum-%ED%95%A8%EC%88%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0ing</guid>
            <pubDate>Wed, 13 Apr 2022 02:21:49 GMT</pubDate>
            <description><![CDATA[<p>어제 순항중이다가, sum 함수에서 난관을 맞이했다.....
매출 단건 조회의 경우, dto로 받아서 뿌려주면 그만인데
여러 건의 매출 조회는 몇 건인지, 총 얼마의 매출이 발생했는지 약환자와 침환자의 경우에 대해서도 각각 건수와 매출액 집계가 필요했다.</p>
<p>그러면 단 건 조회의 dto을 list로 받아오고
그 외의 값을 가지고 와야하는데, 한 번에 db에서 모든 값을 계산해서 가지고 오는 것이 성능적으로 좋을 것 같다는 생각이 들었다.</p>
<pre><code>select count(case when is_acupuncture = 1 then 0 end) as acupunctureCase,
count(case when is_acupuncture = 0 then 0 end) as medicineCase,
count(*) as totalCase,
sum(if(is_acupuncture = 1, amount, 0)) as acupunctureAmount,
sum(if(is_acupuncture = 0, amount, 0)) as medicineAmount,
sum(amount) as totalAmount
from income
where income.patient_id = 4;</code></pre><p>우선 MySQLWorkbench에서 쿼리문을 작성해서 날려보고 이를 적용해서 repository에 메소드를 생성하려고 한다.</p>
<p>총합이나 전체 카운트의 경우, 어렵지 않았다.
그리고, 조건을 걸어서 카운트 하는 경우도 어렵지 않았지만
조건을 걸어서 합을 구하는 것은 해결하지 못하고 있다..!</p>
<p>구글링을 아무리 해봐도 유사한 경우도 나오지 않는다ㅠㅠㅠ
합계를 구해야하는 컬럼과 조건을 체크해야하는 컬럼이 다르기 때문에 문제가 발생하는 것인데
해결되기 전에 미리 정리해두는 것이 좋을 것 같아 기록중..🫠</p>
<ol>
<li><p>메소드를 두 개 혹은 세 개로 쪼개서, count하는 메소드와 매출액 합계를 내는 메소드를 만들고
나중에 이를 합치는 방법도 있다.
하지만, 이런 식으로 만들 경우 불필요하게 db에 들락날락 거리는 느낌이 든다.</p>
</li>
<li><p>원래 설계대로 테이블을 약 환자와 침 환자를 다시 나누고 집계하는 방식도 있을 것 같다.
오히려 join으로는 sum을 구하기 쉬울 것 같고, 단순히 합계의 경우 각각 구한 값을 dto단에서 더해서 리턴해줄 수 도 있을 것 같다.
하지만, 미수금 또는 결제 수단에 따른 합계를 계산한다고 하면 결국 똑같은 문제가 발생할 것 같다.
(각 테이블 내에서 카드결제 집계, 현금결제 집계를 하면 동일한 문제 직면)</p>
</li>
</ol>
<hr>
<p>CaseBuilder와 이용해서 메소드를 만들었는데, InvalidDataAccessApiUsageException가 발생했다.</p>
<p>ExpressionUtils를 이용해서 만든 메소드는 값이 제대로 추출되지 않는다.</p>
<p>너무 이 기능에 많은 시간을 할애하고 있어서
우회해서 처리하는 방식을 생각해보니, 어차피 List로 가지고 오는 값이 있으니까
그 값을 stream으로 더하거나 카운트할 수 있다는 생각이 들었다.</p>
<pre><code>@RequiredArgsConstructor
@Getter
@Builder
public class IncomeSummeryResponseDTO {

    private final long totalCase;
    private final long acupunctureCase;
    private final long medicineCase;

    private final long totalAmount;
    private final long acupunctureAmount;
    private final long medicineAmount;

    private final List&lt;IncomeResponseDTO&gt; incomeResponseDTOS;

    public static IncomeSummeryResponseDTO from(List&lt;IncomeResponseDTO&gt; incomeResponseDTOS) {

        long medicineAmount = incomeResponseDTOS.stream()
                .filter(incomeResponseDTO -&gt; incomeResponseDTO.getIsAcupuncture() == false)
                .mapToLong(incomeResponseDTO -&gt; incomeResponseDTO.getAmount())
                .sum();
        long acupunctureAmount = incomeResponseDTOS.stream()
                .filter(incomeResponseDTO -&gt; incomeResponseDTO.getIsAcupuncture() == true)
                .mapToLong(incomeResponseDTO -&gt; incomeResponseDTO.getAmount())
                .sum();
        long medicineCase = incomeResponseDTOS.stream()
                .filter(incomeResponseDTO -&gt; incomeResponseDTO.getIsAcupuncture() == false)
                .count();

        long acupunctureCase = incomeResponseDTOS.stream()
                .filter(incomeResponseDTO -&gt; incomeResponseDTO.getIsAcupuncture() == true)
                .count();

        return IncomeSummeryResponseDTO.builder()
                .incomeResponseDTOS(incomeResponseDTOS)
                .medicineAmount(medicineAmount)
                .acupunctureAmount(acupunctureAmount)
                .totalAmount(medicineAmount + acupunctureAmount)
                .medicineCase(medicineCase)
                .acupunctureCase(acupunctureCase)
                .totalCase(medicineCase + acupunctureCase)
                .build();
    }
}</code></pre><p>List로 가지고 온 값이니까, DB에는 다시 들리지 않아도 되지만
찜찜한 기분이 든다....😭😭😭</p>
<p>stream으로 연산하는 것과 repository에 다시 접근해서 count,sum 하는 방법 중에서 어떤 것이 메모리나 시간적으로 효율적일지 알아볼 필요가 있다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[jpa 복합키 매핑]]></title>
            <link>https://velog.io/@choitree_/jpa-%EB%B3%B5%ED%95%A9%ED%82%A4-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@choitree_/jpa-%EB%B3%B5%ED%95%A9%ED%82%A4-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Mon, 11 Apr 2022 15:59:13 GMT</pubDate>
            <description><![CDATA[<p>patient와 income을 매핑해놓고, income에 name 컬럼을 두는 것은 좋은 설계가 아니라는 생각이 들었다.
그러면 복합키로 설정하면, 이 문제가 해결될 것으로 생각하고 우선 income 테이블의 name 컬럼을 삭제하고 복합키 설정을 했다.</p>
<pre><code>@Entity
@Table(name = &quot;patient&quot;, uniqueConstraints = { @UniqueConstraint(name = &quot;UniqueNameAndBirthday&quot;, columnNames = { &quot;name&quot;, &quot;birthday&quot; }) })
public class Patient {

    @OneToMany(mappedBy = &quot;patient&quot;, fetch = FetchType.LAZY)
    private List&lt;Income&gt; incomes = new ArrayList&lt;&gt;();
}</code></pre><p>일대다 관계인 환자 테이블에는 fetch를 LAZY로 추가해줬다.</p>
<pre><code>@Entity
@Table(name = &quot;income&quot;)
public class Income {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns(value = {
            @JoinColumn(name = &quot;patient_id&quot;, referencedColumnName = &quot;id&quot;),
            @JoinColumn(name = &quot;patient_name&quot;, referencedColumnName = &quot;name&quot;),
            @JoinColumn(name = &quot;patient_birthday&quot;, referencedColumnName = &quot;birthday&quot;)
    } , foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
    private Patient patient;
}</code></pre><p>다대일 관계인 매출 테이블에는 JoinColumns에 정보를 추가해줬다.</p>
<p>다시 생각하니, 복합키로 매핑할 필요가 없고
DTO에서만 이름, 생일을 받아서 충분히 처리가 가능한 상황이다..
심지어 매핑한 부분으로 로직이 건들여지는 부분도 없어...!!
그래서 코드에서는 이 내용을 삭제했지만, 좋은 학습이였다~~</p>
<blockquote>
<p>참고한 블로그 및 사이트
<a href="https://www.baeldung.com/jpa-join-column">https://www.baeldung.com/jpa-join-column</a>
<a href="https://minssan9.tistory.com/m/42">https://minssan9.tistory.com/m/42</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[jpa에서 unique 제약조건 설정하기]]></title>
            <link>https://velog.io/@choitree_/jpa%EC%97%90%EC%84%9C-unique-%EC%A0%9C%EC%95%BD%EC%A1%B0%EA%B1%B4-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choitree_/jpa%EC%97%90%EC%84%9C-unique-%EC%A0%9C%EC%95%BD%EC%A1%B0%EA%B1%B4-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 11 Apr 2022 08:56:33 GMT</pubDate>
            <description><![CDATA[<p>환자 정보에 대해서, 매출에서 pathvariable로 받는 환자와 매출의 환자가 동일한지 비교하기 위해서
2가지 정보 이상을 매칭 시켜서 unique 제약조건 설정이 필요했다.</p>
<p>봉프가 일반적으로 병원에 갔을 때, 생일과 이름으로 대조해보지 않느냐며 아이디어를 제공해줘서
이름과 생일을 unique 제약조건으로 만들어야겠다고 생각했다.</p>
<p><del>그리고 pId라고 명시한, 병원에서 이미 가지고 있던 차트번호의 경우
자동 생성되는 patient의 id와 혼돈이 있어서 chartId로 네이밍을 변경했다.</del></p>
<p>여튼 제약조건 설정은 엄청 간단하다..!</p>
<h3 id="table-어노테이션에-추가로-명시">@Table 어노테이션에 추가로 명시</h3>
<p>@Table(uniqueConstraints = { @UniqueConstraint(name = &quot;UniqueNumberAndStatus&quot;, columnNames = { &quot;personNumber&quot;, &quot;isActive&quot; }) })</p>
<p><a href="https://www.baeldung.com/jpa-unique-constraints">baeldung</a>에서 확인한 내용인데, 간단하게 UniqueConstraint 어노테이션을 추가하고, 제약조건의 이름과 제약조건에 해당하는 컬럼 이름을 명시하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Querydsl 설정하기]]></title>
            <link>https://velog.io/@choitree_/Querydsl-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choitree_/Querydsl-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 10 Apr 2022 01:37:23 GMT</pubDate>
            <description><![CDATA[<p>오랜만에 블로그를 작성하는 것 같다.
querydsl 강의를 듣고, 쿼리를 짜고 이제 얼추 로직도 구현이 되었다.
JpaRepository로 기본적인 find메소드나 자동완성으로 select 기능을 사용하다가, 디테일한 select가 필요해서 작업 중간에 querydsl을 학습하고 구현하기 시작했다.</p>
<p>하나의 엔티티에 레포지토리를 2개두고, querydsl을 사용하는 클래스에서는 메소드를 구현해서 사용을 했다.
querydsl 관련한 config 클래스도 만들어서 테스트를 한 후, 정상적으로 작동하는 것을 확인했지만 자꾸 nullpointerException이 뜬다..!!!!
patient를 못찾아서 오류가 생긴거라고 생각하고, 정보가 제대로 전달되는지 확인했는데 거기가 문제가 아니였다.
문제가 뭔지도 찾는데 한참 걸렸는데ㅠㅠ 문제는 JPAQueryFactory!!
그니까 레포지토리 자체를 찾지 못하는 문제였다..</p>
<p>우선 레포지토리는 당연하겠지만, 한 엔티티에 하나씩 사용이 가능하다. 나는 두 개의 레포지토리에 모두 어노테이션을 붙여서 못 읽은 것이라고 생각하고 JpaRepository를 상속받은 기본 인터페이스는 주석 처리했지만, 레포지토리의 메소드를 읽어 오지 못했다.</p>
<p>querydsl를 설정하는 방법은 총 3가지가 있다고 하는데, 나는 그 중에 가장 간단한 방법인 config 파일을 만들고, 레포지토리 내부에서 JPAQueryFactory를 선언해서 사용하는 방법으로 코드를 작성했다.
근데 그 부분에서 문제가 있었던 것인지 레포에 아예 접근을 하지 못했다.</p>
<p>그래서, RepositoySupport를 사용하는 방법과 
RepositoryCustom, RepositoryImpl을 사용하는 방법 두 가지 모두 테스트해보았고 정상 작동하는 것을 확인했다.</p>
<p>그러면 내가 만들어서 동작한 코드에 대해서 살펴보겠다.</p>
<hr>
<p>우선 공통적으로는 JPAQueryFactory의 빈 등록이 필요하다.
두 가지 모두 config 클래스를 만들어 준다.
<del>설정을 변경하면서, 클래스 자체를 주석처리 해봤는데, 빈을 찾을 수 없다고 하며 컴파일 과정에서 실행이 실패되었다.</del></p>
<pre><code>
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class QuerydslConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}</code></pre><h2 id="1-querydslrepositorysupport">1. QuerydslRepositorySupport</h2>
<h3 id="장점">장점</h3>
<ul>
<li><p>스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능(단, Sort는 오류 발생)
  : getQuerydsl().applyPagination()</p>
</li>
<li><p>from()으로 시작 가능</p>
</li>
<li><p>EntityManager 제공</p>
<h3 id="한계">한계</h3>
</li>
<li><p>Querydsl 3.x 버전을 대상으로 만들었다.</p>
</li>
<li><p>Querydsl 4.x에서 나온 JPAQueryFactory로 시작할 수 없어서 select로 시작할 수 없음</p>
</li>
<li><p>QueryFactory를 제공하지 않음</p>
</li>
<li><p>스프링 데이터 Sort 기능이 정상 동작하지 않음</p>
<pre><code>@Repository
public class IncomeQueryRepositorySupport extends QuerydslRepositorySupport {

  private final JPAQueryFactory queryFactory;

  public IncomeQueryRepositorySupport(JPAQueryFactory queryFactory) {
      super(Income.class);
      this.queryFactory = queryFactory;
  }

</code></pre></li>
</ul>
<pre><code>public Optional&lt;Income&gt; findLastVisitIncomeByPatient (Patient patient) {
    Income income = queryFactory.selectFrom(QIncome.income)
            .where(QIncome.income.patient.eq(patient))
            .orderBy(QIncome.income.date.desc())
            .fetchFirst();
    return Optional.ofNullable(income);
}

...</code></pre><p>}</p>
<pre><code>[이동욱님 블로그](https://jojoldu.tistory.com/372)를 참고해서 만들었고, 위에 코드는 내가 만든 support 클래스를 그대로 가지고 왔다.
QuerydslRepositorySupport를 상속받은 클래스를 레포지토리로 만들고,
생성자에서 JPAQueryFactory를 주입받아서 사용했다.
~~위에 한계에서는 JPAQueryFactory로 시작할 수 없어서 select문으로 사용이 불가능하다고 했는데, select로 시작해도 정상적으로 코드가 잘 돌아갔다.~~

이렇게 사용하면, JpaRepository를 상속받은 기본적인 인터페이스 레포지토리가 하나 더 필요하다.
save, delete 등은 해당 레포지토리로 사용하고, querydsl을 사용한 코드는 support 레포지토리에서 호출해서 사용하는 방식으로 쓰면 된다.

## 2. Custom, Impl, Repository
위의 방식을 사용할 경우, 2개의 레포지토리를 의존성으로 받아야 하는 단점이 있어서 
Custom Repository를 JpaRepository 상속 클래스에서 사용할 수 있도록 만들어봅니다.

### (1)custom interface 만들기</code></pre><p>public interface IncomeRepositoryCustom {</p>
<pre><code>public Long countVisitByPatient(Patient patient);</code></pre><p>}</p>
<pre><code>사용할 추상 메소드를 만들어준다.
### (2)Impl class 만들기
</code></pre><p>@RequiredArgsConstructor
public class IncomeRepositoryImpl implements IncomeRepositoryCustom{</p>
<pre><code>private final JPAQueryFactory queryFactory;

public Long countVisitByPatient(Patient patient) {
    return queryFactory.selectFrom(QIncome.income)
            .where(QIncome.income.patient.eq(patient))
            .fetchCount();
}</code></pre><p>}</p>
<pre><code>위에서 만든 인터페이스를 구현한 구현체 클래스를 만들어주고, 추상 메소드를 override한다.
JPAQueryFactory를 주입받는다.
### (3)Repository interface 만들기</code></pre><p>@Repository
public interface IncomeRepository extends JpaRepository&lt;Income, Long&gt;, IncomeRepositoryCustom {</p>
<p>}</p>
<p>```
JpaRepository, Custom 인터페이스를 상속하는 인터페이스를 만들고 @Repository 어노테이션을 붙여준다.
만약 페이징 처리가 필요하다면, QuerydslSupport도 상속하는 것 같다.</p>
<hr>
<p>처음에는 파일을 많이 만들고 싶지 않아서 JPAQueryFactory를 필드 주입받아서 레포지토리 클래스를 만드는 방식으로 사용했는데, 원하는대로 작동이 하지 않아서 나머지 두 가지 방식을 사용해보았다.</p>
<p>첫 번째 방법도 결국 레포지토리 2개에 의존받는 것은 동일했고, 두 번째 방법은 파일(클래스/인터페이스)가 더 많이 생기는 것은 있지만 실질적으로 의존성으로 받는 레포지토리는 1개라는 점에 차이가 있었다.</p>
<p>가장 간단한 방법은 실패했지만, 정상적으로 작동한다면 해당 내용도 정리해서 추가해야지!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트]]></title>
            <link>https://velog.io/@choitree_/%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@choitree_/%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Sat, 09 Apr 2022 09:52:17 GMT</pubDate>
            <description><![CDATA[<p>p.145 ~ 183</p>
<h3 id="테스트의-유용성">테스트의 유용성</h3>
<p>테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 또한 테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다. 이를 통해 코드의 결함을 제거해가는 작업, 일명 디버깅을 거치게 되고, 결국 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있따.</p>
<p>웹 화면을 통해 값을 입력하고, 기능을 수행하고, 결과를 확인하는 방법은 가장 흔히 쓰이는 방법이지만, DAO에 대한 테스트로서는 단점이 너무 많다. DAO뿐만 아니라 서비스 클래스, 컨트롤러, JSP 뷰 등 모든 레이어 기능을 다 만들고 나서야 테스트가 가능하다는 점이 가장 큰 문제이다. 테스트를 하는 중에 에러가 나거나 테스트가 실패했다면, 과연 어디에서 문제가 발생했는지를 찾아내야 하는 수고도 필요하다.</p>
<h3 id="작은-단위의-테스트">작은 단위의 테스트</h3>
<p>작은 단위의 코드에 대해 테스트를 수행하는 것을 단위 테스트(unit test)라고 한다.
단위는 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위의 단위라고 보면 되고, 일반적으로 단위는 작을수록 좋다.
테스트 중에 DB가 사용되면 단위 테스트가 아니라고 하기도 하는데,
DB의 상태가 매번 달라지고, 테스트를 위해 DB를 특정 상태로 만들어줄 수 없다면 단위 테스트로서 가치가 없어지기 때문에, 그런 차원에서는 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 하는 것이다.</p>
<p>단위 테스트를 하는 이유는 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서다.</p>
<h3 id="junit">JUnit</h3>
<p>Junit은 프레임워크로, 메인 메소드가 필요하지 않다.</p>
<h4 id="junit-의-조건">JUnit 의 조건</h4>
<ul>
<li>메소드가 public으로 선언되어야 한다.
(접근제어자는 생략하더라도, public으로 생성되는 것으로 알고 있음.)</li>
<li>메소드에 @Test라는 애노테이션을 붙여주어야 한다.</li>
</ul>
<p>테스트 메소드에서 예외 상황이 발생할 수 있는 경우에는 애노테이션에 excepted 엘리먼트를 명시해주면 된다.</p>
<pre><code>@Test(expected = EmptyResultAccessException.class)
public void getUserFailure() throws SQLException {
    ~~~
}</code></pre><h4 id="포괄적인-테스트">포괄적인 테스트</h4>
<p>스프링의 창시자인 로드 존슨은 &quot;항상 네거티브 테스트를 먼저 만들라&quot;라는 조언을 했다고 한다.
테스트를 작성할 때는 부정적인 케이스를 먼저 만드는 습관을 들이는 것이 좋다.
존재하는 정보에 대한 확인을 하는 테스트도 좋지만, 존재하지 않는 객체에 대해서는 어떻게 반응할지를 먼저 결정하고, 이를 확인할 수 있는 테스트를 먼저 만들려고 한다면 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발이 가능하다.</p>
<h4 id="tdd">TDD</h4>
<p>TDD에서는 테스트 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 짧게 가져가도록 권장한다.</p>
<h3 id="annotation">Annotation</h3>
<h4 id="before-beforeeach">@Before (=@BeforeEach)</h4>
<p>모든 테스트 메소드가 실행되기 전에 각각 실행되는 메소드</p>
<h4 id="beforeall">@BeforeAll</h4>
<p>테스트 클래스가 실행되었을 때 한 번 실행되는 메소드</p>
<h4 id="after-aftereach">@After (=@AfterEach)</h4>
<p>모든 테스트 메소드가 실행된 후 각각 실행되는 메소드</p>
<h4 id="afterall">@AfterAll</h4>
<p>테스트 클래스가 끝나기 전에 마지막으로 한 번 실행되는 메소드</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[의존관계 주입(DI)]]></title>
            <link>https://velog.io/@choitree_/%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%A3%BC%EC%9E%85DI</link>
            <guid>https://velog.io/@choitree_/%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%A3%BC%EC%9E%85DI</guid>
            <pubDate>Sat, 02 Apr 2022 18:25:33 GMT</pubDate>
            <description><![CDATA[<p>p.111 ~ 128</p>
<p>스프링의 기본적인 동작 원리가 모두 IoC 방식이라고 할 수 있지만, 스프링이 다른 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라는 새로운 용어를 사용할 때 분명하게 드러난다.
그래서 초기에는 IoC컨테이너라고 불리던 스프링이 지금은 의존관계 주입 컨테이너, DI 컨테이너라고 더 많이 불린다고 한다.</p>
<p>제어의 역전은 쉽게 생각해서 내가 작성한 코드가 직접 제어의 흐름을 담당하지 않고, 그 제어권을 프레임워크가 담당하는 것이라고 이해하면 되는데 라이브러리와 프레임워크의 차이도 그렇게 이해하면 될 것 같다.
제어권을 코드가 가지고 있다면 라이브러리를 사용한 것, 제어권을 가지고 있지 않다면 프레임워크</p>
<p>스프링 컨테이너는 빈이 등록되는 컨테이너라고 생각하면 되는데, 빈은 어노테이션으로 자동 등록되고
거기서 ApplicationContext(BeanFactory)를 통해 빈을 꺼내서 사용할 수 있다.</p>
<hr>
<p>이번 주 내내 프로젝트를 구현하면서, 인터페이스의 구현체를 만드는 것이 의미가 있는지 오히려 파일 자체를 하나 더 만들게 되니까 번거로운 작업이 아닌가 고민했다. 뭐 인터페이스를 상속받아서 구현하는 클래스가 다양한 경우(책에서는 N사, D사와 같은 경우)가 있을 수 있다지만, 솔루션을 만드는 경우도 아니라면 더욱 왜 그렇게까지 하는건지 이해가 안되었는데 의존관계가 여기의 실마리였다!</p>
<h3 id="의존관계">의존관계</h3>
<p><img src="https://media.vlpt.us/images/choitree_/post/6ae30b6d-bcb4-4026-ae57-0fcff5a974de/ab%20%E1%84%8B%E1%85%B4%E1%84%8C%E1%85%A9%E1%86%AB%E1%84%80%E1%85%AA%E1%86%AB%E1%84%80%E1%85%A8.png" alt="">
클래스 A가 B에 의존하고 있음을 나타내는 UML 다이어그램이다.
의존한다는 것은 의존대상(B)가 변하면 그것이 A에 영향을 미친다는 것을 뜻한다.
대표적인 예는 A가 B에 정의된 메소드를 호출해서 사용하는 경우다.
만약 B에 새로운 메소드가 추가되거나 기존 메소드가 변경되면 A도 그에 따라서 수정해야 한다.</p>
<p><img src="https://media.vlpt.us/images/choitree_/post/7706080b-700a-4e90-a784-04c7e27e9113/%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%90%E1%85%A5%E1%84%91%E1%85%A6%E1%84%8B%E1%85%B5%E1%84%89%E1%85%B3%E1%84%85%E1%85%B3%E1%86%AF%E1%84%90%E1%85%A9%E1%86%BC%E1%84%92%E1%85%A1%E1%86%AB%E1%84%82%E1%85%B3%E1%84%89%E1%85%B3%E1%86%AB%E1%84%92%E1%85%A1%E1%86%AB%E1%84%80%E1%85%A7%E1%86%AF%E1%84%92%E1%85%A1%E1%86%B8.png" alt=""></p>
<p>만약 클래스끼리 직접적인 의존관계가 아닌 클래스와 인터페이스간 의존관계가 있고, 인터페이스를 구현한 클래스를 사용한다면 구현체가 변경되거나 내부의 메소드에 변화가 생겨도 UserDao에는 영향을 주지 않는다.
이렇게 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 된다. 결합도가 낮다고 설명할 수 있다.</p>
<hr>
<h3 id="의존관계-주입">의존관계 주입</h3>
<p>주입이라는 건 외부에서 내부로 무엇인가를 넘겨줘야 하는 것인데, 자바에서 오브젝트에 무엇인가를 넣어준다는 개념은 메소드를 실행하면서 파라미터로 오브젝트의 레퍼런스를 전달해주는 방법뿐이다. 가장 손쉽게 사용할 수 있는 파라미터 전달이 가능한 메소드는 바로 생성자다.(p.116)</p>
<p>DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다. 스프링 컨테이너의 IoC는 주로 의존관계 주입 또는 DI라는 데 초점이 맞춰져 있다. 그래서 스프링을 IoC 컨테이너 외에도 DI 컨테이너 또는 DI 프레임워크라고 부르는 것이다.(p.117)</p>
<h3 id="의존관계-검색dependency-lookup과-주입">의존관계 검색(Dependency Lookup)과 주입</h3>
<p>스프링에서는 의존관계 주입 이외에도 의존관계를 스스로 검색해서 이용하는 의존관계 검색(DL)도 있다.
의존관계 검색이 바로 스프링 컨테이너에서 getBean()을 통해서 빈을 찾아서 오브젝트를 가져오는 방법인데, 테스트 코드등을 작성할 때는 static 메소드인 main 메소드에서는 DI를 이용해서 오브젝트를 주입받을 방법이 없기 때문에 의존관계 검색을 사용해서 오브젝트를 가지고 와야 한다.</p>
<p>DI와 DL의 차이점은, DI를 원하는 오브젝트는 자기 자신도 컨테이너가 관리하는 빈으로 등록되어야 하고
DL의 경우에는 자기 자신은 빈으로 등록되어야 할 필요가 없다는 점이다.</p>
<h3 id="메소드를-이용한-의존관계-주입">메소드를 이용한 의존관계 주입</h3>
<p>의존관계는 생성자, 수정자(setter), 메소드, 필드를 이용해서 주입이 가능하다.
수정자의 경우, 1개의 파라미터만 가질 수 있지만, 메소드를 통한 주입은 여러 파라미터를 가질 수 있다.</p>
<pre><code>//수정자 메소드를 이용한 DI
public class UserDao {
    private ConnectionMaker connectionMaker;

    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[꼬리의 꼬리를 무는 querydsl]]></title>
            <link>https://velog.io/@choitree_/%EA%BC%AC%EB%A6%AC%EC%9D%98-%EA%BC%AC%EB%A6%AC%EB%A5%BC-%EB%AC%B4%EB%8A%94-querydsl</link>
            <guid>https://velog.io/@choitree_/%EA%BC%AC%EB%A6%AC%EC%9D%98-%EA%BC%AC%EB%A6%AC%EB%A5%BC-%EB%AC%B4%EB%8A%94-querydsl</guid>
            <pubDate>Thu, 31 Mar 2022 17:30:26 GMT</pubDate>
            <description><![CDATA[<p>querydsl의 강의를 보니, EntityManager가 나온다. Spring Boot를 사용하면, 내가 직접 EntityManager를 사용하지 않더라도 Application이 시작될 때, 자동으로 EntityManager가 bean을 등록한다고 한다.</p>
<h3 id="entitymanager">EntityManager</h3>
<p>EntityManager는 말 그대로, 엔티티를 관리하는 역할을 수행하는 클래스라고 한다.
엔티티 매니저는 내부에 영속성 컨텍스트(Persistence Context)를 두고 엔티티를 관리한다고 한다.
영속성은 JPA의 P....?
영속성 컨텍스트는 엔티티를 영구적으로 저장하는 환경이라고 하는데, 영속성이라는 단어는 봐도봐도 
레이어를 그림으로 보더라도 와닿지는 않는다ㅠㅠ</p>
<h3 id="jpaqueryfactory">JPAQueryFactory</h3>
<h3 id="crudrepository--jparepository">CrudRepository &amp; JpaRepository</h3>
<p><img src="https://images.velog.io/images/choitree_/post/16a2d2aa-5853-49c7-b031-57571081a42b/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-01%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.29.18.png" alt="">
JPA를 사용하더라도, CrudRepository만 사용했는데, JpaRepository가 있었다...!
역시 프로젝트를 구상하고 만들어가는 것이 그냥 개념 학습하는 것보다 더 많은 키워드를 접하고 학습하게 된다.
필요에 의해서 찾아보고 적용해보니까 그런 것 같다!!</p>
<p>여튼! JpaRepository도 결국 CrudRepository가 부모인 인터페이스다.
더 하위로 들어가면, JpaRepositoryImplementation&lt;T,ID&gt;가 자녀고, QuerydslJpaRepository&lt;T,ID extends Serializable&gt;가 자녀의 자녀다.
구현체같은 이름을 가진 JpaRepositoryImplementation&lt;T,ID&gt;는 인터페이스고
QuerydslJpaRepository&lt;T,ID extends Serializable&gt;는 인터페이스가 아닌 클래스다.</p>
<p><img src="https://images.velog.io/images/choitree_/post/f0b4f2c7-6021-4fe4-b95d-bef4ac4b4e67/image.png" alt="">
전부 그려보자면 이런 모양이라고 할 수 있다.</p>
<p>결국 두 인터페이스의 차이는 페이징, 소팅 작업에 대한 기능의 추가라고 이해해도 무방할 것 같다.
CrudRepository는 Iteratable을 리턴했으면, JpaRepository는 List를 리턴하는 차이도 있다. </p>
<p>이전까지는 페이징 처리하는 작업이 없어서 CrudRepository로 충분했지만, 지금 프로젝트에는 JpaRepository를 적용해서 사용해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Querydsl 도입!]]></title>
            <link>https://velog.io/@choitree_/Querydsl-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@choitree_/Querydsl-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Thu, 31 Mar 2022 15:16:28 GMT</pubDate>
            <description><![CDATA[<p>로직을 작성하다보니, 생각보다 복잡하다!
income이 추가될 때마다, patient의 lastVisit 날짜가 업데이트되어야 하고
마찬가지로 수정, 삭제 시에도 날짜가 반영되어야 한다.</p>
<p>날짜는 income중에서도 해당 환자의 가장 최근 방문일을 찾아서, 그 날짜로 반영되어야 한다.</p>
<pre><code>select date from income
where patient_id = 11
order by date DESC limit 1;</code></pre><p>간단하게는 limit를 걸어서 날짜를 추출할 수 있다!
하지만 쿼리문을 반영하기 위해서는? 쿼리문 반영은 여기서 끝일 것인가????</p>
<p>그래서 querydsl을 학습하고 도입해보기로 했다.
부랴부랴 인프런 강의 결제!ㅋㅋㅋㅋㅋㅋ</p>
<p>강의를 순서대로 따라하되, 시간이 부족하니 내 프로젝트에 바로 반영해보려고 한다.</p>
<p><img src="https://images.velog.io/images/choitree_/post/381e971d-f5cf-4573-8204-4c5ce1daed09/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-31%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.14.58.png" alt=""></p>
<p><img src="https://images.velog.io/images/choitree_/post/337aa971-417e-4469-9859-38279f07bf94/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-31%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.15.20.png" alt=""></p>
<p><img src="https://images.velog.io/images/choitree_/post/b9be9183-54a3-4f32-b754-c1d81af27e63/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-31%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.15.33.png" alt=""></p>
<p>build.gradle에 plugins, dependencies 및 querdsl의 디렉토리 정보 등을 추가해준다.
$buildDir/generated/querydsl은 Q클래스가 생성되는 위치다.</p>
<p><img src="https://images.velog.io/images/choitree_/post/57211619-a541-489d-8924-4c585d067f38/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-31%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.15.57.png" alt=""></p>
<p>그리고 other &gt; compileQuerydsl을 실행하면
<img src="https://images.velog.io/images/choitree_/post/4f58ffc6-bfb7-46b2-9829-61574d975ff1/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-31%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.16.47.png" alt="">
Q클래스가 생성된 것을 확인할 수 있다.</p>
<p>git에는 Q클래스를 올리면 안된다고 한다!!
일단은 설정 완료!!!!!!!</p>
]]></description>
        </item>
    </channel>
</rss>