<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>팡트루야</title>
    <link>https://pangtrue.tistory.com/</link>
    <description>배운 것을 정리하는 목적의 블로그입니다. ^__^</description>
    <language>ko</language>
    <pubDate>Sun, 10 May 2026 13:01:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>팡트루야</managingEditor>
    <image>
      <title>팡트루야</title>
      <url>https://tistory1.daumcdn.net/tistory/2734456/attach/9cf391a7768f44378171ad8eaad2a819</url>
      <link>https://pangtrue.tistory.com</link>
    </image>
    <item>
      <title>[Javascript] 프로토타입과 상속</title>
      <link>https://pangtrue.tistory.com/357</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 프로토타입과 상속&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짚고 넘어가야할 Javascript 특징은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Javascript에서 함수는 Javascript 내장 객체인 Function의 생성자로 생성된 객체입니다.&lt;/li&gt;
&lt;li&gt;Javascript에서 상속은 생성자 함수의 프로토타입을 활용합니다.&lt;/li&gt;
&lt;li&gt;Javascript에서 프로토타입은 다른 객체에 공유 프로퍼티를 제공하는 객체입니다.&lt;/li&gt;
&lt;li&gt;함수 객체만이 프로토타입을 가집니다.&lt;br /&gt;함수 객체만이 호출이 가능하고 다른 객체를 생성할 수 있기 때문입니다.&lt;br /&gt;ES6의 화살표 함수는 프로토타입을 가지지 않습니다.&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;br /&gt;new 키워드로 함수를 호출할 때마다 해당 제품의 주문이 들어가고 공장은 프로토타입에 지정된 방식으로 생산합니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;// User 생성자 함수를 만듭니다.
function User(name, interests) {
  this.name = name;
  this.interests = interests;
}
User.prototype.greeting = function() {
  console.log('Hi, I\'m ' + this.name + '.');
}

// 생성자 객체를 역참조하고 있는 `constructor` 프로퍼티를 이용해 누가 객체를 생성했는지 확인할 수 있습니다.
console.log(User.constructor === Function); // true 출력 

// User 생성자 함수가 생성되면 프로토타입 객체를 가집니다.
// User 생성자 함수 안의 프로토타입 객체는 User 생성자 함수에 의해 생성되었음을 확인해볼 수 있습니다.
console.log(User.prototype.constructor === User); // true 출력 

// User 생성자 함수로 user 객체를 생성하면,
// user 객체는 User 생성자 함수의 프로토타입 객체를 참조하는 `__proto__` 프로퍼티를 가집니다.
// 해당 `__proto__` 프로퍼티는 프로토타입 체인에서 링크 역할을 담당합니다.
var user = new User();
console.log(user.__proto__ === User.prototype) // true 출력 &lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// User 생성자 함수를 만듭니다.
function User(name, interests) {
  this.name = name;
  this.interests = interests;
}
User.prototype.greeting = function() {
  console.log('Hi, I\'m ' + this.name + '.');
}

// User 생성자 함수의 .call() 메서드를 호출하는 형태는 자바에서 super()를 호출하는 것과 유사합니다.
// 한 가지 차이점은 call() 메서드의 첫 번째 인자는 객체여야 하며, 이 객체가 실행 컨텍스트의 역할을 합니다.
function TeamMember(name, interests, tasks) {
  User.call(this, name, interests);
  this.tasks = tasks;
}

// 하위 클래스는 상위 클래스를 확장합니다.
// TeamMember 생성자 함수에 의해 생성된 객체들은 User 프로토타입 객체의 프로퍼티를 가지고,
// 각 TeamMember의 객체는 TeamMember 프로토타입을 연결하는 __proto__ 프로퍼티를 가지게 됩니다.
TeamMember.prototype = Object.create(User.prototype);

// User 생성자 함수의 프로토타입 객체의 `greeting()` 메서드를 오버라이딩합니다.
TeamMember.prototype.greeting = function() {
  console.log('I\'m ' + this.name + '. Welcome to the team!');
};

TeamMember.prototype.work = function() {
  console.log('I\'m working on ' + this.tasks.length + ' tasks');
};

console.log(User.prototype === TeamMember.prototype); // false 출력
console.log(User.prototype.constructor === TeamMember.prototype.constructor); // true

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 프로토타입 체인&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;2100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLHXbj/btrvJ8V4ZS1/7pH6Sws1sWQ6ZGmkTkpPp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLHXbj/btrvJ8V4ZS1/7pH6Sws1sWQ6ZGmkTkpPp0/img.png&quot; data-alt=&quot;[그림 1] prototype 객체와 prototype 체인을 통한 상속&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLHXbj/btrvJ8V4ZS1/7pH6Sws1sWQ6ZGmkTkpPp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLHXbj%2FbtrvJ8V4ZS1%2F7pH6Sws1sWQ6ZGmkTkpPp0%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;500&quot; height=&quot;569&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;2100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 1] prototype 객체와 prototype 체인을 통한 상속&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상속(프로토타입 체인)을 아래와 같이 확인해볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1647086052514&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 프로토타입 체인 확인하기
console.log(member.__proto__ === TeamMember.prototype); // true 출력
console.log(TeamMember.__proto__ === User.prototype);   // true 출력
console.log(User.__proto__ === Object.prototype);       // true 출력

// 위 방법보다 더 나은 방법
console.log( TeamMember.prototype.isPrototypeOf(member) );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 __proto__ 프로퍼티는 정말 조심해서 사용해야 합니다.&lt;br /&gt;실수로 __proto__ 프로퍼티를 다른 것으로 변경하면 상속이 깨지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속받지 않은(프로토타입 체인으로 거슬러 올라가지 않아도 되는) 자기 자신의 프로퍼티를 확인해보고 싶다면 아래의 방법을 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1647086335922&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;member.hasOwnProperty('name'); // true 출력
member.hasOwnProperty('move'); // false 출력&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Web/Javascript</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/357</guid>
      <comments>https://pangtrue.tistory.com/357#entry357comment</comments>
      <pubDate>Sat, 12 Mar 2022 20:51:22 +0900</pubDate>
    </item>
    <item>
      <title>[CI CD] Jenkins와 Gitlab 연동 및 CI/CD 구축하기</title>
      <link>https://pangtrue.tistory.com/356</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Jenkins 플러그인 설치&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, Jenkins와 Gitlab을 연동하여 CI/CD를 구축하기 위해선 다음 플러그인을 설치해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1645190567014&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. Bitbucket Pipeline for Blue Ocean
2. Dashboard for Blue Ocean
3. Personalization for Blue Ocean
4. Display URL for Blue Ocean
5. Server Sent Events (SSE) Gateway
6. Events API for Blue Ocean
7. Blue Ocean Pipeline Editor
8. i18n for Blue Ocean
9. Autofavorite for Blue Ocean
10. Blue Ocean
11. NodeJS
12. GitLab
13. Generic Webhook Trigger
14. Gitlab Authentication
15. Gitlab API
16. GitLab Branch Source
17. Gitlab Merge Request Builder
18. Config File Provider
19. Docker
20. Docker Pipeline
21. docker-build-step&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 플러그인들을 설치하기 위해 아래 화면의 [Jenkins 관리] -&amp;gt; [플러그인 관리]를 들어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;1106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJMTjB/btrtH0xX7tJ/uBx6hp8kBLU0QFZB0lYfO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJMTjB/btrtH0xX7tJ/uBx6hp8kBLU0QFZB0lYfO0/img.png&quot; data-alt=&quot;[그림 1] Jenkins 플러그인 설치&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJMTjB/btrtH0xX7tJ/uBx6hp8kBLU0QFZB0lYfO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJMTjB%2FbtrtH0xX7tJ%2FuBx6hp8kBLU0QFZB0lYfO0%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;860&quot; height=&quot;277&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;1106&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 1] Jenkins 플러그인 설치&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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-origin-width=&quot;3076&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UaKwb/btrtIUc0ump/ZRRKjdwMpkzQk1vPocHsw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UaKwb/btrtIUc0ump/ZRRKjdwMpkzQk1vPocHsw1/img.png&quot; data-alt=&quot;[그림 2] Jenkins 플러그인 설치&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UaKwb/btrtIUc0ump/ZRRKjdwMpkzQk1vPocHsw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUaKwb%2FbtrtIUc0ump%2FZRRKjdwMpkzQk1vPocHsw1%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;3076&quot; height=&quot;696&quot; data-origin-width=&quot;3076&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 2] Jenkins 플러그인 설치&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Jenkins 시스템 설정&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 대시보드 화면에서 [Jenkins 관리] -&amp;gt; [시스템 설정] 에 들어간 후, 화면을 내리면 다음과 같이 Gitlab이 나옵니다.&lt;br /&gt;Connection name은 아무 이름이나 주고, 연동할 Gitlab URL을 적은 후, Credentials 을 선택 또는 생성합니다.&lt;br /&gt;그 후, '&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Test Connection&lt;/span&gt;&lt;/b&gt;' 을 누르면 다음과 같이 '&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Success&lt;/span&gt;&lt;/b&gt;' 가 나옵니다. (Credentials 생성은 아래에서 설명합니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;1414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIKouW/btrtH1DKjgH/Zb68NRcCDdapfnXLrf8LVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIKouW/btrtH1DKjgH/Zb68NRcCDdapfnXLrf8LVK/img.png&quot; data-alt=&quot;[그림 3] Jenkins 시스템 설정에서 Gitlab과 연결하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIKouW/btrtH1DKjgH/Zb68NRcCDdapfnXLrf8LVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIKouW%2FbtrtH1DKjgH%2FZb68NRcCDdapfnXLrf8LVK%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;2570&quot; height=&quot;1414&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;1414&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 3] Jenkins 시스템 설정에서 Gitlab과 연결하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Jenkins Pipeline 아이템 만들기&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 대시보드 화면에서 [새로운 Item] 을 클릭합니다.&lt;br /&gt;그럼 아래와 같은 화면이 나올텐데, 다음과 같이 Pipeline을 선택하고, 아이템명을 입력한 후 다음으로 넘어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2926&quot; data-origin-height=&quot;1890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOtRZe/btrtExC5utE/fx6exW6Dlkd5yiTe7pFWsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOtRZe/btrtExC5utE/fx6exW6Dlkd5yiTe7pFWsk/img.png&quot; data-alt=&quot;[그림 3] Pipeline 아이템 생성하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOtRZe/btrtExC5utE/fx6exW6Dlkd5yiTe7pFWsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOtRZe%2FbtrtExC5utE%2Ffx6exW6Dlkd5yiTe7pFWsk%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;2926&quot; height=&quot;1890&quot; data-origin-width=&quot;2926&quot; data-origin-height=&quot;1890&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 3] Pipeline 아이템 생성하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다음과 같은 화면이 나오는데, [Build Triggers] 탭을 누르거나, 화면을 밑으로 내려보면 아래 화면이 나옵니다.&lt;br /&gt;'Build when a change is pushed to Gitlab. ~' 부분을 체크해주고, 아래의 '고급' 버튼을 누르면 'Generate' 라는 버튼이 생깁니다. 해당 버튼을 누르면 Secret token 값이 생성되는데, 이 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Webhook URL&lt;/span&gt;&lt;/b&gt;과 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Secret token&lt;/span&gt;&lt;/b&gt; 값을 기억해둡니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2562&quot; data-origin-height=&quot;1632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIdY0T/btrtDfJpSRj/cICjRqFCgkKkEh9kKVL2KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIdY0T/btrtDfJpSRj/cICjRqFCgkKkEh9kKVL2KK/img.png&quot; data-alt=&quot;[그림 4] Build Triggers 설정하기 (Webhook URL과 Secret token 구하기)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIdY0T/btrtDfJpSRj/cICjRqFCgkKkEh9kKVL2KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIdY0T%2FbtrtDfJpSRj%2FcICjRqFCgkKkEh9kKVL2KK%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;2562&quot; height=&quot;1632&quot; data-origin-width=&quot;2562&quot; data-origin-height=&quot;1632&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 4] Build Triggers 설정하기 (Webhook URL과 Secret token 구하기)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 같은 화면에서 [Pipeline] 탭을 누르거나, 화면을 밑으로 내려보면 아래 화면이 나옵니다.&lt;br /&gt;Jenkins의 Pipeline 스크립트를 아래 에디터에 바로 작성할 수도 있고, Gitlab의 Jenkinsfile로 부터 가져올 수도 있습니다.&lt;br /&gt;Jenkinsfile로 CI/CD 스크립트를 관리하는게 유지보수 측면에서 훨씬 더 좋기 때문에 'Pipeline script from SCM' 을 선택합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2604&quot; data-origin-height=&quot;1788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw97E1/btrtIUREznB/fKGsQbdVvt3GoSsq2WRtZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw97E1/btrtIUREznB/fKGsQbdVvt3GoSsq2WRtZ1/img.png&quot; data-alt=&quot;[그림 5] Pipeline script from SCM 선택하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw97E1/btrtIUREznB/fKGsQbdVvt3GoSsq2WRtZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw97E1%2FbtrtIUREznB%2FfKGsQbdVvt3GoSsq2WRtZ1%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;2604&quot; height=&quot;1788&quot; data-origin-width=&quot;2604&quot; data-origin-height=&quot;1788&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 5] Pipeline script from SCM 선택하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, 아래 화면에서 Gitlab 도메인을 적은 후, Credentials 을 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2520&quot; data-origin-height=&quot;1092&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bki1uM/btrtITkQKnb/CZAoE850x4phEiTlK1Rbe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bki1uM/btrtITkQKnb/CZAoE850x4phEiTlK1Rbe0/img.png&quot; data-alt=&quot;[그림 6] Credentials 생성하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bki1uM/btrtITkQKnb/CZAoE850x4phEiTlK1Rbe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbki1uM%2FbtrtITkQKnb%2FCZAoE850x4phEiTlK1Rbe0%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;2520&quot; height=&quot;1092&quot; data-origin-width=&quot;2520&quot; data-origin-height=&quot;1092&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 6] Credentials 생성하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Credentials 생성 화면에서 중요한 부분은 'Kind' 부분입니다.&lt;br /&gt;요즘은 인증을 위해 ID/PW가 아닌 토큰을 주로 사용하기 때문에 'Gitlab API token'이나 'Gitlab Personal Access token' 종류를 사용하여 Credentials을 만들려 하였고, 이로 인해 많은 시간을 허비했습니다.. (결론적으로 Jenkins 버그입니다.)&lt;br /&gt;따라서, 'Username with password' 로 진행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2838&quot; data-origin-height=&quot;1494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzqzvu/btrtKDohO7Y/vUmT6nyvKcQ7YzCwcBp7cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzqzvu/btrtKDohO7Y/vUmT6nyvKcQ7YzCwcBp7cK/img.png&quot; data-alt=&quot;[그림 7] 'Username with password' 타입으로 Credentials 생성하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzqzvu/btrtKDohO7Y/vUmT6nyvKcQ7YzCwcBp7cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzqzvu%2FbtrtKDohO7Y%2FvUmT6nyvKcQ7YzCwcBp7cK%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;2838&quot; height=&quot;1494&quot; data-origin-width=&quot;2838&quot; data-origin-height=&quot;1494&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 7] 'Username with password' 타입으로 Credentials 생성하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/CI CD</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/356</guid>
      <comments>https://pangtrue.tistory.com/356#entry356comment</comments>
      <pubDate>Fri, 18 Feb 2022 22:29:24 +0900</pubDate>
    </item>
    <item>
      <title>[CI CD] Ubuntu에 Jenkins 설치하기 (with. Docker)</title>
      <link>https://pangtrue.tistory.com/355</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Ubuntu에 Jenkins 설치하기&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 쉘에 접속했다고 가정하겠습니다.&lt;br /&gt;원하는 경로로 이동 후, 다음과 같이 Dockerfile을 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1645185061436&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM jenkins/jenkins:jdk11

