<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>chaen-ing.log</title>
        <link>https://velog.io/</link>
        <description>💻 개발 공부 기록장</description>
        <lastBuildDate>Sun, 01 Feb 2026 10:18:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>chaen-ing.log</title>
            <url>https://velog.velcdn.com/images/chaen-ing/profile/29b3745f-0cd2-44dc-a458-c41dbe93bacf/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. chaen-ing.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chaen-ing" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[프로젝트] @Cacheable
이 내부 메소드 호출 시 동작하지 않는 이유]]></title>
            <link>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Cacheable%EC%9D%B4-%EB%82%B4%EB%B6%80-%EB%A9%94%EC%86%8C%EB%93%9C-%ED%98%B8%EC%B6%9C-%EC%8B%9C-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Cacheable%EC%9D%B4-%EB%82%B4%EB%B6%80-%EB%A9%94%EC%86%8C%EB%93%9C-%ED%98%B8%EC%B6%9C-%EC%8B%9C-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 01 Feb 2026 10:18:07 GMT</pubDate>
            <description><![CDATA[<h2 id="🧩-문제-배경"><strong>🧩 문제 배경</strong></h2>
<hr>
<pre><code class="language-java">@Cacheable(value = &quot;store-opening-hours&quot;, key = &quot;#store.id&quot;)
public StoreInfoResponse getStoreInfo(Long storeId) {
    Store store = findStoreOrThrow(storeId);

    // 생략

    return StoreInfoResponse.from(
        store, representativeTag, getStoreOpeningHours(store));
  }</code></pre>
<p>기존 서비스 로직</p>
<p>문제점 : <code>@Cacheable</code>이 붙어있음에도 redis서버에 키값이 저장이 안됨</p>
<h2 id="✅-트러블-슈팅--해결-과정"><strong>✅ 트러블 슈팅 &amp; 해결 과정</strong></h2>
<hr>
<p>문제점에 대해서 찾아본 결과 <code>@Cacheable</code> <strong>프록시</strong> 기반이기 때문에 <strong>같은 클래스 내부의 메소드 호출 시 동작하지 않음</strong></p>
<p>→ 이 경우 실제로 Redis에 키가 저장되지 않음</p>
<p>*<em>+💡프록시란? *</em></p>
<ul>
<li><strong>프록시 객체</strong> : 실제 객체 대신 앞에 서서 <strong>요청을 가로채고, 필요하면 부가 기능을 수행</strong>한 뒤 실제 객체에 위임하는 <strong>대리 객체</strong></li>
<li>즉, 클라이언트 -&gt; 프록시 객체 -&gt; 실제 객체 순서로 동작 </li>
<li>캐시처리, 트랜잭션 시작/종료, 로깅 등의 공통 관심사를 비즈니스 코드와 분리하기 위해 Spring은 프록시 기반 AOP 사용 (ex. @Cacheable, @Transactional)</li>
<li>동작 방식 : 컨트롤러에서 @Cacheable이 붙은 서비스 로직 호출 -&gt; 프록시 객체가 먼저 호출 가로챔 -&gt; 프록시에서 부가 기능 실행 -&gt; 조건에 따라 실제 서비스 로직 실행 또는 생략 -&gt; 결과 반환</li>
</ul>
<p>따라서 같은 서비스 로직 내에서 프록시 객체를 호출시에는 프록시를 거치지 않으므로 부가 기능 실행이 불가하다</p>
<p>-&gt; 실제 키값을 저장하는 로직 <code>getStoreOpeningHours</code>를 별도의 전용 서비스 클래스로 분리하여 외부에서 호출되도록 구조를 리팩토링</p>
<pre><code class="language-java">// 애노테이션 제거
public StoreInfoResponse getStoreInfo(Long storeId) {
    Store store = findStoreOrThrow(storeId);

    // 생략

    // 여기서 캐싱 담당하는 서비스 다시 호출
    return StoreInfoResponse.from(
        store, representativeTag, storeGoogleApiClient.getStoreOpeningHours(store));
  }</code></pre>
<pre><code class="language-java">// 다른 클래스
@Cacheable(value = &quot;store-opening-hours&quot;, key = &quot;#store.id&quot;)
  public Map&lt;String, List&lt;String&gt;&gt; getStoreOpeningHours(Store store) {
    // 생략
  }</code></pre>
<p>변경 코드</p>
<p>캐싱은 외부 의존성 호출(외부 API) 계층에서 처리하도록 분리해 책임을 명확히 하고, 프록시 객체가 동작 가능하도록 변경</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] Redis 캐싱으로 응답 시간 단축 및 외부 API 호출 절감하기]]></title>
            <link>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Redis-%EC%BA%90%EC%8B%B1%EC%9C%BC%EB%A1%9C-%EC%9D%91%EB%8B%B5-%EC%8B%9C%EA%B0%84-%EB%8B%A8%EC%B6%95-%EB%B0%8F-%EC%99%B8%EB%B6%80-API-%ED%98%B8%EC%B6%9C-%EC%A0%88%EA%B0%90%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Redis-%EC%BA%90%EC%8B%B1%EC%9C%BC%EB%A1%9C-%EC%9D%91%EB%8B%B5-%EC%8B%9C%EA%B0%84-%EB%8B%A8%EC%B6%95-%EB%B0%8F-%EC%99%B8%EB%B6%80-API-%ED%98%B8%EC%B6%9C-%EC%A0%88%EA%B0%90%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 01 Feb 2026 09:54:32 GMT</pubDate>
            <description><![CDATA[<h2 id="🧩-문제-배경"><strong>🧩 문제 배경</strong></h2>
<hr>
<h3 id="가맹점-상세-조회-시-성능-및-비용-문제"><strong>가맹점 상세 조회 시 성능 및 비용 문제</strong></h3>
<ul>
<li>가맹점의 조회수를 저장해서 조회수 별 정렬, 인기 가맹점 등의 기능에 사용해야함<ul>
<li>가맹점 상세보기를 누를때마다 DB에서 조회수를 업데이트 할 시, 트래픽 증가 상황에서 <strong>응답 지연</strong>, <strong>DB 부하</strong> 발생</li>
</ul>
</li>
<li>가맹점 상세보기 api에서 외부 API 기반 정보(영업시간, 길찾기, 메뉴링크 등)를 제공해야함<ul>
<li>상세보기 할때마다 <strong>외부 API 호출시 한도 초과</strong> 및 성능 저하</li>
</ul>
</li>
</ul>
<h2 id="🔧-상세-트러블슈팅-기록"><strong>🔧 상세 트러블슈팅 기록</strong></h2>
<hr>
<p><strong>현재 코드</strong></p>
<pre><code class="language-java">public StoreInfoResponse getStoreInfoWithoutCache(Long storeId, Long userId) {
    Store store = getStoreOrThrow(storeId); // DB 직접 조회
    store.increaseViewCount();

    String representativeTag =
        storeTagCountRepository
            .findRepresentativeTagByStoreId(storeId)
            .map(Tag::getLabel)
            .orElse(null);

    if (userId != null) {
      User user = getUserOrThrow(userId);
      store.setIsScrapped(storeScrapRepository.existsByStoreIdAndUserId(storeId, user.getId()));
    } else {
      store.setIsScrapped(null);
    }

    return StoreInfoResponse.from(
        store, representativeTag, storeGoogleApiClient.getStoreOpeningHours(store));
  }</code></pre>
<ol>
<li>DB에서 Store 조회</li>
<li>조회수 증가</li>
<li>대표 태그 조회</li>
<li>로그인 시: 스크랩 여부 확인</li>
<li>Google API에서 영업시간 조회</li>
<li>DTO 변환</li>
</ol>
<h3 id="🚨-문제-1"><strong>🚨 문제 1:</strong></h3>
<h3 id="조회수-증가-로직으로-인한-db-락-충돌"><strong>조회수 증가 로직으로 인한 DB 락 충돌</strong></h3>
<ul>
<li><strong>현상</strong>: 동시 요청 시 RDB의 visit_count가 충돌</li>
<li><strong>해결</strong>:<ul>
<li>Redis에서 HGETALL store-views로 우선 처리</li>
<li>별도 스케줄러가 1시간 주기로 Redis 값을 RDB에 반영</li>
<li>→ DB 부하 감소 + 동시성 문제 해결</li>
</ul>
</li>
</ul>
<h3 id="🚨-문제-2"><strong>🚨 문제 2:</strong></h3>
<h3 id="google-maps-api-호출-속도-및-한도-문제"><strong>Google Maps API 호출 속도 및 한도 문제</strong></h3>
<ul>
<li><strong>현상</strong>: 상세보기 화면에 있는 영업시간을 보기 위해 외부 API 호출이 필요하나 구글은 월 한도 제한이 있음</li>
<li><strong>해결</strong>:<ul>
<li>검색 결과에서 받은 place_id와 영업시간 정보를 Redis에 캐싱</li>
<li>TTL을 15일 설정으로 일정 주기마다 갱신 가능</li>
<li>→ 응답 속도 단축, 외부 API 호출 횟수 대폭 감소</li>
</ul>
</li>
</ul>
<h2 id="✅-해결-과정"><strong>✅ 해결 과정</strong></h2>
<hr>
<p>Redis를 활용해서 문제를 해결해보기로 !!</p>
<h3 id="redis-사용이유"><strong>Redis 사용이유</strong></h3>
<ul>
<li><strong>인메모리 저장소</strong>로서, <strong>읽기/쓰기 속도가 매우 빠름</strong> (마이크로초 단위)</li>
<li><strong>싱글스레드</strong> 기반으로 명령이 순차 처리되어,
조회수 증가와 같은 연산에서 동시성 이슈를 단순화할 수 있음</li>
<li>트래픽 급증 상황에서도 안정적인 처리 가능</li>
<li>TTL(Time-To-Live) 설정을 통해 <strong>외부 API 응답 캐싱에 최적화</strong></li>
</ul>
<h3 id="redis-사용방법">Redis 사용방법</h3>
<ol>
<li><p><strong>Redis 설치 (Mac) → 로컬을 위해</strong></p>
<pre><code class="language-bash"> brew install redis
 brew services start redis</code></pre>
<p> 또는 도커 내부에 설치</p>
</li>
<li><p><strong>프로젝트 내 설정</strong></p>
<ul>
<li><p>gradle에 redis, cache 관련 dependency 추가</p>
<pre><code class="language-bash">  // Redis
      implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;

      // Spring Boot Cache
      implementation &#39;org.springframework.boot:spring-boot-starter-cache&#39;</code></pre>
</li>
<li><p>application.yml에 redis 정보 추가</p>
<pre><code class="language-bash">    data:
      redis:
        host: localhost
        port: 6379</code></pre>
</li>
<li><p>메인 Application에 <code>@EnableCaching</code> 추가</p>
</li>
<li><p>RedisConfig와 RedisCacheConfig</p>
<pre><code class="language-java">  @Configuration
  @EnableRedisRepositories
  public class RedisConfig {

      @Value(&quot;${spring.data.redis.host}&quot;)
      private String redisHost;

      @Value(&quot;${spring.data.redis.port}&quot;)
      private int redisPort;

      @Value(&quot;${spring.data.redis.password:}&quot;)
      private String redisPassword;

      @Bean
      public RedisConnectionFactory redisConnectionFactory() {
          RedisStandaloneConfiguration redisStandaloneConfiguration =
              new RedisStandaloneConfiguration(redisHost, redisPort);
          redisStandaloneConfiguration.setPassword(redisPassword);
          return new LettuceConnectionFactory(redisStandaloneConfiguration);
      }

      @Bean
      public RedisTemplate&lt;String, Object&gt; redisTemplate() {
          RedisTemplate&lt;String, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();

          StringRedisSerializer stringSerializer = new StringRedisSerializer();
          redisTemplate.setKeySerializer(stringSerializer);
          redisTemplate.setValueSerializer(stringSerializer);
          redisTemplate.setHashKeySerializer(stringSerializer);
          redisTemplate.setHashValueSerializer(stringSerializer);
          redisTemplate.setConnectionFactory(redisConnectionFactory());
          return redisTemplate;
      }
  }
</code></pre>
<pre><code class="language-java">  @EnableCaching
  @Configuration
  public class RedisCacheConfig {

    @Bean
    public CacheManager contentCacheManager(RedisConnectionFactory cf) {
      RedisCacheConfiguration redisCacheConfiguration =
          RedisCacheConfiguration.defaultCacheConfig()
              .serializeKeysWith(
                  RedisSerializationContext.SerializationPair.fromSerializer(
                      new StringRedisSerializer()))
              .serializeValuesWith(
                  RedisSerializationContext.SerializationPair.fromSerializer(
                      new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
              .entryTtl(Duration.ofMinutes(3L)); // 캐시 수명 30분

      return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)
          .cacheDefaults(redisCacheConfiguration)
          .build();
    }
  }
</code></pre>
</li>
</ul>
</li>
</ol>
<ol start="3">
<li><p><strong>서버에서 레디스 설치, 반영</strong></p>
<pre><code class="language-bash"> # 설치
 sudo apt update
 sudo apt install redis-server

 # 서비스 재시작
 sudo systemctl restart redis
 sudo systemctl enable redis-server

 # 접속 확인
 redis-cli ping   # → PONG</code></pre>
<p> ec2에 레디스 설치 &gt; 도커에 같이 띄우기</p>
<pre><code class="language-bash"> version: &#39;3.8&#39;

 services:
   redis:
     image: redis:7
     container_name: redis
     restart: always
     ports:
       - &quot;***&quot;
     command: [&quot;redis-server&quot;, &quot;--requirepass&quot;, &quot;****&quot;]
     volumes:
       - redis-data:/data

 volumes:
   redis-data:</code></pre>
<p>Redis 보안 설정은 환경 변수 기반으로 관리하며, 상세 구성은 생략합니다.
 서버 환경에서는 Redis를 컨테이너 기반으로 분리 배포하여 운영했습니다.</p>
</li>
<li><p><strong>사용하기</strong></p>
<p> 서비스로직에다가 애노테이션을 붙여주면 되고 이 때 <strong>서비스내에서 호출하는 경우에는 사용해도 먹히지 않습니다</strong></p>
<ul>
<li><code>@Cacheable(value = &quot;view-id&quot;, key = &quot;#viewId&quot;)</code> (예시)<ul>
<li>위의애노테이션이 함수위에 붙어있으면 함수 실행전에 redis먼저 확인</li>
</ul>
</li>
<li>또는 <code>RedisTemplate</code>으로 직접 구현</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/92e3bbe5-b98c-4102-a2e4-97b51bbdfdcc/image.png" alt=""></p>
<ul>
<li>value는 크게 상관없음</li>
<li>key기준으로 <strong>먼저</strong> redis에서 찾고 있으면 바로 리턴, 없으면 메소드를 실행한다</li>
<li>key는 파라미터에 있는애</li>
<li>저장되는 값은 return값</li>
<li><code>@CacheEvict(value = &quot;store-id&quot;, allEntries = true)</code><ul>
<li>key = “#storeId” 이런식으로 하면 특정 키 삭제</li>
<li>allEntries = true는 모든 키 삭제</li>
</ul>
</li>
<li><code>@CachePut(value = &quot;store&quot;, key = &quot;#store.id&quot;)</code></li>
<li>업데이트, 캐시 덮어쓰기 용도</li>
</ul>
<h2 id="✨-결과-k6"><strong>✨ 결과 (K6)</strong></h2>
<hr>
<pre><code class="language-jsx">import http from &#39;k6/http&#39;;
import { sleep, check } from &#39;k6&#39;;

export const options = {
  vus: 60, // 60명의 유저가 동시에 요청
  duration: &#39;1s&#39;, // 1초간 유지 → 약 60TPS
};

export default function () {
        const BASE_URL = &#39;***;
        const LOCAL_BASE_URL = &#39;***&#39;;
        const res = http.get(`***`);

        check(res, {
                &#39;status is 200&#39;: (r) =&gt; r.status === 200,
                &#39;response time &lt; 500ms&#39;: (r) =&gt; r.timings.duration &lt; 500,
        });
}</code></pre>
<p>K6을 활용해서 60TPS의 환경에서 테스트를 진행해보았다.</p>
<ol>
<li><strong>기존 로직 (No caching)</strong>
<img src="https://velog.velcdn.com/images/chaen-ing/post/734a24cc-47ab-4fc5-a8c3-bc082c43ce61/image.png" alt=""></li>
</ol>
<ul>
<li><p>전체 응답 중 약 90%가 성공, 약 10%는 실패</p>
</li>
<li><p>19%는 응답시간이 500ms 초과</p>
<table>
<thead>
<tr>
<th>avg (평균 응답 시간)</th>
<th>272.91ms</th>
</tr>
</thead>
<tbody><tr>
<td>min (최소값)</td>
<td>18.33ms</td>
</tr>
<tr>
<td>med (중앙값)</td>
<td>218.63ms</td>
</tr>
<tr>
<td>max (최대값)</td>
<td>627.13ms</td>
</tr>
</tbody></table>
</li>
</ul>
<ol start="2">
<li><strong>캐싱을 적용한 조회 로직</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/ce5b1e45-45d8-4293-a6b3-7e19631ff4cd/image.png" alt=""></p>
<ul>
<li><p>전체 응답 100% 성공</p>
</li>
<li><p>전체 응답시간이 500ms 이내</p>
<table>
<thead>
<tr>
<th>avg (평균 응답 시간)</th>
<th>147.53ms</th>
</tr>
</thead>
<tbody><tr>
<td>min (최소값)</td>
<td>18.09ms</td>
</tr>
<tr>
<td>med (중앙값)</td>
<td>146.54ms</td>
</tr>
<tr>
<td>max (최대값)</td>
<td>381.83ms</td>
</tr>
</tbody></table>
</li>
</ul>
<ol start="3">
<li><strong>실제 배포 서버에서의 테스트</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/52a22be0-3ccb-4fba-af0a-b87092b19d99/image.png" alt=""></p>
<ul>
<li><p>전체 응답 100% 성공</p>
</li>
<li><p>전체 응답시간이 500ms 이내</p>
<table>
<thead>
<tr>
<th>avg (평균 응답 시간)</th>
<th>181.68ms</th>
</tr>
</thead>
<tbody><tr>
<td>min (최소값)</td>
<td>38.58ms</td>
</tr>
<tr>
<td>med (중앙값)</td>
<td>186.98ms</td>
</tr>
<tr>
<td>max (최대값)</td>
<td>494.89ms</td>
</tr>
</tbody></table>
</li>
</ul>
<aside>
💡

<p>캐싱 도입 전에는 평균 응답 시간이 273ms에 달했고, 최대 627ms까지 지연되는 경우도 발생했습니다. 하지만 Redis 기반 조회수·외부 API 결과 캐싱 구조를 설계한 이후, <strong>평균 응답 시간은 147ms로 46% 단축</strong>, <strong>응답 실패율은 0%로 개선</strong>되었습니다.</p>
<p>또한 외부 API 호출 캐싱을 통해 <strong>비용과 한도 문제를 해결</strong>할 수 있었습니다. </p>
</aside>]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] Spring Security에서 JWT와 CustomUserDetails 활용하기]]></title>
            <link>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-Security%EC%97%90%EC%84%9C-JWT%EC%99%80-CustomUserDetails-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-Security%EC%97%90%EC%84%9C-JWT%EC%99%80-CustomUserDetails-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 15 Dec 2024 07:11:12 GMT</pubDate>
            <description><![CDATA[<p>저번 글에서 JWT 토큰을 발급 받는 과정까지 진행했었습니다.
이번 글에서는 JWT 인증 정보를 컨트롤러에서 어떻게 활용해야 할지,
필터에서 처리된 인증 정보를 어떻게 @AuthenticationPrincipal로 가져올 수 있는지에 대해 작동 방식과 사용 예시를 알아보도록 하겠습니다.</p>
<h3 id="🔗-spring-security의-filter-chain">🔗 Spring Security의 Filter Chain</h3>
<p>먼저 스프링에서는 필터를 적용하면 필터가 호출 된 다음에 디스패처 서블릿이 호출된다.</p>
<p><strong>필터 흐름</strong> : HTTP 요청 -&gt; WAS -&gt; 필터 -&gt; 서블릿 -&gt; 컨트롤러</p>
<ul>
<li>예를 들어 인증이 되지 않은 사용자라면 필터에서 적절하지 않은 요청이라고 판단하고 서블릿 호출 없이 끝을 낼 수 있다.</li>
</ul>
<p>필터는 <strong>체인</strong>으로 구성되어서, 중간에 필터를 여러개 자유롭게 추가할 수 있다. </p>
<ul>
<li>예를 들어서 로그를 남기는 필터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.</li>
</ul>
<h3 id="jwtauthfilter">JwtAuthFilter</h3>
<p>JWT 토큰을 추출하고 검사 후 인증객체를 생성하는 과정 또한 필터로 진행할 수 있다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class JwtAuthFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // 헤더에서 JWT 토큰 추출
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);

        // 유효성 검사 후 SecurityContext에 저장
        if (token != null &amp;&amp; jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        // 다음 필터로 넘어가기
        chain.doFilter(request, response);
    }
}</code></pre>
<p>순서는 아래와 같다</p>
<ol>
<li>HTTP 요청 헤더에서 JWT 토큰 추출 (Authorization 헤더)</li>
<li>토큰 유효성 검증 및 사용자 정보 추출</li>
<li>인증 객체 생성(UsernamePasswordAuthenticationToken)</li>
<li>SecurityContextHolder에 인증 객체 저장</li>
</ol>
<p><strong>1,2,3</strong>번은 저번에 작성해준 <code>JwtTokenProvide</code>에 이어서 작성했다.</p>
<h3 id="jwttokenprovider">JwtTokenProvider</h3>
<pre><code class="language-java"> // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = customUserDetailService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, &quot;&quot;, userDetails.getAuthorities());
    }

    // JWT 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws&lt;Claims&gt; claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claimsJws.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    // Request의 Header에서 token 값을 가져오기
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (bearerToken != null &amp;&amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7); // &quot;Bearer &quot; 이후의 토큰을 반환
        }
        return null;
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }</code></pre>
<p><code>resolveToken()</code> : Authorization 헤더에서 토큰 추출</p>
<ul>
<li>HTTP 요청에서 Authorization 헤더를 가져와서</li>
<li>Bearer로 시작하는지 검증 후 이후의 토큰만 반환</li>
</ul>
<p><del>나는 이 과정을 생략했어서 계속 403에러가 났었다...꼭 토큰 추출해줘야된다...</del></p>
<p><code>validateToken()</code> : 토큰의 유효성 검사</p>
<ul>
<li>토큰을 파싱해서 서명 검증 후 유효하면 Claims 객체를 받고</li>
<li>객체의 유효기간이 지났으면 false 반환</li>
</ul>
<p><code>getAuthentication()</code> : 인증 정보를 추출하여 Authentication 객체를 생성</p>
<ul>
<li>토큰을 파싱해서 Subject로 설정했던 PK를 꺼낸다</li>
<li>customUserDetailService에서 UserDetails 객체 생성</li>
<li>UsernamePasswordAuthenticationToken 객체에 유저정보인 CustomUserDetails, 자격증명, 권한정보를 담아보낸다. </li>
</ul>
<p>여기까지 진행후에 Filter에서 SecurityContext에 Authentication 객체를 <code>setAuthentication()</code>으로 저장해주면 컨트롤러 같은 계층에서 요청시에 참조가 가능하다.</p>
<h3 id="customuserdetails--customuserdetailservice">CustomUserDetails &amp; CustomUserDetailService</h3>
<pre><code class="language-java">@AllArgsConstructor
public class CustomUserDetails implements UserDetails {

    private Long id;
    private String nickname;

    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return nickname;
    }

    public Long getId() {
        return id;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}</code></pre>