# 도커를 실행하기 위한 root 계정으로 전환
USER root

# 도커 설치
COPY docker_install.sh /docker_install.sh
RUN chmod +x /docker_install.sh
RUN /docker_install.sh

# 설치 후 'docker' 라는 이름의 그룹을 만든 후, jenkins 계정 생성 후 해당 그룹으로 변경
RUN groupadd -f docker
RUN usermod -aG docker jenkins
USER jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, 위 Dockerfile에 명시된 docker_install.sh 쉘 스크립트 파일을 다음과 같이 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1645185112798&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/sh
apt-get update &amp;amp;&amp;amp; \
apt-get -y install apt-transport-https \
  ca-certificates \
  curl \
  gnupg2 \
  zip \
  unzip \
  software-properties-common &amp;amp;&amp;amp; \
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo &quot;$ID&quot;)/gpg &amp;gt; /tmp/dkey; apt-key add /tmp/dkey &amp;amp;&amp;amp; \
add-apt-repository \
&quot;deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo &quot;$ID&quot;) \
$(lsb_release -cs) \
stable&quot; &amp;amp;&amp;amp; \
apt-get update &amp;amp;&amp;amp; \
apt-get -y install docker-ce&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;다음으로 위 Dockerfile을 가지고 도커 이미지를 생성한 후, 이미지가 제대로 생성되었는지 확인합니다.&lt;br /&gt;docker build 명령어는 Dockerfile을 찾고 읽어들여 도커 이미지를 생성합니다. 이때 'Dockerfile'이라는 파일명이 디폴트값이기 때문에 별도로 지정해주지 않았지만, 만약 'Dockerfile.dev'와 같이 Dockerfile 명을 다르게 줬다면 도커 파일명을 지정해줘야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1645185181589&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker build -t jenkins/myjenkins . // Dockerfile을 가지고 도커 이미지를 만듭니다. (빌드)
$ docker images // 도커 이미지가 제대로 생성되었는지 확인합니다.&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;br /&gt;-p 옵션은 --published 의 약어로 컨테이너 내에서 8080 포트로 실행되는 젠킨스를 Host 서버(여기선 Ubuntu)의 9090 포트와 매핑시키는걸 의미합니다. 이렇게 해주면 Ubuntu 서버의 9090 포트로 접속하면 매핑된 컨테이너의 포트로 들어가지게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1645185347832&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -d -p 9090:8080 --name=jenkinscicd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 위와 같이 docker run 명령어를 터미널에 직접 입력해줄 수도 있지만, 이렇게 되면 매번 명령어를 직접 입력해야하기 때문에 번거롭고, 어떤 옵션을 주어 실행했는지 까먹기 십상입니다. 그래서 보통 docker-compose.yml 파일을 이용합니다.&amp;nbsp;&lt;br /&gt;아래와 같이 젠킨스를 실행하기 위한 스크립트를 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1645185586493&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3&quot;
services:
  jenkins:
    privileged: true
    build: .
    restart: always
    volumes:
      - /home/ubuntu/myTest/jenkinsDir:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      - &quot;9090:8080&quot;
      - &quot;50010:50000&quot;
    expose:
      - &quot;8080&quot;
      - &quot;50000&quot;
    environment:
      TZ: &quot;Asia/Seoul&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 'docker-compose up' 명령어를 터미널에 치면 젠킨스가 도커 컨테이너로 정상 실행됩니다.&lt;/p&gt;</description>
      <category>DevOps/CI CD</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/355</guid>
      <comments>https://pangtrue.tistory.com/355#entry355comment</comments>
      <pubDate>Fri, 18 Feb 2022 21:00:34 +0900</pubDate>
    </item>
    <item>
      <title>[JUnit] JUnit 5</title>
      <link>https://pangtrue.tistory.com/354</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. JUnit 5 테스트 인스턴스&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 클래스에 있는 각각의 테스트 메서드는 서로 다른 인스턴스에서 실행됩니다.&lt;br /&gt;다음 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;class MyTest {
  int value = 0;

  @Test
  void first() { System.out.println(value++); }

  @Test
  void second() { System.out.println(value++); }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 MyTest 클래스를 전체 테스트해보면 second()에서는 1이 찍혀야할거 같지만, 콘솔에 모두 0이 찍힙니다.&lt;br /&gt;테스트 메서드는 서로 독립적이여야 한다는 특징때문에 각 테스트 메서드를 실행하는 인스턴스를 할당하는 것 같습니다.&lt;br /&gt;만약, 하나의 인스턴스에서 실행되도록 만들고 싶다면 &lt;code&gt;@TestInstance&lt;/code&gt; 를 사용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyTest { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. JUnit 5 테스트 순서&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 클래스에 있는 각각의 테스트들은 기본적으로 테스트마다 스레드로 서로 다른 인스턴스들이 실행하도록 하기 때문에 순서가 보장되지 않습니다. 그런데, use-case 테스트와 같이 순서가 보장되어야 하는 경우도 있습니다. 예를 들어, 회원 가입 -&amp;gt; 로그인 -&amp;gt; 조회 등 일련의 절차가 필요할 수 있습니다. 이때 &lt;code&gt;@Order&lt;/code&gt; 애너테이션을 각 테스트마다 할당해 우선순위를 부여할 수 있습니다. (주의할 점은 몇 번째에 실행하겠다는게 아니라 우선순위입니다.) 그리고 각각의 테스트에 &lt;code&gt;@Order&lt;/code&gt; 을 적용하려면 테스트 클래스에 &lt;code&gt;@TestMethodOrder&lt;/code&gt;를 명시해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MyTest {
  @Order(1) // 순서가 낮을수록 더 높은 우선순위를 가집니다.
  @Test void first() {...}

  @Order(2)
  @Test void second() {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. JUnit 5 확장 모델(Extension)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Junit 4의 확장 모델은 @RunWith(Runner), TestRule, MethodRule 이었는데,&lt;br /&gt;Junit 5의 확장 모델은 단 하나, Extension 으로 통일되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장팩(Extension) 등록 방법&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;@ExtendWith&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;프로그래밍 등록 &lt;code&gt;@RegisterExtension&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;자동 등록 자바 &lt;code&gt;ServiceLoader&lt;/code&gt; 이용&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;li&gt;테스트 인스턴스 팩토리&lt;/li&gt;
&lt;li&gt;테스트 인스턴스 후-처리기&lt;/li&gt;
&lt;li&gt;테스트 매개변수 리졸버&lt;/li&gt;
&lt;li&gt;테스트 라이프사이클 콜백&lt;/li&gt;
&lt;li&gt;예외 처리&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3.1 시나리오&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SlowTest 애너테이션이 붙지 않은 테스트 메서드 중 실행 시간이 1005mills 이상이 걸리는 테스트에 한해 @SlowTest 적용을 고려하라는 문자열을 출력하고 싶다고 해보겠습니다. 우선 @SlowTest 애너테이션을 다음과 같이 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Test
@Tag(&quot;slow&quot;)
public @interface SlowTest {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다음과 같이 &lt;code&gt;BeforeTestExecutionCallback&lt;/code&gt; 과 &lt;code&gt;AfterTestExecutionCallback&lt;/code&gt; 을 구현받는 클래스를 만듭니다. (테스트 실행 전 호출될 콜백과 테스트 실행 후 호출될 콜백을 정의합니다.)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    private static final long THRESHOLD = 1000L;

  @Override
  public void beforeTestExecution(ExtensionContext context) throws Exception {
    String testClassName = context.getRequiredTestClass().getName();
    String testMethodName = context.getRequiredTestMethod().getName();
    ExtensionContext.Store store = 
      context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));

    store.put(&quot;START_TIME&quot;, System.currentTimeMillis());
  }

  @Override
  public void afterTestExecution(ExtensionContext context) throws Exception {
    String testClassName = context.getRequiredTestClass().getName();
    String testMethod = context.getRequiredTestMethod();
    String testMethodName = testMethod.getName();
    ExtensionContext.Store store =
      context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));

    Long startTime = store.remove(&quot;START_TIME&quot;, Long.class);
    long duration = System.currentMillis() - startTime;

    SlowTest annotation = testMethod.getAnnotation(SlowTest.class);
    if (duration &amp;gt; THRESHOLD &amp;amp;&amp;amp; annotation == null) {
      System.out.printf(&quot;Please consider mark method [%s] with @SlowTest. \n&quot;, testMethodName);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 Extension을 적용하는 방법은 처음에 얘기했듯 3가지입니다.&lt;br /&gt;첫 째, 선언적인 방법으로 테스트 클래스에 &lt;code&gt;@ExtendWith&lt;/code&gt; 애너테이션을 적용합니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;@ExtendWith(FindSlowTestExtension.class)
class MyTest { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 째, 프로그래밍적인 방법은 위 방법보다 조금 더 유연한 방법입니다. (테스트 클래스마다 THRESHOLD 값을 설정할 수 있게)&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;class MyTest {
  @RegisterExtension
  static FindSlowTestExtension findSlowTestExtension =
    new FindSlowTestExtension(1000L); // 물론 해당 생성자를 만들어야 합니다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. JUnit 4 -&amp;gt; JUnit 5로 마이그레이션하는 방법&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Junit-vintage-engine을 의존성으로 추가하면, JUnit 5의 junit-platform으로 JUnit 3과 4로 작성된 테스트를 실행할 수 있습니다.&lt;/p&gt;</description>
      <category>Programming/JUnit</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/354</guid>
      <comments>https://pangtrue.tistory.com/354#entry354comment</comments>
      <pubDate>Fri, 11 Feb 2022 20:51:04 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Framework]  Spring 핵심 원리 - 컴포넌트 스캔과 의존관계 자동 주입</title>
      <link>https://pangtrue.tistory.com/235</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 컴포넌트 스캔과 의존관계 자동 주입&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 빈을 등록할 때 Java 코드의 @Bean이나 XML의 등을 이용해 설정 정보에 직접 등록할 빈을 명시했습니다. 그런데, 이렇게 등록해야할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 파일도 커지는 문제가 발생합니다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;컴포넌트 스캔&lt;/b&gt;&lt;/span&gt;이라는 기능을 제공합니다.&lt;/p&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;br /&gt;모든 Java 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸립니다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;@ComponentScan(
            basePackages = &quot;com.demo.core&quot;,
            basePackageClasses = AutoConfig.class
)&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;basePackages: 탐색할 패키지의 시작 위치를 지정합니다. (이 패키지를 포함해서 하위 패키지를 모두 탐색)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;basePackages = {&amp;rdquo;com.demo.aa&amp;rdquo;, &amp;ldquo;com.demo.bb&amp;rdquo;}&lt;/code&gt; &amp;lt;- 여러 시작 위치를 지정할 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지정하지 않으면 &lt;code&gt;@ComponentScan&lt;/code&gt;이 붙은 설정 정보 클래스의 패키지가 시작 위치가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;u&gt;basePackage 관련 권장하는 방법&lt;/u&gt;&lt;/b&gt;&lt;br /&gt;개인적(김영한님)으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것. &lt;br /&gt;최근 Spring Boot도 이 방법을 기본으로 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;u&gt;컴포넌트 스캔 대상&lt;/u&gt;&lt;/b&gt;&lt;br /&gt;컴포넌트 스캔은 &lt;code&gt;@Component&lt;/code&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;@Component&lt;/code&gt;: 컴포넌트 스캔에서 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Controller&lt;/code&gt;: 스프링 MVC 컨트롤러에서 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Service&lt;/code&gt;: 스프링 비즈니스 로직에서 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Repository&lt;/code&gt;: 스프링 데이터 접근 계층에서 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Configuration&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;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 사실 애너테이션에는 상속관계라는 것이 없다. 그래서 위 코드처럼 애너테이션이 특정 애너테이션을 들고 있는 것을 인식할 수 있는 것은 Java 언어가 지원하는 기능이 아니고, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Spring이 지원하는 기능&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔의 용도뿐만 아니라 다음 애너테이션이 있으면 Spring은 부가 기능을 수행합니다.&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;@Controller&lt;/code&gt;: Spring MVC 컨트롤러로 인식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Repository&lt;/code&gt;: Spring 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;스프링 예외로 변환&lt;/b&gt;&lt;/span&gt;해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Configuration&lt;/code&gt;: Spring 설정 정보로 인식하고, Spring 빈이 싱글톤을 유지하도록 추가 처리를 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Service&lt;/code&gt;: 사실 &lt;code&gt;@Service&lt;/code&gt;는 특별한 처리를 하지 않습니다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나라고 비즈니스 계층을 인식하는데 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.1 필터&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;includeFilters&lt;/li&gt;
&lt;li&gt;excludeFilters&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MyIncludeComponent&lt;/code&gt;와 &lt;code&gt;MyExcludeComponent&lt;/code&gt;는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target(ElementType.TYPE) // 해당 애너테이션이 어디에 붙을건지를 명시합니다. (클래스, 필드, 메서드 등등)
@Retention(RetentionPolicy.RUNTIME) // 해당 애너테이션을 런타임에 메모리에 올리도록 설정?
@Documented
public @interface MyIncludeComponent{}

@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent{}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ComponentScan(
        includeFilters = @ComponentScan.Filter(
                            type = FilterType.ANNOTATION, // ANNOTATION이 디폴트값.
                            classes = MyIncludeComponent.class)),
        excludeFilters = @ComponentScan.Filter(
                            type = FilterType.ANNOTATION,
                            classes = MyExcludeComponent.class))
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;FilterType 옵션&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;FilterType 옵션은 5가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ANNOTATION: 기본값, 애너테이션을 인식해서 동작합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) &lt;code&gt;org.example.SomeAnnotation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) &lt;code&gt;org.example.SomeClass&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ASPECTJ: AspectJ 패턴 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) &lt;code&gt;org.example..*Service+&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;REGEX: 정규 표현식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) &lt;code&gt;org\.example\.Default.*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CUSTOM: &lt;code&gt;TypeFilter&lt;/code&gt; 라는 인터페이스를 구현해서 처리합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) &lt;code&gt;org.example.MyTypeFilter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2 중복 등록과 충돌&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까요?&lt;br /&gt;다음 두 가지 상황이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동 빈 등록 vs 자동 빈 등록&lt;/li&gt;
&lt;li&gt;수동 빈 등록 vs 자동 빈 등록&lt;/li&gt;
&lt;/ol&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;u&gt;&lt;b&gt;자동 빈 등록 vs 자동 빈 등록&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 이때 이름이 같은 컴포넌트가 있을 경우 스프링은 예외를 던집니다. &lt;code&gt;ConflictingBeanDefinitionException&lt;/code&gt; 예외 발생&lt;/p&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;u&gt;&lt;b&gt;수동 빈 등록 vs 자동 빈 등록&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;예전에는 수동 빈 등록(@Bean으로 직접 등록하는거)을 우선으로 오버라이딩했는데, 최근에는 예외가 발생하도록 바꼈습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 의존관계 자동주입&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존관계 주입은 크게 4가지 방법이 있습니다.&lt;/p&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;수정자 주입(setter 주입)&lt;/li&gt;
&lt;li&gt;필드 주입&lt;/li&gt;
&lt;li&gt;일반 메서드 주입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;생성자 주입&lt;/b&gt;&lt;/u&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;/li&gt;
&lt;li&gt;지금까지 우리가(김영한 강의) 진행했던 방법이 바로 생성자 주입입니다.&lt;/li&gt;
&lt;li&gt;특징: 생성자 호출시점에 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;딱 1번만 호출&lt;/b&gt;&lt;/span&gt;되는 것이 보장됩니다. &amp;lsquo;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;불변&lt;/span&gt;, &lt;span style=&quot;color: #006dd7;&quot;&gt;필수&lt;/span&gt;&lt;/b&gt;&amp;rsquo; 의존관계에 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍은 제약을 두는 것이 후에 관리 측면에서 용이합니다. setter로 다 열어두면 언제 어디서 수정될지 가늠이 안되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 주입에서 중요한 점은 !&lt;br /&gt;생성자가 1개일 때는 &lt;code&gt;@Autowired&lt;/code&gt;를 생략할 수 있습니다.&lt;br /&gt;만약 생성자가 2개 이상이면 &lt;code&gt;@Autowired&lt;/code&gt; 를 명시해줘야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework는 실행될때 크게 두 가지 작업을 합니다.&lt;/p&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;/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;u&gt;&lt;b&gt;필드 주입&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;필드에 &lt;code&gt;@Autowired&lt;/code&gt; 를 붙여 바로 주입하는 방법입니다.&lt;br /&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;/li&gt;
&lt;li&gt;DI 프레임워크가 없으면 아무것도 할 수 없습니다.&lt;/li&gt;
&lt;li&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;@Configuration&lt;/code&gt; 같은 곳에서만 특별한 용도로 사용하자!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;옵션 처리&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;주입할 스프링 빈이 없어도 동작해야 할 때가 있습니다.&lt;br /&gt;그런데 &lt;code&gt;@Autowired&lt;/code&gt;만 사용하면 &lt;code&gt;required&lt;/code&gt; 옵션 기본값이 &lt;code&gt;true&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;code&gt;@Autowired(required=false)&lt;/code&gt;: 자동 주입할 대상이 없으면 setter 메서드 자체가 호출 안됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.springframework.lang.@Nullable&lt;/code&gt;: 자동 주입할 대상이 없으면 null이 입력됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Optional&amp;lt;&amp;gt;&lt;/code&gt;: 자동 주입할 대상이 없으면 &lt;code&gt;Optional.empty&lt;/code&gt;가 입력됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;생성자 주입을 선택해라!&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;과거에는 setter 주입과 필드 주입을 많이 사용했지만, 최근에는 Spring을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장합니다. &lt;br /&gt;그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;불변&amp;rdquo;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없습니다. &lt;br /&gt;오히려 대부분의 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;의존관계는 애플리케이션 종료 전까지 변하면 안됩니다&lt;/b&gt;&lt;/span&gt;.(불변해야 한다.)&lt;/li&gt;
&lt;li&gt;setter 주입을 사용하면, &lt;code&gt;setXxx&lt;/code&gt; 메서드를 &lt;code&gt;public&lt;/code&gt;으로 열어두어야 합니다.&lt;br /&gt;이러면 누군가 실수 변경할 수도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아닙니다.&lt;/li&gt;
&lt;li&gt;생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. (따라서 불변하게 설계 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1 자동 주입하기 위해 조회되는 빈이 2개 이상일 때&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Autowired&lt;/code&gt;는 타입으로 빈을 조회해서 가져옵니다.&lt;br /&gt;만약 아래와 같이 FixDiscountPolicy와 RateDiscountPolicy 두 개를 컴포넌트로 등록하고,&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }

@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 DiscountPolicy 타입에 &lt;code&gt;@Autowired&lt;/code&gt;를 하여 자동 주입을 하면 어떻게 될까요?&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// 아래와 같이 있으면 ac.getBean(DiscountPolicy.class) 로 빈을 조회해서 가져옵니다.
@Autowired
private DiscountPolicy discountPolicy;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DiscountPolicy 타입으로 조회되는 빈이 2개이기 때문에 Spring은 어떤 빈을 주입해야할지 알지 못합니다.&lt;br /&gt;이때 스프링은 &lt;code&gt;NoUniqueBeanDefinitionException&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;code&gt;@Autowired&lt;/code&gt; 필드 명 매칭&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Qualifier&lt;/code&gt; &amp;rarr; &lt;code&gt;@Qualifier&lt;/code&gt; 끼리 매칭 &amp;rarr; 빈 이름 매칭&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Primary&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;code&gt;@Autowired&lt;/code&gt;는 타입 매칭을 시도하고, 이때 빈이 여러 개 있으면 필드명, 파라미터명으로 한번 더 조회합니다.&lt;br /&gt;여기서 파라미터명이란 생성자 주입을 사용했을 때 파라미터명을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Qualifier&lt;/code&gt;는 추가 구분자를 붙여주는 방법입니다.&lt;br /&gt;주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아닙니다!&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Qualifier(&quot;rate&quot;)
public class RateDiscountPolicy { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 컴포넌트에 &lt;code&gt;@Qualifier&lt;/code&gt;를 통해 추가적인 정보를 제공하고,&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Autowired
public OrderServiceImpl(@Qualifier(&quot;rate&quot;) DiscountPolicy discountPolicy) { ... }&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;@Primary&lt;/code&gt; 는 우선순위를 정하는 방법이다. &lt;code&gt;@Autowired&lt;/code&gt; 시에 빈이 여러 개 매칭되면 &lt;code&gt;@Primary&lt;/code&gt; 가 우선권을 가진다. &lt;br /&gt;(자주 사용됨)&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Primary
public class RateDiscountPolicy { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;애너테이션 직접 만들기&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;&lt;code&gt;@Qualifier(&amp;rdquo;mainDiscountPolicy&amp;rdquo;)&lt;/code&gt; 이렇게 문자를 적으면 컴파일시 타입 체크가 안됩니다.&lt;br /&gt;이때 다음과 같이 애너테이션을 만들어서 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier(&quot;mainDiscountPolicy&quot;)
public @interface MainDiscountPolicy { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@MainDiscountPolicy
public class RateDiscountPolicy { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Autowired
public OrderServiceImpl(@MainDiscountPolicy DiscountPolicy discountPolicy) {..}&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;br /&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;/li&gt;
&lt;li&gt;기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용됩니다. &lt;br /&gt;DB 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들입니다.&lt;/li&gt;
&lt;li&gt;업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느 정도 유사한 패턴이 있습니다. &lt;br /&gt;이런 경우 자동 빈 등록 기능(&lt;code&gt;@Controller&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, &lt;code&gt;@Component&lt;/code&gt;)을 적극 사용하는 것이 좋습니다. 이러면 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽습니다.&lt;/li&gt;
&lt;li&gt;기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미칩니다. &lt;br /&gt;그리고 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많습니다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Spring Framework</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/235</guid>
      <comments>https://pangtrue.tistory.com/235#entry235comment</comments>
      <pubDate>Thu, 3 Feb 2022 19:00:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Framework] Spring 핵심 원리 #1</title>
      <link>https://pangtrue.tistory.com/228</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 개요&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring이 제공하는 핵심 가치와 원리를 이해해야 합니다.&lt;br /&gt;왜 Spring을 만들었고, Spring이 왜 이런 기능들을 제공하는지를 살펴봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 오픈 소스는 &amp;lsquo;사파&amp;rsquo;라 불렸고, 표준이 &amp;lsquo;정파&amp;rsquo; 기술로 불렸습니다. EJB는 &amp;lsquo;정파&amp;rsquo;였기 때문에 여러 기업들에서 EJB를 많이 도입했습니다. EJB가 이론적으로는 정말 좋았지만, 현실적으로 너무 어렵고, 느렸습니다. 그리고 비쌌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EJB 엔티티빈 &amp;rarr; 하이버네이트 &amp;rarr; JPA&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 시점에 Java로 개발할 때의 메인이 되는 두 축은 Spring과 JPA입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 프레임워크의 역사 (로드 존슨이 최초 만들었던 3만줄의 Spring 코드로부터 시작하였습니다.)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2003년, Spring 프레임워크 1.0 출시 - XML로 설정&lt;/li&gt;
&lt;li&gt;2006년, Spring 프레임워크 2.0 출시 - XML 편의 기능 지원&lt;/li&gt;
&lt;li&gt;2009년, Spring 프레임워크 3.0 출시 - Java 코드로 설정&lt;/li&gt;
&lt;li&gt;2013년, Spring 프레임워크 4.0 출시 - Java8&lt;/li&gt;
&lt;li&gt;2014년, Spring Boot 1.0 출시&lt;/li&gt;
&lt;li&gt;2017년, Spring 프레임워크 5.0과 Spring Boot 2.0 출시 - 리액티브 프로그래밍 지원&lt;/li&gt;
&lt;li&gt;2020년 9월 현재, Spring 프레임워크 5.2.x, Spring Boot 2.3.x&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2014년에 출시된 Spring Boot는 큰 전환점 중 하나였습니다.&lt;br /&gt;Spring 프레임워크로 주로 웹 애플리케이션을 개발하는데, 이때 두 가지가 크게 어려웠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정&lt;/li&gt;
&lt;li&gt;웹 서버(Tomcat)에다가 Spring 코드 빌드해서 나온 war 파일을 집어넣고 배포하는 작업&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DayoM/btrsaRImMKi/fp1wruu91F2rklABKhC971/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DayoM/btrsaRImMKi/fp1wruu91F2rklABKhC971/img.png&quot; data-alt=&quot;[그림 1] Spring Framework 생태계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DayoM/btrsaRImMKi/fp1wruu91F2rklABKhC971/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDayoM%2FbtrsaRImMKi%2Ffp1wruu91F2rklABKhC971%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;612&quot; height=&quot;377&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 1] Spring Framework 생태계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에는 무수히 많은 서브 프로젝트가 있는데, 당연하게도 가장 중요한 것은 Spring Framework입니다.&lt;br /&gt;Spring Framework 안에 포함된 기술은 다음과 같이 분류할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트, 기타&lt;/li&gt;
&lt;li&gt;웹 기술: Spring MVC, Spring WebFlux&lt;/li&gt;
&lt;li&gt;데이터 접근 기술: 트랜잭션, JDBC, ORM 지원, XML 지원&lt;/li&gt;
&lt;li&gt;기술 통합: 캐시, 이메일, 원격접근, 스케줄링&lt;/li&gt;
&lt;li&gt;테스트: 스프링 기반 테스트 지원&lt;/li&gt;
&lt;li&gt;언어: Kotlin, Grovy&lt;/li&gt;
&lt;li&gt;최근에는 Spring Boot를 통해서 Spring Framework의 기술들을 편리하게 사용할 수 있다.&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;&amp;lsquo;Spring&amp;rsquo; 이라는 단어가 가지는 의미?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 DI 컨테이너 기술&lt;/li&gt;
&lt;li&gt;Spring Framework&lt;/li&gt;
&lt;li&gt;Spring Boot, Spring Framework를 포함한 전체 Spring 생태계&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;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;단순&lt;/span&gt;&lt;/b&gt;합니다. 이 단순한 컨셉이 좋으면 기술이 점점 발전해 나가는 것입니다.&lt;br /&gt;Spring이 지금은 엄청나게 크고 복잡한 프로젝트지만, 최초에는 로드 존슨이 만든 3만줄의 코드에서 시작했습니다. 그럼 Spring의 핵심 개념은 무엇일까요?&lt;/p&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;/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;Spring의 핵심은 다음과 같습니다.&lt;br /&gt;Spring은 Java 언어 기반의 프레임워크로, Java 언어의 가장 큰 특징은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;객체 지향 언어&lt;/b&gt;&lt;/span&gt;라는 것입니다.&lt;br /&gt;Spring은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;객체 지향 언어가 가진 강력한 특징을 살려내는&lt;/b&gt;&lt;/span&gt; 프레임워크입니다.&lt;br /&gt;Spring은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;좋은 객체 지향&lt;/b&gt;&lt;/span&gt; 애플리케이션을 개발할 수 있게 도와주는 프레임워크입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 좋은 객체 지향 설계의 5가지 원칙(SOLID)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 코드로 유명한 로버트 마틴이 좋은 객체지향 설계의 5가지 원칙을 정리했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SRP(Single Responsibility Principle) : 단일 책임 원칙&lt;/li&gt;
&lt;li&gt;OCP(Open/Close Principle) : 개방-폐쇄 원칙&lt;/li&gt;
&lt;li&gt;LSP(Liskov Substitution Priciple) : 리스코프 치환 원칙&lt;/li&gt;
&lt;li&gt;ISP(Interface Segregation Principle) : 인터페이스 분리 원칙&lt;/li&gt;
&lt;li&gt;DIP(Dependency Inversion Principle) : 의존관계 역전 원칙&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;size18&quot;&gt;&lt;u&gt;&lt;b&gt;1. SRP(Single Responsibility Principle, 단일 책임 원칙)&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;하나의 클래스는 하나의 책임만&lt;/span&gt;&lt;/b&gt; 가져야 한다는 원칙입니다. 하지만, 실무를 하다보면 이 하나의 책임이라는 것이 상당히 모호하게 다가올 수 있습니다. 하나의 책임이 엄청나게 큰 책임일 수도 있고, 아주 조그마한 책임일 수도 있는 것이죠. 이때, 중요한 기준은 &amp;lsquo;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;변경&lt;/span&gt;&lt;/b&gt;&amp;rsquo;입니다. 변경이 있을 때 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;파급 효과가 적으면&lt;/span&gt;&lt;/b&gt; SRP를 잘 따른 것입니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어, UI 변경, 객체의 생성과 사용을 분리하는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;2. OCP(Open/Close Principle, 개방-폐쇄 원칙) - 5가지 중 가장 중요한 원칙&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 요소(컴포넌트)는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;확장에는 열려있으나, 변경에는 닫혀있어야 한다&lt;/b&gt;&lt;/span&gt;는 원칙입니다.&lt;br /&gt;다형성만을 활용하면 이 원칙을 지킬 수 있을까요? (인터페이스는 그대로 놔두고 기능을 추가한 구현체만 새로 생성)&lt;br /&gt;다형성만을 사용하게되면 한 가지 문제가 생깁니다.&lt;br /&gt;객체의 생성을 담당하는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;클라이언트 코드가 구현체를 직접 선택&lt;/b&gt;&lt;/span&gt;해줘야 하기 때문에, 변경된 구현체로 새로 코드를 변경해줘야 합니다. 즉, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;다형성만을 활용한다고 해서 제대로된 OCP를 지킬 수는 없습&lt;/b&gt;&lt;/span&gt;니다.&lt;br /&gt;이를 해결하기 위해 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;객체를 생성하고 연관관계를 맺어주는 별도의 조립자(DI/IoC 컨테이너)가 필요&lt;/b&gt;&lt;/span&gt;합니다.&lt;br /&gt;바로 이 역할을 Spring이 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;3. LSP(Liskov Substitution Principle, 리스코프 치환 원칙) - 쉬운 원칙&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체는 프로그램의 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;정확성을 깨뜨리지 않으면서&lt;/span&gt;&lt;/b&gt; 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙입니다.&lt;br /&gt;&amp;lsquo;자동차&amp;rsquo; 라는 인터페이스가 있고 &amp;lsquo;엑셀&amp;rsquo;, &amp;lsquo;브레이크&amp;rsquo; 라는 기능이 있다고 가정해보겠습니다. 이때, 당연히 &amp;lsquo;엑셀&amp;rsquo;이라는 기능은 차를 앞으로 가게끔 하는 기능이어야하는데, 어떤 구현체가 &amp;lsquo;엑셀&amp;rsquo; 기능에 차를 뒤로가게끔 로직을 짰다면 어떻게 될까요? 컴파일 시점에서는 당연히 문제가 없겠지만, 프로그램의 정확성은 깨지게 됩니다. 즉, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;인터페이스 규약에 맞게 구현체가 기능을 만들어야한다&lt;/b&gt;&lt;/span&gt;는 원칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;4. ISP(Interface Segregation Principle, 인터페이스 분리 원칙)&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범용적인 인터페이스 하나보다 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;구체적인 인터페이스 여러 개로 분리&lt;/span&gt;&lt;/b&gt;하라는 원칙입니다.&lt;br /&gt;&amp;rsquo;자동차&amp;rsquo; 인터페이스 &amp;rarr; &amp;lsquo;운전&amp;rsquo; 인터페이스, &amp;lsquo;정비&amp;rsquo; 인터페이스와 같이 분리합니다.&lt;br /&gt;위와 같이 구체적인 기능 하나하나로 인터페이스를 쪼개면 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;클라이언트 코드도 분리&lt;/span&gt;&lt;/b&gt;할 수 있습니다.&lt;br /&gt;&amp;rsquo;정비&amp;rsquo; 인터페이스 자체가 변해도, &amp;lsquo;운전자&amp;rsquo; 클라이언트에 전혀 영향이 없습니다. &lt;br /&gt;따라서, 인터페이스가 명확해지고, 대체 가능성이 높아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;5. DIP(Dependency Inversion Principle, 의존관계 역전 원칙) - 중요한 원칙(OCP와 연관있음)&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rdquo;구현체가 아니라 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;추상화에 의존&lt;/b&gt;&lt;/span&gt;해야 한다&amp;rdquo; 의존성 주입은 이 원칙을 따르는 방법 중 하나입니다.&lt;br /&gt;의존성을 내가 직접 적어주는게 아니라, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;누군가로부터 주입&lt;/b&gt;&lt;/span&gt;받는다면 나는 추상화에만 의존할 수 있습니다.&lt;br /&gt;즉, 객체를 생성하는 부분을 분리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향의 핵심은 다형성입니다. 하지만, 다형성만을 가지고는 쉽게 부품을 갈아 끼우듯이 개발할 수 없습니다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;구현 객체를 변경하기 위해 클라이언트 코드도 함께 변경&lt;/b&gt;&lt;/span&gt;해줘야하기 때문입니다. 즉, 다형성만으로는 OCP, DIP를 지킬 수 없습니다. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;구현체를 주입해주는 DI/IoC 컨테이너가 필요&lt;/b&gt;&lt;/span&gt;합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Spring Framework&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 설계의 5가지 원칙에서 다형성만으로는 OCP, DIP를 지킬 수 없다는 것을 알았습니다.&lt;br /&gt;Spring은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;DI 컨테이너를 제공하여 OCP, DIP를 가능하게 지원&lt;/b&gt;&lt;/span&gt;합니다.&lt;br /&gt;이로 인해, 클라이언트 코드의 변경없이 기능을 확장할 수 있습니다. (쉽게 부품을 교체하듯이 개발)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 생성시 적어주는 &amp;lsquo;Artifact&amp;rsquo;가 빌드시 이름이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;IoC(Inversion of Control, 제어의 역전)&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 MemberServiceImpl은 MemberRepository를 사용하는 클라이언트 입장이고, MemberRepository는 서버 입장입니다. 다음과 같이 클라이언트가 직접 구현체를 생성하는 코드는 클라이언트가 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;서버 코드에 대한 제어권을 직접 가지고 있는&lt;/b&gt;&lt;/span&gt; 것과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public class MemberServiceImpl {
    private MemberRepository memberRepository = new MemoryMemberRepository();

    public void save(Member member) {
        memberRepository.save(member);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 객체지향 설계 원칙인 OCP, DIP를 위반합니다. 기능을 확장하기 위해 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;새로운 구현체를 만들어 갈아끼우기 위해선 클라이언트 코드를 변경&lt;/b&gt;&lt;/span&gt;해야하기 때문입니다. 그리고 SRP 원칙도 위반합니다. 한 클래스는 하나의 책임만을 가져야하는데 위 코드는 &amp;lsquo;생성&amp;rsquo;, &amp;lsquo;기능의 사용&amp;rsquo; 두 가지 책임을 가지기 때문입니다. 따라서, 아래와 같이 &amp;lsquo;생성&amp;rsquo;을 담당하는 부분을 분리해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public class AppConfig {
    public static MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public static MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
public class MemberServiceImpl {
    private MemberRepository memberRepository; // 구현체를 직접 생성하지 않습니다.

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    // ... 생략 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberServiceImpl을 사용하는 클라이언트가 AppConfig를 이용해서 MemberServiceImpl을 가져옵니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        MemberService memberService = AppConfig.memberService();
        memberService.save( ... );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberServiceImpl 입장에서는 MemberRepository에 대한 제어권을 자신이 가지고 있는게 아니라, AppConfig가 가지고 있습니다. 즉, 제어권이 역전되었는데 이를 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;IoC(제어의 역전)&lt;/b&gt;&lt;/span&gt;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;프레임워크 vs 라이브러리&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;작성한 코드를 제어하고 대신 실행&lt;/b&gt;&lt;/span&gt;한다면 프레임워크입니다. (Spring MVC에서 컨트롤러의 메서드를 개발자가 직접 호출하지 않고, 멤버변수에 구현체를 직접 할당하지 않습니다. 따라서 프레임워크입니다.)&lt;br /&gt;반면, 내가 작성한 코드가 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;서버 코드의 제어권을 가지고 있고, 실행을 직접&lt;/b&gt;&lt;/span&gt;한다면 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 AppConfig를 IoC 컨테이너 또는 DI 컨테이너라 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;Spring 컨테이너 생성&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// 애너테이션 기반의 Java 설정 클래스를 이용해서 Spring 컨테이너를 만듭니다.
// XML 기반으로 만들려면 XML~ApplicationContext 와 같은 이름의 구현체를 사용합니다.
ApplicationContext ctx = 
                    new AnnotationConfigApplicationContext(AppConfig.class);&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;ApplicationContext 를 Spring 컨테이너라 합니다.&lt;/li&gt;
&lt;li&gt;ApplicationContext 는 인터페이스입니다.&lt;/li&gt;
&lt;li&gt;Spring 컨테이너는 XML 기반으로 만들 수 있고, 애너테이션 기반의 Java 설정 클래스로 만들 수 있습니다.&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;사실 더 정확하게는 Spring 컨테이너를 얘기할 때 &amp;lsquo;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;BeanFactory&lt;/b&gt;&lt;/span&gt;&amp;rsquo;, &amp;lsquo;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;ApplicationContext&lt;/b&gt;&lt;/span&gt;&amp;rsquo; 둘을 구분해서 이야기합니다. 하지만, &amp;lsquo;BeanFactory&amp;rsquo;를 직접 사용하는 경우는 거의 없으므로 일반적으로 &amp;lsquo;ApplicationContext&amp;rsquo;를 Spring 컨테이너라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;244&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oeD3k/btrr4xjRng4/n6eaBn4DUouvkbHxVJG53K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oeD3k/btrr4xjRng4/n6eaBn4DUouvkbHxVJG53K/img.png&quot; data-alt=&quot;[그림 2] Spring 최상위 인터페이스인 BeanFactory와 상속 관계도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oeD3k/btrr4xjRng4/n6eaBn4DUouvkbHxVJG53K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoeD3k%2Fbtrr4xjRng4%2Fn6eaBn4DUouvkbHxVJG53K%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;244&quot; height=&quot;252&quot; data-origin-width=&quot;244&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 2] Spring 최상위 인터페이스인 BeanFactory와 상속 관계도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BeanFactory&lt;/b&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;/li&gt;
&lt;li&gt;스프링 빈을 관리하고 조회하는 역할을 담당합니다.&lt;/li&gt;
&lt;li&gt;&amp;lsquo;getBean()&amp;rsquo; 과 같은 메서드가 다 BeanFactory에 들어있는 기능입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApplicationContext&lt;/b&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;/li&gt;
&lt;li&gt;BeanFatory뿐만 아니라 여러가지 인터페이스를 상속받습니다.&lt;/li&gt;
&lt;li&gt;EnvironmentCapable, MessageSource, ResourceLoader 등을 추가로 상속받습니다.&lt;/li&gt;
&lt;li&gt;MessageSource: 국제화 기능을 제공합니다. 영어권이면 영어 출력, 한국이면 한국어 출력&lt;/li&gt;
&lt;li&gt;EnvironmentCapble: 로컬, 개발, 운영 환경을 구분하는 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;ApplicationEventPublisher: 이벤트를 발행하고 구독하는 모델을 편리하게 지원하는 기능입니다.&lt;/li&gt;
&lt;li&gt;ResourceLoader: 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회하는 기능을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeanFactory만을 직접 사용할 일은 거의 없습니다. 부가기능이 더해진 ApplicationContext를 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Spring Framework 설정 형식 (Java 코드, XML, Groovy, 기타)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘에는 XML 기반으로 Spring 컨테이너를 만드는 경우는 거의 없습니다. Spring Boot 자체가 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;애너테이션 기반의 Java 설정 클래스&lt;/b&gt;&lt;/span&gt;로 편리하게 Spring 컨테이너를 생성하도록 많은 지원을 해주고 있습니다. 하지만, Spring은 유연하게 설계되어 있기 때문에 XML, Java 코드뿐만 아니라 Groovy, 심지어 개인이 스스로 만들어 사용할 수 있도록 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;다양한 설정 형식을 지원&lt;/b&gt;&lt;/span&gt;합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6xsc7/btrsc056Jrx/wjLYyTfUX4CmTk7CdWObZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6xsc7/btrsc056Jrx/wjLYyTfUX4CmTk7CdWObZ0/img.png&quot; data-alt=&quot;[그림 3] 다양한 설정 형식을 지원&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6xsc7/btrsc056Jrx/wjLYyTfUX4CmTk7CdWObZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6xsc7%2Fbtrsc056Jrx%2FwjLYyTfUX4CmTk7CdWObZ0%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;441&quot; height=&quot;272&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 3] 다양한 설정 형식을 지원&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 ApplicationContext 인터페이스를 구현하면 개발자 스스로 설정 형식을 새로 만들수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;XML 설정&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 Spring Boot를 많이 사용하면서 XML 기반의 설정은 잘 사용하지 않습니다. 그러나 아직 많은 레거시 프로젝트들이 XML로 되어있고, 또 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;XML을 사용하면 컴파일없이 빈 설정 정보를 변경&lt;/b&gt;&lt;/span&gt;할 수 있는 장점도 있으므로 한 번쯤 배워두는 것도 괜찮습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Spring은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;어떻게 이런 다양한 설정 형식을 지원&lt;/b&gt;&lt;/span&gt;할 수 있는 걸까요?&lt;br /&gt;Spring이 다양한 설정 형식을 지원하는데의 중심에는 &amp;lsquo;BeanDefinition&amp;rsquo; 이라는 추상화가 있습니다.&lt;br /&gt;쉽게 이야기해서 &amp;lsquo;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;역할과 구현을 개념적으로 나눈 것&lt;/b&gt;&lt;/span&gt;&amp;rsquo;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XML을 읽어서 BeanDefinition을 만들면 된다.&lt;/li&gt;
&lt;li&gt;Java 코드를 읽어서 BeanDefinition을 만들면 된다.&lt;/li&gt;
&lt;li&gt;Spring 컨테이너는 설정 형식이 Java 코드인지 XML인지 몰라도 됩니다. 오직 BeanDefinition만 알면 된다.&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;&amp;lsquo;BeanDefinition&amp;rsquo; 을 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;빈 설정 메타정보&lt;/b&gt;&lt;/span&gt;라고 합니다.&lt;br /&gt;&amp;rsquo;@Bean&amp;rsquo;, &amp;lsquo;&amp;rsquo; 당 각각 하나씩 메타 정보가 생성됩니다.&lt;br /&gt;Spring 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Singleton 컨테이너&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;웹 애플리케이션과 싱글톤&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.&lt;/li&gt;
&lt;li&gt;대부분의 Spring 애플리케이션은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;웹 애플리케이션&lt;/b&gt;&lt;/span&gt;이다. (물론 웹이 아닌 애플리케이션도 개발 가능)&lt;/li&gt;
&lt;li&gt;웹 애플리케이션은 보통 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;여러 고객이 동시에 요청&lt;/b&gt;&lt;/span&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;웹의 특성상 여러 고객이 동시에 요청을 하는데, 만약 그때마다 MemberService와 같은 객체를 new로 생성한다면 JVM 메모리 낭비뿐만 아니라 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;객체를 매번 생성하고 GC가 객체를 다시 소멸&lt;/b&gt;&lt;/span&gt;시키는 과정이 무수히 일어날텐데 이때의 오버헤드가 엄청 심할 것입니다. 이를 해결하기 위해 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;객체를 1개만 생성하고 요청마다 공유&lt;/b&gt;&lt;/span&gt;하는 방식(싱글톤)으로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴: 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.&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;/li&gt;
&lt;li&gt;의존관계상 클라이언트가 구체 클래스에 의존한다. &amp;rarr; DIP를 위반합니다.&lt;/li&gt;
&lt;li&gt;클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.&lt;/li&gt;
&lt;li&gt;테스트하기 어렵다.&lt;/li&gt;
&lt;li&gt;내부 속성을 변경하거나 초기화하기 어렵다.&lt;/li&gt;
&lt;li&gt;private 생성자로 자식 클래스를 만들기 어렵다.&lt;/li&gt;
&lt;li&gt;결론적으로 유연성이 떨어진다.&lt;/li&gt;
&lt;li&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;그런데 Spring 컨테이너는 싱글톤이 가지는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;무수히 많은 문제점들을 극복하고 사용&lt;/b&gt;&lt;/span&gt;할 수 있게 해줍니다.&lt;br /&gt;스프링 컨테이너는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;싱글톤 패턴의 문제점을 해결&lt;/b&gt;&lt;/span&gt;하면서, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;객체 인스턴스를 싱글톤으로 관리&lt;/b&gt;&lt;/span&gt;합니다.&lt;br /&gt;싱글톤 객체를 생성하고 관리하는 기능을 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;싱글톤 레지스트리&lt;/b&gt;&lt;/span&gt;라고 합니다.&lt;br /&gt;즉, 개발자가 MemberService 코드를 만들면 어떠한 싱글톤 관련 코드가 없지만, 싱글톤으로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;참고: Spring의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아닙니다. 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공합니다. (하지만 거의 99%는 싱글톤으로 사용합니다.)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1643598265839&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;참고: Spring의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아닙니다. 
요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공합니다. (하지만 거의 99%는 싱글톤으로 사용합니다.)&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;size18&quot;&gt;&lt;u&gt;&lt;b&gt;싱글톤 방식의 주의점⭐️&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴이든 Spring과 같은 싱글톤 컨테이너를 사용하든 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;여러 클라이언트가 하나의 같은 객체 인스턴스를 공유&lt;/b&gt;&lt;/span&gt;하기 때문에 싱글톤 객체는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;상태를 유지(stateful)하게 설계하면 안됩&lt;/b&gt;&lt;/span&gt;니다.&lt;br /&gt;무상태(stateless)로 설계해야 합니다!&lt;/p&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;/li&gt;
&lt;li&gt;가급적 읽기만 가능해야 합니다.&lt;/li&gt;
&lt;li&gt;필드 대신에 Java에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 합니다.&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;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;@Configuration과 싱글톤&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration은 사실 싱글톤을 위해 존재하는 것입니다.&lt;/p&gt;</description>
      <category>Web/Spring Framework</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/228</guid>
      <comments>https://pangtrue.tistory.com/228#entry228comment</comments>
      <pubDate>Mon, 31 Jan 2022 00:39:31 +0900</pubDate>
    </item>
    <item>
      <title>[SourceTree] 소스트리 Gitlab 연동하기</title>
      <link>https://pangtrue.tistory.com/353</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Gitlab 연동&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 소스트리를 실행한 후, [설정] -&amp;gt; [계정] -&amp;gt; [추가] 를 누릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;647&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q58Rr/btrr3mPrjBx/ZskSc9MWQfUoGCx99BL6I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q58Rr/btrr3mPrjBx/ZskSc9MWQfUoGCx99BL6I0/img.png&quot; data-alt=&quot;[그림 1] [설정]-&amp;amp;gt;[계정]-&amp;amp;gt;[추가]&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q58Rr/btrr3mPrjBx/ZskSc9MWQfUoGCx99BL6I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ58Rr%2Fbtrr3mPrjBx%2FZskSc9MWQfUoGCx99BL6I0%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;481&quot; height=&quot;440&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;647&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 1] [설정]-&amp;gt;[계정]-&amp;gt;[추가]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 Gitlab에 들어가서 Access Token을 발급받아야 하는데요, 이 과정은 설명에서 생략하겠습니다.&amp;nbsp;&lt;br /&gt;위에서 발급받은 Access Token을 아래 화면의 '암호'에 적어주시면 됩니다.&lt;br /&gt;이때, 사용자 이름은 @ 을 명시해줘야하고, 프로토콜은 SSH가 아닌 HTTPS를 선택합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMyoMa/btrr2z9zW6i/VEE0P4AwYWQiDrAKkSLnZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMyoMa/btrr2z9zW6i/VEE0P4AwYWQiDrAKkSLnZ1/img.png&quot; data-alt=&quot;[그림 2] 계정 추가하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMyoMa/btrr2z9zW6i/VEE0P4AwYWQiDrAKkSLnZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMyoMa%2Fbtrr2z9zW6i%2FVEE0P4AwYWQiDrAKkSLnZ1%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;503&quot; height=&quot;338&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 2] 계정 추가하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>VCS</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/353</guid>
      <comments>https://pangtrue.tistory.com/353#entry353comment</comments>
      <pubDate>Sat, 29 Jan 2022 12:01:24 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Data JPA] 순수 JPA와 Spring Data JPA</title>
      <link>https://pangtrue.tistory.com/352</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;0. Spring-Data-JPA 개요&amp;nbsp;&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 상속받는 JpaRepository의 상속 관계도는 다음과 같습니다. (모두 인터페이스)&lt;br /&gt;JpaRepository ---&amp;gt; PagingAndSortingRepository ---&amp;gt; CrudRepository ---&amp;gt; Repository&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaRepository 인터페이스에는 다음의 메서드가 정의되어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;findAll()&lt;/li&gt;
&lt;li&gt;findAllById()&lt;/li&gt;
&lt;li&gt;saveAll()&lt;/li&gt;
&lt;li&gt;getOne()&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 쿼리 메서드 기능&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring-Data-JPA에는 쿼리 메서드라고 하는 기능이 있습니다.&lt;br /&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;/li&gt;
&lt;li&gt;메서드 이름으로 JPA NamedQuery 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Query&lt;/code&gt; 애너테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;메서드 이름으로 쿼리 생성&lt;/u&gt;&lt;/b&gt;&lt;br /&gt;먼저, 메서드 이름으로 쿼리 생성하는 기능부터 살펴보겠습니다.&lt;br /&gt;Repository 인터페이스 내에 특정 규칙에 맞춰 메서드를 정의하면 알아서 기능을 만들어주는(?) 기능입니다.&lt;br /&gt;해당 방법은 where에 조건 파라미터가 많을수록 메서드명이 길어진다는 단점이 있습니다. 그래서 조건절에 걸 파라미터가 1~2개라면 해당 방법을 사용하면 되고, 그 이상이라면 3번째 기능인 &lt;code&gt;@Query&lt;/code&gt; 애너테이션을 이용해 쿼리를 직접 정의하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
  // select * from Member
  // where username = :name and 
    List&amp;lt;Member&amp;gt; findByUsernameAndAgeGreaterThan(String name, int age);

  List&amp;lt;Member&amp;gt; findTop3By(); // 'By' 뒤에 아무 것도 없으면 전체 조회, Top3는 'limit 3' 을 의미합니다.

  List&amp;lt;Member&amp;gt; findByUsernameIn(List&amp;lt;String&amp;gt; names); // `where m.username in :names` 을 의미합니다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 규칙에 맞춰 메서드명을 작성하면, Spring-Data-JPA는 사용자가 어떤 작업을 원하는지 유추할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;메서드 이름으로 JPA NamedQuery 호출&lt;/u&gt;&lt;/b&gt;&lt;br /&gt;JPA의 NamedQuery는 다음과 같이 자주 사용될법한 쿼리를 미리 정의해두는 기능(?)입니다.&lt;br /&gt;(JPA의 NamedQuery는 실무에서는 잘 사용되지 않는 기능입니다.)&lt;br /&gt;NamedQuery에서 진짜 중요한 한 가지는 정의한 쿼리에 오타가 있으면 컴파일 에러를 발생시킨다는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@NamedQuery(
        name = &quot;Member.findByUsername&quot;,
      query = &quot;select m from Member m where m.username&quot; = :username
)
public class Member {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Entity 클래스에 &lt;code&gt;@NamedQuery&lt;/code&gt;를 정의해두고, 아래와 같이 리포지토리에서 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {

  // Spring Data JPA는 'Entity클래스명.메서드명' 의 이름을 가진 NamedQuery가 있는지 우선적으로 찾기 때문에,
  // 지금 상황에선 @Query 를 생략할 수 있습니다.
  @Query(name = &quot;Member.findByUsername&quot;)
  List&amp;lt;Member&amp;gt; findByUsername(@Param(&quot;username&quot;) String username);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;@Query를 사용해서 리포지토리의 메서드에 쿼리를 직접 정의하기&lt;/u&gt;&lt;/b&gt;&lt;br /&gt;실무에서 많이 사용되는 방법으로 다음과 같이 리포지토리 메서드 위에 쿼리를 바로 적어줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
  @Query(&quot;select m from Member m where m.username = :username and m.age = :age&quot;)
  List&amp;lt;Member&amp;gt; findUser(@Param(&quot;username&quot;) String username, @Param(&quot;age&quot;) int age);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 DTO로 조회하고 싶다면 다음과 같이 조회합니다. (Querydsl을 사용하면 간략해집니다.)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
  @Query(&quot;select com.demo.dto.MemberDto(m.id, m.username, t.name) from Member m join Team t&quot;)
  List&amp;lt;MemberDto&amp;gt; findMemberDto();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 반환 타입&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA는 리포지토리 메서드가 여러 가지 반환 타입을 가질 수 있도록 지원합니다.&lt;br /&gt;그 중, Optional 반환 타입의 반환에 대해서 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
  Optional&amp;lt;Member&amp;gt; findOptionalByUsername(String name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional로 반환받으면 다음과 같이 테스트해볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Test
void 옵셔널_테스트() {
  Optional&amp;lt;Member&amp;gt; result = memberRepository.findOptionalByUsername(&quot;김철수&quot;);
  Member findMember = result.orElseGet(() -&amp;gt; new Member(&quot;&quot;, 0));
  assertThat(findMember.getUsername()).isEqualsTo(&quot;김철수&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 페이징과 정렬&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 JPA를 사용했을 때의 페이징 방식과 Spring Data JPA를 사용했을 때의 페이징 방식의 차이를 살펴보겠습니다.&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;검색 조건: 나이가 10살&lt;/li&gt;
&lt;li&gt;정렬 조건: 이름으로 내림차순&lt;/li&gt;
&lt;li&gt;페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 순수 JPA를 사용했을 때의 페이징 쿼리와 총 갯수를 구하는 메서드는 다음과 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;Member&amp;gt; findByPaging(int age, int offset, int limit) {
  return em.createQuery(&quot;select m from Member m where m.age = :age order by m.username desc&quot;)
                .setParameter(&quot;age&quot;, age)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();
}

public long totalCount(int age) {
  return em.createQuery(&quot;select count(m) from Member m where m.age = :age&quot;, Long.class)
                .setParameter(&quot;age&quot;, age)
                .getSingleResult();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data 프로젝트는 페이징과 정렬 쿼리를 표준화 하였습니다. (Spring-Data-JPA든, Spring-Data-MongoDB든 다 똑같습니다!)&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;org.springframework.data.domain.Sort&lt;/code&gt; : 정렬 기능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.springframework.data.domain.Pageable&lt;/code&gt; : 페이징 기능 (내부에 Sort 를 포함하고 있습니다.)&lt;/li&gt;
&lt;/ul&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;org.springframework.data.domain.Page&lt;/code&gt; : 추가 count 쿼리 결과를 포함하는 페이징&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.springframework.data.domain.Slice&lt;/code&gt; : 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1 조회)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&lt;/code&gt; (자바 컬렉션) : 추가 count 쿼리 없이 결과만 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA를 이용한 페이징 쿼리 메서드는 다음과 같은 형태를 가집니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
  Page&amp;lt;Member&amp;gt; findByAge(int age, Pageable pageable);
}&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;@Test
public void findByAgeTest() {
  int age = 10;
  // 첫 번째 파라미터: 가져올 페이지 번호 (Spring Data는 페이지 번호를 0번부터 시작합니다.)
  // 두 번째 파라미터: 페이지당 갯수
  PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, &quot;username&quot;));

  Page&amp;lt;Member&amp;gt; page = memberRepository.findByAge(age, pageRequest);

  assertThat(page.getContent().get(0).getUsername()).isEqualTo(&quot;member5&quot;);
  assertThat(page.getTotalElements()).isEqualTo(5);
  assertThat(page.getNumber()).isEqualTo(0); // 가져온 페이지의 페이지 번호를 확인합니다.
  assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 갯수를 확인합니다.
  assertThat(page.isFirst()).isTrue(); // 가져온 페이지가 첫 번째 페이지인지를 확인합니다.
  assertThat(page.hasNext()).isTrue(); // 가져온 페이지의 다음 페이지가 존재하는지 확인합니다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 벌크성 수정 쿼리&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 순수 JPA를 사용했을 때의 벌크성 수정 쿼리를 날리는 메서드는 다음과 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface MemberJpaRepository {
  public int bulkAgePlus(int age) {
    return em.createQuery(&quot;update Member m set m.age = m.age + 1 where m.age &amp;gt;= :age&quot;)
                  .setParameter(&quot;age&quot;, age)
                  .executeQuery();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA를 사용했을 때의 벌크성 수정 쿼리를 날리는 메서드는 다음과 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {

  @Modifying(clearAutomatically = true)
  @Query(&quot;update Member m set m.age = m.age + 1 where m.age &amp;gt;= :age&quot;)
  int bulkAgePlus(@Param(&quot;age&quot;) int age);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. @EntityGraph&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능을 이해하기 위해선 JPA의 &lt;code&gt;join fetch&lt;/code&gt; 기능에 대한 이해가 필요합니다.&lt;br /&gt;다음과 같이 &lt;code&gt;.findAll()&lt;/code&gt; 로 Member를 모두 조회해온 후, &lt;code&gt;.getTeam()&lt;/code&gt; 을 호출하면 LAZY 조회라는 가정하에 실행될 때마다 쿼리가 나갑니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void findMemberLazy() {
  Team teamA = new Team(&quot;teamA&quot;); teamRepository.save(teamA);
  Team teamB = new Team(&quot;teamB&quot;); teamRepository.save(teamB);

  Member member1 = new Member(&quot;member1&quot;, 10, teamA); memberRepository.save(member1);
  Member member2 = new Member(&quot;member2&quot;, 20, teamB); memberRepository.save(member2);

  em.flush();
  em.clear();

  List&amp;lt;Member&amp;gt; members = memberRepository.findAll();

  for (Member member : members) {
    System.out.println(&quot;member = &quot; + member.getTeam()); // 이때마다 team을 조회해오기 위해 쿼리를 날립니다.(N+1문제)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 N+1 문제를 방지하기 위해 fetch join을 사용하는데, fetch join을 활용하면 &lt;b&gt;연관된 엔티티 값을 한번에 세팅&lt;/b&gt;해줍니다. (프록시 객체가 아니게 됨)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {

  @Query(&quot;select m from Member m left join fetch m.team&quot;)
  List&amp;lt;Member&amp;gt; findMemberFetchJoin();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 매번 @Query에 fetch join 관련 쿼리를 작성하는걸 간소화한 기능이 @EntityGraph 입니다.&lt;br /&gt;attributePaths의 속성 값으로 같이 조회해올 연관 엔티티를 명시합니다. (당연히 @EntityGraph도 내부적으로는 fetch join입니다.)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {

  @Override
  @EntityGraph(attributePaths = {&quot;team&quot;})
  List&amp;lt;Member&amp;gt; findAll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. JPA Hint &amp;amp; Lock&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL이 아니라 JPA 구현체에게 힌트를 제공해줄 수 있습니다. (패스)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 JPA의 트랜잭션과 Lock 매커니즘이란게 있구나.. 선에서 넘기겠습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {

  @Lock(LockModeType.PESSIMISTIC_WRITE)
  List&amp;lt;Member&amp;gt; findLockByUsername(String username);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7. 사용자 정의 리포지토리 구현 (Querydsl 쓸 때 실무에서 많이 사용됨)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA 리포지토리는 인터페이스만 정의하면 구현체는 Spring이 자동으로 생성해줍니다.&lt;br /&gt;그런데 구현체를 직접 커스텀하고 싶다면 어떻게 해야 할까요? 예를 들면 다음과 같은 이유로 구현체를 사용자가 직접 만들어주고 싶을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA 직접 사용하기 위해 (&lt;code&gt;EntityManager&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Spring JDBC Template 사용하기 위해&lt;/li&gt;
&lt;li&gt;MyBatis 사용하기 위해&lt;/li&gt;
&lt;li&gt;DB 커넥션 직접 사용하기 위해&lt;/li&gt;
&lt;li&gt;Querydsl 사용하기 위해&lt;/li&gt;
&lt;li&gt;등등 ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 추가해줄 메서드를 정의할 인터페이스를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepositoryCustom {
  List&amp;lt;Member&amp;gt; findMemberCustom();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Spring Data JPA가 알아서 만들어주는 구현체가 아닌, 직접 구현체를 정의해줍니다.&lt;br /&gt;여기서 중요한 규칙이 있는데, 위 인터페이스명은 아무렇게나 해도 상관없지만, 위 인터페이스를 구현하는 클래스명은 리포지토리명+Impl 이어야합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

  private final EntityManager em;

  public List&amp;lt;Member&amp;gt; findMemberCustom() {
    return em.createQuery(&quot;select m from Member m&quot;, Member.class)
                      .getResultList();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 리포지토리에 연결시켜줍니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt;, MemberRepositoryCustom {

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8. Auditing&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 운영을 위해 Entity 클래스를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶을 수 있습니다.&lt;/p&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;/li&gt;
&lt;li&gt;등록자&lt;/li&gt;
&lt;li&gt;수정자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 순수 JPA를 사용할 경우 아래와 같이 Entity 클래스를 만든 후, Member와 같은 다른 Entity에서 상속을 받도록 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@MappedSuperclass
public class JpaBaseEntity {

  @Column(updateable = false)
  private LocalDateTime createdDate;
  private LocalDateTime updatedDate;

  @PrePersist
  public void prePersist() {
    LocalDateTime now = LocalDateTime.now();
    createdDate = now;
    updatedDate = now;
  }

  @PreUpdate
  public void preUpdate() {
    LocalDateTime now = LocalDateTime.now();
    updatedDate = now;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 순수 JPA 코드는 Spring Data JPA를 활용해 간소화할 수 있습니다.&lt;br /&gt;우선, 스프링 부트 설정 클래스에 &lt;code&gt;@EnableJpaAuditing&lt;/code&gt; 을 명시합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@EnableJpaAuditing
@SpringBootApplication
public class DemoApplication {
  //
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, 다음과 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {

    @CreatedDate
    @Column(updateable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Web/Spring Data JPA</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/352</guid>
      <comments>https://pangtrue.tistory.com/352#entry352comment</comments>
      <pubDate>Sun, 23 Jan 2022 18:24:12 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스(Lv.5) : JAVA] 방의 개수</title>
      <link>https://pangtrue.tistory.com/350</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 문제&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(0, 0) 좌표에서 시작해 각 숫자별로 다음과 같은 방향으로 이동합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIKhY6/btrqMcoac7L/a6bnjFYgpTJkw7yNmZsKd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIKhY6/btrqMcoac7L/a6bnjFYgpTJkw7yNmZsKd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIKhY6/btrqMcoac7L/a6bnjFYgpTJkw7yNmZsKd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIKhY6%2FbtrqMcoac7L%2Fa6bnjFYgpTJkw7yNmZsKd0%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;150&quot; height=&quot;155&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0이면 위로 이동, 1이면 대각선 위로 이동, 2면 우측으로 이동 ... 이런 식입니다.&lt;br /&gt;이동하는 방향이 담긴 배열이 주어졌을 때, 생성되는 방의 갯수를 return 하도록 solution 함수를 작성하세요. (방: 사방이 막혀있는 것)&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;방향이 담긴 배열의 크기 x는 1 &amp;lt;= x &amp;lt;= 100,000 입니다.&lt;/li&gt;
&lt;li&gt;배열의 원소는 0 이상 7 이하입니다.&lt;/li&gt;
&lt;li&gt;방은 다른 방으로 둘러 싸여질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 풀이&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 좌표를 담을 자료구조가 필요합니다.&lt;br /&gt;보통 (0, 0) 좌표는 arr[0][0], (1, 0) 좌표는 arr[0][1] 처럼 2차원 배열을 이용하는데, 해당 문제는 이렇게 미리 2차원 배열을 선언해둘 수 없습니다. 입력으로 주어지는 배열의 크기가 최대 10만이기 때문에 (0, 0) 좌표를 기준으로 6(좌측이동)만 10만개 주어지면 (-100000, 0) 좌표가 되고, 2(우측이동)만 10만개 주어지면 (100000, 0) 좌표가 되기 때문에 미리 선언해야될 배열의 크기가 200,001 * 200,001 이 되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Map 을 사용했고, key로 x, y 좌표를 담은 Node 클래스, value로 해당 좌표와 연결되는 좌표들의 리스트 즉, 간선의 정보를 담았습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Map&amp;lt;Node, List&amp;lt;Node&amp;gt;&amp;gt; map = new HashMap&amp;lt;&amp;gt;();

class Node {
  int x, y;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 방이 생성되는 조건입니다.&lt;br /&gt;방이 생성되기 위해선 다음 두 가지 조건을 모두 만족해야 합니다.&lt;/p&gt;
&lt;ol 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;/li&gt;
&lt;/ol&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-origin-width=&quot;1348&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b73xXg/btrqQd0dMps/wyCfO8MriRmsRk7Us9tgnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b73xXg/btrqQd0dMps/wyCfO8MriRmsRk7Us9tgnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b73xXg/btrqQd0dMps/wyCfO8MriRmsRk7Us9tgnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb73xXg%2FbtrqQd0dMps%2FwyCfO8MriRmsRk7Us9tgnk%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;300&quot; height=&quot;88&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;396&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;p data-ke-size=&quot;size16&quot;&gt;입력으로 주어지는 배열의 값이 [2, 5, 2, 7] 라고 해보겠습니다.&lt;br /&gt;위 로직대로라면 방이 1개 생성되어야 합니다. 하지만, 실제로는 방이 2개 생성되었습니다. 이는 교차점 때문입니다.&lt;br /&gt;1칸의 이동을 2칸에 대응되도록 움직여준다면 이를 해결할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 코드&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import java.util.*;

public class Solution {

    public static void main(String[] args) {
        int[] arrows;

        arrows = new int[]{6, 6, 6, 4, 4, 4, 2, 2, 2, 0, 0, 0, 1, 6, 5, 5, 3, 6, 0};
        System.out.println(solution(arrows)); // 답: 3
    }

    public static int solution(int[] arrows) {
        int answer = 0;

        int[] dx = { 0,  1, 1, 1, 0, -1, -1, -1};
        int[] dy = {-1, -1, 0, 1, 1,  1,  0, -1};
        Node curNode = new Node(0, 0);

        // 방문 여부 관련 선언
        // key = 시작 node의 hashcode, value = 연결된 node들의 hashcode
        Map&amp;lt;Node, List&amp;lt;Node&amp;gt;&amp;gt; visited = new HashMap&amp;lt;&amp;gt;();

        for (int arrow : arrows) {
            for (int i = 0; i &amp;lt;= 1; i++) { // 교차점 처리를 위한 스케일업
                Node nextNode = new Node(curNode.x + dx[arrow], curNode.y + dy[arrow]);

                // 처음 방문하는 경우 = map에 키값이 없는 경우
                if (!visited.containsKey(nextNode)) {
                    // 리스트에 연결점 추가
                    visited.put(nextNode, makeEdgeList(curNode));

                    if (visited.get(curNode) == null) {
                        visited.put(curNode, makeEdgeList(nextNode));
                    } else {
                        visited.get(curNode).add(nextNode);
                    }

                // 해당 정점을 재방문했고, 간선을 처음 통과하는 경우
                } else if (!visited.get(nextNode).contains(curNode)) {
                    visited.get(nextNode).add(curNode);
                    visited.get(curNode).add(nextNode);
                    answer++;
                }

                // 이동 완료
                curNode = nextNode;
            }
        }

        return answer;
    }

    private static List&amp;lt;Node&amp;gt; makeEdgeList(Node node) {
        List&amp;lt;Node&amp;gt; edge = new ArrayList&amp;lt;&amp;gt;();
        edge.add(node);
        return edge;
    }

    private static class Node {
        int x, y;

        public Node(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Node node = (Node) o;
            return x == node.x &amp;amp;&amp;amp; y == node.y;
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/Programmers</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/350</guid>
      <comments>https://pangtrue.tistory.com/350#entry350comment</comments>
      <pubDate>Sat, 15 Jan 2022 16:22:29 +0900</pubDate>
    </item>
    <item>
      <title>[DevOps/Docker] 도커</title>
      <link>https://pangtrue.tistory.com/349</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 도커란?&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커는 애플리케이션을 컨테이너화 하여 Host OS와 독립적으로 실행하는 기술입니다.&amp;nbsp;&lt;br /&gt;Linux, Windows 등 어떤 OS든 상관없이 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;컨테이너화된 소프트웨어는 항상 동일하게 실행&lt;/span&gt;&lt;/b&gt;됩니다. (SW를 환경으로부터 격리)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 도커와 기존의 가상화 기술과의 차이&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 도커 이전에도 이런 SW 가상화 기술이 존재했었단 사실을 알고 계신가요?&lt;br /&gt;도커 이전에 가장 많이 쓰였던 방식은 VirtureBox와 같은 가상 머신 위에 VM을 띄우는 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;하이퍼 바이저&lt;/span&gt;&lt;/b&gt;&amp;nbsp;방식입니다.&lt;br /&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;br /&gt;도커 컨테이너에서 돌아가는 애플리케이션은 컨테이너가 제공하는 격리 기능 내부에 샌드박스가 있지만, 여전히 Host와 OS 커널을 공유합니다. 따라서, &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;컨테이너 내부에서 실행되는 프로세스를 Host 시스템에서 볼 수 있습니다&lt;/span&gt;&lt;/b&gt;. (권한이 있다는 전제 하에)&lt;/li&gt;
&lt;li&gt;가상 머신 (하이퍼 바이저 방식)&lt;br /&gt;가상 머신과 함께 VM 내부에서 실행되는 모든 것은 Host 시스템과 독립적입니다.&lt;br /&gt;Host 시스템은 하드웨어 자원 일부를 VM에 할당하고, VirtureBox와 같은 가상머신 플랫폼이 VM에서 돌아가는 프로세스를 관리합니다. 각각의 VM마다 OS를 구동한 후, 애플리케이션을 실행시킵니다. 사용법이 간단하지만 굉장히 느리죠..!&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;아래 보시는 것처럼 하이퍼 바이저는 OS를 각각 띄워야하기 때문에 굉장히 무거운 반면, 컨테이너는 OS를 구동할 필요가 없어 가볍습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDddx5/btrnHSEIA8b/SV0zUKiRo27FhVfdWlLCt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDddx5/btrnHSEIA8b/SV0zUKiRo27FhVfdWlLCt0/img.png&quot; data-alt=&quot;[그림 1] 컨테이너 방식과 하이퍼 바이저 방식 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDddx5/btrnHSEIA8b/SV0zUKiRo27FhVfdWlLCt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDddx5%2FbtrnHSEIA8b%2FSV0zUKiRo27FhVfdWlLCt0%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;500&quot; height=&quot;287&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 1] 컨테이너 방식과 하이퍼 바이저 방식 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 엄청나다는 것은 이제 알겠는데.. 컨테이너를 어떻게 격리시키는 것일까요?&lt;br /&gt;컨테이너 격리를 위해 Linux 커널의 기능 중에 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Cgroup&lt;/span&gt;&lt;/b&gt;과 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;네임스페이스&lt;/span&gt;&lt;/b&gt;라는 격리 기술이 있는데 이를 이용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C Group&lt;br /&gt;CPU, 메모리, Network Bandwith, HDD 입출력 등 프로세스의 시스템 리소스 사용량을 관리합니다.&lt;br /&gt;어떤 애플리케이션이 사용하는 시스템 리소스양이 너무 많다면 C group에 넣어 리소스 사용량을 제한시킵니다.&lt;/li&gt;
&lt;li&gt;네임스페이스&lt;br /&gt;Linux에서 사용되는 하나의 시스템에서 프로세스를 격리시키는 경량 프로세스 가상화 기술입니다.&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;즉, 컨테이너 방식도 크게보면 하나의 Linux VM을 사용하는 것이라 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgyfMQ/btrnBsBh0G4/6OSm5jnCHwRheAZykYKNik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgyfMQ/btrnBsBh0G4/6OSm5jnCHwRheAZykYKNik/img.png&quot; data-alt=&quot;[그림 2] 컨테이너 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgyfMQ/btrnBsBh0G4/6OSm5jnCHwRheAZykYKNik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgyfMQ%2FbtrnBsBh0G4%2F6OSm5jnCHwRheAZykYKNik%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;230&quot; height=&quot;296&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 2] 컨테이너 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 도커의 생명주기&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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-origin-width=&quot;2598&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btUHyG/btrnPCCrOkc/jQkVAooVpecpvLvWzuGEEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btUHyG/btrnPCCrOkc/jQkVAooVpecpvLvWzuGEEk/img.png&quot; data-alt=&quot;[그림 1] 컨테이너의 생명주기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btUHyG/btrnPCCrOkc/jQkVAooVpecpvLvWzuGEEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtUHyG%2FbtrnPCCrOkc%2FjQkVAooVpecpvLvWzuGEEk%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;650&quot; height=&quot;34&quot; data-origin-width=&quot;2598&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[그림 1] 컨테이너의 생명주기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 생명주기에 해당하는 명령어를 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1639404459773&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Docker Image를 통해 컨테이너를 생성합니다. 
# 파일 스냅샷만 컨테이너의 저장 공간에 적재되고, 커맨드는 실행되지 않습니다.
$ docker create {컨테이너ID 또는 이미지명} 

# 컨테이너를 실행시킵니다. (Docker Image에 있는 커맨드를 실행합니다.)
$ docker start {컨테이너ID 또는 이미지명}

# 위 create와 start를 한 번에 진행합니다. (주로 run을 이용해 실행합니다.)
$ docker run {컨테이너ID 또는 이미지명}

# 실행 중인 컨테이너가 하던 작업을 마저 다 할 수 있도록 텀을 두고 안전하게 중지시킵니다.
# SIGTERM 후에 SIGKILL 시그널을 보냅니다.
$ docker stop {컨테이너ID 또는 이미지명}

# 실행 중인 컨테이너를 즉시 중지시킵니다.
# 곧바로 SIGKILL 시그널을 보냅니다.
$ docker kill {컨테이너ID 또는 이미지명}

# 중지된 컨테이너를 삭제합니다. (중지된 컨테이너는 `docker ps -a`로 확인할 수 있습니다.)
$ docker rm {컨테이너ID 또는 이미지명}
$ docker rm `docker ps -a -q` # 모든 컨테이너를 삭제합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Docker Image 생성하고 실행하기&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Image를 생성하기 위해선 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Dockerfile을 작성&lt;/span&gt;&lt;/b&gt;해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile에 Docker Image를 만들기위한 여러가지 내용을 적어줄 수 있지만, 필수적으로 다음 두 가지 내용이 들어가야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Base Image와 dependency 설치를 위한 명령어 (파일 스냅샷)&lt;/li&gt;
&lt;li&gt;Docker Image가 실행되어 컨테이너로 시작될 때 실행할 명령어 (컨테이너 시작시 실행될 명령어)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 애플리케이션을 Docker Imager로 만드는 경우 Dockerfile은 다음과 같이 구성될 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1639529302830&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Base Image를 구성합니다.
FROM node:alpine 

# 컨테이너 내부의 워킹 디렉토리를 설정합니다.
# 이 경우 컨테이너 내부에 /usr/src/app/ 디렉토리 안에 관련 파일이 위치합니다.
WORKDIR /usr/src/app 

# 로컬에 있는 package.json 파일을 컨테이너 내부 ./ 경로에 복사합니다.
COPY package.json ./

# 컨테이너 내부에서 해당 명령어를 실행하여 dependency를 다운받습니다.
RUN npm install

# 로컬 경로에 있는 모든 프로젝트 파일을 컨테이너 내부 ./ 경로에 복사합니다.
# 위에서 package.json만 미리 COPY한 이유는 해당 Dockerfile로 Docker Image를 생성할 때마다
# package.json 파일에 변경이 없어도 매번 dependency를 설치받기 때문입니다. 
# 이렇게 하면 package.json 파일에 변경사항이 없으면 npm install 부분까지 캐시를 사용합니다.
COPY ./ ./

CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Dockerfile을 만들었다면, 터미널에서 다음 명령어로 빌드 및 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1639529540509&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Dockerfile을 이용해 Docker Image 생성하기 위한 build 명령어
$ docker build -t {내 도커ID}/{저장소명} ./

# 생성한 Docker Image를 실행하여 컨테이너로 돌아가게 만들기
$ docker run -d -p 8080:8080 {내 도커ID}/{저장소명}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Docker Volume&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법으로 Docker Image를 생성해 실행하면 애플리케이션이 컨테이너로 동작하게 됩니다.&lt;br /&gt;그런데 만약 소스 코드를 수정하게되면 어떻게 될까요? 다시 Docker Image를 생성하고 실행해야 변경된 소스가 반영됩니다.&lt;br /&gt;이때 Docker Volume 기능을 이용해 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;컨테이너 내부의 파일과 로컬에 있는 파일을 매핑(참조)&lt;/span&gt;&lt;/b&gt;하여 문제를 해결할 수 있습니다.&lt;br /&gt;다음과 같이 Docker Image를 실행할 때 volume을 명시합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1639530003383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 처음 -v 옵션으로 준 값은 매핑하지 않을 파일이고,
# 두 번째 -v 옵션으로 준 값은 매핑할 파일입니다.
# Node.js 앱을 컨테이너로 실행시킬 경우, 굳이 로컬에 npm install로 node_modules을 둘 필요가 없습니다.
$ docker run -d -p 8080:8080 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app {내 도커ID}/{저장소명}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. Docker Compose&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Image를 생성하고 실행하려면 터미널에 빌드 명령어와 실행 명령어를 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;직접 타이핑&lt;/span&gt;&lt;/b&gt;해야합니다.&lt;br /&gt;더구나 위에서 봤듯이, Docker Image를 실행할 때 volume 설정만 더해도 타이핑이 상당히 길어집니다.&lt;br /&gt;docker-compose.yml 파일에 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;터미널에 타이핑해야할 것들을 명시&lt;/span&gt;&lt;/b&gt;해두고 사용하면 타이핑 양도 적어지고, 재사용할 수도 있습니다.&lt;br /&gt;다음과 같이 docker-compose.yml 파일을 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1639530657825&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'
services:
  react: # 컨테이너명 
    build: # 해당 디렉토리에 있는 Dockerfile을 이용해 빌드합니다. (Docker Image 생성용)
      context: . # Dockerfile이 있는 위치를 명시합니다.
      dockerfile: Dockerfile.dev # Dockerfile명을 명시합니다. (디폴트는 Dockerfile)
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - /usr/src/app/node_modules
      - ./:/usr/src/app # :(콜론)을 기준으로 좌측이 매핑할 로컬 경로이고, 우측이 컨테이너 경로입니다.
      stdin_open: true
  
  tests: # 컨테이너명
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - /usr/src/app/node_modules
      - ./:/usr/src/app
    command: [&quot;npm&quot;, &quot;run&quot;, &quot;test&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 보시는것처럼 docker-compose.yml에는 여러 컨테이너에 대한 작업을 적어줄 수 있는데요, docker-compose의 또 다른 역할은 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;멀티 컨테이너 상황에서 네트워크 연결을 쉽게 하는 것&lt;/span&gt;&lt;/b&gt;입니다. 만약 클라이언트 애플리케이션을 위한 컨테이너를 띄우고, 서버 애플리케이션을 위한 컨테이너를 띄웠다고 가정해보겠습니다. 이때 클라이언트와 서버 컨테이너는 서로 통신할 수 있어야하는데, 이때 docker-compose를 이용하면 쉽게 설정해줄 수 있습니다.&lt;/p&gt;</description>
      <category>DevOps/Docker</category>
      <author>팡트루야</author>
      <guid isPermaLink="true">https://pangtrue.tistory.com/349</guid>
      <comments>https://pangtrue.tistory.com/349#entry349comment</comments>
      <pubDate>Sun, 12 Dec 2021 00:20:28 +0900</pubDate>
    </item>
  </channel>
</rss>