<p>UserDetails를 implement 해주면 되고 난 권한정보는 생략하고 필요한 것만 세팅해줬다.
프로젝트에서 필요한것들로 커스텀해주면 된다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    // pk 받아서 해당 유저를 찾아 CustomUserDetails로 반환
    @Override
    public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
        User user =
                userRepository
                        .findById(Long.parseLong(id))
                        .orElseThrow(() -&gt; new UsernameNotFoundException(USER_NOT_FOUND.getMessage()));

        return new CustomUserDetails(user.getId(), user.getNickname());
    }
}</code></pre>
<h3 id="필터-체인에-추가">필터 체인에 추가</h3>
<pre><code class="language-java"> @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화
                .sessionManagement(
                        session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 비활성화
                .authorizeHttpRequests(
                        authorize -&gt;
                                authorize
                                        .requestMatchers(&quot;/v3/api-docs/**&quot;, &quot;/swagger-ui/**&quot;, &quot;/swagger-ui.html&quot;)
                                        .permitAll() // Swagger 경로는 누구나 접근 가능
                                        .requestMatchers(&quot;/api/v1/auth/**&quot;)
                                        .permitAll()
                                        .anyRequest()
                                        .authenticated() // 그 외의 경로는 인증된 사용자만 접근 가능
                        )
                .addFilterBefore(
                        new JwtAuthFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가

        return http.build();
    }</code></pre>
<h3 id="컨트롤러에서-사용하기">컨트롤러에서 사용하기</h3>
<p>위의 과정을 완료하면 스프링 security holder에 인증 객체가 저장이 된 것으로 컨트롤러에서 사용할 수 있다.</p>
<pre><code class="language-java">@PostMapping(&quot;/profile&quot;)
public ResponseEntity&lt;ApiResponse&lt;?&gt;&gt; profile(
            @Valid @RequestBody AddUserProfileRequest request,
            @AuthenticationPrincipal CustomUserDetails customUserDetails) {

    userService.updateProfile(request, customUserDetails.getId());

    return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}</code></pre>
<p>Filter가 요청을 처리하면서 Authentication 객체를 SecurityContextHolder에 저장하게되고
컨트롤러에서는 @AuthenticationPrincipal을 통해 CustomUserDetails 객체를 바로 주입받아 위와 같이 사용할 수 있다</p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/7cd00419-38f7-47dd-9153-f5167bfdfc80/image.png" alt=""><img src="https://velog.velcdn.com/images/chaen-ing/post/588682f9-1963-4d41-9d16-50dcfe58e236/image.png" alt="">authorization header를 포함한 상태에서(자물쇠모양) 요청을 보내면 실행이 되는 것 확인완료!</p>
<p>여기까지 <strong>카카오 소셜 로그인 + jwt 토큰을 사용한 인증 + spring security에서 CustomUserDetails</strong> 활용에 대해 알아보았다.</p>
<p>📌 앞으로...</p>
<ul>
<li>Spring Security에 대해 더 공부가 필요할듯</li>
<li>권한도 추가해보자</li>
</ul>
<hr>
<p>참고 블로그 🙇
<a href="https://velog.io/@win-luck/Springboot-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-Jwt-%ED%86%A0%ED%81%B0-%EB%B0%9C%EA%B8%89-%EB%B0%8F-API-%EA%B2%80%EC%A6%9D">https://velog.io/@win-luck/Springboot-카카오-소셜로그인-Jwt-토큰-발급-및-API-검증</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] Spring boot에서 jwt를 이용한 카카오 로그인  구현]]></title>
            <link>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-boot%EC%97%90%EC%84%9C-jwt%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-boot%EC%97%90%EC%84%9C-jwt%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Thu, 12 Dec 2024 10:56:03 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 <strong>카카오 api를 이용한 회원가입, 로그인 + jwt 토큰 발급 과정</strong>에 대해 정리해보겠습니다.
<img src="https://velog.velcdn.com/images/chaen-ing/post/ce0398e8-ac6c-4ceb-90e6-f5a256aea881/image.png" alt=""></p>
<p>위의 그림이 카카오 공식 문서에 나와있는 서비스 로그인 과정으로
글로 다시 적어보자면 아래와 같습니다</p>
<blockquote>
</blockquote>
<ol>
<li>클라이언트가 카카오 로그인 요청 </li>
<li>클라이언트는 카카오로부터 code를 받아 서버로 전달 </li>
<li>code를 통해 서버에서 카카오로 토큰 발급 요청 </li>
<li>카카오는 code 등을 검증 후 토큰을 서버로 전달</li>
<li>서버에서 토큰으로 유저 정보 조회, 등록</li>
<li>서버에서 유저에게 JWT 토큰을 생성 및 전달</li>
<li>유저는 요청마다 jwt토큰을 포함해서 서버에 요청</li>
</ol>
<p>차근차근..해봅시다</p>
<h3 id="일단-초기세팅">일단 초기세팅</h3>
<p><a href="https://developers.kakao.com">https://developers.kakao.com</a></p>
<p><strong>kakaodevelopers</strong>에 들어가서 <strong>내 애플리케이션</strong> &gt; <strong>애플리케이션 추가하기</strong>
이름이랑 아이콘 이런거 설정해서 하나 만들어준다.<img src="https://velog.velcdn.com/images/chaen-ing/post/135b3018-9d3f-4bae-a67e-23ec336cb3e7/image.png" alt=""><img src="https://velog.velcdn.com/images/chaen-ing/post/71f0abc0-3740-4d7a-95f5-dbf4d57576ce/image.png" alt="">생성한 애플리케이션으로 들어가서 아래 항목들을 설정해줘야한다.
<img src="https://velog.velcdn.com/images/chaen-ing/post/3098a6a7-1830-42c8-8292-e9704e3b63d0/image.png" alt=""></p>
<ul>
<li><strong>카카오 로그인</strong> : ON으로 설정</li>
<li><strong>동의항목</strong> : 비즈앱으로 등록하면 닉네임, 프로필사진, 카카오계정을 필수/선택으로 받을 수 있다. 이름, 성별, 연령대 등등은 사업자번호가 있어야한다고.. 🥹 나는 세개다 필수로 받아줬다!</li>
<li><strong>Redirect URI</strong> : 카카오 로그인에서 code를 받을때 사용하는 리다이렉트 주소라고 보면된다. 프론트에서 받는거라서 3030으로 설정해주면 되고 난 테스트를 위해 8080까지 2개 등록해줬다.
<a href="http://localhost:8080/kakao/callback">http://localhost:8080/kakao/callback</a>
<a href="http://localhost:3030/kakao/callback">http://localhost:3030/kakao/callback</a></li>
</ul>
<p>내 애플리케이션에서 앱 키 &gt; <strong>REST API  키</strong>와 <strong>redirect uri</strong>는 <code>env</code>파일 등에 넣어놓고 유출되지 않게 사용하기</p>
<p>다음은 <code>build.gradle</code> 설정</p>
<pre><code class="language-java">
    //OAuth2
    implementation &#39;org.springframework.security:spring-security-oauth2-client&#39;
    implementation platform(&#39;org.springframework.boot:spring-boot-dependencies:3.3.5&#39;)
    implementation &#39;org.springframework.boot:spring-boot-starter-webflux&#39;

    //JWT
    implementation &#39;io.jsonwebtoken:jjwt-api:0.11.5&#39;
    implementation &#39;io.jsonwebtoken:jjwt-impl:0.11.5&#39;
    implementation &#39;io.jsonwebtoken:jjwt-jackson:0.11.5&#39;

    //Spring WebFlux
    implementation &#39;org.springframework.boot:spring-boot-starter-webflux&#39;

</code></pre>
<p> OAuth2, JWT, webflux 설정을 추가해준다</p>
<h3 id="web-client-설정">Web Client 설정</h3>
<p>카카오 api와의 통신을 위해 <strong>WebClientConfig</strong>를 추가</p>
<pre><code class="language-java"> @Configuration
public class WebClientConfig { // Spring WebFlux에서 HTTP 요청을 비동기로 처리하기 위한 WebClient를 설정

    // Netty HTTP 클라이언트 설정
    @Bean
    public ReactorResourceFactory resourceFactory() {
        ReactorResourceFactory factory = new ReactorResourceFactory();
        factory.setUseGlobalResources(false);
        return factory;
    }

    @Bean
    public WebClient webClient() {
        // HTTP 클라이언트 설정
        Function&lt;HttpClient, HttpClient&gt; mapper =
                client -&gt;
                        HttpClient.create()
                                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) // 연결 시간 초과 1초로 설정
                                .doOnConnected(
                                        connection -&gt;
                                                connection
                                                        .addHandlerLast(new ReadTimeoutHandler(10))
                                                        .addHandlerLast(new WriteTimeoutHandler(10))) // 읽기 및 쓰기 시간 초과 10초로 설정
                                .responseTimeout(Duration.ofSeconds(1)); // 응답 시간 초과 1초로 설정

        // HTTP 클라이언트와 연결
        ClientHttpConnector connector = new ReactorClientHttpConnector(resourceFactory(), mapper);

        // WebClient 생성
        return WebClient.builder().clientConnector(connector).build();
    }
}</code></pre>
<p><a href="https://velog.io/@win-luck/Springboot-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-Jwt-%ED%86%A0%ED%81%B0-%EB%B0%9C%EA%B8%89-%EB%B0%8F-API-%EA%B2%80%EC%A6%9D">https://velog.io/@win-luck/Springboot-카카오-소셜로그인-Jwt-토큰-발급-및-API-검증</a>
해당블로그 참고했습니다</p>
<h3 id="카카오-로그인-api-구현">카카오 로그인 API 구현</h3>
<p><strong>1. Authorization Code 받아오기 (프론트)</strong></p>
<pre><code class="language-bash">https://kauth.kakao.com/oauth/authorize
?client_id=${REST_API_KEY}
&amp;redirect_uri=${REDIRECT_URI}
&amp;response_type=code</code></pre>
<p>클라이언트에서 카카오 로그인 버튼을 누르면 (해당 주소로 요청을 보내면) 카카오 인증 서버로 요청이가고 Redirect URI에 code가 전달된다.</p>
<pre><code class="language-bash">http://localhost:8080/kakao/callback?code={코드번호}</code></pre>
<p>해당 코드번호를 서버로 넘겨주면 됨</p>
<p><strong>2. Authorization Code를 통해 Access Token 받아오기 + Access Token을 통해 사용자 정보 가져오기</strong></p>
<p><strong>AuthController</strong></p>
<pre><code class="language-java">@RequiredArgsConstructor
@RestController
@RequestMapping(&quot;/api/v1/auth&quot;)
@Tag(name = &quot;Auth&quot;, description = &quot;인증 API&quot;)
public class AuthController {

    private final KakaoAuthService kakaoAuthService;

    @Operation(summary = &quot;카카오 로그인&quot;, description = &quot;카카오 로그인을 진행합니다.&quot;)
    @GetMapping(&quot;/login/kakao&quot;)
    public ResponseEntity&lt;ApiResponse&lt;Object&gt;&gt; kakaoLogin(@RequestParam String code) {

        return ResponseEntity.status(HttpStatus.OK)
                .body(ApiResponse.from(kakaoAuthService.kakaoLogin(code)));
    }
}</code></pre>
<p><strong>kakaoAuthService</strong></p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
@Slf4j
@Transactional(readOnly = true)
public class KakaoAuthService {

    private final KakaoApiClient kakaoApiClient;
    private final JwtTokenProvider jwtTokenProvider;
    private final UserService userService;

    @Transactional
    public String kakaoLogin(String code) {
        String accessToken =
                kakaoApiClient.getAccessToken(code); // 1. Authorization Code를 Access Token으로 교환

        Long userId = isSignedUp(accessToken); // 2. Access Token을 이용해 사용자 정보를 가져오고 없으면 회원가입

        HashMap&lt;Long, String&gt; map = new HashMap&lt;&gt;();
        map.put(userId, jwtTokenProvider.createToken(userId.toString())); // 3. JWT 토큰을 생성하여 반환한다.

        return map.get(userId);
    }

    @Transactional
    public Long isSignedUp(String token) {
        KakaoUserInfoResponse userInfo = kakaoApiClient.getUserInfo(token);
        return userService.findOrCreateUser(userInfo);
    }
}</code></pre>
<p><strong>KakaoApiClient</strong></p>
<pre><code class="language-java">@RequiredArgsConstructor
@Slf4j
@Component
public class KakaoApiClient { // kakao API를 호출하기 위한 전용 class

    private final WebClient webClient;
    private static final String USER_INFO_URI = &quot;https://kapi.kakao.com/v2/user/me&quot;;
    private static final String TOKEN_REQUEST_URI = &quot;https://kauth.kakao.com/oauth/token&quot;;

    @Value(&quot;${KAKAO_API_KEY}&quot;)
    private String kakaoApiKey;

    @Value(&quot;${KAKAO_REDIRECT_URI}&quot;)
    private String kakaoRedirectUri;

    // 카카오 API 호출 : Authorization code -&gt; Access Token
    public String getAccessToken(String code) {
        try {
            KakaoTokenResponse response =
                    webClient
                            .post()
                            .uri(TOKEN_REQUEST_URI)
                            .header(&quot;Content-Type&quot;, &quot;application/x-www-form-urlencoded&quot;)
                            .bodyValue(
                                    &quot;grant_type=authorization_code&amp;client_id=&quot;
                                            + kakaoApiKey
                                            + &quot;&amp;redirect_uri=&quot;
                                            + kakaoRedirectUri
                                            + &quot;&amp;code=&quot;
                                            + code)
                            .retrieve()
                            .bodyToMono(KakaoTokenResponse.class)
                            .block();

            return response.accessToken();

        } catch (WebClientResponseException e) {
            throw new RuntimeException(&quot;Failed to fetch access token from Kakao.&quot;, e);
        }
    }

    // 카카오 API 호출 : Access Token -&gt; 사용자 정보 조회
    public KakaoUserInfoResponse getUserInfo(String token) {
        try {
            return webClient
                    .get()
                    .uri(USER_INFO_URI)
                    .header(&quot;Authorization&quot;, &quot;Bearer &quot; + token)
                    .retrieve()
                    .bodyToMono(KakaoUserInfoResponse.class)
                    .block();
        } catch (WebClientResponseException e) {
            throw new RuntimeException(&quot;Failed to fetch access token from Kakao.&quot;, e);
        }
    }
}</code></pre>
<p>여기가 카카오 api와 통신하는 전부라고 보면되는데 먼저 코드를 토큰으로 교환해주기 위해서 <code>getAccessToken</code>을 호출한다. oauth/token 으로 apikey, redirect uri, code를 보내고 토큰을 받으면 된다. 여기까지 하면 카카오 서버에 회원이 등록된 것이라고 보면된다.</p>
<p>토큰을 받으면 이 토큰을 통해 카카오 api에서 사용자 정보를 조회할 수 있다.
<code>getUserInfo</code> 메소드를 통해 먼저 카카오 api에서 정보를 가져오고 이 정보가 데이터베이스에 저장되어있지 않은경우(신규 유저의 경우)에는 리포지토리에 저장해준다. 이부분은 <code>userService</code>에 자유롭게 작성하면된다.</p>
<p>이를 위해 작성해준 dto들이다.</p>
<pre><code class="language-java">public record KakaoAccount(
        Boolean profile_needs_agreement,
        Boolean email_needs_agreement,
        KakaoProfile profile,
        String email
) {}

public record KakaoProfile(
        String nickname,
        String profile_image_url // 프로필 이미지 URL
) {}

public record KakaoProperties(
        String profile_image, 
        String thumbnail_image
) {}

public record KakaoTokenResponse(
        @JsonProperty(&quot;access_token&quot;) String accessToken,
        @JsonProperty(&quot;token_type&quot;) String tokenType,
        @JsonProperty(&quot;expires_in&quot;) int expiresIn,
        @JsonProperty(&quot;refresh_token&quot;) String refreshToken,
        @JsonProperty(&quot;scope&quot;) String scope) {}

public record KakaoUserInfoResponse(
        Long id, 
        KakaoProperties properties,
        KakaoAccount kakao_account) {}</code></pre>
<p>kakao developers 에서 설정했던 것들 토대로 필요한것들만 받아와주면 된다.
<a href="https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info">https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info</a>
<strong>REST API</strong> &gt; <strong>사용자 정보 가져오기</strong> 에서 <code>properties</code>, <code>profile</code>에 대해 자세히 볼 수 있다.</p>
<h3 id="jwt토큰을-생성해서-반환하기">JWT토큰을 생성해서 반환하기</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@Component
public class JwtTokenProvider {

    @Value(&quot;${spring.jwt.secret}&quot;)
    private String secretKey;

    @Value(&quot;${spring.jwt.access.token.expiration}&quot;)
    private long accessTokenExpiration;

    private final UserDetailsService userDetailsService;

    // 객체 초기화, secretKey를 Base64로 인코딩
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userId) {

        Claims claims = Jwts.claims().setSubject(userId); // JWT payload 에 저장되는 정보단위
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + accessTokenExpiration)) // 토큰 유효시간 설정 (5시간)
                .signWith(SignatureAlgorithm.HS512, secretKey) // 암호화 알고리즘, 시크릿 키 설정
                .compact();
    }
</code></pre>
<p><code>jwt secret key</code>와 만료시간은 <code>env</code> 파일에 저장해주었다.
<code>jwt secret key</code>는 아무거로나 해도 되는데 HS512 알고리즘 사용을 위해서는 최소 512비트 이상으로 설정해줘야한다.</p>
<p>여기까지하면 카카오 로그인 + jwt 토큰 완료!
swagger를 통해 확인해보겠다<img src="https://velog.velcdn.com/images/chaen-ing/post/2fe7d2f0-65c4-4661-9aa4-2339664839a8/image.png" alt="">코드를 넣고 excute를 하면 아래와 같은 jwt 토큰을 발급해준다<img src="https://velog.velcdn.com/images/chaen-ing/post/8a2b566f-e1e1-4e32-865e-e4bb87f9d275/image.png" alt="">이 토큰을 Authorize에 넣고 로그인해주면 인증완료<img src="https://velog.velcdn.com/images/chaen-ing/post/cb83163d-76e9-482b-93b4-6b43678af9f9/image.png" alt=""></p>
<p>그러나 토큰을 제대로 사용하려면 모든 요청에 포함시켜야하고 유효성 검사하는 로직등이 필요하다.
다음에는 자동 필터링 과정과 컨트롤러에서 사용할수있는 예시에 대해 알아보자.</p>
<p>📌 <strong>고민되는것들</strong></p>
<ul>
<li>웹기준으로 작성했는데 안드로이드 환경에서도 잘 돌아갈까?</li>
<li>code를 프론트에서 넘겨주는 절차가 잘 작동할지?</li>
</ul>
<hr>
<p>참고 블로그 🙇
<a href="https://velog.io/@win-luck/Springboot-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-Jwt-%ED%86%A0%ED%81%B0-%EB%B0%9C%EA%B8%89-%EB%B0%8F-API-%EA%B2%80%EC%A6%9D">https://velog.io/@win-luck/Springboot-카카오-소셜로그인-Jwt-토큰-발급-및-API-검증</a>
<a href="https://velog.io/@jiwoow00/Spring-boot-JWT-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8">https://velog.io/@jiwoow00/Spring-boot-JWT-이용한-백엔드-카카오-로그인</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] Spring boot에서 env 파일 사용하기]]></title>
            <link>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-boot%EC%97%90%EC%84%9C-env-%ED%8C%8C%EC%9D%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chaen-ing/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-boot%EC%97%90%EC%84%9C-env-%ED%8C%8C%EC%9D%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 04 Dec 2024 13:35:22 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 원래 아래와 같은 방법으로 환경변수를 관리하고 있었다.</p>
<pre><code class="language-java">spring:
  config:
    activate:
      on-profile: local
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: &#39;jdbc:mysql://localhost:3306/name&#39;
    username: ${SPRING_DATASOURCE_USERNAME} # ex) root
    password: ${SPRING_DATASOURCE_PASSWORD} # ex) 0000
  jpa:
  ...</code></pre>
<p>yml 파일에서 ${} 이 부분에 각자 값을 넣어서 로컬 DB에 접근하는 방식이다. DB 접근은 잘되지만, 모르고 git에 푸시해버릴 수 있는 위험성이 있으므로 <code>env</code>파일을 통해 관리해보기로 했다.</p>
<p>Spring Boot 2.4 이상에서는 spring.config.import 기능을 사용하여 <code>.env</code> 파일을 쉽게 로드하고, 이를 application.properties에서 사용할 수 있다.</p>
<p><strong>1. 루트 디렉토리에 .env 파일 생성</strong></p>
<pre><code>#로컬 DB
DB_USER= ex) root
DB_PASS= ex) 0000
DB_URL= ex) jdbc:mysql://localhost:3306/name</code></pre><p>왼쪽에는 변수명 오른쪽에는 사용할 유저명, 비밀번호, URL등을 입력해주면 된다. &quot;&quot; 또는 &#39;&#39; 사용할 필요 X!!</p>
<p><strong>2. gitignore에 추가</strong>
gitignore에 .env를 추가해준다.
<img src="https://velog.velcdn.com/images/chaen-ing/post/5bd3f146-7e2a-4531-8a60-19aee4714da5/image.png" alt="">추가가 제대로 되었다면 .env파일이 이렇게 노란색으로 보인다</p>
<p><strong>3. 기존 yml파일 수정</strong></p>
<pre><code class="language-java">  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASS}</code></pre>
<p>env파일에서 설정한 변수명을 ${변수명} 형식으로 yml파일에 넣어준다</p>
<p><strong>4. yml파일에 .env import</strong>
<img src="https://velog.velcdn.com/images/chaen-ing/post/aba69500-ab6a-49c1-a8a7-0bb8c416188c/image.png" alt=""> 3번까지 했는데 위와 같은 에러가 떴다. .env 파일에서 설정된 DB_URL 값이 Spring Boot의 application.properties에서 제대로 읽히지 않았다는 것... dotenv import, Config 작성 등등... 이것저것 다 시도해봤는데 결론은 이것</p>
<pre><code class="language-java">spring:
  config:
    import: optional:file:.env[.properties]  # .env import</code></pre>
<p>yml파일에 이것만 추가해주면 끝!!
잘안되는 경우에는 env파일명이 제대로 설정되어있는지 검토하는 것을 추천한다...(나는 .env가 아니라 env로 해놔서 꽤 오래 찾음...;;)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] PR보낸 브랜치에 commit 추가하기]]></title>
            <link>https://velog.io/@chaen-ing/Git-push-%ED%9B%84-commit-%EC%B6%94%EA%B0%80</link>
            <guid>https://velog.io/@chaen-ing/Git-push-%ED%9B%84-commit-%EC%B6%94%EA%B0%80</guid>
            <pubDate>Tue, 03 Dec 2024 06:08:30 GMT</pubDate>
            <description><![CDATA[<p>작업을 완료해서 Push, pull request까지 만든 후에 리뷰를 받아 커밋을 추가해야할 때! PR을 또 보내는 것 대신 사용할 수 있는 방법이 2가지가 있다</p>
<p><strong>1. ammend를 통해 PR 보낸 커밋을 수정하고 강제로 덮어쓰기</strong></p>
<pre><code>git add .
git commit --amend -m &quot;메시지&quot;
git push -f origin 메인브랜치</code></pre><p><code>ammend</code> 명령은 이전 커밋을 수정하여 새 커밋 메시지와 함께 덮어쓰는 명령어이다.
새 커밋을 생성하지 않고, 기존 커밋의 내용과 메시지를 변경한다.
장점은 PR에 불필요한 커밋 히스토리가 남지 않아 깔끔하다는 것
그러나 단점은 기존 커밋을 덮어쓰는 것이기 때문에 되돌리려면 귀찮아진다.</p>
<p>나는 원래 이 방법을 사용했었는데 <strong>수정을 여러번 해야할 경우</strong>에는 여러번 덮어쓰게 되다보니 제대로 반영이 안되는 상황이 많이 발생했었다.</p>
<p><code>ammend</code> 명령어는 혼자 사용하는 리포지토리이거나, 또는 간단하고 적은 수정일때 사용하는 것을 권장한다!</p>
<p><strong>2. PR에 새 커밋 추가 (권장 방법)</strong></p>
<pre><code>git add .
git commit -m &quot;새로운 메시지&quot;
git push origin 메인브랜치
</code></pre><p><img src="https://velog.velcdn.com/images/chaen-ing/post/817f7f81-a2fc-4289-a4e0-b9170fe6cfd1/image.png" alt="">
현재는 <code>ammend</code> 없이 동일하게 add, commit, push 하는 방법으로 사용하고 있는데 위의 사진처럼 리뷰를 받은 후의 커밋 기록이 남는다.
이렇게 하면 기존 커밋을 보존 가능하고 변경 사항 확인이 쉽다는 장점이 있다!</p>
<p>PR 보낸 브랜치에 추가 작업을 할 때, 용도에 맞게 접근 방식을 선택해서 사용하도록 합시다~~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] git rebase 안될때]]></title>
            <link>https://velog.io/@chaen-ing/Git-git-rebase-%EC%95%88%EB%90%A0%EB%95%8C</link>
            <guid>https://velog.io/@chaen-ing/Git-git-rebase-%EC%95%88%EB%90%A0%EB%95%8C</guid>
            <pubDate>Tue, 03 Dec 2024 05:48:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaen-ing/post/32f31e8c-90f7-4ba4-a346-2543f5c132ab/image.png" alt=""> 위와 같은 로그를 플젝하면서 자주 접하게 되었다!!</p>
<pre><code>1. check out &quot;메인 브랜치&quot;
2. git pull
3. check out &quot;작업할 브랜치&quot;
4. git rebase &quot;메인 브랜치&quot;</code></pre><p>나는 보통 위 순서대로 원격저장소에서 최신 반영사항을 가져오는데 이 중 4번 *<em>rebase *</em>에서 conflict가 발생한 상황이다.</p>
<p><code>rebase</code>는 브랜치의 커밋을 다른 브랜치 위로 다시 정렬하여 깔끔한 커밋 히스토리를 만드는 데 사용한다.</p>
<p>hint를 보면 git rebase <strong>--continue, skip, abort</strong>가 있는데 각각 아래와 같은 용도로 사용한다</p>
<blockquote>
<p><strong>Continue</strong>
Rebase 중 conflict가 발생하면 수동으로 해결한 후, Rebase를 계속 진행(continue로 계속 진행해주면 됨)</p>
</blockquote>
<blockquote>
<p><strong>Skip</strong>
conflict가 발생한 곳을 건너뛰고 rebase를 계속 진행
건너뛴 커밋 내용이 히스토리에서 사라질 수 있음</p>
</blockquote>
<blockquote>
<p><strong>abort</strong>
rebase를 중단하고 시작하기 전 상태로 되돌림 
충돌이 너무 많은 경우에 사용할 수 있다</p>
</blockquote>
<p>평범하게 conflict가 난 상황이라면 <code>rebase --continue</code>로 해결해주면서 진행하면 됨</p>
<p>그러나 답이 없는 경우.. 
<code>rebase --abort</code> : rebase 중단
<code>git reset --hard origin/&lt;branch-name&gt;</code> : 원격 브랜치를 강제로 덮어쓰기
위의 방법을 이용해서 강제로 해결?할 수 있다. 
그러나 위의 경우 커밋하지 않은 내용, 로컬 커밋 등이 삭제되므로 신중하게 사용해야한다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 / java] 3055 : 탈출]]></title>
            <link>https://velog.io/@chaen-ing/%EB%B0%B1%EC%A4%80-java-3055-%ED%83%88%EC%B6%9C</link>
            <guid>https://velog.io/@chaen-ing/%EB%B0%B1%EC%A4%80-java-3055-%ED%83%88%EC%B6%9C</guid>
            <pubDate>Mon, 04 Nov 2024 17:03:25 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/3055">https://www.acmicpc.net/problem/3055</a></p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/ffbb6f17-ff8a-47bb-b8fd-31aaae46096d/image.png" alt=""></p>
<p>첫째 줄에 50보다 작거나 같은 자연수 R과 C가 주어진다.</p>
<p>R개 줄에는 티떱숲의 지도가 주어지며, 문제에서 설명한 문자만 주어진다. &#39;D&#39;와 &#39;S&#39;는 하나씩만 주어진다.</p>
<p>첫째 줄에 고슴도치가 비버의 굴로 이동할 수 있는 가장 빠른 시간을 출력한다. 만약, 안전하게 비버의 굴로 이동할 수 없다면, &quot;KAKTUS&quot;를 출력한다.</p>
<h4 id="🔎-문제-정리">🔎 문제 정리</h4>
<p><strong>R행 C열</strong>의 지도 존재</p>
<ul>
<li><strong>고슴도치 (S)</strong>  <ul>
<li>비버의 위치(D)까지 _최소 이동 횟수_로 이동해야함</li>
<li>물과 돌 통과 불가</li>
<li>물이 찰 예정인 칸으로도 이동 불가</li>
<li>상하좌우로 이동</li>
</ul>
</li>
<li><em>* 물 (</em>)** <ul>
<li>돌 통과 불가</li>
<li>비버의 위치로 확장 불가</li>
<li>상하좌우로 확장</li>
</ul>
</li>
<li><strong>빈곳 (.)</strong></li>
<li><strong>돌 (x)</strong></li>
</ul>
<h4 id="🔎-문제-해결-순서">🔎 문제 해결 순서</h4>
<ol>
<li><p>고슴도치의 위치, 현재 맵 등 입력 정보를 받아준다</p>
</li>
<li><p><code>bfs()</code> &amp; <code>flood()</code></p>
</li>
<li><p>먼저 <code>bfs()</code>에서 <code>flood()</code> 호출</p>
<p> 물이 차오를 예정인 곳에 고슴도치가 방문 불가능 하므로 먼저 물이 차오를 곳을 검토해준다.</p>
<pre><code>check 배열에 못가는 곳은 -1로 체크</code></pre><ul>
<li>동서남북으로 물 흐르도록</li>
<li>이미 방문했거나 돌이 있는 곳으로는 X</li>
<li>종료 노드로는 물이 흐를 수 X</li>
</ul>
</li>
<li><p>다시 <code>bfs()</code>로</p>
<blockquote>
<p>이때 <strong>BFS</strong>란?
 <strong>Breadth-First Search</strong>
 그래프에서 탐색을 할때 깊게 탐색하는 것이 아니라 넓게 탐색하는 방법이다.
 반대로 DFS는 깊게 그래프를 탐색하는 방식이다.</p>
</blockquote>
<p> BFS이므로 Queue를 사용한다.
 반복문을 돌면서 check 배열에 이동횟수를 넣어준다. </p>
<ul>
<li>돌 or 물 있는곳 X</li>
<li>이미 갔던곳 X</li>
<li>→ 즉 0인 곳만 갈 수 있음</li>
</ul>
</li>
<li><p>종료노드에 도달못하면 false, 도달하면 true를 메인으로 넘겨준다</p>
<ul>
<li>false : KAKTUS 출력</li>
<li>true : 이동 횟수를 출력</li>
</ul>
</li>
</ol>
<h4 id="🔎-코드">🔎 코드</h4>
<pre><code class="language-java">package boj3055;

import java.io.*;
import java.util.*;

public class Main {
    static char[][] map;
    static int[][] check;
    static int[] DX = {1,-1,0,0};
    static int[] DY = {0,0,1,-1};
    static Queue&lt;Node&gt; water;
    static Node endNode;

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

        StringTokenizer st = new StringTokenizer(br.readLine());
        int R = Integer.parseInt(st.nextToken());
        int C = Integer.parseInt(st.nextToken());

        map = new char[R][C];
        check = new int[R][C];

        Node startNode = new Node(0,0); // 고슴도치 위치
        water = new LinkedList&lt;&gt;();

        // map 입력받기
        for(int i = 0; i &lt; R; i++){
            String line = br.readLine();
            for(int j = 0; j &lt; C; j++){
                map[i][j] = line.charAt(j);
                if (line.charAt(j)== &#39;S&#39;){  // 시작
                    startNode = new Node(i,j);
                }else if(line.charAt(j) == &#39;*&#39;){    // 물
                    check[i][j] = -1;
                    water.offer(new Node(i,j));
                }else if(line.charAt(j) == &#39;X&#39;){    // 돌
                    check[i][j] = -1;
                }else if(line.charAt(j) == &#39;D&#39;){    // 종료
                    endNode = new Node(i,j);
                }
            }
        }

        boolean flag = bfs(R,C,startNode);
        if(!flag)   System.out.println(&quot;KAKTUS&quot;);
        else System.out.println(check[endNode.x][endNode.y]);
    }

     static boolean bfs(int r, int c, Node startNode){
        Queue&lt;Node&gt; queue = new LinkedList&lt;&gt;();
        queue.offer(startNode);

        int time = 0;

        while(!queue.isEmpty()) {
            time++;
            flood(r,c);    // 물 흐름

            int size = queue.size();
            while(size--&gt;0) {    // 현재 큐에 있는 횟수만큼 반복
                Node node = queue.poll();
                int minTime = check[node.x][node.y];

                for (int i = 0; i &lt; 4; i++) {
                    int dx = node.x + DX[i];
                    int dy = node.y + DY[i];

                    if (dx &lt; 0 || dx &gt;= r || dy &lt; 0 || dy &gt;= c) { // 범위
                        continue;
                    } else if (check[dx][dy] == 0) {
                        check[dx][dy] = time;
                        queue.offer(new Node(dx, dy));
                    }

                    if (dx == endNode.x &amp;&amp; dy == endNode.y) // 종료노드 도달시 끝
                        return true;


                }
            }


        }
        return false;   // 종료노드에 도달 불가
    }

    static void flood(int r, int c) {
        int size = water.size();

        for (int i = 0; i &lt; size; i++) {
            Node node = water.poll();

            for (int j = 0; j &lt; 4; j++) {
                int dx = node.x + DX[j];
                int dy = node.y + DY[j];

                if (dx &lt; 0 || dx &gt;= r || dy &lt; 0 || dy &gt;= c) { // 범위 초과
                    continue;
                } else if (check[dx][dy] == -1) {   // 이미 방문 or 돌
                    continue;
                } else if (dx == endNode.x &amp;&amp; dy == endNode.y) {
                    continue;
                } else {
                    check[dx][dy] = -1;
                    water.offer(new Node(dx, dy));
                }
            }
        }
    }
}
    class Node{
        int x, y;

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

</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] @RequestParam, @ModelAttribute, @RequestBody 총정리]]></title>
            <link>https://velog.io/@chaen-ing/Spring-RequestParam-ModelAttribute-RequestBody-%EC%B4%9D%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@chaen-ing/Spring-RequestParam-ModelAttribute-RequestBody-%EC%B4%9D%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 15 Oct 2024 16:09:47 GMT</pubDate>
            <description><![CDATA[<p>클라이언트에서 서버로 전달된 요청을 객체로 바인딩하기 위해 사용하는 여러 방법들에 대해서 정리해보겠습니다.</p>
<p>먼저 시작하기에 앞서 <strong>클라이언트에서 서버로 요청 데이터를 전달할 때</strong>는 주로 다음 <strong>3가지 방법</strong>을 사용합니다.</p>
<blockquote>
</blockquote>
<ul>
<li><strong>GET - 쿼리 파라미터</strong><ul>
<li><code>/...?username=hello&amp;age=20</code></li>
<li>메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달</li>
<li>예) 검색, 필터, 페이징등에서 많이 사용하는 방식</li>
</ul>
</li>
<li><strong>POST - HTML Form</strong><ul>
<li>content-type: <code>application/x-www-form-urlencoded</code></li>
<li>메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello &amp; age=20</li>
<li>예) 회원 가입, 상품 주문, HTML Form 사용</li>
</ul>
</li>
<li><strong>HTTP message body</strong>에 데이터를 직접 담아서 요청<ul>
<li>HTTP API에서 주로 사용, JSON, XML, TEXT</li>
<li>데이터 형식은 주로 JSON 사용</li>
<li>POST, PUT, PATCH</li>
</ul>
</li>
</ul>
<p>위의 두가지는 GET 쿼리 파라미터 전송 방식이든, POST HTML Form 전송 방식이든 형식이 같아 구분 없이 조회 가능하므로 요청 파라미터 조회 라고 합니다.</p>
<p>→ 즉 크게 2가지로 <strong>요청 파라미터 조회 방식</strong>, <strong>메세지 바디 방식</strong>이 있습니다.</p>
<h2 id="📌-requestparam">📌 @RequestParam</h2>
<p><code>@RequestParam</code> 애노테이션은 클라이언트가 전송하는 파라미터를 <strong>1:1로 받아서 바인딩</strong>할때 사용합니다.</p>
<pre><code class="language-java">@ResponseBody    
@RequestMapping(&quot;/request-param&quot;)
public String requestParam(
        @RequestParam(&quot;username&quot;) String memberName,
        @RequestParam(&quot;age&quot;) int memberAge) {

        log.info(&quot;username={} age={}&quot;, memberName, memberAge);
        return &quot;ok&quot;;
    }</code></pre>
<p>위의 예시 코드에서 <code>/request-param?username=kim&amp;age=20</code>이라는 요청이 들어오면
→ getParameter(&quot;username&quot;)을 통해 memberName에 값이 바인딩됩니다</p>
<ul>
<li>HTTP 파라미터 이름과 변수 이름이 같으면 (name=”xx”) 생략 가능
ex) <code>@RequestParam String username</code></li>
<li>String , int , Integer 등의 단순 타입이면 @RequestParam도 생략 가능
ex) <code>String username</code></li>
<li>그러나 스프링 3.2부터 파라미터 이름 인식 문제가 발생하는 경우가 있으니 생략하지 않는 것을 추천합니다..(명시적으로도 생략하지 않는 것을 추천)</li>
</ul>
<p>추가적으로 파라미터 필수여부(<strong>required</strong>), 기본값 적용(<strong>defaultValue</strong>)도 적용 가능합니다.</p>
<h2 id="📌-modelattribute">📌 @ModelAttribute</h2>
<p>@ModelAttribute는 요청파라미터를 받아서 필요한 객체를 만들고 그 <strong>객체에 값을 넣어주는 과정 자동화</strong>시키는 애노테이션입니다.</p>
<pre><code class="language-java">@ResponseBody
@RequestMapping(&quot;/model-attribute&quot;)
public String modelAttribute(@ModelAttribute HelloData helloData) {
    log.info(&quot;username = {} age = {}&quot;, helloData.getUsername(), helloData.getAge());
    return &quot;ok&quot;;
}</code></pre>
<p>위의 코드 예시를 통해 과정을 살펴보겠습니다.</p>
<p>스프링 MVC는 <code>@ModelAttribute</code>가 있으면 생성자를 통해 HelloData를  <strong>객체 생성</strong>
→ 요청 파라미터의 이름으로 HelloData 객체의 <strong><code>property</code></strong>를 찾음 
(객체에 getxxx, setxxx 메서드가 있으면 xxx라는 프로퍼티를 갖고있는 것)
→ <strong><code>setter</code></strong> 호출해서 파라미터의 값을 바인딩</p>
<p>즉 해당 애노테이션을 사용하려면 생성자와 Setter함수가 필수적으로 있어야합니다</p>
<p>@ModelAttribute에서는 타입이 잘못들어가면 <code>BindException</code>이 발생합니다.</p>
<p>또한 @RequestParam과 동일하게 애노테이션 <strong>생략</strong>이 가능합니다. </p>
<ul>
<li>Stirng, int, Integer 등 단순 타입 : @RequestParam</li>
<li>나머지 : @ModelAttribute</li>
</ul>
<h2 id="📌-requestbody">📌 @RequestBody</h2>
<p>@RequestBody는 <strong>Http 메시지 바디</strong>에 내용을 담아서 요청이 오면 이를 변환할 때 사용하는 애노테이션입니다.
메시지 바디로 들어온 메시지는 @RequestParam이나 @ModelAttribute로 당연히 읽을 수 없습니다.</p>
<pre><code class="language-java">@ResponseBody
@PostMapping(&quot;/request-body-string&quot;)
public String requestBodyString(@RequestBody String messageBody) throws IOException {
    log.info(&quot;messageBody = {}&quot;, messageBody);

    return &quot;ok&quot;;
}</code></pre>
<pre><code class="language-java">@ResponseBody
@PostMapping(&quot;/request-body-json&quot;)
public HelloData requestBodyJson(@RequestBody HelloData data) {
    log.info(&quot;username={}, age={}&quot;, data.getUsername(), data.getAge());
    return data;
}</code></pre>
<p>String, <strong>JSON</strong> 타입 모두 <code>@RequestBody</code> 애노테이션을 통해 받을 수 있습니다.
받은 메시지 바디는 내부의 <strong>HttpMessageConverte</strong>가 알아서 String 또는 <strong>객체 타입</strong>으로 변환해줍니다. </p>
<p>이때 @ModelAttribute처럼 setter를 사용하는 것이 아니라 생성자를 통해 객체를 생성합니다.</p>
<p>주로 JSON -&gt; 객체 타입 변환에 많이 사용됩니다.</p>
<p>응답의 경우에도 <code>@ResponseBody</code>를 사용하면 컨버터가 자동으로 변환해줍니다.</p>
<p>(컨버터에 관한 내용은 추후에 자세히 써보겠습니다..!)</p>
<h2 id="✔️-정리">✔️ 정리</h2>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center">@RequestParam</th>
<th align="center">@ModelAttribute</th>
<th align="center">@RequestBody</th>
</tr>
</thead>
<tbody><tr>
<td align="center">HTTP 요청 종류</td>
<td align="center">요청 파라미터 조회</td>
<td align="center">요청 파라미터 조회</td>
<td align="center">메시지 바디</td>
</tr>
<tr>
<td align="center">특징</td>
<td align="center">- 요청 파라미터의 1:1 바인딩<br> - 파라미터의 필수값 여부, 기본값 지정 가능<br> - 생략 가능</td>
<td align="center">- 요청 파라미터를 받고 객체 자동 생성 및 바인딩<br> - 생략 가능</td>
<td align="center">- http 메시지 바디 내용 자동 변환</td>
</tr>
<tr>
<td align="center">사용 예시</td>
<td align="center">- 검색이나 필터링<br> - GET</td>
<td align="center">- 검증이 필요한 상황<br>- GET</td>
<td align="center">- HTTP API에서 주로 사용<br> - JSON을 전송해야하는 상황<br> - POST</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[어떤 개발자가 되고 싶은가]]></title>
            <link>https://velog.io/@chaen-ing/%EC%96%B4%EB%96%A4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%90%98%EA%B3%A0-%EC%8B%B6%EC%9D%80%EA%B0%80</link>
            <guid>https://velog.io/@chaen-ing/%EC%96%B4%EB%96%A4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%90%98%EA%B3%A0-%EC%8B%B6%EC%9D%80%EA%B0%80</guid>
            <pubDate>Tue, 01 Oct 2024 15:49:26 GMT</pubDate>
            <description><![CDATA[<p>코테이토 개블쓰 스터디 덕분에 새로운 주제로 글을 써보게 되었다.
개인적인 글은 처음이라 조금 부끄럽다...</p>
<h3 id="🚗-산업공학과에서-컴퓨터학부로">🚗 산업공학과에서 컴퓨터학부로</h3>
<p>사실 나는 21년도에 숭실대학교 산업정보시스템공학과(이하 산공)에 입학했었다. 고등학생 때는 하고 싶은 것을 잘 몰랐어서 이것저것 하다보니  결국 대부분 교과로 원서를 쓰게 되었다. 그래도 공대에 가고 싶다! 라는 막연한 생각은 있었기에 모두 공대로만 지원했었고 결과적으로 숭실대 산공과에 입학하게 되었다. 산공에 대해 간단하게 설명하자면 여기도 분야가 매우 넓어서 다양한 것들을 배우게 된다. 1학년때 들었던 것만 해도 경영 + 화학 + 프로그래밍 정도가 생각난다.</p>
<p>산공과의 프로그래밍 수업에서 처음 코딩을 접하게 되었는데 너무 너무 재미있었고 성적도 잘 나왔었다. 지금 생각해보면 함수 정도의 정말 간단한 수준이였는데 우매함의 봉우리에 빠졌었던 것 같다. 아무튼 이때 코딩에 흥미가 생겨서 개발자라는 진로를 처음으로 생각해보게 된 것 같다. 이를 위해 컴퓨터에 대해 더 자세히, 체계적으로 배워보고 싶어서 전과를 고려하게 되었다. 특별하게 한 활동이 없어서 여름방학 때 초등학교에서 진행했던 코딩교육과 프로그래밍 수업내용을 바탕으로 자소서를 제출했고 결과적으로는 전과에 성공했다.</p>
<h3 id="💻-컴퓨터학부에서의-2-3학년">💻 컴퓨터학부에서의 2, 3학년</h3>
<p>전과 후 컴퓨터학부에서 들었던 2학년 수업들은 정말 어려웠었다... 객체지향, 자료구조, 알고리즘 등등 중요한 과목들이 2학년때 많은데 기초가 부족했었기에 따라가기 쉽지 않았고 하면서도 &#39;이게 뭐지..?&#39; 싶었던 때가 많았던 것 같다. 그렇게 2학년은 방황도 하고 이것저것 시도해보는 시간이였던 것 같다. 그러다보니 3학년때부터는 기초를 다시 쌓는 것과 전공 공부 열심히 하는 것이 자연스레 우선이 되었다. 어려웠던 내용도 여러번 다시보니까 이해가 되었고 이때 전공을 공부하는 방식도 나름대로 터득했던 것 같다.</p>
<h3 id="🏃-앞으로-해야할일들">🏃 앞으로 해야할일들</h3>
<p>그러나...
아직 부족한 점이 너무 너무 많다. 그동안 너무 전공 과목들 공부하느라 여력이 없었던 것 같고 제대로 백엔드 공부를 해보고자 올해는 휴학을 하게되었다. 아래는 올해~내년 초까지 이루고 싶은 목표들이다.</p>
<ol>
<li>프로젝트 경험 쌓기
백엔드와 프론트 중에서 고민하다가 백엔드로 진로를 정하게 된지가 얼마 안되어서 프로젝트 경험이 부족하다. 디자이너, 프론트, PM등 여러 분야의 사람들과 협업하는 것에 대해 많이 배우고 싶다. 올해 목표는 시작하게 된 프로젝트들을 잘 완수하고 많이 배워가는 것이다!</li>
<li>스프링과 자바 더 공부하기
단순히 사용하는 것이 아니라 제대로 알고, 이해하고 사용할 수 있었으면 좋겠다. 일단 스프링에 대해서는 듣고 있는 김영한님 스프링 강의를 꾸준히 듣고 적용해보면서 내것으로 만들고 싶다.</li>
<li>블로그 꾸준히 &amp; 잘 쓰기
현재 블로그에 있는 내용은 공부하면서 노션에 정리했던 내용을 백업하는 글이 대부분인 것 같다. 사람들에게 더 잘 읽힐 수 있고 의미 있는 글을 쓰기 위해 노력해봐야겠다. </li>
</ol>
<h3 id="💡-어떤-개발자가-되고-싶은가">💡 어떤 개발자가 되고 싶은가</h3>
<p>당연히 자기할일은 잘해야한다고 생각한다. 그래서 먼저 깊이 있는 개발자가 되고싶다. (열심히 하자..!) 그리고 다음으로는 소통을 잘하는 개발자가 되어야한다고 생각한다. 프로젝트를 할때도 다양한 파트분들과 이야기하고 협력할 일이 많기에 소통의 중요성을 톡톡히 느끼고 있다. 마지막으로는 개발자가 되기를 선택한 이상 계속 발전해나가고 공부해야한다고 생각한다. 그래서 항상 나의 모토지만 느리더라도 꾸준한 개발자가 되고싶다! </p>
<p>이번 글을 쓰면서 지금까지의 시간을 되돌아보고 나의 생각을 정리해볼 수 있는 좋은 기회가 되었던 것 같다. 앞으로도 화이팅 ~❗️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데이터베이스] SQL(2)]]></title>
            <link>https://velog.io/@chaen-ing/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-SQL2</link>
            <guid>https://velog.io/@chaen-ing/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-SQL2</guid>
            <pubDate>Wed, 25 Sep 2024 15:19:02 GMT</pubDate>
            <description><![CDATA[<h3 id="51-aggregate-functions--집계-함수"><strong>5.1 Aggregate Functions : 집계 함수</strong></h3>
<p>5개의 집계함수 지원. 각 함수는 테이블의 속성에 지원가능하며, 하나의 value를 리턴</p>
<ul>
<li><code>avg</code></li>
<li><code>min</code></li>
<li><code>max</code></li>
<li><code>sum</code></li>
<li><code>count</code></li>
</ul>
<pre><code class="language-sql">select count(*) --()안에 key value가 들어가면 같은 결과
from student;</code></pre>
<p>student 테이블에서 튜플의 개수를 리턴</p>
<p>null값이 있으면 세지 않는다</p>
<pre><code class="language-sql">Select avg(salary), max(salary), min(salary)
from professor
where deptName= ’CS’;</code></pre>
<p>: 컴퓨터학과의 교수들의 평균, 최대, 최소 연봉을 출력</p>
<pre><code class="language-sql">Select count(distinct pID)
from teaches
where semester=’Fall’ and year=2014;</code></pre>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/659b32ac-c5d7-4ed5-bd85-0fb14cfc3e15/image.png" alt=""></p>
<p>teaches 테이블</p>
<p>2014년 가을에 강의한 교수들의 아이디 반환 </p>
<p>distinct를 사용했으므로 중복없이 : 한교수가 여러과목 했었어도 한개만 셈</p>
<p>ex) count(distinct gender) : 2 → (M/F니까)</p>
<p><strong>Group By Clause</strong></p>
<p><code>group by</code> : 전체 테이블을 속성값으로 튜플을 분류하고, 나누어진 각 그룹에 대해 집계함수 적용</p>
<pre><code class="language-sql">Select deptName, count(*)
from professor
group by deptName;</code></pre>
<p>deptName 속성값으로 전체 튜플을 몇개의 그룹으로 나눈후에 각 그룹에 대해 count</p>
<pre><code class="language-sql">Select deptName, avg(salary)
from professor
group by deptName;</code></pre>
<p>professor 테이블에서 deptName 속성값으로 그룹을 분류한 후, 각 그룹에 대해 salary 속성의 평균값 구함
<img src="https://velog.velcdn.com/images/chaen-ing/post/f0f01b72-0dbb-4c22-ac64-f997821512e5/image.png" alt=""></p>
<p>위의 두 예제에 대한 결과값</p>
<p>주의사항</p>
<pre><code class="language-sql">Select deptName, pID, avg(salary)
from professor
group by deptName;</code></pre>
<p>group by절에 나온 속성과 집계함수만이 select 절에 나올 수 있음</p>
<p>→ pID는 그룹수보다 많이 존재하므로 뭘 보여줘야하는지 선정불가</p>
<pre><code class="language-sql">Select avg(salary)
from professor
group by deptName;</code></pre>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/ccde4d92-048f-405a-a20e-77bffaef1e26/image.png" alt=""></p>
<p>group by절에 나온 속성은 select절에 나오는 것이 좋으나 반드시 나와야하는 것은 아님</p>
<p>→ 위 예시의 경우 deptName 생략시 어떤 그룹에 대한 평균값인지 알 수 없음</p>
<p><strong>Having Clause</strong></p>
<p>group by절로 생성한 그룹에 대해 임의 조건을 명시하는데 사용</p>
<p>→ 반드시 group by가 먼저 나와야함! 없으면 잘못된 쿼리</p>
<pre><code class="language-sql">Select deptName, avg(salary)
from professor
group by deptName
having avg(salary) &gt; 6900;</code></pre>
<p>deptName 기준으로 그룹 생성</p>
<p>→ 각 그룹에 대해 avg(salary) &gt; 6900 조건 만족하는 절만 남김</p>
<p>→ 만족하는 절에 대해 select 뒤에 나오는거 출력</p>
<p><code>having</code> : 각 그룹에 대해 적용하여 만족하는 그룹이 다음 단계로 이관됨</p>
<p>where : 각 튜플에 적용하여 조건을 만족하는 튜플이 grouping 등의 다음단계로 넘어가는 것 </p>
<p>→ 둘이 동시에 존재하면 where절 조건이 먼저 적용되고 group by, having 순서로</p>
<pre><code class="language-sql">employee(name, salary, dno) -- dno는 dnumber 참조하는 외래키
department(dnumber, dname, location)</code></pre>
<p>다섯명 이상의 직원을 가진 부서에 대해, 부서의 이름과 $40,000이상을 버는 직원 수를 리턴</p>
<pre><code class="language-sql">Select dname, count(*)                
from department, employee
where dnumber=dno and salary&gt;40000
group by dname
having count(*)&gt;5;</code></pre>
<p>where 조건문이 가장먼저 실행되는데 여기서 salary &gt; 40000이상인 직원튜플만 남게됨</p>
<p>→ 직원수를 먼저세고 salary를 비교해야함</p>
<pre><code class="language-sql">Select dname, count(*) 
from department, employee
where dnumber=dno and salary&gt;40000 and
dno in (select dno from employee             
                group by dno           
                having count(*)&gt;5)
group by dname;</code></pre>
<p><code>dno in (세트)</code> : 세트안에 dno 존재시 참, 아닐시 거짓</p>
<p>→ 해당 문장 내부에서 직원수가 5명이상인 부서의 no 구할 수 있다</p>
<p>→ 하지만 이것도 문제 있다!</p>
<p>만약 관리처 부서의 직원이 10명인데 모두 40000불 이하의 연봉일때 : &lt;관리처, 0&gt; 튜플이 결과 테이블에 속해야하나 관리처 튜플이 결과에 전혀 속하지 않게됨</p>
<p>where절의 salary &gt; 40000절에 의해 모든 직원 튜플이 사라져서 후에 dno in에 관리처가 속하게 되더라도 적용할 튜플이 없음</p>
<p><strong>Null Values and Aggregates</strong></p>
<p>count(*)을 제외한 집계 함수는 기본적으로 null값 무시</p>
<p>sum : 널값 무시하고 계산</p>
<p>만약 salary라는 속성의 모든 값이 널이라면?</p>
<p>→ count(salary) : 0리턴</p>
<p>→ 다른 모든 집계함수들 null 리턴</p>
<p>ex) sum(salary) : null리턴</p>
<p>Null Example</p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/ee5df260-73aa-4d5b-b4c0-df77fe0f2818/image.png" alt=""></p>
<p>myTable 테이블</p>
<pre><code class="language-sql">select count(hourWage) from myTable;
// 결과 2</code></pre>
<p>count(*) 아니므로 null값 무시</p>
<pre><code class="language-sql">Select count(distinct hourWage) from mytable;
// 결과 2</code></pre>
<p>위에랑 같은 결과 나옴 </p>
<p>마찬가지로 null은 무시</p>
<pre><code class="language-sql">Select sum(hourWage) from mytable;</code></pre>
<p>널값은 무시하고 연산 : (5000+6000)/2 = 5500 나옴</p>
<pre><code class="language-sql">Select hourWage, count(*) from mytable group by hourWage;</code></pre>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/b5dbfad7-19cd-41c1-9faa-e02dd83d95e9/image.png" alt=""></p>
<p>group by hourWage하면 Null값을 하나의 값으로 취급</p>
<p>count(*)이므로 널값 무시하지 않는다</p>
<h3 id="52-joined-relations"><strong>5.2 Joined Relations</strong></h3>
<p>join 연산은 2개의 relations을 받아 하나의 relation을 결과로 리턴한다</p>
<p>조인에 필요한거 2가지</p>
<ul>
<li>join type<ul>
<li>inner : match하는것만 → inner 키워드 생략가능. join 연산은 inner join 의미</li>
<li>outer left : outer는 다 match 아닌것도</li>
<li>outer right</li>
<li>outer full</li>
</ul>
</li>
<li>join condition : match 기준 정함<ul>
<li>nutural</li>
<li>on<pred></li>
<li>using(A1,…An)</li>
</ul>
</li>
</ul>
<p>4*3 = 12가지의 join가능 + cartition product 3개</p>
<p><strong>Outer Joins</strong></p>
<p>외부 조인은 값 매치가 되지 않아 손실되는 정보를 유지하려고 하는 연산</p>
<p>일차적으로 조인연산을 수행하고, 조인 연산에서 제외된 튜플들을 널 값을 이용해 결과 테이블에 첨가</p>
<p><strong>Join Conditions : 조인 조건</strong></p>
<p>두 입력 테이블에서 어떤 조건으로 튜플이 매치되는지 결정하고, 어떤 속성이 결과 테이블에 나타나는지 결정</p>
<ul>
<li><p>natural</p>
<p>  공통되는 속성이 조인 속성</p>
<p>  공통되는 속성은 결과 테이블에 한번만 나온다</p>
</li>
<li><p>on <predicate></p>
</li>
<li><p>using(A1,…An)</p>
<p>  내추럴 조인과 비슷</p>
<p>  using(A1,…An) : A에 컬럼을 다 적으면 natural이랑 같은 셈</p>
</li>
</ul>
<p><strong>조인 연산 예시</strong></p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/857d9ff1-b165-4fa0-9442-fe7b570cd33a/image.png" alt=""></p>
<pre><code class="language-sql">myCourse inner join myPrereq 
on myCourse.cID = myPrereq.cID</code></pre>
<p>내부 조인 : 매치가 되는 튜플만 결과 테이블에 나오게됨</p>
<p>매치조건 : on</p>
<p>→ on <pred> : pred 조건에 만족하는 것만 매치됨</p>
<table>
<thead>
<tr>
<th>myCourse.cID</th>
<th>title</th>
<th>deptName</th>
<th>credit</th>
<th>myPrereq.cID</th>
<th>prereqCID</th>
</tr>
</thead>
<tbody><tr>
<td>BIO-301</td>
<td>Genetics</td>
<td>Biology</td>
<td>4</td>
<td>BIO-301</td>
<td>BIO-101</td>
</tr>
<tr>
<td>CS-301</td>
<td>DB</td>
<td>CS</td>
<td>4</td>
<td>CS-301</td>
<td>CS-101</td>
</tr>
</tbody></table>
<pre><code class="language-sql">myCourse left outer join myPrereq 
on myCourse.cID = myPrereq.cID</code></pre>
<p>left outer join : 1차적으로 join한 후 left테이블인 myCourse의 튜플 다 살림</p>
<p>매치조건 위와 동일(하지만 매치안되는 것도 살아있다)</p>
<table>
<thead>
<tr>
<th>myCourse.cID</th>
<th>title</th>
<th>deptName</th>
<th>credit</th>
<th>myPrereq.cID</th>
<th>prereqCID</th>
</tr>
</thead>
<tbody><tr>
<td>BIO-301</td>
<td>Genetics</td>
<td>Biology</td>
<td>4</td>
<td>BIO-301</td>
<td>BIO-101</td>
</tr>
<tr>
<td>CS-301</td>
<td>DB</td>
<td>CS</td>
<td>4</td>
<td>CS-301</td>
<td>CS-101</td>
</tr>
<tr>
<td>CS-302</td>
<td>AI</td>
<td>CS</td>
<td>3</td>
<td>null</td>
<td>null</td>
</tr>
</tbody></table>
<pre><code class="language-sql">myCourse natural left outer join myPrereq
myCourse left outer join myPrereq using(cID)
-- 위 두개 동일한 쿼리</code></pre>
<p>natural : 공통되는 속성이 조인속성이고, 속성 하나만 남김</p>
<p>left outer join</p>
<table>
<thead>
<tr>
<th>cID</th>
<th>title</th>
<th>deptName</th>
<th>credit</th>
<th>prereqCID</th>
</tr>
</thead>
<tbody><tr>
<td>BIO-301</td>
<td>Genetics</td>
<td>Biology</td>
<td>4</td>
<td>BIO-101</td>
</tr>
<tr>
<td>CS-301</td>
<td>DB</td>
<td>CS</td>
<td>4</td>
<td>CS-101</td>
</tr>
<tr>
<td>CS-302</td>
<td>AI</td>
<td>CS</td>
<td>3</td>
<td>null</td>
</tr>
</tbody></table>
<h3 id="53-nested-subqueries--중첩-서브질의"><strong>5.3 Nested Subqueries : 중첩 서브질의</strong></h3>
<p>SQL은 중첩되는 서브 질의에 대한 매커니즘 제공</p>
<p>서브쿼리는 다른 SQL 문장 안에 위치할 수 있는 select-from-where 문장임</p>
<p>set membership과 set comparison을 테스트할 때 자주 사용</p>
<p><strong>Single-row Subquery = scalar subquery</strong></p>
<p>단일 튜플을 반환하는 서브질의</p>
<pre><code class="language-sql">Select name
from professor
where salary = (select salary from professor where pID=‘10’)           
            and pID &lt;&gt; ‘10’;
-- &lt;&gt;는 != 의미</code></pre>
<p>괄호 안에 있는 서브질의는 1개의 튜플을 반환하므로 single row subquery = scalar subquery</p>
<p><code>=</code> 기호는 반환값이 한개일때만 사용 가능</p>
<pre><code class="language-sql">Select name
from professor
where salary =     (select max(salary)             
                                    from professor             
                                    where deptName=‘CS’);</code></pre>
<p>max()값은 항상 단일값이므로 유효한 SQL 문장</p>
<p><strong>IN Operator</strong></p>
<p><code>in</code> 연산자는 단일값이 다수값에 속하는지 검사</p>
<pre><code class="language-sql">Select name, salary 
from professor
where pID in (10, 21, 22);

Select name, salary
from professor
where pID=10 or pID=21 or pID=22;</code></pre>
<p>세명 다 있다면 다 리턴</p>
<p>속하는 것만 나온다고 보면됨</p>
<p><strong>Comparison Operators</strong></p>
<p>값 하나 간의 비교는 간단하나 값 하나와 여러값 간 비교는 간단하지 않음</p>
<p>SQL은 이를 위한 연산자(some, any, all, in)을 제공</p>
<pre><code>(1 &gt; 2) : false
(3 &gt;all {2, 3}) : false // 모든조건이 만족해야하는데  아니므로
(3 &gt;some {2, 3}) : true
(3 &gt;any {2, 3}) : true     // any ≡ some</code></pre><p>all : 모두 참이여야 참</p>
<p>some(또는 any) : 여러값 중 적어도 하나가 참이면 참</p>
<p><strong>Some(any)절 정의</strong></p>
<p><code>= some</code> 은 속한다는 의미의 <code>in</code> 연산자와 동일</p>
<p><code>≠ some</code>은 다수개중의 하나와도 같지 않으면 참인데 <code>not in</code>은 전체에 속하지 않아야 참이므로 동일하지 않음</p>
<pre><code>(5 &lt;some {0, 5, 6}) = true
(5 &lt;some {0, 5}) = false // 모든 조건이 만족하지 않으므로
(5 =some {0, 5}) = true
(5 ≠some {0, 5}) = true (since 0 ≠ 5) // 하나만이라도 같지 않으므로 참</code></pre><p><strong>all절 정의</strong></p>
<pre><code>(5 &lt;all {0, 5, 6}) = false // 모든조건이 만족해야하는데 아님
(5 &lt;all {6, 7}) = true
(5 =all {4, 5}) = false
(5 ≠all {6, 7}) = true (since 5≠6 and 5≠7) // 모든조건이 !=를 만족</code></pre><p><code>≠ all</code>은 모든 원소와 동일하지 않아야 참이므로 <code>not in</code>과 동일의미</p>
<p><code>= all</code>과 <code>in</code>은 동일하지 않음</p>
<p><strong>Set Comparison Example</strong></p>
<pre><code class="language-sql">Select distinct T.name
from professor as T, professor as S
where T.salary &gt; S.salary and S.deptName=’CS’; 

-- 위 아래 동일 

Select namefrom professor
where salary &gt; some (select salary                  
                                            from professor                 
                                            where deptName=’CS’);</code></pre>
<p>CS학과 교수 중 적어도 한명보다는 봉급이 많은 교수 이름 구하는 질의</p>
<pre><code class="language-sql">Select name
from professor
where salary &gt;all     (select salary             
                                            from professor             
                                            where deptName=’CS’);

-- 위아래 동일

Select name     
from professor     
where salary &gt; (select max(salary)             
                                from professor             
                                where deptName=’CS’);</code></pre>
<p>CS학과의 모든 교수보다 연봉이 높은 교수</p>
<p>*<em>Correlated Subqueries
*</em>
중첩질의에서 외부 쿼리에서 사용되는 테이블과 내부 쿼리에서 사용되는 테이블은 서로 상관없어서, 내부 쿼리가 외부 쿼리에 상관없이 실행 가능</p>
<p>내부쿼리가 외부쿼리의 테이블을 참조하면 이를 상관서브질의(상관중첩질의)라고 함</p>
<ul>
<li>서브쿼리는 외부 테이블에서 한 개의 튜플을 골라 이에대해 내부 중첩 질의 수행</li>
<li>시간 매우 많이 듦</li>
</ul>
<p><strong>exits Construct</strong></p>
<p>exists : 인자 형태로 표현되는 서브질의의 결과과 존재하면 참을 반환함</p>
<p>→ 즉, 내부질의를 수행하여 결과 튜플이 반환되면 참</p>
<pre><code class="language-sql">Select S.cID
from teaches as S
where S.semester = ’Fall’ and S.year= 2009 
            and exists (select *           
                                    from teaches as T         
                                    where T.semester = ’Fall’ and T.year= 2010               
                                                and S.cID = T.cID);</code></pre>
<p>2009년 가을과 2010년 가을에 둘다 개설된 강의의 번호 구하는 쿼리</p>
<p>exits 표현을 사용하는 상관 중첩질의</p>
<p>외부 테이블을 S로 rename하고 이를 exits내부 절에서도 사용</p>
<p><strong>exits without Correlation</strong></p>
<p>상관 서브 질의를 사용하지 않는 exits 연산자 사용은 의미 없음</p>
<pre><code class="language-sql">Select distinct sID        // nonsense query 
from student 
where exists (select cID // where절 다 빼도 의미 같음              
                            from course              
                            where deptName = ’CS’);
</code></pre>
<p>해당 쿼리는 중첩질의가 외부 질의문과 관련이 없음</p>
<p><strong>for all 쿼리</strong></p>
<p>for all 의미를 구현하는 연산자 SQL에서 제공 X → <code>not exits</code> 사용</p>
<pre><code class="language-sql">Select S.sID, S.name
from student as S
where not exists ( (select cID               
                                        from course               
                                        where deptName = ’CS’)
                                        except (select T.cID            
                                                        from takes as T            
                                                        where S.sID = T.sID) );</code></pre>
<p>  <img src="https://velog.velcdn.com/images/chaen-ing/post/3d8f0922-58b2-47a5-aa59-717a10b3c056/image.png" alt=""></p>
<p>X - Y = 공집합 </p>
<p>→ X 가 Y에 속함을 의미</p>
<p>→ <code>not exist (X except Y)</code>로 구현</p>
<p><strong>Unique Construct</strong></p>
<p><code>unique</code> 구성요소는 서브질의 결과에 중복성이 있는지 찾음</p>
<pre><code class="language-sql">Select C.cID
from course as C
where unique (select T.cID                   
                            from teaches as T                   
                            where C.cID=T.cID and T.year=2009);

--위아래동일

Select C.cIDfrom course as C
where 1 &gt;= (select count(T.cID)             
                        from teaches as T            
                        where C.cID=T.cID and T.year=2009);</code></pre>
<p>2009년에 최대 1번 강의한 과목을 찾아라(1번 이하로 개설된 과목)</p>
<pre><code class="language-sql">unique{&lt;1,2&gt;, &lt;1,2&gt;}        : false
unique{&lt;1,2&gt;, &lt;1,3&gt;}     : true
unique{&lt;1,2&gt;, &lt;1,null&gt;}     : true
unique{&lt;1,null&gt;, &lt;1,null&gt;}     : true</code></pre>
<p>null값이 나오는 튜플은 중복으로 나와도 unique하다고 판단</p>
<p>unique(r)에서 만약 r이 공집합이면 unique(r)은 참</p>
<p><strong>from절 서브질의</strong></p>
<p>서브질의 from절 안에서 사용 가능</p>
<pre><code class="language-sql">Select deptName, avgSalary
from (select deptName, avg(salary) as avgSalary         
            from professor         
            group by deptName)
where avgSalary &gt; 6900;

Select deptName, avgSalary
from (select deptName, avg(salary)        
            from professor       
             group by deptName) as deptAvg(deptName, avgSalary)
where avgSalary &gt; 6900;

Select deptName, avg(salary)
from professor
group by deptName
having avg(salary) &gt; 6900;
-- 세개다동일 마지막은 from절 내 서브질의 안쓰고 having절 이용</code></pre>
<p>평균연봉이 6900이상인 과 이름과 평균 연봉 구하기</p>
<pre><code class="language-sql">Select max(totalSalary)
from (select deptName, sum(salary)          
            from professor          
            group by deptName) as deptTot(deptName, totalSalary);

-- 위아래 동일

Select sum(salary)
from professor     
group by deptName
having sum(salary) &gt;= all (select sum(salary)
                                                  from professor
                                                  group by deptName);</code></pre>
<p>총 연봉이 가장 높은 과의 총 연봉을 구하시오</p>
<p><strong>lateral 절</strong></p>
<p>from절에서 선행관계 또는 서브질의를 참조하게함</p>
<pre><code class="language-sql">Select P1.name, P1.salary, avgSalary
from professor P1, lateral (select avg(P2.salary) as avgSalary                                  
                                                        from professor P2                                  
                                                        where P1.deptName= P2.deptName);</code></pre>
<pre><code class="language-sql">Select name, salary, avg(salary)         // syntax error
from professor
group by deptName;</code></pre>
<p>name, salary, avg(salary)값 개수가 다 틀려서 틀린 쿼리</p>
<p><strong>With 절</strong></p>
<p>SQL 문장의 결과를 임시적으로 저장하는 효과</p>
<pre><code class="language-sql">With     maxBudget(value) as         
            (select max(budget)        
            from department)
select deptName, budget
from department, maxBudget
where department.budget = maxBudget.value;</code></pre>
<pre><code class="language-sql">with deptTotal(deptName, value) as        
            (select deptName, sum(salary)        
                from professor        
                group by deptName),
            deptTotalAvg(value) as        
            (select avg(value)        
                from deptTotal)
select deptName
from deptTotal, deptTotalAvg
where deptTotal.value &gt; deptTotalAvg.value;</code></pre>
<p>deptTotal : deptName당 해당과에 속하는 교수의 봉급 합</p>
<p>deptTotalAvg : deptName당 교수봉급합의 평균값</p>
<p>*<em>Scalar Subquery *</em></p>
<p>서브질의가 오직 한개의 속성을 반환하고 동시에 속성값으로 한개를 반환한다면, 서브질의가 연산식에서 값이 반환되는 어떤 곳이라도 나타날 수 있게함</p>
<p>→ 이런 서브질의를 scalar 서브질의라고함</p>
<pre><code class="language-sql">Select deptName, (select count(*)             
                                    from professor p1            
                                    where d1.deptName = p1.deptName)
from department d1;</code></pre>
<p>스칼라 서브질의 예시</p>
<h3 id="54-ranking"><strong>5.4 Ranking</strong></h3>
<p>SQL 서버 : Select top(3) * from professor ordery by salary</p>
<p>// 상위 연봉 3인</p>
<p>MySQL : Select * from professor order by salary desc limit 3 offset 1;</p>
<p>// 상위 연봉 3인인데 최고 1명 제외</p>
<h3 id="55-more-features"><strong>5.5 More Features</strong></h3>
<p>blob, clob : 대용량 객체 저장 관리</p>
<hr>
<p>  📖 데이터베이스 1: 이론과 실제, 이상호, 진샘미디어</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024 54회 SQLD 시험 준비 + 후기 + 정리요약노트]]></title>
            <link>https://velog.io/@chaen-ing/2024-54%ED%9A%8C-SQLD-%EC%8B%9C%ED%97%98-%EC%A4%80%EB%B9%84-%ED%9B%84%EA%B8%B0-%EC%A0%95%EB%A6%AC%EC%9A%94%EC%95%BD%EB%85%B8%ED%8A%B8</link>
            <guid>https://velog.io/@chaen-ing/2024-54%ED%9A%8C-SQLD-%EC%8B%9C%ED%97%98-%EC%A4%80%EB%B9%84-%ED%9B%84%EA%B8%B0-%EC%A0%95%EB%A6%AC%EC%9A%94%EC%95%BD%EB%85%B8%ED%8A%B8</guid>
            <pubDate>Sat, 24 Aug 2024 04:11:13 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaen-ing/post/6e572f1e-440f-4d5a-9821-8d951e7ab200/image.png" alt=""></p>
<h3 id="📚-시험-준비-과정--후기">📚 시험 준비 과정 &amp; 후기</h3>
<p>먼저 <code>SQLD 노랭이</code> 책을 전체적으로 한번 + 틀린것만 다시 -&gt; 총 2회독 진행했습니다.
모르는 문제들은 <code>데이터 전문가 포럼 카페</code>에서 정말 많이 도움을 얻었습니다 👍</p>
<p><a href="https://cafe.naver.com/sqlpd?iframe_url=/ArticleSearchList.nhn%3Fsearch.clubid=21771779%26search.menuid=80%26search.media=0%26search.searchdate=all%26search.defaultValue=1%26search.exact=%26search.include=%26userDisplay=15%26search.exclude=%26search.option=0%26search.sortBy=date%26search.searchBy=1%26search.includeAll=%26search.query=12%26search.viewtype=title%26search.page=3">데이터 전문가 포럼 카페</a></p>
<p>풀면서 생소한 개념들이 꽤많길래 <code>유선배 SQL 개발자</code> 책도 뒤늦게 참고했습니다. (특히 ROLL UP 이런부분 설명 잘 되어있어서 요부분 헷갈리신다면 추천)
참고로 SQLD 시험이 2024에 개정이 된 것으로 알고있는데 22,23 책 참고해도 괜찮으니 아무거나 빌려 보시면 될 것 같습니다.</p>
<p>기출문제는 study with yuna 티스토리 참고했습니다.
<a href="https://yunamom.tistory.com/category/IT%EC%9E%90%EA%B2%A9%EC%A6%9D%20%EA%B3%B5%EB%B6%80/SQLD%20%EA%B8%B0%EC%B6%9C%EB%AC%B8%EC%A0%9C">Study with yuna</a></p>
<p>54회 시험 난이도는 평이했던 것 같고 노랭이나 기출에서 한번쯤 본 문제들이 대부분이였던 것 같았습니다. 노랭이에 있는 엄청 복잡한 문제들 같은거는 전혀 안나온 것 같습니다.</p>
<h3 id="📚-정리-요약-노트">📚 정리 요약 노트</h3>
<p><a href="https://cream-browser-e86.notion.site/SQLD-cb6c50a01ad84cf9a8c0bc50417cb9c0?pvs=4">https://cream-browser-e86.notion.site/SQLD-cb6c50a01ad84cf9a8c0bc50417cb9c0?pvs=4</a></p>
<p>공부하면서 정리한 내용입니다. 직접한거라서 빠진부분이 있을 수도 있지만 그래도 아까워서 올려봄.. </p>
<p>암튼 끝!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데이터베이스] SQL(1)]]></title>
            <link>https://velog.io/@chaen-ing/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-SQL1</link>
            <guid>https://velog.io/@chaen-ing/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-SQL1</guid>
            <pubDate>Tue, 06 Aug 2024 16:26:41 GMT</pubDate>
            <description><![CDATA[<p><strong>3.1 Database Languages</strong></p>
<p>DBMS는 유저와의 의사소통을 위해 데이터베이스 언어를 제공해야하고, 사용자는 언어를 이용하여 요구사항을 표현</p>
<p>데이터베이스 언어 분류 - 기능적 관점</p>
<ul>
<li>DDL : Data Definition Language</li>
<li>DML : Data Manipulation Language</li>
<li>DCL : Data Control Language</li>
</ul>
<p>데이터 언어 분류 - 표현 방식 관점</p>
<ul>
<li>Procedural : 절차적</li>
<li>Non Procedural : 비절차적</li>
</ul>
<p><strong>Data Definition Language : DDL</strong></p>
<p>데이터베이스 스키마에 대한 조작 담당하는 영역</p>
<p>스키마 생성, 삭제, 변경 등 담당</p>
<p>스키마에 관련되는 도메인, 데이터 제약 조건(ic)등을 표현할 수 있는 기능 제공</p>
<p>DDL 컴파일러는 데이터 사전에 저장되는 테이블을 생성함</p>
<p>ex)</p>
<pre><code class="language-jsx">Create table professor (        
        pID                char(5),            
        name               varchar(20),               
        deptName      varchar(20),                
        salary               numeric(8,2));</code></pre>
<p><strong>Data Manipulation Language : DML</strong></p>
<p>데이터의 인스턴스를 조작하는 언어</p>
<p>데이터 갬색, 생성, 조회, 삭제, 변경 등 기능 제공</p>
<p>사용자는 DML을 이용하여 질의를 생성하여 DBMS에 전달처리하게 하는데, 이런 측면에서 query language라고도 함</p>
<p><strong>Data Control Language : DCL</strong></p>
<p>스키마와 인스턴스 제외한 다른 객체 조작</p>
<ul>
<li>트랜잭션 시작/종료</li>
<li>세션 시작/종료</li>
<li>백업/복구</li>
<li>데이터 권한 부여 및 취소</li>
<li>사용자 계정 관리</li>
</ul>
<p><strong>Procedural vs Declarative : 절차적 vs 비절차적</strong></p>
<p>Procedural 절차적 언어</p>
<p>무슨 데이터를 요구하고 어떻게 데이터를 얻을 것인지 명시함 → 처리 방법 및 절차 명시</p>
<p>대부분의 언어는 prodecural → ex) 관계대수, C, C++, java…</p>
<p>Non Procedural(Decalrative) 비절차적 언어</p>
<p>어떻게 얻을 건지에 대한 내용없이 무슨 데이터가 필요한지만 명시 → 방법 및 절차 X 원하는 데이터만 명시</p>
<p>ex) SQL, PRolog, Lisp</p>
<p>비절차적 언어가 절차적 언어보다 진보된 언어. 하지만 컴퓨터 관점에서는 어렵고 복잡하다.</p>
<p><strong>Relational Database Languages : 관계형 데이터베이스 언어</strong></p>
<p>순수 관계형 데이터베이스 언어</p>
<p>이론적으로 개발되어있으나 상용 시스템에 구현되지 않은 언어</p>
<ul>
<li>관계 대수</li>
<li>튜플 관계 해석</li>
<li>도메인 관계 해석</li>
</ul>
<p>→ 세개 모두 질의어 표현력이 동일하고 언어간 상호변환 가능</p>
<p>실제 시스템에서 구현되어 있는 언어</p>
<ul>
<li>SQL</li>
<li>QUEL</li>
<li>Query by Example</li>
<li>LDL</li>
</ul>
<p><strong>SQL Overview</strong></p>
<p>= Structed Query Language의 약어</p>
<p>관계형 데이터베이스 언어</p>
<p>DDL + DML + DCL을 모두 포함하는 언어</p>
<p>관계형 데이터 베이스에서 사실상의 공식적인 표준 언어</p>
<p><strong>3.2 DDL SQL</strong></p>
<p>SQL언어의 DDL부분은 다음과 같은 것들을 정의함</p>
<ul>
<li>관계 스키마</li>
<li>속성의 도메인</li>
<li>무결성 제약(ic)</li>
<li>관계에 연관되는 인덱스</li>
<li>관계 저장을 위한 물리적 저장 구조</li>
</ul>
<p>⇒ 테이블과 컬럼 정의하는 명령어. 생성, 수정, 삭제 등의 데이터 전체 골격 결정</p>
<p>⇒ 데이터베이스의 데이터를 정의하는 언어로, DB관리자가 주로 사용. 스키마, 도메인, 테이블, 뷰, 인덱스 등을 정의하거나 변경, 삭제</p>
<p>SQL 명칭은 대소문자 구분이 없다 → Name = NAME = name = nAmE</p>
<p>인용부호 내에서는 대소문자 구분</p>
<p>세미콜론은 문장의 끝을 표시</p>
<p><strong>Domain Types in SQL</strong></p>
<p>char(n) : 길이가 n인 고정 길이 문자 스트링</p>
<p>varchar(n) : 최대 길이가 n인 가변 길이 문자 스트링</p>
<p>int : integer</p>
<p>smallint : Small integer</p>
<p>numeric(p, d) : 소수점 이하 자릿수 지정. p는 유효숫자 개수이고 d는 소수점 뒤에 나오는 숫자개수</p>
<p>→ ex) numeric(5,2) : xxx.xx 형태</p>
<p>real, double precision </p>
<p>float(n) : 소수점이 n자리 이상 나타내는듯?</p>
<p><strong>Create Table</strong></p>
<p><code>create table</code> 문장은 새로운 테이블 정의하여 생성함.</p>
<pre><code class="language-sql">Create table R     
        (A1 D1,     
            ...,      
            An Dn,    
            (integrity-constraint1),     
            ...,     
            (integrity-constraintk));</code></pre>
<p>R : relation 이름</p>
<p>각각의 A : relation에 있는 속성의 이름</p>
<p>D : 속성 A의 데이터타입 → 즉, 도메인</p>
<p>마지막에는 관련 데이터 무결성 제약이 나옴</p>
<p>Example)</p>
<pre><code class="language-sql">Create table professor (      
        pID           char(5),        
        name         varchar(20) not null,       
        deptName      varchar(20),        
        salary           numeric(8,2));

- Insert into professor values (‘10’, ’Lee’, ’CS’, 7500);
- Insert into professor values (‘11’, ‘Choi’, ’CS’, 7000);</code></pre>
<p>professor 테이블을 정의하며, 4개의 속성 가짐</p>
<p>각각 이름과 데이터 타입 정의함</p>
<p>두번째 name 속성은 널값을 허용하지 않는 not null 무결성 제약을 갖고 있음</p>
<p>→ Insert : professor 테이블에 튜플을 추가하는 명령어</p>
<p>위의 두개 insert 명령을 수행하면 두개의 튜플이 생성됨.</p>
<p>인용부호는 데이터타입이 문자형일때 사용. 기본적으로 single quote</p>
<p><strong>Integrity Constraints : 무결성 제약</strong></p>
<p>주로 세가지를 많이 사용</p>
<ul>
<li>not null : 널 값 허용 X</li>
<li>primary key(A1, …, An) : 테이블의 주 키 선언</li>
<li>foreign key(A1, …, An) references R : 외래 키 선언</li>
</ul>
<pre><code class="language-sql">Create table professor (    
        pID             char(5),       
        name         varchar(20) not null,         
        deptName    varchar(20),     salary         numeric(8,2),         
        primary key (pID),       
        foreign key (deptName) references department);</code></pre>
<p>주키 : pID</p>
<p>외래키 : deptName → department 테이블을 참조하는 외래키</p>
<p>→ 속성을 명시적으로 언급하지 않아도됨. 외래키는 참조되는 테이블의 주키만 참조하므로</p>
<p>→ 언급한다면 foreign key (deptName) references department(department의 주키); 이런식으로</p>
<p>무결성 제약은 선언시 이름 명기 가능 : 후에 제약 삭제/변경 용이</p>
<p>→ ex) constraint myFirstForeignKey foreign key (deptName) references department;</p>
<p><strong>University Database Creation</strong></p>
<pre><code class="language-sql">Create table department (    
        deptName    varchar(20) primary key, //주키가 한개일땐 이런식으로 가능
        chairman    char(5),    
        building        varchar(30),     
        budget        numeric(10,0)    
        foreign key (chairman) references professor);
        -- foreign key (chairman) references professor(pID);로 참조 속성 명시할 수도 있음

Create table professor (     
        pID               char(5) primary key,          
        name           varchar(20) not null,      
        deptName     varchar(20),    
        salary        numeric(10,2),         
        foreign key (deptName) references department); 

Create table student (     
        sID               char(5) 
        primary key,          name           varchar(20) not null,    
        gender        char(1),     
        deptName     varchar(20),    
        GPA        numeric(3,2),    
        totalCredit    integer,         
        foreign key (deptName) references department),     
        check (gender in (‘F’, ‘M’)));
        -- gender 속성은 &#39;F&#39; or &#39;M&#39;만 가져야하는 제약

Create table course (    
        cID        char(5) primary key,    
        title        varchar(20),    
        deptName    varchar(20),    
        credit        integer,    
        foreign key     (deptName) references department);    

Create table teaches (         
        pID            char(5),        
        cID         char(5),        
        semester      char(10),         
        year           numeric(4,0),        
        classroom      char(5),    
        primary key     (pID, cID, semester, year), // 주키가 4개 속성의 조합       
        foreign key     (pID) references professor,    
        foreign key     (cID) references course,    
        foreign key     (classroom) references room);

Create table takes (         
        sID            char(5),        
        cID         char(5),        
        semester      char(10),         
        year           numeric(4,0),        
        grade           varchar(2),    
        primary key     (sID, cID, semester, year),        
        foreign key     (sID) references student,    
        foreign key     (cID) references course);

Create table room (    
        roomID     char(5) primary key,       
        building      varchar(30),         
        capacity    numeric(6,0));</code></pre>
<p><strong>Drop/Alter Table</strong></p>
<p><code>Drop</code> : 스키마 삭제</p>
<p>→ ex) Drop table student;</p>
<p><code>Alter</code> : 스키마 변경</p>
<pre><code class="language-sql">Alter table r add A D;
Alter table r drop A; 

Alter table student add constraint myConst foreign key (deptName) references DEPARTMENT on delete cascade;
-- 이런 예시도 있다</code></pre>
<ol>
<li>r 이라는 테이블에 A라는 속성을 추가. D는 A의 도메인</li>
<li>A는 r이라는 테이블의 속성을 제거</li>
</ol>
<p><strong>Drop vs Delete</strong></p>
<p>Drop : 스키마 전체를 삭제하는 DDL 기능. 테이블전체를 날려버림. 그니까 당연히 내용도 날아감</p>
<p>→ ex) Drop table student; // student 테이블 전체 삭제</p>
<p>Delete : 테이블의 내용(즉, 튜플)을 삭제하는 DML 문장. 테이블 스키마는 남아있음</p>
<p>→ ex) Delete from student; // student 테이블의 내용 삭제</p>
<p>DDL에서 중요한 키워드 3개 : CREATE / ALTER / DROP</p>
<p><strong>3.3 DML SQL</strong></p>
<p>대표적인 키워드 4개</p>
<ul>
<li>Selecet : 검색</li>
<li>Insert : 입력</li>
<li>Delete : 삭제</li>
<li>Update : 갱신</li>
</ul>
<p><strong>Insertion : 입력</strong></p>
<p><code>insert</code> 데이터베이스 튜플을 입력하는 연산</p>
<pre><code class="language-sql">Insert into course values (’437’, ’Advanced Databases’, ’CS’, 4);
Insert into course (cID, title, deptName, credit)values (’437’, ’Advanced Databases’, ’CS’, 4);</code></pre>
<p>위의 두가지 질의문 동일하다</p>
<pre><code class="language-sql">Insert into course values (’777’, ’undecided’, ’CS’, null);</code></pre>
<p>null이라고 명시하면 해당 속성에는 값이 들어가지 않음</p>
<pre><code class="language-sql">Insert into professor select * from professor;</code></pre>
<p>insert문장에 select from where 표현 사용가능</p>
<p>이렇게 할 시 select from where이 평가되기 전까지 튜플이 테이블에 입력되지 않음</p>
<p>해당 문장의 효과는 튜플개수가 2배되는 것</p>
<p><strong>Deletion</strong></p>
<pre><code class="language-sql">Delete from professor;</code></pre>
<p>professor 테이블에 있는 모든 튜플을 삭제함</p>
<p>튜플 삭제되어도 스키마는 존재 </p>
<p>→ 스키마 삭제는 drop</p>
<pre><code class="language-sql">Delete from professor where deptName=’EE’;</code></pre>
<p>professor 테이블에서 과가 EE인 튜플 삭제</p>
<pre><code class="language-sql">Delete from professor 
where deptName in (select deptName                
                                        from department                      
                                        where building = ’Vision Hall’);</code></pre>
<p>중첩질의</p>
<p>Vision Hall에 있는 학과의 교수 튜플 제거</p>
<pre><code class="language-sql">Delete from professor
where salary &lt; (select avg(salary) from professor);</code></pre>
<p>봉급이 교수 평균 봉급보다 작은 교수 삭제</p>
<ol>
<li><p>집계함수는 where절에 직접 나올 수 없으므로 중첩질의 사용</p>
</li>
<li><p>교수가 삭제됨에 따라 평균 값도 변할 수 있는데, SQL에서는 처음에 평균값 계산하고 이를 근거로 연산 수행</p>
</li>
</ol>
<p><strong>Updates</strong></p>
<p><code>Update ~ set ~ where</code></p>
<p>Increase salaries of professors whose salary is over 7000 by 3%, and all others receive a 5% raise을 SQL으로 표현</p>
<pre><code class="language-sql">Update professor      
set salary = salary*1.03     
where salary &gt; 7000;

Update professor        
set salary = salary*1.05    
where salary &lt;= 7000;</code></pre>
<p>교수 봉급을 인상하는 갱신 문장 : 봉급이 7000 이상이면 3% 나머지는 5% 인상한다.</p>
<p>update를 여러번 할 때는 순서가 중요함</p>
<p>→ 3%인상 후에 5% 인상하게되면 3%인상으로 7000 이상이 된 교수는 인상이 중복 적용됨</p>
<pre><code class="language-sql">Update professorset salary = case            
                                                            when salary &lt;= 7000 then salary*1.05            
                                                            else salary*1.03             
                                                        end;</code></pre>
<p>case 사용하여 만든 동일한 갱신 문장</p>
<pre><code class="language-sql">Update student S
set S.totalCredit =         
            (select sum(credit)            
            from takes natural join course         
            where S.sID=takes.sID and grade &lt;&gt; ’F’ and grade is not null);</code></pre>
<p>student 테이블의 totalCredit 속성 값 갱신함.</p>
<p>grade 속성이 F가 아니고 grade가 널값이 아닌 과목의 credit 속성 값의 합을 구하여 totalCredit 값으로 갱신</p>
<p>set 절에서 scalar subquery가 사용됨</p>
<pre><code class="language-sql">Update student
set totalCredit = 0
where totalCredit is null;</code></pre>
<p>만약 어떤 학생이 과목을 하나도 이수하지 않았다면 sum(credit)은 널값이 나옴</p>
<p>→ 이를 0으로 갱신하고자 하면 위의 갱신문 사용하면됨</p>
<p><strong>3.4 Select SQL Statements</strong></p>
<p><strong>select 절</strong></p>
<pre><code class="language-sql">select A1, A2, ..., An        -- 생략 불가 attribute list    
from R1, R2, ..., Rm        -- 생략 불가 relation list    
where P            -- selection predicate    
group by &lt;grouping attributes&gt;    
having &lt;conditions&gt;    
order by &lt;ordering attributes&gt;;</code></pre>
<p><code>select</code> 6개의 절 가질 수 있음</p>
<p>select와 from은 생략불가. 나머지는 생략가능하나 순서 지켜야함</p>
<p>having 절은 group by절이 나와야만 나올 수 있음</p>
<p>select 문의 결과는 relation</p>
<pre><code class="language-sql">Select name  
from professor;
-- professor 테이블에서 교수 이름이 있는 name 속성 열만 출력</code></pre>
<p>select 절은 질의 결과에서 사용자가 보고 싶은 속성 리스트를 가짐</p>
<p>→ 관계 대수의 project 연산과 대응됨</p>
<pre><code class="language-sql">Select *        
from professor;
-- professor 테이블 출력</code></pre>
<p><code>*</code>는 ‘모든 속성’을 의미한다</p>
<pre><code class="language-sql">Select pID, name, deptName, salary/12
from professor;
-- professor 테이블에서 해당속성들을 차례로 보여줌 
-- salary/2도 자동연산해서 보여줌 -&gt; 당연하지만 값이 아예 바뀌는 것은 아님</code></pre>
<p>select 절에 나오는 속성은 수식 표현도 가능하다</p>
<pre><code class="language-sql">Select distinct deptName
from professor;
-- 중복제거하고 학과 이름 보여줌</code></pre>
<p>SQL은 결과 테이블의 튜플의 중복 허용</p>
<p>→ 허용하지 않으려면 select 절에 위치하는 속성에 distinct 키워드</p>
<p>위의 예제처럼 하면 중복되는 학과의 이름은 제거</p>
<pre><code class="language-sql">Select all deptName      
from professor;
-- 학과 이름만 보여줌
-- Select deptName from professor;과 동일</code></pre>
<p>all 키워드는 중복 허용</p>
<p>디폴트이기때문에 생략해도 똑같음</p>
<p><strong>where 절</strong></p>
<p><code>where</code> 절은 결과 튜플이 만족해야하는 조건을 명시함</p>
<p>→ 관계대수의 select 연산에 대응</p>
<pre><code class="language-sql">from professor
where deptName = ‘CS&#39; and salary &gt; 8000;</code></pre>
<p>CS과이면서 연봉이 8000 이상인 교수를 검색하는 질의</p>
<p>and, or, not 같은 논리연산자 사용 가능. 적용순위는 산술연산자보다 후 순위</p>
<p><strong>from 절</strong></p>
<p><code>from</code>절에는 질의에 관련있는 테이블을 나열해야함</p>
<p>→ 관계 대수의 카티시안 곱 연산에 대응</p>
<pre><code class="language-sql">Select *
from professor, teaches;</code></pre>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/dcac271e-7ac6-40fc-90e7-6b421bd8bddd/image.png" alt=""></p>
<p>가능한 모든 professor, teacher의 쌍을 생성함. 카티시안곱 연산 결과를 리턴하는 질의</p>
<p><strong>SQL Execution Model : 실행 과정</strong></p>
<p><code>from</code>절에 명시된 각 테이블에서 하나의 튜플을 가져온다</p>
<p>→ <code>where</code>절에 명시된 조건에 적용</p>
<p>→ 참이라면 튜플을 <code>group by</code>절로 보냄 : from절에 있는 모든 조합의 튜플에 대해 위의 과정 실행</p>
<p>→ group by절에 명시된 속성을 이용하여 서브그룹을 생성</p>
<p>→ 각 그룹에 대해 <code>having</code> 절의 조건을 적용하여, 조건을 참으로 만드는 서브그룹을 구함</p>
<p>→ 서브그룹에 대해 <code>order by</code> 절을 적용하여 디스플레이</p>
<p><strong>Joins</strong></p>
<p>조인은 where절에서 명시할 수 있음</p>
<pre><code class="language-sql">Select name, cID
from professor, teaches
where professor.pID=teaches.pID;</code></pre>
<p><img src="blob:https://velog.io/48184730-e399-4599-a4c8-6e401409dda3" alt="업로드중.."></p>
<p>where절의 <code>=</code>는 professor테이블의 pid속성과 teaches테이블의 pid 속성간의 equi조인 표현</p>
<p>→ <code>equi조인</code> :  두 테이블의 중복되는 속성(여기선 Pid)가 같은것만 남김</p>
<p>⇒ 즉 pID를 기준으로 두 테이블을 합치는것</p>
<p>→ 여기서 pID 속성이 두번나오므로 하나를 제거하게되면 : <code>natural join</code></p>
<p>pID속성이 두 테이블 모두에 존재하므로 속성이름만 사용하면 혼돈 발생 가능 → 테이블 이름도 명시</p>
<pre><code class="language-sql">Select title, name
from teaches, course, professor
where teaches.cID=course.cID and teaches.pID=professor.pID 
and course.deptName=‘CS‘;</code></pre>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/7de1f7f6-1aa1-4057-a0fe-9365f9b72ee9/image.png" alt=""></p>
<p>teaches, course, professor간에 equi조인이 필요한 질의</p>
<p>deptName은 course와 professor 모두에 존재하므로 course.deptName으로 표현</p>
<p><strong>Natural Joins</strong></p>
<p><code>natural join</code></p>
<p>자연조인은 두 테이블에서 동일한 이름을 가지는 속성간에 조인연산을 적용하며 결과 테이블에는 조인속성에 대한 중복이 제거하여 한번만 나옴</p>
<pre><code class="language-sql">Select *     -- 8 attributes 
from professor natural join teaches;</code></pre>
<p>professor 테이블과 teaches 테이블에는 pID 속성이 공통존재</p>
<p>→ 둘 중 하나는 제거됨 : 총 속성 8개 테이블 리턴</p>
<pre><code class="language-sql">Select *    -- 9 attributes but semantically equivalent
from professor, teaches
where professor.pID=teaches.pID;</code></pre>
<p>이렇게하면 professor.pID와 teaches.pID 두 속성 모두 테이블에 나오게되므로 총 9개의 속성 테이블을 리턴하게됨</p>
<p>동일한 이름을 가진 속성으로 인해 원하지 않은 조인 연산이 일어나지 않도록 주의해야함</p>
<p>교수 이름과 그 교수가 강의하는 과목명을 검색하는 질의어를 만들어보자</p>
<pre><code class="language-sql">Select name, title
from professor natural join teaches natural join course;</code></pre>
<p>위의 질의는 틀린것</p>
<p>deptName이 professor, course테이블에 공통 속성으로 존재 : 이 속성에 대해 Natural join</p>
<p>→ 교수가 소속학과에서 한 수업만 나오게됨 </p>
<p>→ 교수가 다른학과에서 개설한 교과목 정보 포함안됨 : Wrong!</p>
<pre><code class="language-sql">Select name, title
from professor natural join teaches, course
where teaches.cID= course.cID;

Select name, title
from (professor natural join teaches) join course using(cID);
-- using : 동일한 이름을 가진 모든 속성 X, 명시된 속성만을 조인 속성으로 하는 것

Select name, title
from teaches, course, professor
where teaches.cID=course.cID and teaches.pID=professor.pID;</code></pre>
<p><strong>Rename Operations</strong></p>
<p><code>as</code> 키워드를 통해 테이블과 속성 재명명 가능</p>
<p>→ old-name as new-name</p>
<pre><code class="language-sql">Select pID, name, salary/12 as monthlySalary
from professor;</code></pre>
<p>salary/12를 monthlySalary로 재명명 → 영구적으로 바뀌는건 아님. Select문은 다 그럼</p>
<pre><code class="language-sql">Select sID, name myName, deptName
from student;         -- “name” is renamed
Select sID, name, myName, deptName
from student;         -- wrong attribute name, syntax error</code></pre>
<p>키워드 as는 생략가능 : 생략하려면 as만 그냥 빼면됨</p>
<ol>
<li>as를 생략하고 name을 myName으로 재명명한 것</li>
<li>4개의 속성 테이블을 리턴하는 문장 → wrong!</li>
</ol>
<pre><code class="language-sql">Select distinct T.name -- T.name이라고 있어도 이렇게 출력되는것 아님. name이라고 나옴
from professor as T, professor as S
where T.salary &gt; S.salary and S.deptName = ‘CS’;
-- 소프트웨어 학부의 최소 아무 1명보다 돈을 많이 받는 사람들의 목록
-- 최저가 6000이니 그 이상인 사람들(중복 없이)</code></pre>
<p>두번 재명명도 가능</p>
<p>원래 from절에 명기된 모든 테이블에서 한개의 튜플을 가져와서 where절 조건 적용</p>
<p>→ from절에 동일 테이블 두번 재명명</p>
<p>→ professor 테이블에서 한번에 두개 튜플 가져오는 효과 </p>
<p>→ 비교 연산할 때 사용</p>
<p><strong>String Operations</strong></p>
<p><code>like</code> 연산자는 패턴을 활용하여 스트링에 대한 비교연산 제공</p>
<p><code>%</code> : 길이에 무관한 임의 스트링 → 몇개의 문자가와도 상관없다는 의미</p>
<p><code>_</code> : 하나의 character</p>
<pre><code class="language-sql">Select name
from professor
where name like &#39;%da%‘;</code></pre>
<p>da가 이름중에 아무곳이나 들어가있는 교수이름 검색</p>
<p>→ ex) dda(O) da(O) dan(O)</p>
<pre><code class="language-sql">select name    
from professor
where name like &#39;%n_&#39;;    </code></pre>
<p>1개의 스트링이 와야함을 의미</p>
<p>→ ex) yang(O), han(X)</p>
<pre><code class="language-sql">Select cID
from course
where title like ‘100\%&#39;  escape  &#39;\&#39;;</code></pre>
<p>100%를 가지는 스트링을 검색하는 질의</p>
<p>%를 임의의 스트링을 의미하게하지말고 퍼센트 기호로 인식하게 하기위해 탈출 기호 사용</p>
<p><strong>Tuples Ordering</strong></p>
<p><code>order by</code> : 결과 테이블의 튜플 정렬에 사용</p>
<pre><code class="language-sql">Select distinct name
from professor
order by name;</code></pre>
<p>기본값 오름차순 → a,b,…,c 순서로</p>
<p>내림차순으로 하려면 desc 키워드 추가 → ex) order by name desc</p>
<pre><code class="language-sql">Order by deptName desc, name</code></pre>
<p>한개 이상의 속성기준으로 정렬 가능</p>
<p>deptName값 기준 내림차순으로 1차정렬 후, 동일한 값 사이에서는 name 속성기준 오름차순 정렬</p>
<p><strong>“Where” clause predicates : where 절 연산자</strong></p>
<pre><code class="language-sql">Select name
from professor
where salary between 5000 and 6000; -- 5000&lt;= 이고 &lt;=6000</code></pre>
<p><code>between</code> : 값 구간 의미함. 경계값 포함</p>
<pre><code class="language-sql">Select name, cID
from professor, teaches
where (professor.pID, deptName) = (teaches.pID, ’CS’);
-- where professor.pID=teaches.pID and deptName=&#39;CS&#39;;과 동일</code></pre>
<p>튜플 사이의 비교가능 </p>
<p>괄호로 임시튜플 생성하여 비교하는 개념</p>
<p><strong>Set Operations</strong></p>
<p><code>union</code>, <code>intersect</code>, <code>except:차집합</code></p>
<p>자동으로 중복제거 → 안하려면 <code>union all</code> 이런식으로</p>
<pre><code class="language-sql">// 튜플이 r 테이블에서 m번 나오고, s 테이블에서 n번 나올때
m+n times in “r union all s”
min(m, n) times in “r intersect all s”
max(0, m–n) times in “r except all s”</code></pre>
<p>union : 두 입력 멀티셋을 중복 상관없이 더함</p>
<p>intersect : 튜플이 입력멀티셋 테이블에서 적게나오는 횟수만큼 결과 멀티셋 테이블에 나옴</p>
<pre><code class="language-sql">Find course IDs that ran in Fall 2009 or in Fall 2010
(select cID from teaches where semester = ‘Fall’ and year = 2009) 
union
(select cID from teaches where semester = ‘Fall’ and year = 2010);

Find course IDs that ran in Fall 2009 and in Fall 2010
(select cID from teaches where semester = ‘Fall’ and year = 2009) 
intersect
(select cID from teaches where semester = ‘Fall’ and year = 2010);

Find course IDs that ran in Fall 2009 but not in Fall 2010
(select cID from teaches where semester = ‘Fall’ and year = 2009) 
except
(select cID from teaches where semester = ‘Fall’ and year = 2010);</code></pre>
<p>2009 가을 or/and/but not 2010가을에 강의한 course IDs를 리턴</p>
<p><strong>3.5 Null Values</strong></p>
<p>튜플의 속성 값으로 널 값 가질 수 있음</p>
<p>Unknown : 값이 존재하는데 잘 모름</p>
<p>Not exist : 값 자체가 없음</p>
<p>널 값이 포함되는 산술연산의 결과는 널 값</p>
<p>→ ex) 5 + null = null</p>
<pre><code class="language-sql">Select name
from professor
where salary is null;
-- where deptName=&quot;&quot;; 이런건 틀린거</code></pre>
<p>널값의 존재여부는 <code>is null</code> 사용</p>
<p>Three-valued Logic</p>
<p>널 값이 속한 비교연산자의 결과는 unknown</p>
<p>→ ex) 5 &lt; null, null &lt;&gt; null 이런거 다 결과 unknown</p>
<p>따라서 비교연산자의 결과는 참, 거짓, unknown 3가지</p>
<pre><code class="language-sql">OR:     
(unknown or true) = true           
(unknown or false) = unknown           
(unknown or unknown) = unknown

AND:     
(true and unknown) = unknown                 
(false and unknown) = false             
(unknown and unknown) = unknown

NOT:      
(not unknown) = unknown</code></pre>
<p>where절에서 사용시 결과가 참인 경우만 사용자에게 반환</p>
<p>false, unknown은 둘다 반환X</p>
<pre><code class="language-sql">true AND false -&gt; take minimum of 1 and 0 -&gt; 0 -&gt; false
True OR unknown -&gt; take maximum of 1 and 0.5 -&gt; 1 -&gt; true
Not false -&gt; (1 – 0) -&gt; 1 -&gt; true
Not unknown -&gt; (1 – 0.5) -&gt; 0.5 -&gt; unknown</code></pre>
<p>true : 1, false : 0, unknown : 0.5</p>
<p>AND : 최소를 취함</p>
<p>OR : 최대를 취함</p>
<p>NOT : (1-value)를 취함</p>
<hr>
<p>📖 데이터베이스 1: 이론과 실제, 이상호, 진샘미디어</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[aws ec2에 docker 배포]]></title>
            <link>https://velog.io/@chaen-ing/aws-ec2%EC%97%90-docker-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@chaen-ing/aws-ec2%EC%97%90-docker-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Fri, 12 Jul 2024 10:55:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaen-ing/post/d1683298-227a-45df-b6ae-4cf28a19c089/image.png" alt=""></p>
<p>m1맥 사용, gradle 기준
준비할 것 : 스프링 프로젝트, ec2 생성하고 프로젝트와 연결 -&gt; 추후에 글쓰도록 하겠음
나중에 다시 볼 수 있도록 최대한 자세히 써보겠음</p>
<p><strong>1. Dockerfile 생성</strong></p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/8fb35317-de2e-443a-ad29-de9eca07f370/image.png" alt="">경로 참고</p>
<pre><code>FROM amazoncorretto:17.0.7-alpine
COPY build/libs/*.jar shoppingmall.jar
ENV TZ Asia/Seoul
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;,&quot;-Dspring.profiles.active=prod&quot;, &quot;shoppingmall.jar&quot;]</code></pre><p>위의 내용을 Dockerfile에 넣어주도록 하자</p>
<blockquote>
<p>jar파일 경로가 틀리면 에러 난다. 경로 잘 체크해서 넣어주도록 하자</p>
</blockquote>
<blockquote>
<p>스프링 3.x.x 버전은 자바 17버전</p>
</blockquote>
<p><strong>2. build.gradle 수정</strong></p>
<pre><code>bootJar {
    jar.enabled = true
}

jar {
    manifest {
        attributes &#39;Main-Class&#39;: &#39;com.testing.test.TestingApplication&#39;
    }
    enabled = false
}</code></pre><p>다음과 같은 내용을 추가해준다</p>
<p><strong>3. jar 파일 빌드</strong></p>
<pre><code>./gradlew clean build -x test
</code></pre><p>3~5번 과정은 intelij ide console에서 입력</p>
<p><strong>4. 도커 로그인 후 이미지 생성</strong>
이때 도커 계정이 필요하므로 없을시 회원가입
도커 허브를 켜놓은 상태라면 비밀번호 없이도 로그인 가능</p>
<pre><code>docker login 
// 성공시 Login Succeeded가 뜰것임</code></pre><blockquote>
<p>로그인이 안되는 경우 (깃허브로 도커 회원가입)
도커 허브 &gt; account setting &gt; security &gt; access token 발급 받기
$ docker login -u [사용자이름] 
입력 후 password에 발급받은 토큰 넣어주면 로그인된다</p>
</blockquote>
<p>로그인 후에 도커 이미지를 빌드해준다</p>
<pre><code>docker build -t [유저이름]/[이미지명] --platform linux/amd64 .

</code></pre><p>m1 맥은 --platform 부분도 넣어줘야함</p>
<p>마지막에 .도 생략하면안된다</p>
<p>도커 데스크탑에서 images에 들어가면 생성된것 확인가능</p>
<pre><code>docker images</code></pre><p>명령어로도 확인 가능하다</p>
<p><strong>5. 도커 이미지 push</strong></p>
<pre><code>docker push [사용자명]/[이미지명]</code></pre><p><img src="https://velog.velcdn.com/images/chaen-ing/post/12a16526-2762-4a59-b27a-662ec29c0a58/image.jpg" alt=""></p>
<p><strong>6. 터미널에서 ec2 인스턴스 접속 및 우분투 도커 설치</strong></p>
<pre><code>ssh -i [pem 키] ubuntu@[ip]</code></pre><p>우분투에 도커 설치는 
<a href="https://velog.io/@jbro321/Docker-Ubuntu-22.04.3%EC%97%90-docker-%EC%84%A4%EC%B9%98">https://velog.io/@jbro321/Docker-Ubuntu-22.04.3에-docker-설치</a>
해당 블로그 참고함</p>
<p>도커 잘 설치되었는지 확인하려면 docker -v 명령어 입력해보자</p>
<p><strong>7. docker image pull &amp; run</strong></p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/73088fa5-1b88-413b-aad2-e2733ecac713/image.png" alt=""></p>
<pre><code>sudo docker pull [사용자명]/[이미지명]
sudo docker run -d -p 8080:8080 [사용자명]/[이미지명]</code></pre><p><strong>8. 사이트 접속해보기</strong></p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/8c94247e-cb92-43b3-bc1f-6ca75b257d9f/image.png" alt="">
접속할때는 public ip 주소:8080으로 접속해주면된다</p>
<p>*<em>+) 참고사항들 *</em></p>
<blockquote>
<p>Bind for 0.0.0.0:8080 failed: port is already allocated.에러 해결법
$ sudo lsof -i tcp:8080
위에서 실행중인 프로그램 pid로 종료시키기
$ sudo kill -9 [pid]</p>
</blockquote>
<blockquote>
<p><a href="https://seulcode.tistory.com/557">https://seulcode.tistory.com/557</a>
위 블로그 참조하여 권한부여를 해줘야 재접속했을때도 제대로 작동한다</p>
</blockquote>
<blockquote>
<pre><code>docker ps -a</code></pre></blockquote>
<pre><code>를 통해서 콘테이너 목록들 확인한 후 안쓰는 컨테이너들 정리해주기</code></pre><p>docker stop [id 또는 이름]
docker rm [id 또는 이름]</p>
<pre><code>컨테이너 실행 중지하고 지워주면됨

&gt; 접속안될때 재접속 방법
똑같이 ec2 인스턴스 접속 후 </code></pre><p>docker ps -a</p>
<pre><code>위 명령어로 container id 혹은 콘테이너 이름을 확인한 후. </code></pre><p>sudo docker start [cotainer id 또는 이름]</p>
<p>```
위 명령어를 통해 재접속</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 기본] 빈 스코프]]></title>
            <link>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EB%B9%88-%EC%8A%A4%EC%BD%94%ED%94%84</link>
            <guid>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EB%B9%88-%EC%8A%A4%EC%BD%94%ED%94%84</guid>
            <pubDate>Tue, 11 Jun 2024 07:43:40 GMT</pubDate>
            <description><![CDATA[<h2 id="빈-스코프란">빈 스코프란?</h2>
<p>지금까지 우리는 스프링빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링이 종료될때까지 유지된다고 학습했다 </p>
<p>→ 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문</p>
<p>→ 스코프 : 빈이 존재할 수 있는 범위</p>
<p><strong>스프링은 다음과 같은 다양한 스코프 지원</strong></p>
<ul>
<li><code>싱글톤</code> : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프</li>
<li><code>프로토타입</code> : 스프링컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프</li>
<li><code>웹 관련 스코프</code><ul>
<li><code>request</code> : 웹 요청이 들어오고 나갈때까지 유지되는 스코프</li>
<li>session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프</li>
<li>application : 웹의 서블릿 컨텐스와 같은 범위로 유지되는 스코프</li>
</ul>
</li>
</ul>
<p>빈 스코프는 다음과 같이 지정할 수 있다</p>
<pre><code class="language-java">@Scope(&quot;prototype&quot;)
@Component
public class HelloBean {...}</code></pre>
<p>컴포넌트 스캔 자동 등록</p>
<pre><code class="language-java">@Scope(&quot;prototype&quot;)
@Bean
prototypeBean HelloBean() {
    return new HelloBean();
}</code></pre>
<p>수동 등록</p>
<h2 id="프로토타입-스코프">프로토타입 스코프</h2>
<p>싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈 반환</p>
<p>반면에, 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환<img src="https://velog.velcdn.com/images/chaen-ing/post/56aa9501-b232-43fc-91ca-f2ed2309bfec/image.png" alt=""></p>
<ol>
<li>싱글톤 스코프의 빈을 스프링 컨테이너에 요청</li>
<li>스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환</li>
<li>이후에 스프링 컨테이너에 같은 요청이 와도 <code>같은 객체 인스턴스</code>의 스프링 빈 반환</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/aed7ef47-da15-42c2-90f0-27a45c845108/image.png" alt=""><img src="https://velog.velcdn.com/images/chaen-ing/post/52c05981-cced-4dbc-8fec-b1cee9cb4f70/image.png" alt=""></p>
<ol>
<li>프로토타입 스코프의 빈을 스프링 컨테이너에 요청</li>
<li>스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입</li>
<li>스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환</li>
<li>이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 생성해서 반환</li>
</ol>
<p><strong>정리</strong></p>
<p>핵심은 스프링 컨테이너는 <code>프로토타입 빈을 생성</code>하고, <code>의존관계 주입, 초기화까지만 처리</code>한다는 것</p>
<p>클라이언트에 빈을 반환하고, 이후에는 스프링 컨테이너는 생성된 프로토타입 빈 관리 X</p>
<p>프로토타입 빈을 관리할 책임은 프로토타입 빈을 반환받은 클라이언트에 있다. 그래서 @PreDestory같은 종료 메서드가 호출되지 않는다</p>
<pre><code class="language-java">@Test
    void singletonBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println(&quot;singletonBean1 = &quot; + singletonBean1);
        System.out.println(&quot;singletonBean2 = &quot; + singletonBean2);
        Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();
    }

    @Scope(&quot;singleton&quot;)
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println(&quot;SingletonBean.init&quot;);
        }

        @PreDestroy
        public void destroy(){
            System.out.println(&quot;SingletonBean.destroy&quot;);
        }
    }</code></pre>
<p>싱글톤 빈</p>
<pre><code class="language-java">@Test
    void prototypeBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println(&quot;find prototypeBean1&quot;);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println(&quot;find prototypeBean2&quot;);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println(&quot;prototypeBean1 = &quot; + prototypeBean1);
        System.out.println(&quot;prototypeBean2 = &quot; + prototypeBean2);

        Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
    }

    @Scope(&quot;prototype&quot;)
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println(&quot;PrototypeBean.init&quot;);
        }

        // 프로토타입빈은 close자체가 안됨
        @PreDestroy
        public void destroy(){
            System.out.println(&quot;PrototypeBean.destroy&quot;);
        }
    }</code></pre>
<p>프로토타입 빈</p>
<p>싱글톤 빈 - 스프링 컨테이너 생성지점에 초기화 메서드 실행됨</p>
<p>프로토타입 빈 - 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화메서드도 실행됨</p>
<p>프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 2개 생성되고 초기화도 2번 실행됨</p>
<p>싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료메서드가 실행되지만, 프로토타입 빈은 스프링 컨테이너가 생성, 의존관계 주입, 초기화까지만 관여하므로 종료메서드 실행안됨 → 빈.destroy()로 직접 종료해줄 수 있음</p>
<p><strong>프로토타입 빈의 특징 정리</strong></p>
<ul>
<li>스프링 컨테이너에 요청할 때 마다 <code>새로 생성</code></li>
<li>스프링 컨테이너는 프로토타입 빈의 <code>생성과</code> <code>의존관계 주입</code> 그리고 <code>초기화</code>까지만 관여</li>
<li><code>종료메서드가 호출안됨</code></li>
<li>프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야함. 종료 메서드에 대한 호출도 <code>클라이언트가 직접</code> 해야함</li>
</ul>
<h2 id="프로토타입-스코프---싱글톤-빈과-함께-사용시-문제점">프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점</h2>
<p>스프링 컨테이너에 프로토타입빈 요청시 항상 새로운 객체 인스턴스 생성해서 반환. 하지만 싱글톤빈과 함께 사용할때는 의도한대로 잘 동작하지 않으므로 주의해야한다</p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/748b4199-da98-4c59-b9d5-44c1e25f3ae6/image.png" alt=""></p>
<ol>
<li>클라이언트 A는 스프링 컨테이너에 프로토타입 빈 요청</li>
<li>스프링 컨테이너는 프로토타입 빈을 새로생성해서 반환(”x01”). 해당 빈의 count 필드값 0</li>
<li>클라이언트는 조회한 프로토타입 빈에 addCount()를 호출하면 count 필드는 1이됨</li>
</ol>
<p>결과적으로 프로토타입 빈(”x01”)의 count는 1</p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/3eb88605-e80c-4942-9d85-5cdfca022dc4/image.png" alt=""></p>
<ol>
<li>클라이언트 B는 스프링 컨테이너에 프로토타입 빈 요청</li>
<li>스프링 컨테이너는 빈 새로 생성해서 반환(”x02”). 해당 빈의 count 필드값 0</li>
<li>클라이언트는 조회한 프로토타입 빈에 addCount()를 호출하면서 count필드 +1</li>
</ol>
<p>프로토타입 빈(”x02”)의 count는 1</p>
<p><strong>싱글톤 빈에서 프로토타입 빈 사용</strong></p>
<p>이번에는 clientBean이라는 싱글톤 빈이 의존관계 주입을 통해 프로토타입 빈을 주입받아 사용하는 예를 보자<img src="https://velog.velcdn.com/images/chaen-ing/post/5faeec30-f676-472e-8682-8f38b4b035cb/image.png" alt=""></p>
<p>clientBean은 싱글톤이므로, 보통 스프링 컨테이너 생성 지점에 함께 생성되고, 의존관계 주입도 발생</p>
<ol>
<li>clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다</li>
<li>스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환한다. 프로토타입 빈의 count 필드값은 0</li>
</ol>
<p>이제 clientBean은 프로토타입 빈을 내부 필드에 보관<img src="https://velog.velcdn.com/images/chaen-ing/post/91654d47-a79b-47db-9466-6881f0aa6d15/image.png" alt=""></p>
<p>클라이언트 A는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환됨</p>
<ol start="3">
<li>클라이언트 A는 <code>clientBean.logic()</code> 호출</li>
<li>clientBean은 prototypeBean의 <code>addCount()</code>를 호출해서 프로토타입 빈의 count를 증가한다. count값이 1이됨</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/eacfe34d-aa80-41ee-8e99-204d9a1a5fa2/image.png" alt=""></p>
<p>똑같이 클라이언트 B는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환됨</p>
<p>여기서 <strong>중요한점</strong> - <strong>clientBean이 내부에 가지고있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지, 사용할 때마다 새로 생성되는 것이 아님!!</strong></p>
<ol start="5">
<li>클라이언트 B는 <code>clientBean.logic()</code> 호출</li>
<li>clientBean은 prototypeBean의 <code>addCount()</code>를 호출해서 프로토타입 빈의 count를 증가한다. count값이 2가됨</li>
</ol>
<pre><code class="language-java">public class SingletonWithPrototypeTest {

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

    @Test
    void singletonClientUsePrototype(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertThat(count2).isEqualTo(2);
    }

    @Scope(&quot;singleton&quot;)
    static class ClientBean{
        private final PrototypeBean prototypeBean;  // 생성 시점에 주입

        @Autowired
        public ClientBean(PrototypeBean prototypeBean){
            this.prototypeBean = prototypeBean;
        }

        public int logic(){
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }

    }

    @Scope(&quot;prototype&quot;)
    static class PrototypeBean{
        private int count = 0;

        public void addCount(){
            count++;
        }

        public int getCount(){
            return count;
        }

        @PostConstruct
        public void init(){
            System.out.println(&quot;PrototypeBean.init &quot;+this);
        }

        @PreDestroy
        public void destroy(){
            System.out.println(&quot;PrototypeBean.destroy&quot;);
        }

    }

}
</code></pre>
<p>스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것 이 문제다.</p>
<p>아마 원하는 것이 이런 것은 아닐 것이다. 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, <code>사용할 때 마다 새로 생성해서 사용하는 것을 원할 것</code>이다.</p>
<p>+) 참고 :</p>
<p>여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.</p>
<p>예를 들어서 clientA, clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.
clientA prototypeBean@x01</p>
<p>clientB prototypeBean@x02</p>
<p>물론 사용할 때 마다 새로 생성되는 것은 아니다.</p>
<h2 id="프로토타입-스코프---싱글톤-빈과-함께-사용시-provider로-문제-해결">프로토타입 스코프 - <strong>싱글톤 빈과 함께 사용시</strong> Provider<strong>로 문제 해결</strong></h2>
<p>가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 컨테이너에 새로 요청하는 것</p>
<pre><code class="language-java"> static class ClientBean {
     @Autowired
      private ApplicationContext ac;

    public int logic() {
             PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
             prototypeBean.addCount();
             int count = prototypeBean.getCount();
             return count;
    } 
}</code></pre>
<p>실행해보면 ac.getBean()을 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인 가능</p>
<p>→ 이렇게 직접 필요한 의존관계를 찾는 것을 <code>Dependency Lookup(DL) 의존관계 조회</code>라고 함 (DI가 아니라)</p>
<p>그런데 이렇게 스프링 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워짐</p>
<p>지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 <code>DL</code> 정도의 기능만 제공하는 무언가가 있으면됨</p>
<p><strong>ObjectFactory, ObjectProvider</strong></p>
<p>지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 <code>ObjectProvider</code> → <code>ObjectFactory</code>에 편의기능을 추가한 것</p>
<pre><code class="language-java">
@Scope(&quot;singleton&quot;)
static class ClientBean{
        @Autowired
        private ObjectProvider&lt;PrototypeBean&gt; prototypeBeanProvider;

        public int logic(){
                PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
                prototypeBean.addCount()
                int count = prototypeBean.getCount();
        return count;
    }
}</code></pre>
<p>실행해보면 <code>prototypeBeanProvider.getObject()</code>를 통해 항상 새로운 프로토타입 빈이 생성되는 것 확인 가능</p>
<p><code>ObjectProvider</code>의 <code>getObject()</code>를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 → <code>DL</code></p>
<p>스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock코드 만들기 쉬워짐</p>
<p>ObjectProvider는 딱 필요한 DL 정도의 기능만 제공</p>
<p>특징</p>
<ul>
<li><code>ObjectFactory</code> : 기능 단순, 별도의 라이브러리 필요 X, 스프링에 의존</li>
<li><code>ObjectProvider</code> : ObjectFactory 상속, 옵션/스트림 처리 등 편의 기능 많음, 별도의 라이브러리 필요 X, 스프링에 의존</li>
</ul>
<p><strong>JSR-330 Provider</strong></p>
<p><code>javax.inject.Provider</code> 라는 JSR-330 자바 표준을 사용하는 방법 → 스프링 부트 3.0 이상은 <code>jakarta.inject.Provider</code>사용한다.</p>
<p>먼저 gradle에 <code>jakarta.inject:jakarta.inject-api:2.0.1</code> 라이브러리를 추가해줘야함</p>
<pre><code class="language-java">
@Scope(&quot;singleton&quot;)
static class ClientBean{
        @Autowired
        private Provider&lt;PrototypeBean&gt; provider;

        public int logic(){
                PrototypeBean prototypeBean = provider.get();
                prototypeBean.addCount()
                int count = prototypeBean.getCount();
        return count;
    }
}</code></pre>
<p>수정코드</p>
<p>실행해보면 <code>provider.get()</code>을 통해 항상 새로운 프로토타입 빈 생성 확인가능</p>
<p>provider의 <code>get()</code> 호출 시 내부에서 스프링 컨테이너를 통해 해당 빈 찾아서 반환 → <code>DL</code></p>
<p>자바 표준이고, 기능이 단순하므로 단위테스트 만들거나 mock 코드 만들기 쉬워짐</p>
<p>Provider는 딱 필요한 DL 정도의 기능만 제공</p>
<p>특징</p>
<ul>
<li>get() 메서드 하나로 기능 매우 단순</li>
<li>별도의 라이브러리 필요</li>
<li>자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능</li>
</ul>
<p>정리</p>
<p>그러면 프로토타입 빈을 언제 사용할까? </p>
<p>→ 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. 그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때 문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.</p>
<p><code>ObjectProvider</code> , <code>JSR330 Provider</code> 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.</p>
<p>+) 스프링이 제공하는 메서드에 @Lookup 에노테이션 사용하는 방법도 있긴함</p>
<p>+) 실무에서 어떤것을 사용할 것인가? ObjectProvider vs JSR-300</p>
<p>→ 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야한다면 JSR-330 </p>
<p>→ 다른 컨테이너 사용할 일이 없다면, 스프링의 ObjectProvider사용하면 될 것</p>
<h2 id="웹-스코프">웹 스코프</h2>
<p>웹 스코프 특징</p>
<ul>
<li>웹 환경에서만 동작</li>
<li>프로토타입과 다르게 <code>스프링이 해당 스코프의 종료시점까지 관리</code> → 즉, <code>종료 메서드 호출</code></li>
</ul>
<p>웹 스코프 종류</p>
<ul>
<li><code>request</code> : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리한다</li>
<li><code>session</code> : HTTP Session과 동일한 생명주기를 가지는 스코프</li>
<li><code>application</code> :  서블릿 컨텍스트( <code>ServletContext</code> )와 동일한 생명주기를 가지는 스코프</li>
<li><code>websocket</code> : 웹 소켓과 동일한 생명주기를 가지는 스코프</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/170fee91-18c9-467a-939b-b2d208032aac/image.png" alt=""></p>
<p>클라이언트 A,B가 동시 요청하더라도 각각 다른 빈이 생성되고 관리됨</p>
<p>서비스 로직에서는 만들었던 빈을 사용함</p>
<p>응답이 나가면 destroy</p>
<h2 id="request-스코프-예제-만들기">request 스코프 예제 만들기</h2>
<p>웹 환경이 동작하도록 라이브러리 추가</p>
<p>build.gradle에 <code>implementation &#39;org.springframework.boot:spring-boot-starter-web’</code> 추가</p>
<p>→ 8080 포트로 웹 애플리케이션 실행됨</p>
<p>+) spring-boot-starter-web 라이브러리 추가시 스프링 부트는 내장 톰캣 서버 활용해서 웹 서버와 스프링 함께 실행시킴</p>
<p>+) 스프링 부트는 웹 라이브러리가 없으면 우리가 지금까지 학습한 AnnotationConfigApplicationContext 기반으로 애플리케이션 구동. 웹 라이브러리 추가되면 AnnotationConfigServletWebServerApplicationContext기반</p>
<p>동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴때 사용하기 좋은 것이 바로 request 스코프</p>
<pre><code class="language-java"> [d06b992f...] request scope bean create
 [d06b992f...][http://localhost:8080/log-demo] controller test
 [d06b992f...][http://localhost:8080/log-demo] service id = testId
 [d06b992f...] request scope bean close</code></pre>
<p>다음과 같이 로그가 남도록 request 스코프를 활용해서 추가기능 개발해보자</p>
<p>기대하는 공통 포맷 : [UUID][requestURL][message]</p>
<ul>
<li>UUID를 사용해서 HTTP 요청 구분</li>
<li>requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인</li>
</ul>
<pre><code class="language-java">package hello.core.common;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Scope(value = &quot;request&quot;)
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message){
        System.out.println(&quot;[&quot;+uuid+&quot;][&quot;+requestURL+&quot;][&quot;+message+&quot;]&quot;);
    }

    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();    // 랜덤하게 UUID 생성
        System.out.println(&quot;[&quot;+uuid+&quot;] request scope bean create:&quot;+this);
    }

    @PreDestroy
    public void close(){
        System.out.println(&quot;[&quot;+uuid+&quot;] request scope bean close:&quot;+this);
    }
}
</code></pre>
<p><code>@Scope(value=”request”)</code>를 사용해서 request 스코프로 지정 → HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸</p>
<p>빈 생성 시점에 자동으로 <code>@PostConstruct</code> 초기화메서드 사용해서 uuid 생성 및 저장. 빈은 요청 당 하나씩 생성되므로 uuid로 구별가능</p>
<p>빈 종료시점에 <code>@PreDestroy</code>로 종료</p>
<p><code>requestURL</code>은 빈이 생성되는 시점에는 알 수 없으므로 외부에서 setter로 입력받는다</p>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping(&quot;log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log(&quot;controller test&quot;);
        logDemoService.logic(&quot;testId&quot;);
        return &quot;OK&quot;;
    }
}
</code></pre>
<p>로거가 잘 작동하는지 확인하는 테스트용 컨트롤러</p>
<p>여기서 <code>HttpServletRequest</code>를 통해 요청 URL받음 → <a href="http://localhost:8080/log-demo">http://localhost:8080/log-demo</a></p>
<p>이렇게 받은 url값을 requestURL에 저장. myLogger는 HTTP 요청 당 각각 <code>구분</code>되므로 다른 HTTP 요청때문에 <code>값이 섞을 걱정 X</code></p>
<p>컨트롤러에서는 controller test라는 로그 남김</p>
<p>+) MyLogger에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋음 → 여기서는 단순화하기 위해</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log(&quot;service id = &quot;+id);
    }
}</code></pre>
<p>비즈니스 로직이 있는 서비스 계층에서도 로그를 출력해보자</p>
<p>request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘긴다면 </p>
<p>→ 파라미터가 많아 지저분, 더 문제는 requestURL같은 웹과 관련된 정보과 관련없는 서비스 계층까지 넘어가게됨.</p>
<p>→ 서비스 계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에 좋음</p>
<p>→ request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger의 멤버변수에 저장해서 코드와 계층 깔끔하게 유지가능</p>
<pre><code class="language-java">
 Error creating bean with name &#39;myLogger&#39;: Scope &#39;request&#39; is not active for the
 current thread; consider defining a scoped proxy for this bean if you intend to
 refer to it from a singleton;</code></pre>
<p>그러나 에러 발생…</p>
<p>request 스코프빈은 스프링 애플리케이션 실행 지점에 아직 생성되지 않음 → 고객의 요청이 와야 생성가능</p>
<h2 id="스코프와-provider">스코프와 provider</h2>
<pre><code class="language-java">public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider&lt;MyLogger&gt; myLoggerProvider;

    @RequestMapping(&quot;log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);

        myLogger.log(&quot;controller test&quot;);
        logDemoService.logic(&quot;testId&quot;);
        return &quot;OK&quot;;
    }
}
</code></pre>
<pre><code class="language-java">public class LogDemoService {
    private final ObjectProvider&lt;MyLogger&gt; myLoggerProvider;
    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log(&quot;service id = &quot;+id);
    }
}</code></pre>
<p>컨트롤러와 서비스 코드 약간만 수정해주면됨</p>
<p><code>ObjectProvider</code>사용 → <code>getObject()</code></p>
<p>순서 :</p>
<p>getObject하는 시점에 생성됨</p>
<p>→ 이때 init() 호출해서 uuid 생성</p>
<p>→ logDemo에서 url 받음</p>
<p>→ log찍고, logic찍고</p>
<p>→ close()</p>
<p>바로바로 로그가 찍히는데 이때 UUID가 요청마다 다르고, 요청끼리는 같게 유지되는 것을 볼 수 있음 → 유저 구분이 가능</p>
<p><code>ObjectProvider</code>덕분에 getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다</p>
<p><code>getObject()</code>를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다</p>
<p><code>getObject()</code>를 LogDemoController, LogDemoService에서 각각 따로 한번씩 호출해도 같은 HTTP 요청이면 <code>같은 스프링빈</code> 반환 → 직접 구분하려면 정말 힘들것</p>
<h2 id="스코프와-프록시">스코프와 프록시</h2>
<pre><code class="language-java">@Component
 @Scope(value = &quot;request&quot;, proxyMode = ScopedProxyMode.TARGET_CLASS)
 public class MyLogger {
}</code></pre>
<p><code>proxyMode = ScopedProxyMode.TARGET_CLASS</code>를 추가</p>
<p>나머지 코드도 Provider 없애고 다시 돌려놓기</p>
<ul>
<li>적용 대상이 클래스면 TARGET_CLASS</li>
<li>적용 대상이 인터페이스면 INTERFACES</li>
</ul>
<p>이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해둘 수 있다</p>
<p><strong>웹 스코프와 프록시 동작 원리</strong></p>
<pre><code class="language-java">System.out.println(&quot;myLogger = &quot; + myLogger);

// 결과
myLogger = class hello.core.common.MyLogger$$SpringCGLIB$$0</code></pre>
<p><code>CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입</code></p>
<ul>
<li>@Scope의 proxymode = scopeProxyMode.TARGET_CLASS를 설정하면 스프링 컨테이너는 CGLIB이라는 바이트코드를 조작하는 라이브러리를 사용해서 MyLogger를 상속받은 가짜 프록시 객체를 생성</li>
<li>결과를 확인해보면 우리가 등록한 순수한 MyLogger클래스가 아닌 CGLIB이 붙은 클래스가 생성된 것 확인가능</li>
<li>그리고 스프링 컨테이너에 myLogger라는 이름으로 진짜 대신 이 가짜 프록시 객체를 등록</li>
<li>ac.getBean(”myLogger”, MyLogger.class)로 주입해도 이 가짜 프록시 객체 조회됨</li>
<li>그래서 의존관계 주입도 이 가짜 프록시 객체가 주입됨</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/6ee02cca-0602-45f4-b268-e7a67ca2e802/image.png" alt=""></p>
<p><code>가짜프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있음</code></p>
<ul>
<li><p>가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있음</p>
</li>
<li><p>클라이언트가 myLogger.logic()을 호출하면 사실 가짜 프록시 객체의 메서드를 호출한것</p>
<p>  → 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic()을 호출</p>
</li>
<li><p>객체 사용하는 클라이언트 입장에서는 원본인지 아닌지 모르게 동일하게 사용가능(<code>다형성</code>)</p>
</li>
</ul>
<p><strong>동작 정리</strong></p>
<ul>
<li>CGLIB라는 라이브러리로 내 클래스를 상속 받은 <code>가짜 프록시 객체를 만들어서 주입</code>한다.</li>
<li>이 가짜 프록시 객체는 실제 <code>요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직</code>이 들어있다.</li>
<li>가짜 프록시 객체는 <code>실제 request scope와는 관계가 없</code>다. 그냥 가짜이고, 내부에 단순한 <code>위임 로직만</code> 있고, <code>싱글톤 처럼 동작</code>한다.</li>
</ul>
<p><strong>특징 정리</strong></p>
<ul>
<li>프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.</li>
<li>사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 <code>진짜 객체 조회를 꼭 필요한 시점까지 지연처리</code> 한다는 점이다.</li>
<li>단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.</li>
<li>꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다</li>
</ul>
<p><strong>주의점</strong></p>
<ul>
<li>마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.</li>
<li>이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.</li>
</ul>
<hr>
<p>인프런 스프링 입문 - 김영한 강의를 듣고 정리한 내용입니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 기본] 빈 생명주기 콜백]]></title>
            <link>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EB%B9%88-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EC%BD%9C%EB%B0%B1-ruxx9tts</link>
            <guid>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EB%B9%88-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EC%BD%9C%EB%B0%B1-ruxx9tts</guid>
            <pubDate>Tue, 11 Jun 2024 07:40:01 GMT</pubDate>
            <description><![CDATA[<h2 id="빈-생명주기-콜백-시작">빈 생명주기 콜백 시작</h2>
<p>데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.</p>
<pre><code class="language-java">public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig{
        @Bean
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl(&quot;http://hello-spring.dev&quot;);
            return networkClient;
        }
    }
}
</code></pre>
<pre><code class="language-java">public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println(&quot;생성자 호출, url = &quot;+url);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    //서비스 시작시 호출
    public void connect(){
        System.out.println(&quot;connect: &quot;+url);
    }

    public void call(String message){
        System.out.println(&quot;call: &quot;+url+&quot; message = &quot;+message);
    }

    //서비스 종료시 호출
    public void disconnect(){
        System.out.println(&quot;close &quot;+url);
    }

}</code></pre>
<p>ApplicationContext ac 대신</p>
<p><code>ConfigurableApplicationContext</code> ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class); 또는 <code>AnnotationConfigApplicationContext</code></p>
<p>→ 기본 ApplicationContext에는 close기능이 없음</p>
<p>ConfigurableApplicationContext가 AnnotationConfigApplicationContext의 상위 인터페이스</p>
<p>실행결과</p>
<pre><code class="language-java">생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지</code></pre>
<p>생성자 부분을 보면 url 정보 없이 connect가 호출되는 것을 확인할 수 있음</p>
<p>객체를 생성하는 단계에는 url이 없고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해서 setUrl() 이 호출되어야 url이 존재하게 된다</p>
<p><strong>스프링 빈 라이프 사이클</strong></p>
<p><code>객체 생성</code> → <code>의존관계 주입</code></p>
<p>스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료됨.</p>
<p>따라서 초기화 작업은 의존관계 주입 완료 후에 호출해야함 → 개발자가 어떻게 알 수 있을까?</p>
<p>→ <code>스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능 제공</code></p>
<p><strong>스프링 빈의 이벤트 라이프 사이클</strong></p>
<p><code>스프링 컨테이너 생성</code> → <code>스프링 빈 생성</code> → <code>의존관계 주입</code> → <code>초기화 콜백</code> → <code>사용</code> → <code>소멸전 콜백</code> → <code>스프링 종료</code></p>
<p><code>초기화 콜백</code> : 빈 생성, 빈 의존관계 주입 완료 후 호출</p>
<p><code>소멸전 콜백</code> : 빈 소멸 직전에 호출</p>
<p><strong>스프링은 다양한 방식으로 생명주기 콜백 지원</strong></p>
<ul>
<li>인터페이스(InitializaingBean, DesposableBean)</li>
<li>설정 정보에 초기화 메서드, 종료 메서드 지정</li>
<li>@PostConstuct, @PreDestroy 애노테이션 지원</li>
</ul>
<p>+) 객체의 생성과 초기화를 분리하자</p>
<p>생성자 : 필수정보(파라미터)를 받고 메모리를 할당해서 객체 생성하는 책임을 가짐</p>
<p>초기화 : 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작 수행</p>
<p>따라서 생성자 안에서 무거운 초기화 작업을 함께하는 것보다 분리하는 것이 유지보수 관점에서 좋음. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 처리하는 것이 나을 수 있다</p>
<h2 id="인터페이스initializaingbean-desposablebean">인터페이스(InitializaingBean, DesposableBean)</h2>
<pre><code class="language-java">public class NetworkClient implements InitializingBean, DisposableBean {

    // 위와 동일한 내용
    // 아래 오버라이딩만 추가

        @Override
          public void afterPropertiesSet() throws Exception{
           System.out.println(&quot;NetworkClient.afterPropertiesSet&quot;);
           connect();
           call(&quot;초기화 연결 메시지&quot;);
           }

      @Override
       public void destroy() throws Exception {
        System.out.println(&quot;NetworkClient.destroy&quot;);            
        disconnect();
       }
}</code></pre>
<p><code>InitializingBean</code>은 <code>afterPropertiesSet()</code> 메소드로 초기화 지원</p>
<p><code>DisposableBean</code>은 <code>destroy()</code> 메소드로 소멸 지원</p>
<p>실행결과</p>
<pre><code class="language-java">생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
16:12:08.592 [main] DEBUG o.s.c.a.AnnotationConfigApplicationContext --
                Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@738dc9b, started on Sun Apr 07 16:12:08 KST 2024
NetworkClient.destroy
close http://hello-spring.dev</code></pre>
<p>생성 → 의존관계 주입 → 초기화 단계로 잘 진행됨</p>
<p><strong>초기화, 소멸 인터페이스 단점</strong></p>
<ul>
<li>스프링 전용 인터페이스임. 해당 코드가 스프링 전용 인터페이스에 의존</li>
<li>초기화, 소멸메서드의 이름 변경 불가</li>
<li>내가 코드를 고칠 수 없는 외부 라이브러리에 적용 불가</li>
</ul>
<p>인터페이스를 사용하는 초기화, 종료 방법은 최근에는 잘 사용 X</p>
<h2 id="빈-등록-초기화-소멸-메서드-지정">빈 등록 초기화, 소멸 메서드 지정</h2>
<pre><code class="language-java">public class NetworkClient{

        // 아래만 변경
    public void init(){
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }

    public void close(){
        System.out.println(&quot;NetworkClient.close&quot;);
        disconnect();
    }

}</code></pre>
<pre><code class="language-java">@Configuration
    static class LifeCycleConfig{
        @Bean(initMethod = &quot;init&quot;, destroyMethod = &quot;close&quot;)
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl(&quot;http://hello-spring.dev&quot;);
            return networkClient;
        }
    }</code></pre>
<p>설정 정보에서 @Bean에다가 초기화, 소멸 메서드 지정</p>
<p>출력결과 동일하게 나옴</p>
<p><strong>특징</strong></p>
<ul>
<li>메서드 이름을 자유롭게 줄 수 있음</li>
<li>스프링 빈이 스프링 코드에 의존 X</li>
<li>코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있음</li>
</ul>
<p><strong>종료 메서드 추론</strong></p>
<ul>
<li><code>@Bean의 destroyMethod</code> 속성에는 아주 특별한 기능이 있다.</li>
<li>라이브러리는 대부분 <code>close</code> , <code>shutdown</code> 이라는 이름의 종료 메서드를 사용한다.</li>
<li>@Bean의 <code>destroyMethod</code> 는 기본값이 <code>(inferred)</code> (추론)으로 등록되어 있다.</li>
<li>이 추론 기능은 <code>close</code> , <code>shutdown</code> 라는 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출해준다.</li>
<li>따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.</li>
<li>추론 기능을 사용하기 싫으면 <code>destroyMethod=&quot;&quot;</code> 처럼 빈 공백을 지정하면 된다.</li>
</ul>
<h2 id="애노테이션-postconstuct-predestroy">애노테이션 @PostConstuct, @PreDestroy</h2>
<pre><code class="language-java">public class NetworkClient{

        // 아래만 변경

    @PostConstruct
    public void init(){
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }

    @PreDestroy
    public void close(){
        System.out.println(&quot;NetworkClient.close&quot;);
        disconnect();
    }
}</code></pre>
<p><code>@PostConstruct</code> , <code>@PreDestroy</code> 이 두 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.</p>
<p><strong>특징</strong></p>
<ul>
<li>최신 스프링에서 가장 권장하는 방법이다.</li>
<li>애노테이션 하나만 붙이면 되므로 매우 편리하다.</li>
<li>패키지를 잘 보면 <code>javax.annotation.PostConstruct</code> 이다. 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.</li>
<li>컴포넌트 스캔과 잘 어울린다.</li>
<li>유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.</li>
</ul>
<p><strong>정리</strong></p>
<p><strong>@PostConstruct, @PreDestroy 애노테이션을 사용하자</strong></p>
<p>코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean의 initMethod, destroyMethod를 사용하자.</p>
<hr>
<p>인프런 스프링 입문 - 김영한 강의를 듣고 정리한 내용입니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 기본] 의존관계 자동 주입]]></title>
            <link>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%9E%90%EB%8F%99-%EC%A3%BC%EC%9E%85</link>
            <guid>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%9E%90%EB%8F%99-%EC%A3%BC%EC%9E%85</guid>
            <pubDate>Tue, 11 Jun 2024 07:39:08 GMT</pubDate>
            <description><![CDATA[<h2 id="다양한-의존관계-주입-방법">다양한 의존관계 주입 방법</h2>
<p>의존관계 주입은 크게 4가지 방법이 있음</p>
<ul>
<li><code>생성자 주입</code></li>
<li><code>수정자 주입(setter 주입)</code></li>
<li><code>필드 주입</code></li>
<li><code>일반 메서드 주입</code></li>
</ul>
<p><strong>생성자 주입</strong></p>
<p>이름 그대로 <code>생성자</code>를 통해서 의존관계를 주입받는 방법</p>
<p>지금까지 진행했던 방법</p>
<p>컴포넌트 스캔시 → 스프링 빈 등록할때 생성자 호출 → 이때 @Autowired가 생성자에 붙어있으면 빈에서 의존관계 찾아서 주입</p>
<p>특징</p>
<ul>
<li><p>생성자 호출시점에 <code>딱 1번만</code> 호출되는 것이 보장</p>
</li>
<li><p>“<code>불변, 필수</code>” 의존관계에 사용</p>
<p>  → 불변 : ex) 공연중에 배우를 절대 바꾸지 않을경우. 처음부터 다 확정하고 시작하는 경우</p>
<p>  → 필수 : ex) final이 붙어있으므로 생성자에 값을 다 채워넣어야함. null값 X</p>
<pre><code class="language-java">  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  @Autowired
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
  }</code></pre>
</li>
</ul>
<p><strong>중요!</strong> <code>생성자가 딱 1개만 있으면 @Autowired 생략</code>해도 괜찮음</p>
<p><strong>수정자주입</strong></p>
<p>setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법</p>
<pre><code class="language-java">private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;

@Autowired
public void setMemberRepository(MemberRepository memberRepository){
    System.out.println(&quot;memberRepository = &quot; + memberRepository);
    this.memberRepository = memberRepository;
}

@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy){
    System.out.println(&quot;discountPolicy = &quot; + discountPolicy);
    this.discountPolicy = discountPolicy;
}</code></pre>
<p>생성자주입 : 생성자를 호출해야하므로 빈 등록하면서 의존관계 주입이 동시에 일어남</p>
<p>수정자주입 : 빈 등록이 다 끝난 후에 의존관계 주입</p>
<p>특징</p>
<ul>
<li><p><code>선택, 변경 가능성</code>이 있는 의존관계에 사용</p>
<p>  → 선택 : @Autowired(required = false)</p>
</li>
<li><p>자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.</p>
</li>
</ul>
<p><strong>필드주입</strong></p>
<p>필드에 바로 주입하는 방법</p>
<pre><code class="language-java">@Autowired
private MemberRepository memberRepository;

@Autowired
private DiscountPolicy discountPolicy;</code></pre>
<p>특징</p>
<ul>
<li>코드가 간결하다는 장점, 외부에서 변경 불가하다는 단점이 있다 → 결국 그러면 또 setter를 만들어야함</li>
<li>DI 프레임워크가 없으면 아무것도 할 수 없다</li>
<li><code>사용하지 말자!</code> 그러나 아래의 경우에는 사용해도 ㄱㅊ음<ul>
<li>애플리케이션의 실제 코드와 관계 없는 테스트 코드</li>
<li>스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용</li>
</ul>
</li>
</ul>
<p><strong>일반 메서드 주입</strong></p>
<p>일반 메서드를 통해서 주입받을 수 있다</p>
<p>특징</p>
<ul>
<li>한번에 여러 필드를 주입 받을 수 있다</li>
<li><code>일반적으로 잘 사용 X</code> : 생성자 주입이나 수정자 주입으로 대체가능</li>
</ul>
<pre><code class="language-java">private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;

@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}</code></pre>
<p>+) 당연한 이야기지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작 → ex) Member같은 클래스에서 @Autowired해도 아무 기능 X</p>
<h2 id="옵션-처리">옵션 처리</h2>
<p>주입할 스프링 빈이 없어도 동작해야할 때가 있음</p>
<p>→ 그런데 @AutoWired만 사용하면 required 옵션의 기본값이 true로 되어있어서 자동 주입 대상이 없으면 오류 발생</p>
<p>자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.</p>
<ul>
<li>@Autowired(<code>required=false</code>) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨</li>
<li>org.springframework.lang.<code>@Nullable</code> : 자동 주입할 대상이 없으면 null이 입력된다.</li>
<li><code>Optional</code>&lt;&gt; : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.</li>
</ul>
<pre><code class="language-java">    static class TestBean{

        // 1. Autowired(required = false)
        @Autowired(required = false)
        public void setNoBean1(Member noBean1){
            System.out.println(&quot;noBean1 = &quot; + noBean1);
        }

                // 2. @Nullable
        @Autowired
        public void setNoBean2(@Nullable Member noBean2){
            System.out.println(&quot;noBean2 = &quot; + noBean2);
        }

                // 3. Optional&lt;&gt;
        @Autowired
        public void setNoBean3(Optional&lt;Member&gt; noBean3){
            System.out.println(&quot;noBean3 = &quot; + noBean3);
        }
</code></pre>
<p>Member는 스프링 빈이 아님!</p>
<p>noBean1은 아예 호출이 안됨</p>
<p>noBean2 = null</p>
<p>noBean3 = Optional.empty</p>
<p>+) @Nullable, Optional은 스프링 전반에 걸쳐서 지원됨. 생성자 자동 주입에서 특정 필드에만 사용해도됨</p>
<h2 id="생성자-주입을-선택해라">생성자 주입을 선택해라!</h2>
<p>과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장</p>
<p>→ 이유 1 : <strong><code>불변</code></strong></p>
<ul>
<li><p>대부분의 의존관계 주입은 종료시점까지 변경할일이 없음. 오히려 대부분의 의존관계는 종료시점까지 변하면 안됨</p>
</li>
<li><p>수정자 주입을 사용하면, setXxx 메서드를 public으로 열어둬야함</p>
<p>  → 누군가 실수로 변경할 수도 있고, 변경하면 안되는 메소드를 열어두는것은 좋은 설계 방법이 X</p>
</li>
<li><p>생성자 주입은 객체 생성시 딱 1번만 호출되므로 이후에 호출되는 일이 없음. 따라서 불변하게 설계 가능</p>
</li>
</ul>
<p>→ 이유 2 : <strong><code>누락</code></strong></p>
<pre><code class="language-java">private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }</code></pre>
<p>위와같이 수정자 의존관계인 경우</p>
<pre><code class="language-java">@Test
 void createOrder() {
     OrderServiceImpl orderService = new OrderServiceImpl();
     orderService.createOrder(1L, &quot;itemA&quot;, 10000);
 }</code></pre>
<p>이렇게 테스트 수행하면 실행은 되나 Null Point Exception 발생</p>
<p>OrderServiceImpl에 memberRepository, discountPolicy모두 의존관계 주입 누락</p>
<p>생성자 주입을 사용하면 위와같은 경우에서 컴파일 오류가 발생하여 어떤 값을 필수로 주입해야하는지 알 수 있음 → new OrderServiceImpl()여기에 빨간줄 뜸</p>
<p>→ 이유 3 : <strong><code>final 키워드</code></strong></p>
<p>final 키워드를 포함시키면 실수로 값을 설정하지 않는 오류를 막을 수 있음 : 컴파일 오류 발생 → final은 값이 무조건 있어야하므로</p>
<p>생성자 주입이 아닌 다른 방식들은 모두 생성자 이후에 호출되므로 final 사용자체가 불가능</p>
<p><strong>정리</strong></p>
<p>생성자 주입 방식은 프레임워크에 의존하지 않고 순수한 자바 언어의 특징을 잘 살리는 방법</p>
<p>기본적으로 생성자 주입을 사용하고 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면됨 → 동시 사용 가능</p>
<p><code>항상 생성자 주입을 선택하고 가끔 옵션이 필요하면 수정자 주입</code>을 선택해라. 필드 주입은 사용하지 말자</p>
<h2 id="롬복과-최신-트렌드">롬복과 최신 트렌드</h2>
<p>막상 개발을 해보면 대부분이 불변이고 그래서 생성자에 final 키워드 사용하게됨</p>
<p>→ 생성자도 만들고, 주입 받은 값을 대입하는 코드도 만들어야하고…</p>
<p>필드 주입처럼 편리하게 사용하는 방법이 없을까?!</p>
<p>먼저 lombok 라이브러리 gradle 수정하고 다운 받는등.. 기본 작업을 해준다!</p>
<pre><code class="language-java"> @Component
 public class OrderServiceImpl implements OrderService {
     private final MemberRepository memberRepository;
     private final DiscountPolicy discountPolicy;

        @Autowired
     public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
 discountPolicy) {
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
     }
}</code></pre>
<p>이게 기본 코드 → 여기서 생성자가 1개인 상황이라면 @Autowired는 생략 가능 </p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
</code></pre>
<p><code>@RequiredArgsConstructor</code> → final이 붙은 변수를 파라미터로하는 생성자 자동으로 만들어줌. 여기 보이지는 않지만 실제 호출 가능</p>
<p>완전히 동일한 기능을 하지만 코드가 아주 간결해짐!!!</p>
<p><strong>정리</strong></p>
<p>최근에는 <code>생성자를 딱 1개 두고, @Autowired를 생략하는 방법</code> 주로 사용. 여기에 롬복 라이브러리의 <code>@RequiredArgsConstructor</code>를 사용하면 기능은 다 제공하면서 코드는 깔끔하게 사용가능</p>
<h2 id="조회-빈이-2개-이상---문제">조회 빈이 2개 이상 - 문제</h2>
<p>@Autowired는 타입으로 조회하므로 선택된 빈이 2개 이상이면 문제 발생</p>
<pre><code class="language-java">@Autowired
private DiscountPolicy discountPolicy;

// 같은 동작 

ac.getBean(DiscountPolicy.class);</code></pre>
<p>예를 들어, FixDiscountPolicy, RateDiscountPolicy 2개를 모두 @Component로 등록하면 둘다 같은</p>
<p>discountPolicy 타입이므로 NoUniqueBeanDefinitionException발생</p>
<p>→ expected single matching bean but found 2: fixDiscountPolicy, rateDiscountPolicy</p>
<p>아래는 해결방법 3가지에 대한 내용이다</p>
<h2 id="autowired-필드-명-qualifier-primary">@Autowired <strong>필드 명</strong>, @Qualifier, @Primary</h2>
<ol>
<li><p><strong>@Autowired 필드 명 매칭</strong></p>
<p> <code>@Autowired</code>는 타입 매칭을 시도하고, 이때 빈이 여러개 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭</p>
<pre><code class="language-java"> // 기존 코드
 @Autowired
 private DiscountPolicy discountPolicy

 // 필드 명을 빈 이름으로 변경
 @Autowired
 private Discount policy rateDiscountPolicy</code></pre>
<p> 필드명이 rateDiscountPolicy이므로 정상주입됨</p>
<p> 필드명 매칭은 먼저 타입 매칭을 시도하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.</p>
<p> <strong>@Autowired 매칭 정리</strong></p>
<ol>
<li>타입 매칭</li>
<li>타입 매칭의 결과가 2개 이상일때 <code>필드 명, 파라미터 명</code>으로 빈 이름 매칭</li>
</ol>
</li>
<li><p><strong>@Qualifier → Qualifier끼리 매칭 → 빈 이름 매칭</strong></p>
<p> <code>@Qualifier</code>는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.</p>
<pre><code class="language-java"> @Component
 @Qualifier(&quot;mainDiscountPolicy&quot;)
 public class RateDiscountPolicy implements DiscountPolicy{
     ...
 }</code></pre>
<pre><code class="language-java"> @Component
 @Qualifier(&quot;fixDiscountPolicy&quot;)
 public class FixDiscountPolicy implements DiscountPolicy{
     ...
 }</code></pre>
<p> RateDiscountPolicy에는 “mainDiscountPolicy”</p>
<p> fixDiscountPolicy에는 “fixDiscountPolicy”라는 구분자를 붙여준다</p>
<pre><code class="language-java">  @Autowired
     public OrderServiceImpl(MemberRepository memberRepository, @Qualifier(&quot;mainDiscountPolicy&quot;) DiscountPolicy discountPolicy){
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
     }</code></pre>
<p> OrderServiceImpl에서 이런식으로 사용</p>
<p> 만약 “mainDiscountPolicy” Qualifier를 못찾으면 어떻게될까?</p>
<p> → mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다</p>
<p> → Qualifier는 Qualifier를 찾는 용도로만 사용하는 것이 좋다!</p>
<p> <strong>@Qualifier 매칭 정리</strong></p>
<ol>
<li>@Qualifier끼리 매칭</li>
<li>빈 이름 매칭</li>
<li>NoSuchBeanDefinitionException 예외 발생</li>
</ol>
</li>
<li><p><strong>@Primary 사용</strong></p>
<p> 우선순위를 정하는 방법이다</p>
<p> @Autowired시에 여러번 매칭되면 <code>@Primary</code>가 우선권을 가짐</p>
<pre><code class="language-java"> @Component
 @Primary
 public class RateDiscountPolicy implements DiscountPolicy{
     ...
 }</code></pre>
<p> @Primary만 붙여주면됨</p>
<p> 매우 간단 → @Qualifier보다 간단 : 모든 코드에 @Qualifier를 붙여줘야함</p>
<p> 메인과 보조 구현체가 있을때 메인에는 @Primary를 걸어놓는다던지 그러한 방식으로 사용</p>
</li>
</ol>
<p><strong>@Primary, @Qualifier 활용</strong></p>
<p>코드에서 자주 사용하는 <code>메인 데이터베이스의 커넥션</code>을 획득하는 스프링 빈이 있고, </p>
<p>코드에서 특별한 기능으로 가끔 사용하는 <code>서브 데이터베이스의 커넥션</code>을 획득하는 스프링 빈이 있다고 생각해보자.</p>
<p><code>메인 데이터베이스의 커넥션</code>을 획득하는 스프링 빈은 <code>@Primary</code> 를 적용해서 조회하는 곳에서</p>
<p>@Qualifier 지정 없이 편리하게 조회하고, </p>
<p><code>서브 데이터베이스 커넥션 빈</code>을 획득할 때는 <code>@Qualifier</code> 를 지정해서 명시적으로 획득 하는 방식으로 사용</p>
<p>하면 코드를 깔끔하게 유지할 수 있다. </p>
<p>물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier 를 지정해주는 것은 상관없다.</p>
<p><strong>우선순위</strong></p>
<p>스프링은 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은범위의 선택권이 우선순위가 높음</p>
<p>@Primary는 기본값처럼 동작</p>
<p><code>@Qualifier는 매우 상세하게 동작 → 우선권이 높음</code></p>
<h2 id="애노테이션-직접-만들기">애노테이션 직접 만들기</h2>
<p>@Qualifier(”mainDiscountPolicy”) 이렇게 문자를 적으면 컴파일시 타입 체크가 안됨. 다음과 같은 애노테이션을 만들어서 문제 해결 가능</p>
<pre><code class="language-java">package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier(&quot;mainDiscountPolicy&quot;)
public @interface MainDiscountPolicy {

}
</code></pre>
<p>위와같은 방식으로 애노테이션 생성가능</p>
<p>여기에 @Qualifier도 있는 것을 볼 수 있음</p>
<pre><code class="language-java">@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{
    ...
}</code></pre>
<p>Qualifier와 같은 방법으로 사용</p>
<p>애노테이션에는 상속이라는 개념 X. 이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능. 다른 애노테이션들도 함께 조합하여 사용할 수 있지만 무분별하게 사용하진 말자</p>
<h2 id="조회한-빈이-모두-필요할-때-list-map">조회한 빈이 모두 필요할 때, List, Map</h2>
<p>의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있음</p>
<p>예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인의 종류를 선택할 수 있다고 가정 → 스프링을 사용하면 소위말하는 전략패턴을 매우 간단하게 구현 가능</p>
<pre><code class="language-java">public class AllBeanTest {

    @Test
    void findAllBean(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, &quot;userA&quot;, Grade.VIP);
        int discountPrice = discountService.discount(member,10000,&quot;fixDiscountPolicy&quot;);

        Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
        Assertions.assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPolicy = discountService.discount(member, 20000, &quot;rateDiscountPolicy&quot;);
        Assertions.assertThat(rateDiscountPolicy).isEqualTo(2000);
    }

    static class DiscountService{
        private final Map&lt;String, DiscountPolicy&gt; policyMap;
        private final List&lt;DiscountPolicy&gt; policies;

        @Autowired
        public DiscountService(Map&lt;String, DiscountPolicy&gt; policyMap, List&lt;DiscountPolicy&gt; policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println(&quot;policyMap = &quot; + policyMap);
            System.out.println(&quot;policies = &quot; + policies);
        }

        public int discount(Member member, int price, String discountCode){
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member,price);
        }
    }
}
</code></pre>
<p>DiscountService는 Map으로 <code>모든 DiscountPolicy를 주입받음</code> </p>
<p>→ fixDiscountPolicy, rateDiscountPolicy가 주입됨</p>
<p>discount()메서드는 discountCode로 만약 rateDiscountPolicy가 넘어오면 map에서 rateDiscountPolicy 스프링빈을 찾아서 실행</p>
<p><code>Map&lt;String, DiscountPolicy&gt;</code> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
<code>List&lt;DiscountPolicy&gt;</code> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.</p>
<p>만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다</p>
<h2 id="자동-수동의-올바른-실무-운영-기준">자동, 수동의 올바른 실무 운영 기준</h2>
<p><strong>편리한 자동 기능을 기본으로 사용하자</strong></p>
<p>어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고, 어떤 경우에 설정 정보를 통해 수동으로 빈을 등록하고, 의존관계도 수동으로 주입해야할까?</p>
<p>→ 스프링이 나오고 시간이 갈수록 점점 자동을 선호하는 추세</p>
<p>@Component뿐 아니라 @Controller, @Service, @Repository처럼 계층에 맞춰 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원</p>
<p>설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작 부분을 명확하게 나누는 것이 이상적이지만 개발자입장에서 스프링 빈을 하나 등록할 때 @Component만 넣어주면 끝나는일을 @Configuration 설정정보에 가서 @Bean을 적고 기타등등…매우 번거로움!</p>
<p>또 관리할 빈이 많아서 설정 정보가 커지면 관리자체가 부담이됨</p>
<p>결정적으로 자동 빈 등록을 사용해도 OCP, DIP 지킬 수 있음!</p>
<p><strong>수동 빈 등록은 언제 사용하면 좋을까?</strong></p>
<p>애플리케이션은 크게 업무 로직, 기술 지원 로직으로 나눌 수 있음</p>
<ul>
<li><code>업무 로직 빈</code> : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.</li>
<li><code>기술 지원 빈</code> : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.</li>
</ul>
<p>업무 로직은 숫자도 매우 많고, 한번 개발해야하면 컨트롤러, 서비스, 리포지토리처럼 어느정도 유사한 패턴이 있다 → 자동 기능 적극 사용 권장 → 문제도 찾기 쉬움</p>
<p>기술 지원 로직은 업무 로직과 비교해서 수가 매우 적고 광범위하게 영향을 미침. 또한 문제가 어디서 발생했는지 적용은 잘 되고 있는지도 파악하기 어려움 → 수동 빈 등록을 통해 명확하게 들어내는 것이 좋다</p>
<p><strong>애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는것이 유지보수 하기 좋다.</strong></p>
<p><strong>비즈니스 로직 중에서 다형성을 적극 활용할 때</strong></p>
<p>의존관계 자동 주입 - 조회한 빈이 모두 필요할 때 → List, map</p>
<p>DiscountService 가 의존관계 자동 주입으로 Map&lt;String,DiscountPolicy&gt; 에 주입을 받는 상황같은 경우에는 수동등록이 낫다. </p>
<p>자동으로 할경우 특정 패키지에 같이묶어두는것이 좋다.</p>
<p>스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외</p>
<p>스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다.</p>
<p><strong>정리</strong></p>
<p>편리한 <code>자동 기능을 기본</code>으로 사용하자</p>
<p><code>직접 등록하는 기술 지원 객체는 수동</code> 등록</p>
<p><code>다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자</code></p>
<hr>
<p>인프런 스프링 핵심 원리 기본편 - 김영한 강의를 듣고 정리한 내용입니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 기본] 컴포넌트 스캔]]></title>
            <link>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94</link>
            <guid>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94</guid>
            <pubDate>Tue, 11 Jun 2024 07:34:51 GMT</pubDate>
            <description><![CDATA[<h2 id="컴포넌트-스캔과-의존관계-자동-주입-시작하기">컴포넌트 스캔과 의존관계 자동 주입 시작하기</h2>
<p>지금까지는 스프링 빈 등록할 때 @Bean이나 &lt;bean&#39;&gt; 등을 통해서 등록했음</p>
<p>→ 등록할게 수십, 수백개가되면 등록하기 귀찮고 설정정보도 커지고 누락 발생</p>
<p>→ 설정 정보가 없어도 <code>자동으로 스프링 빈을 등록</code>하는 <code>컴포넌트 스캔</code>이라는 기능 제공</p>
<p>또 <code>의존관계도 자동으로 주입</code>하는 <code>@Autowired</code>라는 기능도 제공</p>
<pre><code class="language-java">package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
        // 설정 정보는 컴포넌트 스캔 대상에서 제외
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}
</code></pre>
<p><code>@ComponentScan</code>을 설정정보에 붙여주면됨</p>
<ul>
<li>기존 AppConfig와 다르게 @Bean으로 등록한 클래스가 하나도 없다</li>
</ul>
<p><code>excludeFilters</code>를 이용해서 @Configuration이 붙은 설정정보들을 제외해줌 → 일반적으로 잘 사용하진 않지만, 현재는 기존 코드를 최대한 유지하기 위해서 이렇게</p>
<p>MemoryMemberRepository, MemberServiceImpl, RateDiscountPolicy, OrderServiceImple <code>클래스에 @Component</code>를 추가해줌</p>
<p>클래스 안의 <code>생성자에는 @Autowired</code>를 붙여줌으로서 빈에서 자동으로 맞는 객체를 찾아등록하도록해줌</p>
<p>이전에 AppConfig에서는 <code>@Bean</code> 으로 직접 설정 정보를 작성했고, 의존관계도 직접 명시했다. 이제는 이런 설 정 정보 자체가 없기 때문에, 의존관계 주입도 이 클래스 안에서 해결해야 한다.</p>
<p>멤버서비스 -&gt; 멤버서비스impl(멤버리포지토리)
오더서비스 -&gt; 오더서비스impl(멤버리포지토리, 할인정책)
할인 정책 -&gt; Rate할인정책
멤버리포지토리 -&gt; 메모리멤버리포지토리</p>
<p>이 관계에서 뒤에것들에만 @Component</p>
<pre><code class="language-java">public class AutoAppConfigTest {

    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}</code></pre>
<p>위 테스트가 잘 동작하는 것을 알 수 있음</p>
<ol>
<li><strong>@ComponentScan</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/09ee51d6-b92e-4f90-a072-236b8dd8affc/image.png" alt=""></p>
<p><code>@ComponentScan</code>은 <code>@Component</code>가 붙은 모든 클래스를 스프링 빈에 등록</p>
<p>이때 스프링 빈 이름은 클래스명을 사용하되 <code>맨 앞글자만 소문자</code></p>
<p>이름을 지정하고 싶으면 @Component(”memberService2”)이런식으로 하면됨</p>
<ol start="2">
<li><strong>@Autowired 의존관계 자동 주입</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/893762e8-7024-4220-bc8c-52543918c904/image.png" alt=""></p>
<p>생성자에 <code>@Autowired</code>를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링빈을 찾아서 주입</p>
<p>이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입</p>
<ul>
<li>getBean(MemberRepository.class)와 동일하다고 이해하면됨</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/644c7aed-1379-48d3-9c9d-33603a4a2fab/image.png" alt=""></p>
<h2 id="탐색-위치와-기본-스캔-대상">탐색 위치와 기본 스캔 대상</h2>
<p><strong>탐색할 패키지의 시작 위치 지정</strong></p>
<pre><code class="language-java">package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
        // member패키지 하위 패키지 탐색
        basePackages = &quot;hello.core.member&quot;,
        // 설정 정보는 컴포넌트 스캔 대상에서 제외
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}
</code></pre>
<p><code>basePackages</code> : 탐색할 패키지의 시작 위치를 지정. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다</p>
<ul>
<li>basePackages = {”hello.core”, “hello.service”}로 여러개 지정도 가능</li>
</ul>
<p><code>디폴트</code>는 @ComponentScan이 붙은 설정 정보 클래스(AppConfig.class)의 패키지가 시작위치가됨 → 현재 경우 hello.core 패키지가 디폴트</p>
<p><strong>권장 방식</strong></p>
<p>패키지 위치를 지정하지 않고, <code>설정 정보 클래스의 위치를 프로젝트 최상단</code>에 두는 것. 최근 스프링 부트도 이 방법을 기본으로 제공함.</p>
<p>ex) 프로젝트 구조</p>
<ul>
<li>com.hello</li>
<li>com.hello.service</li>
<li>com.hello.repository</li>
</ul>
<p>com.hello → 프로젝트 시작루트, 여기에 AppConfig같은 메인 설정 정보를 두고 @ComponentScan 애노테이션을 붙이고 <code>basePackages 지정 생략</code></p>
<p>스프링 부트 사용시 스프링 부트의 대표 시작 정보인 <code>@SpringBootApplication</code>를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다. 그리고 이 설정 안에 @ComponentScan이 들어있다. → 따로 @ComponentScan이 필요 X</p>
<pre><code class="language-java">@SpringBootApplication
public class CoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoreApplication.class, args);
    }

}</code></pre>
<p><strong>컴포넌트 스캔 기본 대상</strong></p>
<p>컴포넌트 스캔은 @Component뿐만 아니라 다음 내용도 추가로 대상에 포함</p>
<ul>
<li>@Component : 컴포넌트 스캔에서 사용</li>
<li>@Controller : 스프링 MVC 컨트롤러에서 사용</li>
<li>@Service : 스프링 비즈니스 로직에서 사용</li>
<li>@Repository : 스프링 데이터 접근 계층에서 사용</li>
<li>@Configuration : 스프링 설정 정보에서 사용</li>
</ul>
<pre><code class="language-java"> @Component
 public @interface Controller {
 }

 @Component
 public @interface Service {
 }

 @Component
 public @interface Configuration {
}</code></pre>
<p>해당 클래스의 소스코드를 보면 @Component를 포함하고 있는 것을 알 수 있음</p>
<p>+) 사실 애노테이션에는 상속관계라는 것이 없다. 그래서 이렇게 애노테이션이 특정 애노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고, 스프링이 지원하는 기능이다.</p>
<p>컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가기능을 수행</p>
<ul>
<li>@Controller : 스프링 MVC 컨트롤러로 인식</li>
<li>@Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해줌</li>
<li>@Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 함</li>
<li>@Service : 특별한 처리를 하진 않음. 개발자들이 핵심 비즈니스 로직이 여기 있겠구나라고 비즈니스 계층을 인식하는데 도움이됨</li>
</ul>
<p>+) useDefaultFilters 옵션은 기본으로켜저있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.</p>
<h2 id="필터">필터</h2>
<pre><code class="language-java">package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
</code></pre>
<p>컴포넌트 스캔 대상에 추가할 애노테이션</p>
<p>추가하지 않을 애노테이션도 동일하게하고 MyExcludeComponent 인터페이스로 만들어준다</p>
<pre><code class="language-java">package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}
</code></pre>
<p>컴포넌트 스캔 대상에 추가할 클래스 → @MyIncludeComponent 적용</p>
<p>제외할 클래스 BeanB도 동일하게 만들고 → @MyExcludeComponent 적용</p>
<pre><code class="language-java">public class ComponentFilterAppConfigTest {

    @Test
    void filterScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean(&quot;beanA&quot;, BeanA.class);
        Assertions.assertThat(beanA).isNotNull();

        //ac.getBean(&quot;beanB&quot;, BeanB.class);
        org.junit.jupiter.api.Assertions.assertThrows(
                NoSuchBeanDefinitionException.class,
                () -&gt; ac.getBean(&quot;beanB&quot;, BeanB.class));
    }

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes = MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes = MyExcludeComponent.class))
    static class ComponentFilterAppConfig{

    }
}</code></pre>
<p>BeanA가 스프링 빈에 등록되고, BeanB는 등록안된 것 확인 가능</p>
<p><strong>FilterType 옵션</strong>
FilterType은 5가지 옵션이 있다.</p>
<p>ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.</p>
<ul>
<li>ex) <code>org.example.SomeAnnotation</code></li>
</ul>
<p>ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.</p>
<ul>
<li><p>ex) <code>org.example.SomeClass</code></p>
<pre><code class="language-java">  @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
     }</code></pre>
<p>  이런식으로 A도 제외가능</p>
</li>
</ul>
<p>ASPECTJ: AspectJ 패턴 사용</p>
<ul>
<li>ex) <code>org.example..*Service+</code></li>
</ul>
<p>REGEX: 정규 표현식</p>
<ul>
<li>ex) <code>org\.example\.Default.*</code></li>
</ul>
<p>CUSTOM: <code>TypeFilter</code> 이라는 인터페이스를 구현해서 처리</p>
<ul>
<li>ex) <code>org.example.MyTypeFilter</code></li>
</ul>
<p>크게 많이 사용하진 않는다</p>
<h2 id="중복-등록과-충돌">중복 등록과 충돌</h2>
<p>컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게될까?</p>
<ol>
<li><p>자동 빈 등록 vs 자동 빈 등록</p>
<p> 스프링에서 오류 발생시킴 → <code>ConflictingBeanDefinitionException</code> 예외 발생</p>
</li>
<li><p>수동 빈 등록 vs 자동 빈 등록</p>
<pre><code class="language-java"> @Configuration
 @ComponentScan(
         basePackages = &quot;hello.core.member&quot;,
         excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
 )
 public class AutoAppConfig {
     @Bean(name = &quot;memoryMemberRepository&quot;)
     public MemberRepository memberRepository() {
         return new MemoryMemberRepository();
     }
 }</code></pre>
<p> 수동으로 등록한 빈</p>
<p> 이 경우 수동 빈 등록이 우선권을 가짐 → 수동 빈이 자동빈을 오버라이딩해버림</p>
<p> 개발자가 이런 결과를 의도했다면 수동이 우선권을 가지는 것이 좋지만 개발자가 의도하지 않은 경우가 대부분 → 이때 잡기 어려운 버그가 만들어짐!</p>
<p> 그래서 최근 스프링부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꿈</p>
<p> → @SpringBootApplication애노테이션이 있는 CoreApplication을 실행해보면 오류남</p>
</li>
</ol>
<hr>
<p>인프런 스프링 핵심 원리 기본편 - 김영한 강의를 듣고 정리한 내용입니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 기본] 싱글톤 컨테이너]]></title>
            <link>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</link>
            <guid>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</guid>
            <pubDate>Tue, 11 Jun 2024 07:31:51 GMT</pubDate>
            <description><![CDATA[<h2 id="웹-애플리케이션과-싱글톤">웹 애플리케이션과 싱글톤</h2>
<p>스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생</p>
<p>대부분의 스프링 애플리케이션은 웹 애플리케이션</p>
<p>웹 애플리케이션은 보통 <code>여러 고객이 동시에</code> 요청을 함<img src="https://velog.velcdn.com/images/chaen-ing/post/8165cb8e-5268-460f-9fa9-c6dacb00b31a/image.png" alt=""></p>
<p>고객들이 요청할때마다 객체가 계속 새로 생성됨</p>
<pre><code class="language-java">@Test
    @DisplayName(&quot;스프링 없는 순수한 DI 컨테이너&quot;)
    void pureContainer(){
        AppConfig appConfig = new AppConfig();
        // 1. 조회 : 호출할 때 마다 객체를 생성
        MemberService memberService = appConfig.memberService();

        // 2. 조회 : 호출할 때 마다 객체를 생성
        MemberService memberService2 = appConfig.memberService();

        // 참조값이 다른 것을 확인
        System.out.println(&quot;memberService = &quot; + memberService);
        System.out.println(&quot;memberService2 = &quot; + memberService2);

        // memverService1 != memverService2
        Assertions.assertThat(memberService).isNotSameAs(memberService2);
    }</code></pre>
<p>테스트 실행시 다른 객체가 생성된 것을 확인할 수 있음</p>
<p>스프링 없는 순수한 DI 컨테이너인 AppConfig는 <code>요청할 때마다 객체를 새로 생성</code></p>
<p>고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸됨 → <code>메모리 낭비 심하다</code></p>
<p>해결방안 : 해당 객체가 딱 1개만 생성되고 공유하도록 설계 → <code>싱글톤 패턴</code></p>
<h2 id="싱글톤-패턴">싱글톤 패턴</h2>
<p>클래스의 인스턴스가 딱 <code>1개만</code> 생성되는 것을 보장하는 디자인 패턴</p>
<p>→ 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야함 </p>
<p>→ <code>private</code> 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야함</p>
<pre><code class="language-java">package hello.core.singleton;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class SingletonService {

        // 1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();

    // 2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용
    public static SingletonService getInstance(){
        return instance;
    }

    // 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성 못하게 막는다
    private SingletonService(){
    }

    public void logic(){
        System.out.println(&quot;싱글톤 객체 로직 호출&quot;);
    }
}
</code></pre>
<ol>
<li><p>자기자신을 내부의 private으로 가지고 있음. static이라서 class레벨에 올라가서 딱 하나만 생성</p>
</li>
<li><p>인스턴스에 접근가능한 유일한 방법 : getInstance() 메서드</p>
<p> 이때 항상 같은 인스턴스를 반환함</p>
</li>
<li><p>1개의 객체 인스턴스만 존재해야하므로, 혹시라도 외부에서 new로 생성하지 못하도록 막음</p>
</li>
</ol>
<pre><code class="language-java">@Test
    @DisplayName(&quot;싱글톤 패턴을 적용한 객체 사용&quot;)
    void singletonServiceTest(){
        // new SingletonService(); private으로 생성자를 막아두었으므로 컴파일 오류 발생

        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println(&quot;singletonService1 = &quot; + singletonService1);
        System.out.println(&quot;singletonService2 = &quot; + singletonService2);

        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
    }</code></pre>
<p>호출할 때마다 같은 객체 인스턴스를 반환하는것을 확인할 수 있음</p>
<p>isSameas → == 이걸 의미</p>
<p>isEqualto</p>
<p>싱글톤 패턴을 구현하는 방법은 여러가지가 있는데 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택한것</p>
<p>싱글톤 패턴을 적용하면 고객의 요청이 올때마다 객체를 생성하는 것이 아니라 만들어진 객체를 공유하여 효율적으로 사용가능</p>
<p><strong><code>싱글톤 패턴 문제점</code></strong></p>
<ul>
<li>싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.</li>
<li>의존관계상 클라이언트가 구체 클래스에 의존한다 → DIP를 위반한다.</li>
<li>클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.</li>
<li>테스트하기 어렵다.</li>
<li>내부 속성을 변경하거나 초기화 하기 어렵다.</li>
<li>private 생성자로 자식 클래스를 만들기 어렵다.</li>
<li>결론적으로 유연성이 떨어진다.</li>
<li>안티패턴으로 불리기도 한다.</li>
</ul>
<h2 id="싱글톤-컨테이너">싱글톤 컨테이너</h2>
<p><code>스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다</code></p>
<p>지금까지 배운 스프링빈이 바로 싱글톤으로 관리되는 빈이다</p>
<p><code>싱글톤 컨테이너</code></p>
<p>스프링 컨테이너는 <code>싱글톤 패턴을 적용하지 않아도</code> 객체 인스턴스를 싱글톤으로 관리 → 컨테이너는 객체를 하나만 생성해서 관리</p>
<p>스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.</p>
<p>스프링 컨테이너의 이런 기능 덕분에 <code>싱글톤 패턴의 모든 단점을 해결</code>하면서 객체를 싱글톤으로 유지가능</p>
<ul>
<li>싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도됨</li>
<li>DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤 사용가능</li>
</ul>
<pre><code class="language-java">@Test
    @DisplayName(&quot;스프링 컨테이너와 싱글톤&quot;)
    void springContainer(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService = ac.getBean(&quot;memberService&quot;,MemberService.class);
        MemberService memberService2 = ac.getBean(&quot;memberService&quot;,MemberService.class);

        // 참조값이 동일함
        System.out.println(&quot;memberService = &quot; + memberService);
        System.out.println(&quot;memberService2 = &quot; + memberService2);

        // memverService1 == memverService2
        Assertions.assertThat(memberService).isSameAs(memberService2);

    }</code></pre>
<p>참조값이 동일하기때문에 테스트 성공<img src="https://velog.velcdn.com/images/chaen-ing/post/70078574-db8f-4c2f-8ecc-daef939f03c8/image.png" alt=""></p>
<p>고객이 요청할때마다 객체를 생성하는 것이 아니라 이미 만들어져있는 객체를 공유하여 효율적으로 재사용</p>
<p>+) 스프링의 기본 빈 등록 방식은 싱글톤이지만 싱글톤 말고 요청할때마다 새로 객체 생성해서 반환하는 기능도 제공 → 빈 스코프</p>
<h2 id="싱글톤-방식의-주의점">싱글톤 방식의 주의점</h2>
<p>싱글톤 패턴이든, 스프링같은 싱글톤 컨테이너를 사용하든 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 하나의 같은 객체 인스턴스를 공유하기때문에 싱글톤 객체는 상태를 유지(<code>stateful</code>)하게 설계하면안됨</p>
<p>→ <code>무상태 stateles</code>s로 설계해야함</p>
<ul>
<li>특정 클라이언트에 의존적인 필드가 있으면 안됨</li>
<li>특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됨</li>
<li>가급적 읽기만 가능해야함</li>
<li><code>필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal</code> 등을 사용해야함</li>
</ul>
<p>스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있음!</p>
<pre><code class="language-java">public class StatefulService {

    private int price; // 상태를 유지하는 필드

    public void order(String name, int price){
        System.out.println(&quot;name = &quot;+name + &quot; price = &quot;+price);
        this.price = price; //여기가 문제
    }

    public int getPrice(){
        return price;
    }

}</code></pre>
<pre><code class="language-java">class StatefulServiceTest {

    @Test
    void statefulServiceSingleton(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA : A사용자 10000원 주문
        statefulService1.order(&quot;userA&quot;,10000);
        //ThreadB : B사용자 20000원 주문
        statefulService2.order(&quot;userB&quot;,20000);

        //ThreadA : 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println(&quot;price = &quot; + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig{
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }
    }

}</code></pre>
<p>StatefulService의 price필드는 <code>공유되는 필드</code>인데, 특정 클라이언트가 값을 변경해버리면 문제가 발생</p>
<p>사용자 A가 10000원을 주문하여 this.price에 10000원이 할당됨</p>
<p>→ 사용자 B가 20000원을 주문하여 20000원이 할당</p>
<p>→ 사용자 A가 자신의 객체를 이용하여 주문 조회를 해도 실체 객체 인스턴스가 하나기때문에 20000원이 찍힘 : 문제 발생!!</p>
<p>스프링빈은 항상 <code>무상태로</code> 설계해야함!</p>
<pre><code class="language-java">public int order(String name, int price){
        System.out.println(&quot;name = &quot;+name + &quot; price = &quot;+price);
        return price;
    }</code></pre>
<pre><code class="language-java">
int userAPrice = statefulService1.order(&quot;userA&quot;,10000);
int userBPrice = statefulService2.order(&quot;userB&quot;,20000);

System.out.println(&quot;price = &quot; + userAPrice);</code></pre>
<p>이런식으로 지역변수를 할당해서 문제를 해결할 수 있음</p>
<h2 id="configuration과-싱글톤">@Configuration<strong>과 싱글톤</strong></h2>
<pre><code class="language-java">@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
    }
}</code></pre>
<p>@Bean memberService() → MemoryMemberRepository()</p>
<p>@Bean orderService() → MemoryMemberRepository(), RateDiscountPolicy()</p>
<p>이렇게되면 MemoryMemberRepository가 2개 생성되면서 싱글톤이 깨지는 것 아닌가?라는 의문점이 들 수 있음</p>
<pre><code class="language-java">    @Test
    void configurationTest(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean(&quot;memberService&quot;,MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean(&quot;orderService&quot;,OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean(&quot;memberRepository&quot;, MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println(&quot;memberService -&gt; memberRepository = &quot; + memberRepository1);
        System.out.println(&quot;orderService -&gt; memberRepository = &quot; + memberRepository2);
        System.out.println(&quot;memberRepository = &quot; + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }</code></pre>
<p>검증결과 memberRepository 인스턴스는 <code>모두 같은 인스턴스가 공유하여 사용</code></p>
<p>스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성한다. 이때 memberRepository()는 </p>
<ol>
<li>처음 등록 1번</li>
<li>memberService()에서 한번</li>
<li>orderService()에서 한번</li>
</ol>
<p>총 세번 호출되는가? → 아님</p>
<p>실제로 각 <code>한번씩만</code> 호출된다</p>
<p>call AppConfig.memberService</p>
<p>call AppConfig.memberRepository</p>
<p>call AppConfig.orderService</p>
<h2 id="configuration과-바이트코드-조작의-마법">@Configuration<strong>과 바이트코드 조작의 마법</strong></h2>
<pre><code class="language-java">@Test
    void configurationDeep(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println(&quot;bean.getClass() = &quot; + bean.getClass());
    }</code></pre>
<p>AnnnotationConfigApplicationContext에 파라미터로 넘긴값은 스프링 빈에 등록되므로 Appconfig도 스프링빈이됨</p>
<p>Appconfig 스프링 빈을 조회해서 클래스 정보를 출력해본 결과</p>
<p><code>bean.getClass() = class hello.core.AppConfig$$SpringCGLIB$$0</code></p>
<p>위와같은 결과가 나옴</p>
<p>순수한 클래스라면 class hello.core.AppConfig 이런 결과가 나와야함<img src="https://velog.velcdn.com/images/chaen-ing/post/c36b758f-9f44-4869-b493-140e0ac638ff/image.png" alt="">스프링이 CGLIB이라는 바이트코드 조작 라이브러리를 사용해서 
AppConfig 클래스를 <code>상속받은 임의의 다른 클래스</code>를 만들고, 그 <code>다른 클래스를 스프링빈으로 등록</code>한 것이다</p>
<p>이 임의의 다른 클래스가 <code>싱글톤이 보장</code>되도록 해줌</p>
<p>AppConfig@CGLIB 예상코드</p>
<pre><code class="language-java">@Bean
public MemberRepository memberRepository() {

    if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
        return 스프링 컨테이너에서 찾아서 반환;
    } else { //스프링 컨테이너에 없으면
        기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 
        return 반환
    }
}</code></pre>
<p>memberRepository는 바이트코드를 조작해서 다음과 같은 형식으로 다시 작성될 것 </p>
<p>@Bean이 붙은 메소드마다 이미 등록되어있으면 존재하는 빈을 반환하고 없으면 생성해서 스프링빈에 등록하고 반환 → <code>싱글톤!</code></p>
<p>+) AppConfig@CGLIB은 AppConfig의 자식타입이므로, AppConfig 타입으로 조회가능</p>
<p><strong>@Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까?</strong></p>
<p>AppConfig가 CGLIB 기술없이 순수한 AppConfig로 스프링빈에 등록됨</p>
<p>→ bean = class hello.core.AppConfig</p>
<p>호출결과도 MemberRepository가 총 3번 호출됨</p>
<p>→ 여기서 각각 인스턴스 또한 다 다름</p>
<p>즉, 싱글톤이 깨짐</p>
<p>@Bean만 사용해도 스프링빈으로 등록되지만 <code>싱글톤 보장 X</code></p>
<p>스프링 설정 정보는 항상 <code>@Configuration</code>을 사용하도록 하자</p>
<hr>
<p>인프런 스프링 핵심 원리 기본편 - 김영한 강의를 듣고 정리한 내용입니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 기본] 스프링 컨테이너와 스프링 빈]]></title>
            <link>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88</link>
            <guid>https://velog.io/@chaen-ing/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88</guid>
            <pubDate>Sun, 02 Jun 2024 15:45:23 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-컨테이너-생성">스프링 컨테이너 생성</h2>
<pre><code class="language-java">//스프링 컨테이너 생성
ApplicationContext applicationContext =new AnnotationConfigApplicationContext(AppConfig.class);</code></pre>
<p><code>ApplicationContext</code>를 스프링 컨테이너라고함</p>
<p><code>ApplicationContext</code>는 인터페이스임 → 다형성 O</p>
<p><code>new AnnotationConfigApplicationContext(AppConfig.class);</code> 이부분이 구현체</p>
<ol>
<li>스프링 컨테이너 생성
 <img src="https://velog.velcdn.com/images/chaen-ing/post/f50f9d4a-7a40-42cf-9e1b-d78a46a99959/image.png" alt=""></li>
</ol>
<pre><code>`new AnnotationConfigApplicationContext(AppConfig.class);`

생성시 구성정보를 지정해주어야해서 AppConfig.class를 매개변수로 넣는것</code></pre><ol start="2">
<li>스프링 빈 등록
 <img src="https://velog.velcdn.com/images/chaen-ing/post/41423793-0f7e-463e-a12e-f9b7428ce0b1/image.png" alt=""></li>
</ol>
<pre><code>@Bean붙은 메소드를 다 호출해서 `메소드 이름을 빈 이름(키)`, `리턴하는 객체를 빈 객체(값)`으로 빈 저장소에 저장

빈이름은 메소드 이름을 사용하지 @Bean(name=&quot;memberService2&quot;)이런식으로 부여도 가능

빈 이름은 `항상 다른 이름`을 부여해야함</code></pre><ol start="3">
<li>스프링 빈 의존관계 설정 - 준비
 <img src="https://velog.velcdn.com/images/chaen-ing/post/c8e61852-1039-42d9-ad93-568e75726715/image.png" alt=""></li>
</ol>
<ol start="4">
<li>스프링 빈 의존관계 설정 - 완료
 <img src="https://velog.velcdn.com/images/chaen-ing/post/c2586ea0-bdcb-4c20-a868-4a5f157fb0a0/image.png" alt=""></li>
</ol>
<pre><code>설정 정보를 참고해서 동적 `의존관계를 주입`</code></pre><p>스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져있다</p>
<h2 id="컨테이너에-등록된-모든-빈-조회">컨테이너에 등록된 모든 빈 조회</h2>
<pre><code class="language-java">public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName(&quot;모든 빈 출력하기&quot;)
    void findAllBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); // 모든 빈 이름 꺼내기
        // iter
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println(&quot;name = &quot; + beanDefinitionName + &quot;object = &quot;+bean);
        }
    }

    @Test
    @DisplayName(&quot;애플리케이션 빈 출력하기&quot;)
    void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); // 모든 빈 이름 꺼내기
        // iter
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            // 역할은 3가지 존재
            // ROLE_APPLICAITON : 내가 등록한 빈만 고르는 조건
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println(&quot;name = &quot; + beanDefinitionName + &quot;object = &quot;+bean);
            }

        }
    }
}
</code></pre>
<p><code>getBeanDefinitionNames()</code> : 스프링에 등록된 모든 빈 이름 조회 → String 빈 이름 리턴</p>
<p><code>getBean(빈이름)</code> : 빈이름으로된 빈객체를 조회</p>
<p>beanDefinition.getRole</p>
<ul>
<li><code>ROLE_APPLICATION</code> : 일반적으로 사용자가 정의한 빈</li>
<li><code>ROLE_INFRASTRUCTURE</code> : 스프링이 내부에서 사용하는 빈</li>
</ul>
<h2 id="스프링-빈-조회---기본">스프링 빈 조회 - 기본</h2>
<p>가장 기본적인 빈 조회 방법</p>
<ul>
<li><code>ac.getBean(빈이름, 타입)</code></li>
<li><code>ac.getBean(타입)</code></li>
</ul>
<p>조회대상 스프링 빈이 없으면 예외 발생<code>NoSuchBeanDefinitionException: No bean named &#39;xxxxx&#39; available</code></p>
<pre><code class="language-java">public class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName(&quot;빈 이름으로 조회&quot;)
    void findBeanByName(){
        MemberService memberService = ac.getBean(&quot;memberService&quot;, MemberService.class);
        System.out.println(&quot;memberService = &quot; + memberService);
        System.out.println(&quot;memberService.getClass() = &quot; + memberService.getClass());
        // MemberServiceImpl이 잘 출력됨
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName(&quot;이름없이 타입으로만 조회&quot;)
    void findBeanByType(){
        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    // 인터페이스 아닌 구체타입도 가능하다. 그러나 아래는 추상이 아닌 구현에 의존한 코드이므로 좋은 코드는 아니긴함
    @Test
    @DisplayName(&quot;구체 타입으로 조회&quot;)
    void findBeanByName2(){
        MemberService memberService = ac.getBean(&quot;memberService&quot;,MemberServiceImpl.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName(&quot;빈 이름으로 조회X&quot;)
    void findBeanByNameX(){
        // MemberService xxxx = ac.getBean(&quot;xxxx&quot;, MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class, 
                ()-&gt;ac.getBean(&quot;xxxx&quot;, MemberService.class));
    }

}
</code></pre>
<p>빈 타입으로 조회, 빈이름+타입으로 조회, 구체타입으로 조회 모두 가능</p>
<p>Assertions의 isInstanceOf(MemberServiceImpl.class)로 테스트 가능하다</p>
<p>구체타입으로 조회는 추천 X 유연성이 떨어짐</p>
<p>없는 빈 이름으로 조회하면 예외가 터짐</p>
<p>assertThrow(..)은 예외가 발생해야지 테스트가 성공하는 테스트</p>
<h2 id="스프링-빈-조회---동일한-타입이-둘-이상">스프링 빈 조회 - 동일한 타입이 둘 이상</h2>
<p>조회 시 같은 타입의 스프링 빈이 두개 이상이면 오류 발생 : <code>NoUniqueBeanDefinitionException</code> </p>
<p>→ 빈 이름 지정해줘야함</p>
<p>ac.getBeansOfType()으로 타입의 모든 빈 조회가능</p>
<pre><code class="language-java">public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName(&quot;타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다&quot;)
    void findBeanByTypeDuplicate(){
        //MemberRepository bean = ac.getBean(MemberRepository.class);
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -&gt; ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName(&quot;타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다&quot;)
    void findBeanByName(){
        MemberRepository memberRepository = ac.getBean(&quot;memberRepository1&quot;,MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);

    }

    @Test
    @DisplayName(&quot;특정 타입을 모두 조회하기&quot;)
    void findBeanByType(){
        Map&lt;String, MemberRepository&gt; beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println(&quot;key = &quot; + key+&quot;value = &quot;+beansOfType.get(key));
        }
        System.out.println(&quot;beansOfType = &quot; + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);

    }

    // 중복 타입이 있는 config를 여기서만 사용할 수 있게 생성
    // static : 이 안에서만 쓰겠다
    @Configuration
    static class SameBeanConfig{
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

        @Bean
        public MemberRepository memberRepository2(){
            return new MemoryMemberRepository();
        }

    }
}
</code></pre>
<p>동일 타입의 빈이 두개이상이라면 빈이름을 통해서 getBean해주면됨</p>
<p><code>getBeansOfType</code>은 빈이름, 빈객체를 <code>맵</code>으로 리턴</p>
<h2 id="스프링-빈-조회---상속-관계">스프링 빈 조회 - 상속 관계</h2>
<p>부모타입으로 조회하면 <code>자식타입도 함께</code> 조회된다.</p>
<p>자바 객체의 최고 부모인 Object 타입으로 조회하면 모든 스프링빈 조회<img src="https://velog.velcdn.com/images/chaen-ing/post/cf9aff71-cf86-4699-ab48-1d8a8bc0a5a5/image.png" alt=""></p>
<pre><code class="language-java">public class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상있으면, 중복 오류가 발생한다&quot;)
    void findBeanByParentTypeDuplicate(){
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        assertThrows(NoUniqueBeanDefinitionException.class,
                ()-&gt;ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상있으면, 빈 이름을 지정하면 된다&quot;)
    void findBeanByParentTypeBeanName(){
        DiscountPolicy rateDiscoutPolicy = ac.getBean(&quot;rateDiscountPolicy&quot;,DiscountPolicy.class);
        assertThat(rateDiscoutPolicy).isInstanceOf(DiscountPolicy.class);
    }

    @Test
    @DisplayName(&quot;특정 하위 타입으로 조회&quot;)
    void findBeanBySubType(){
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName(&quot;부모 타입으로 모두 조회&quot;)
    void findAllBeanByParentType(){
        Map&lt;String, DiscountPolicy&gt; beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for(String key:beansOfType.keySet()){
            System.out.println(&quot;key = &quot;+key+&quot;value = &quot;+beansOfType.get(key));
        }
    }

    @Test
    @DisplayName(&quot;부모 타입으로 모두 조회하기 - Object&quot;)
    void findAllBeanByObjectType(){
        Map&lt;String, Object&gt; beansOfType = ac.getBeansOfType(Object.class);
        for(String key:beansOfType.keySet()){
            System.out.println(&quot;key = &quot;+key+&quot;value = &quot;+beansOfType.get(key));
        }
    }

    @Configuration
    static class TestConfig{
        @Bean
        public DiscountPolicy rateDiscountPolicy(){
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy(){
            return new FixDiscountPolicy();
        }
    }
}
</code></pre>
<p>부모타입으로 조회 → 자식이 둘이상이면 오류 발생 → 빈 이름 지정해줘야함</p>
<p>특정 하위 타입으로도 조회가능 → 비추</p>
<p>부모타입으로 모두 조회 가능 getBeansOfType → 자바 최상위 클래스인 Object.class로도 조회 가능</p>
<h2 id="beanfactory와-applicationcontext">BeanFactory와 ApplicationContext</h2>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/5b09a499-189b-467b-a86c-c27d11bb1c74/image.png" alt=""></p>
<p><strong>BeanFactory</strong></p>
<p>스프링 컨테이너의 <code>최상위</code> 인터페이스</p>
<p>스프링 빈 <code>관리, 조회</code> 역할</p>
<p>getBean()을 제공</p>
<p><strong>ApplicationContext</strong></p>
<p>BeanFactory기능을 모두 상속받아서 제공 </p>
<p>→ 둘의 차이가 무엇일까? : 애플리케이션 개발에는 빈 관리, 조회 뿐만 아닌 많은 기능들이 추가로 필요함</p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/73dac66b-a46c-4111-91ca-d8a13a10a26f/image.png" alt=""></p>
<p>MessageSource 메세지 소스를 활용한 국제화 기능 : 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력하는 기능</p>
<p>EnvironmentCapable 환경변수 : 로컬, 개발, 운영등을 구분해서 처리</p>
<ul>
<li>로컬 : 내 컴퓨터에서 개발하는 환경</li>
<li>개발 : 테스트 서버에서 테스트하는 환경</li>
<li>운영 : 실제 프로덕션에 나가는 환경</li>
</ul>
<p>ApplicationEventPublisher 애플리케이션 이벤트 : 이벤트를 발행하고 구독하는 모델 편리하게 지원</p>
<p>ResourceLoader 편리한 리소스 조회 : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회</p>
<p>정리하자면</p>
<p>ApplicationContext는 BeanFactory의 <code>기능(관리, 조회) + 편리한 부가기능</code>을 제공</p>
<p>BeanFactory를 직접 사용할일은 거의 없고, ApplicationContext만 쓴다고 보면됨 → 두개 모두 <code>스프링 컨테이너</code>라고 한다</p>
<h2 id="다양한-설정-형식-지원---자바-코드-xml"><strong>다양한 설정 형식 지원</strong> - <strong>자바 코드</strong>, XML</h2>
<p>스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어있음 → 자바 코드, XML, Groovy 등등…</p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/bb64ae2b-2151-428d-bc1c-de02e6035ecc/image.png" alt=""></p>
<p>annotation 기반 자바 코드 설정 사용</p>
<ul>
<li>지금까지 했던것</li>
<li>AnnotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class)</li>
</ul>
<p><code>XML 설정 사용</code></p>
<ul>
<li>최근에 잘 사용하진 않지만, 아직 많은 레거시 프로젝트들이 XML로 되어있고 컴파일이 필요없다는 장점이 있음</li>
</ul>
<pre><code class="language-java">public class XmlAppContext {

    @Test
    void xmlAppContext(){
        ApplicationContext ac = new GenericXmlApplicationContext(&quot;appConfig.xml&quot;);
        MemberService memberService = ac.getBean(&quot;memberService&quot;, MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}
</code></pre>
<p>GenericXmlApplicationContext(”appConfig.xml”) 이것만 다름</p>
<pre><code class="language-java">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;

    &lt;bean id=&quot;memberService&quot; class=&quot;hello.core.member.MemberServiceImpl&quot;&gt;
        &lt;constructor-arg name=&quot;memberRepository&quot; ref=&quot;memberRepository&quot;/&gt;
    &lt;/bean&gt;

    &lt;bean id=&quot;memberRepository&quot; class=&quot;hello.core.member.MemoryMemberRepository&quot;/&gt;

    &lt;bean id=&quot;orderService&quot; class=&quot;hello.core.order.OrderServiceImpl&quot;&gt;
        &lt;constructor-arg name=&quot;memberRepository&quot; ref=&quot;memberRepository&quot;/&gt;
        &lt;constructor-arg name=&quot;discountPolicy&quot; ref=&quot;discountPolicy&quot;/&gt;
    &lt;/bean&gt;

    &lt;bean id=&quot;discountPolicy&quot; class=&quot;hello.core.discount.RateDiscountPolicy&quot;/&gt;
&lt;/beans&gt;</code></pre>
<p>코드 형식만 다르지 기존 AppConfig와 다르지 않음</p>
<h2 id="스프링-빈-설정-메타-정보---beandefinition"><strong>스프링 빈 설정 메타 정보</strong> - BeanDefinition</h2>
<p>스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까</p>
<p>→ 이것도 <code>역할과 구현</code>을 개념적으로 나눈 것</p>
<p>→ 자바코드인지, XML인지 스프링 컨테이너는 몰라도됨. 그냥 읽어서 BeanDefinition을 만들면됨</p>
<p><code>BeanDefinition</code>을 <code>빈 설정 메타정보</code>라고함 → @Bean, <bean>당 각각 하나씩 메타정보가 생성됨</p>
<p>스프링 컨테이너는 이 메타정보를 기반으로 스프링빈 생성
<img src="https://velog.velcdn.com/images/chaen-ing/post/eaec5d02-2381-41e3-b81c-2c7335aa7280/image.png" alt=""></p>
<p>BeanDefinition 자체가 인터페이스임</p>
<p><img src="https://velog.velcdn.com/images/chaen-ing/post/e6fa707f-661d-4c6a-a666-1662abf7cbce/image.png" alt=""></p>
<p>AnnotationConfigApplicationContext : AnnotatedBeanDefinitionReader를 통해서 Appconfig.class를 읽음</p>
<p>→ 설정 정보를 읽어서 BeanDefinition을 생성</p>
<p>GenericXmlApplicationContext : 동일하게 XmlBeanDefinitionReader를 통해 appConfig.xml을 읽고 BeanDefinition 생성</p>
<pre><code class="language-java">ublic class BeanDefinitionTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName(&quot;빈 설정 메타정보 확인&quot;)
    void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for(String beanDefinitionName : beanDefinitionNames){
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                System.out.println(&quot;beanDefinitionName = &quot; + beanDefinitionName+&quot; beanDefinition = &quot;+beanDefinition);
            }
        }
    }
}
</code></pre>
<p><strong>BeanDefinition 정보</strong></p>
<p>BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
Scope: 싱글톤(기본값)
lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연
처리 하는지 여부
InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용
하면 없음)</p>
<p>BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할수도 있다 → 그러나 쓸일은 잘 없음</p>
<hr>
<p>  인프런 스프링 핵심 원리 기본편 - 김영한 강의를 듣고 정리한 내용입니다</p>
]]></description>
        </item>
    </channel>
</rss>