<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jaden_94.log</title>
        <link>https://velog.io/</link>
        <description>JaDeN</description>
        <lastBuildDate>Tue, 05 Mar 2024 01:32:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jaden_94.log</title>
            <url>https://images.velog.io/images/jaden_94/profile/fc5e3437-8885-4a76-94c9-285c107fc1e8/me.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jaden_94.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jaden_94" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[컴공 용어 모음집]]></title>
            <link>https://velog.io/@jaden_94/%EC%BB%B4%EA%B3%B5-%EC%9A%A9%EC%96%B4-%EB%AA%A8%EC%9D%8C%EC%A7%91</link>
            <guid>https://velog.io/@jaden_94/%EC%BB%B4%EA%B3%B5-%EC%9A%A9%EC%96%B4-%EB%AA%A8%EC%9D%8C%EC%A7%91</guid>
            <pubDate>Tue, 05 Mar 2024 01:32:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jaden_94/post/050989f5-eed6-492b-9bc8-77ac5151790e/image.png" alt=""></p>
<h1 id="동시성concurrency">동시성(Concurrency)</h1>
<p>여러 작업이 동시에 실행되는 것 처럼 보이는 것
EX) 하나의 CPU가 여러 작업을 번갈아 처리한다. </p>
<h1 id="병렬성paralleslism">병렬성(Paralleslism)</h1>
<p>하나 이상의 작업이 실제로 동시에 실행되는 것
EX) 이미지 처리 작업을 진행할 때 하나의 이미지를 여러 부분으로 분할하여 각 부분을 서로 다른 CPU가 처리한다. </p>
<h1 id="교착상태deadlock">교착상태(Deadlock)</h1>
<p>둘 이상의 프로세스가 다른 프로세스가 점유하고 있는 자원을 서로 기다리고 있는 상태</p>
<h1 id="데이터레이크">데이터레이크</h1>
<p>생성되는 데이터를 모두 저장해두는 것 (가공X)</p>
<h1 id="데이터웨어하우스">데이터웨어하우스</h1>
<p>생성된 데이터를 필터링/패키지화 해서 저장해두는 것</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 actuator]]></title>
            <link>https://velog.io/@jaden_94/%EC%8A%A4%ED%94%84%EB%A7%81-actuator</link>
            <guid>https://velog.io/@jaden_94/%EC%8A%A4%ED%94%84%EB%A7%81-actuator</guid>
            <pubDate>Wed, 30 Aug 2023 05:53:50 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-actuator-활성화">스프링 actuator 활성화</h2>
<pre><code>// yml 에 추가
// shutdown 엔드포인트 활성화 예시
management:
  endpoint: 
    shutdown:
      enabled: true </code></pre><h2 id="스프링-actuator-노출">스프링 actuator 노출</h2>
<pre><code>// yml 에 추가
// 모든 endpoints 들을 web (http) 에 노출 예시 
management:
  endpoints:
    web:
      exposure: 
        include: &quot;*&quot;</code></pre><h2 id="스프링-actuator-endpoints">스프링 actuator endpoints</h2>
<ol>
<li>beans : 스프링 컨테이너에 등록된 스프링 빈을 보여준다.</li>
<li>conditions : condition 을 통해서 빈을 등록할 때 평가 조건과 일치하거나 일치하지 않는 이유를 표시한다.</li>
<li>configprops : @ConfigurationProperties 를 보여준다.</li>
<li>env : Environment 정보를 보여준다.</li>
<li>health : 애플리케이션 헬스 정보를 보여준다.</li>
<li>httpexchanges : HTTP 호출 응답 정보를 보여준다. HttpExchangeRepository 를 구현한 빈을 7. 별도로등록해야 한다.</li>
<li>info : 애플리케이션 정보를 보여준다.</li>
<li>loggers : 애플리케이션 로거 설정을 보여주고 변경도 할 수 있다.</li>
<li>metrics : 애플리케이션의 메트릭 정보를 보여준다.</li>
<li>mappings : @RequestMapping 정보를 보여준다.</li>
<li>threaddump : 쓰레드 덤프를 실행해서 보여준다.</li>
<li>shutdown : 애플리케이션을 종료한다. (default = 비활성화) </li>
</ol>
<h3 id="health">health</h3>
<p>애플리케이션 헬스 정보 상세히 보기</p>
<pre><code>// yml 에 추가
management:
  endpoint:
    health:
      show-details: always</code></pre><h3 id="info">info</h3>
<p>os, java 정보 확인 </p>
<pre><code>// yml 에 추가
management:
  info:
    java:
      enabled: true
    os:
      enabled: true</code></pre><p>build 정보 확인 </p>
<pre><code>// build.gradle 에 추가
 springBoot {
      buildInfo()
}</code></pre><p>현재 build 된 git 정보 확인</p>
<pre><code>// build.gradle 에 plugin 추가
plugins { 
        id &quot;com.gorylenko.gradle-git-properties&quot; version &quot;2.4.1&quot;
    }</code></pre><h3 id="loggers">loggers</h3>
<p>실시간 로그레벨 변경 가능</p>
<pre><code>post 요청을 통해 실시간으로 로그레벨을 변경 가능하다.
url : http://localhost:8080/actuator/loggers/me.jaden.actuator.controller
body(json) :
{
    &quot;configuredLevel&quot; : &quot;TRACE&quot;
}</code></pre><h2 id="액츄에이터-엔드포인트-접근-포트번호-변경">액츄에이터 엔드포인트 접근 포트번호 변경</h2>
<pre><code>// yml에 추가
management:
  server:
    port: 9292</code></pre><h1 id="마이크로미터">마이크로미터</h1>
<h2 id="마이크로미터를-통해-매트릭-등록">마이크로미터를 통해 매트릭 등록</h2>
<h3 id="meterregistry">MeterRegistry</h3>
<p>마이크로미터 기능을 제공하는 컴포넌트
스프링을 통해 주입받아 카운터, 게이지등을 등록한다.</p>
<ul>
<li><p>비즈니스 로직에 직접 카운터 기능 추가</p>
<pre><code>  @Override
  public void order() {
      log.info(&quot;주문&quot;);
      stock.decrementAndGet();

      Counter.builder(&quot;my.order&quot;)
              .tag(&quot;class&quot;, this.getClass().getName())
              .tag(&quot;method&quot;, &quot;order&quot;)
              .description(&quot;order&quot;)
              .register(meterRegistry)
              .increment();

  }</code></pre></li>
<li><p>AOP 애노테이션을 활용한 카운터 기능 추가 </p>
</li>
</ul>
<ol>
<li><p>CountedAspect 빈 등록 </p>
<pre><code> @Bean
 public CountedAspect countedAspect(MeterRegistry meterRegistry) {
     return new CountedAspect(meterRegistry);
 }</code></pre></li>
<li><p>비즈니스 로직에 애노테이션 추가</p>
<pre><code> @Override
 @Counted(&quot;my.order&quot;)
 public void order() {
     log.info(&quot;주문&quot;);
     stock.decrementAndGet();
 }</code></pre></li>
</ol>
<h1 id="프로매태우스-설정">프로매태우스 설정</h1>
<p>설치 URL 
<a href="https://prometheus.io/download">https://prometheus.io/download</a></p>
<ol>
<li><p>application 에 의존성 추가 </p>
<pre><code>//  yml에 추가
implementation &#39;io.micrometer:micrometer-registry-prometheus&#39;</code></pre></li>
<li><p>promethus.yml 설정</p>
<pre><code> # 추가
 - job_name: &quot;spring-actuator&quot;
   metrics_path: &#39;/actuator/prometheus&#39; // 메트릭 경로 설정 
   scrape_interval: 1s // 수집주기 설정 (일반적으로 10 ~ 15s) 
   static_configs:
     - targets: [&#39;localhost:8080&#39;] // 타겟 서버경로</code></pre></li>
<li><p>counter 와 gauage 
값이 실시간으로 변화하는 gauage 는 그래프로 그대로 시각화 해도 의미파악이 
쉽다.
그에 반해 counter 는 값이 지속적으로 증가만 함으로 그래프로 시각화 할 때는 increase(), rate() 같은 함수와 같이 사용해주어야 의미파악이 쉽게된다.</p>
</li>
</ol>
<h1 id="그라파나">그라파나</h1>
<p>설치 URL
<a href="https://grafana.com/grafana/download">https://grafana.com/grafana/download</a></p>
<p>10.1.0 버전 설치
curl -O <a href="https://dl.grafana.com/enterprise/release/grafana-enterprise-10.1.0.darwin-amd64.tar.gz">https://dl.grafana.com/enterprise/release/grafana-enterprise-10.1.0.darwin-amd64.tar.gz</a></p>
<p>그라파나 공유대시보드 (스프링)
<a href="https://grafana.com/grafana/dashboards/11378-justai-system-monitor/">https://grafana.com/grafana/dashboards/11378-justai-system-monitor/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[10분MySQL - 스토어드 프로시저]]></title>
            <link>https://velog.io/@jaden_94/10%EB%B6%84MySQL-%EC%8A%A4%ED%86%A0%EC%96%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80</link>
            <guid>https://velog.io/@jaden_94/10%EB%B6%84MySQL-%EC%8A%A4%ED%86%A0%EC%96%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80</guid>
            <pubDate>Fri, 04 Aug 2023 05:22:24 GMT</pubDate>
            <description><![CDATA[<p>스토어드 프로시저는 MySQL 에서 프로그래밍이 필요할 때 사용하는 데이터베이스 개체이다. </p>
<h2 id="스토어드-프로시저-생성-방법">스토어드 프로시저 생성 방법</h2>
<pre><code>DELMITER $$ // 구분자 기호는 다른 기호여도 상관없다
CREATE PROCEDURE 스토어드_프로시저_이름()
BEGIN
 // SQL 코딩 
END $$
DELMITER ;</code></pre><h2 id="스토어드-프로시저-실행-방법">스토어드 프로시저 실행 방법</h2>
<pre><code>CALL 스토어드_프로시저_이름();</code></pre><h2 id="if-문">IF 문</h2>
<pre><code>IF &lt;조건식&gt; THEN
 SQL 문장
END IF; </code></pre><h2 id="if-else-문">IF ELSE 문</h2>
<pre><code>IF &lt;조건식&gt; THEN
 SQL 문장
ELSE 
 SQL 문장
END IF; 

ex) 
DELIMITER %% 
CREATE PROCEDURE ifProc()
BEGIN 
 DECLARE myNum INT: // declare 예약어를 통해 myNum 변수 선언
 SET myNum = 200;   // 변수에 값 대입
   IF myNum = 100 THEN 
    SELECT &#39;100입니다.&#39;;
   ESLSE 
    SELECT &#39;100이 아닙니다.&#39;;
   END IF 
END %%
DELIMITER;</code></pre><h2 id="쿼리-결과를-변수에-대입하기">쿼리 결과를 변수에 대입하기</h2>
<pre><code>DECLARE debutDate DATE; // 변수 선언

SELECT debute_date INTO debutDate  // SELECT INTO 를 사용해 대입한다. 
FROM market_db.member
WHERE mem_id = &#39;APN&#39;; </code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[10분MySQL - JOIN]]></title>
            <link>https://velog.io/@jaden_94/10%EB%B6%84MySQL-JOIN</link>
            <guid>https://velog.io/@jaden_94/10%EB%B6%84MySQL-JOIN</guid>
            <pubDate>Fri, 04 Aug 2023 04:48:51 GMT</pubDate>
            <description><![CDATA[<p>JOIN 이란 두 개의 테이블을 서로 묶어서 하나의 결과를 만들어 내는 것을 말한다. </p>
<h1 id="내부조인inner-join">내부조인(inner join)</h1>
<p>가장 일반적인 형태의 join 이다. 일반적으로 그냥 join 을 한다고 하면 inner join 을 한다고 생각하면 된다. </p>
<p>join 을 하기 위해서는 테이블이 일대다(1:n) 관계로 연결되어 있어야 한다. 
1:n 관계로 연결되어 있다는 것은 1번 테이블의 구분자를 2번 테이블이 FK로 여러개 가질 수 있다는 것을 의미한다.
예를들어, 회원과 구매테이블이 있을 때 1번 회원이 여러가지 상품을 구매할 수 있다면 회원과 구매는 1:n 관계를 가지고 있다고 할 수 있다.</p>
<p>다음과 같은 쿼리로 inner join 을 할 수 있다.</p>
<pre><code>SELECT &lt;열 목록&gt; 
FROM &lt;1번 테이블&gt;
 INNER JOIN &lt;2번 테이블&gt; // INNER JOIN 대신 JOIN 이라 써도 INNER JOIN 된다.
 ON &lt;조인될 조건&gt; 
WHERE &lt;검색 조건&gt; // 생략가능

ex)
SELECT * 
FROM buy
 INNER JOIN member
 ON buy.member_id = member.member_id
WHERE buy.member_id = &#39;jaden&#39;</code></pre><p>별칭을 사용해서 쿼리를 간단하게 만들 수 있다.</p>
<pre><code>ex)
SELECT * 
FROM buy b
 INNER JOIN member m
 ON b.member_id = m.member_id
WHERE b.member_id = &#39;jaden&#39;</code></pre><h1 id="외부조인outter-join">외부조인(outter join)</h1>
<p>내부조인은 두 테이블에 모두 데이터가 있어야만 결과가 나오게 된다.
만약 한쪽에만 데이터가 있는 경우도 쿼리가 되어야 한다면 외부조인을 사용할 수 있다. </p>
<p>쿼리는 다음과 같다</p>
<pre><code>SELECT &lt;열 목록&gt;
FROM &lt;1번 테이블&gt; &lt;-- LEFT 테이블
 &lt;LEFT || RIGHT || FULL&gt; OUTER JON &lt;2번 테이블&gt; &lt;-- RIGHT 테이블
 ON &lt;조인될 조건&gt;
WHERE &lt;검색조건&gt; // 생략가능

ex)
SELECT m.member_id, m.member_name, b.product_name
FROM member m 
 LEFT OUTER JOIN buy b // LEFT OUTER JOIN 은 LEFT JOIN 으로 축약가능하다 
 ON m.member_id = b.member_id
ORDER BY m.member_id;</code></pre><p>LEFT OUTER JOIN 의 의미는 LEFT 테이블의 내용은 모두 출력되어야 한다라는 의미이다. RIGHT OUTER JOIN 도 마찬가지로 RIGHT 테이블의 내용은 모두 출력된다.
FULL OUTER JOIN 은 왼쪽 및 오른쪽 테이블 어디든 한쪽에라도 데이터가 있다면 출력된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis 캐시로 사용하기]]></title>
            <link>https://velog.io/@jaden_94/Redis-%EC%BA%90%EC%8B%9C%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jaden_94/Redis-%EC%BA%90%EC%8B%9C%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 03 Aug 2023 02:26:55 GMT</pubDate>
            <description><![CDATA[<h3 id="환경세팅">환경세팅</h3>
<ol>
<li><p>docker redis 설치</p>
<pre><code>docker run --name redis -p 6379:6379 -d redis:alpine</code></pre></li>
<li><p>docker MySQL 설치</p>
<pre><code>docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=&lt;password&gt; -d -p 3306:3306 mysql:latest</code></pre><ol start="3">
<li><p>JDK 17, spring boot 3.1.2</p>
</li>
<li><p>dependecies</p>
<pre><code>dependencies {
// spring
implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;
implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
implementation &#39;org.springframework.boot:spring-boot-starter-cache&#39;

// db driver
implementation &#39;com.mysql:mysql-connector-j&#39;

// etc
compileOnly &#39;org.projectlombok:lombok&#39;
annotationProcessor &#39;org.projectlombok:lombok&#39;

// test
testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
}
</code></pre></li>
</ol>
</li>
</ol>
<h2 id="rediscacheconfiguration-빈-등록">RedisCacheConfiguration 빈 등록</h2>
<pre><code>import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisCacheConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(60)) // 캐시된 엔트리들의 만료 시간(TTL - Time to Live)을 설정
                .disableCachingNullValues() // null 값을 캐시로 저장하지 않도록 하는 옵션
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) // 캐시의 키를 직렬화(serialize)하는 방식을 지정
                )
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) // 캐시의 값(value)을 직렬화하는 방식을 지정
                );
    }
}</code></pre><h2 id="조회-시-캐싱-적용">조회 시 캐싱 적용</h2>
<pre><code>import lombok.RequiredArgsConstructor;
import me.jaden.redisstudy.member.api.request.MemberJoin;
import me.jaden.redisstudy.member.api.response.MemberView;
import me.jaden.redisstudy.member.domain.Member;
import me.jaden.redisstudy.member.repository.MemberRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    public Long joinMember(MemberJoin memberJoin) {
        Member joinedMember = memberRepository.save(memberJoin.convertToMember());
        return joinedMember.getId();
    }

     // 캐싱적용
    @Transactional(readOnly = true)
    @Cacheable(value = &quot;memberView&quot;, key = &quot;#memberId&quot;)
    public MemberView findById(Long memberId) {
        Member member = findMember(memberId);
        return MemberView.convertFromMember(member);
    }

    private Member findMember(Long memberId) {
        return memberRepository.findById(memberId)
                .orElseThrow(() -&gt; new EmptyResultDataAccessException(memberId + &quot;에 해당하는 회원이 존재하지 않습니다.&quot;, 1));
    }
}</code></pre><h2 id="테스트코드">테스트코드</h2>
<pre><code>import me.jaden.redisstudy.member.api.response.MemberView;
import me.jaden.redisstudy.member.domain.Member;
import me.jaden.redisstudy.member.repository.MemberRepository;
import me.jaden.redisstudy.member.service.MemberService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@SpringBootTest
public class CacheTest {
    @MockBean
    private MemberRepository memberRepository;

    @Autowired
    private MemberService memberService;

    @Autowired
    private CacheManager cacheManager;

    private static final Long MEMBER_ID = 1l;

    @AfterEach
    void removeCache() {
        Cache cache = cacheManager.getCache(&quot;memberView&quot;);
        if (cache != null) {
            cache.evict(MEMBER_ID);
        }
    }

    @Test
    void cacheTest() {
        Member member = Member.builder()
                .id(MEMBER_ID)
                .name(&quot;jaden&quot;)
                .age(29)
                .email(&quot;jaden@email.com&quot;)
                .build();

        given(memberRepository.findById(MEMBER_ID))
                .willReturn(Optional.of(member));

        MemberView cacheMiss = memberService.findById(MEMBER_ID);
        MemberView cacheHit = memberService.findById(MEMBER_ID);

        assertThat(cacheMiss).isEqualTo(MemberView.convertFromMember(member));
        assertThat(cacheHit).isEqualTo(MemberView.convertFromMember(member));

        verify(memberRepository, times(1)).findById(MEMBER_ID);
    }
}</code></pre><h4 id="깃헙"><a href="https://github.com/jadenkim94/redis-study">깃헙</a></h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[10분MySQL - 변수]]></title>
            <link>https://velog.io/@jaden_94/10%EB%B6%84MySQL-%EB%B3%80%EC%88%98</link>
            <guid>https://velog.io/@jaden_94/10%EB%B6%84MySQL-%EB%B3%80%EC%88%98</guid>
            <pubDate>Wed, 02 Aug 2023 01:01:49 GMT</pubDate>
            <description><![CDATA[<p>SQL 에서도 변수를 선언해 사용할 수 있다. </p>
<p>변수선언 및 값 대입은 다음과 같다</p>
<pre><code>SET @변수이름 = 변수 값;

// examples 

SET @myVar1 = 5;
SET @myVar2 = 4.25;

SELECT @myVar1; // 5
SELECT @myVar1 + @myVar2; // 9.250000..

SET @txt = &#39;가수 이름==&gt; &#39;;
SET @height = 166;

SELECT @txt, mem_name FROM member WHERE height &gt; @height;
/* 결과값 예시
가수 이름==&gt; 소녀시대
가수 이름==&gt; 트와이스
*/</code></pre><h3 id="주의사항">주의사항</h3>
<ol>
<li>LIMT 에서는 변수를 사용할 수 없다.<pre><code>SET @count = 3;
SELECT mem_name, height FROM member ORDER BY height LIMIT @count; // 문법 오류</code></pre></li>
<li>PREPARE, EXECUTE-USING 을 사용해 변수를 대입할 수 있다.<pre><code>SET @count = 3;
PREPARE mySQL FROM &#39;SELECT mem_name, height FROM member ORDER BY height LIMIT ?&#39;;
</code></pre></li>
</ol>
<p>EXECUTE mySQL USING @count;
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[10분MySQL - 데이터형식]]></title>
            <link>https://velog.io/@jaden_94/10%EB%B6%84MySQL-%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%98%95%EC%8B%9D</link>
            <guid>https://velog.io/@jaden_94/10%EB%B6%84MySQL-%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%98%95%EC%8B%9D</guid>
            <pubDate>Wed, 02 Aug 2023 00:52:29 GMT</pubDate>
            <description><![CDATA[<h1 id="정수형">정수형</h1>
<ol>
<li>TINYINT
1바이트</li>
</ol>
<p>-128 ~ 127
2. SMALLINT
2바이트
-32768 ~ 32767
3. INT 
4바이트
약 -21억 ~ +21억
4. BIGINT
8바이트
약 -900경 ~ +900경 </p>
<h3 id="unsigned-예약어">UNSIGNED 예약어</h3>
<p>음수를 제외시키는 예약어 </p>
<pre><code>CREATE TABLE member 
(
 height TINYINT UNSIGNED  // tinyint 는 -127 ~ 128 표현범위를 가지지만 UNSIGNED 예약어로 0~255를 표현한다. 
); </code></pre><h1 id="문자형">문자형</h1>
<p>1.CHAR
고정길이 문자형. CHAR(10) 에 3글자를 저장하면 7자리는 낭비하는 셈
최대 255자 
장점: 성능면에서 VARCHAR 보다 빠르다. 
2.VARCHAR
가변길이 문자형. VARCHAR(10) 에 3글자를 저장하면 3자리만 사용한다.
최대 16383자</p>
<h1 id="대량-데이터">대량 데이터</h1>
<p>1.TEXT
최대 65535자
2.LONGTEXT
최대 약 42억자 
3.BLOB
최대 65535 바이트
4.LONGBLOLB
최대 약 42억 바이트</p>
<h3 id="text와-blob">TEXT와 BLOB</h3>
<p>TEXT는 문자형 저장, BLOB은 이진 데이터 저장(사진, 동영상)</p>
<h1 id="실수형">실수형</h1>
<ol>
<li>FLOAT 
4바이트, 소수점 아래 7자리</li>
<li>DOUBLE
8바이트, 소수점 아래 15자리 </li>
</ol>
<h1 id="날짜형">날짜형</h1>
<ol>
<li>DATE
날짜만 저장, YYYY-MM-DD 형식 </li>
<li>TIME
시간만 저장, HH:MM:SS 형식</li>
<li>DATETIME
날짜 및 시간 저장, YYYY-MM-DD HH:MM:SS </li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[디자인패턴-팩토리메소드]]></title>
            <link>https://velog.io/@jaden_94/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-%ED%8C%A9%ED%86%A0%EB%A6%AC%EB%A9%94%EC%86%8C%EB%93%9C</link>
            <guid>https://velog.io/@jaden_94/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-%ED%8C%A9%ED%86%A0%EB%A6%AC%EB%A9%94%EC%86%8C%EB%93%9C</guid>
            <pubDate>Mon, 31 Jul 2023 14:39:40 GMT</pubDate>
            <description><![CDATA[<h1 id="팩토리메소드-패턴">팩토리메소드 패턴?</h1>
<p>구체적으로 어떤 인스턴스를 만들지는 서브 클래스가 정하도록 하는 생성패턴</p>
<h2 id="구조-ref-by리팩토링구루">구조 (ref by.<a href="https://refactoring.guru/design-patterns/factory-method">리팩토링구루</a>)</h2>
<p><img src="blob:https://velog.io/1959ec18-7e75-4d01-8e7a-c37a439ef75f" alt="업로드중.."></p>
<h2 id="장점">장점</h2>
<p>Creator 와 Product 간 결합도를 낮춰 OCP 원칙을 지키면서 확장할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[디자인패턴-싱글톤]]></title>
            <link>https://velog.io/@jaden_94/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-%EC%8B%B1%EA%B8%80%ED%86%A4</link>
            <guid>https://velog.io/@jaden_94/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-%EC%8B%B1%EA%B8%80%ED%86%A4</guid>
            <pubDate>Mon, 31 Jul 2023 09:52:41 GMT</pubDate>
            <description><![CDATA[<h1 id="싱글톤-패턴">싱글톤 패턴?</h1>
<p>시스템 내에서 사용 할 인스턴스를 하나만 제공할 수 있도록 하는 생성패턴</p>
<h3 id="왜-필요한가">왜 필요한가?</h3>
<p>시스템 런타임, 환경 세팅에 대한 정보등 인스턴스가 여러개가 있어서는 안되는 경우가 있다. </p>
<h2 id="구현방법">구현방법</h2>
<ol>
<li>생성자를 외부에서 사용할 수 없도록 해야한다.<pre><code>public class Single {
 private Single() {}; // 접근제어자를 통해 외부에서 생성자 접근 못하도록 한다.
}
</code></pre></li>
</ol>
<pre><code>2. 외부에서 인스턴스에 접근할 수 있는 메소드를 제공해야한다.</code></pre><p>public class Single {</p>
<pre><code>private Single() {};</code></pre><p>// 외부에서 인스턴스에 접근 가능한 메소드 제공
    public static Single getInstance() { 
        return new Single();
    }</p>
<p>}</p>
<pre><code>3. 외부에서 인스턴스에 접근할 때 동일한 인스턴스를 제공해야한다.</code></pre><p>public class Single {</p>
<pre><code>private static Single instance;

private Single() {};

public static Single getInstance() {
    if(instance == null) {
        instance = new Single();
    }
    return instance;
}</code></pre><p>}</p>
<pre><code>4. 멀티쓰레드 환경에서도 안전하게 싱글톤 인스턴스를 제공해야 한다. 
4.1 synchronized 를 활용한다.</code></pre><p>public class Single {</p>
<pre><code>private static Single instance;

private Single() {};

public static synchronized Single getInstance() {
    if(instance == null) {
        instance = new Single();
    }
    return instance;
}</code></pre><p>}</p>
<pre><code>4.2 클래스 로딩 시점에 인스터를 생성해 사용한다.</code></pre><p>public class Single {</p>
<pre><code>private static final Single INSTANCE = new Single();

private Single() {};

public static Single getInstance() {
    return INSTANCE;
}</code></pre><p>}</p>
<pre><code>4.3 double checked locking 을 통해 synchronized 비용을 최소화한다. 
- 4.1 에서는 인스턴스 접근시 항상 syncronized 되지만, double checked locking 을 활용하면 인스턴스가 한번 생성된 상태라면 synchronized 처리가 되지 않음으로 성능상 뛰어나다</code></pre><p>public class Single {</p>
<pre><code>private static volatile Single instance;

private Single() {};

public static Single getInstance() {
    if(instance == null) {
        synchronized (Settings.class) {
            if(instance == null) {
                instance = new Single();
            }
        }
    }
    return instance;
}</code></pre><p>}</p>
<pre><code>4.4 static inner 클래스 활용 </code></pre><p>public class Single {</p>
<pre><code>private Single() {};

private static class SingleHolder() {
    private static final Single INSTANCE = new Single();
}

public static Single getInstance() {
    return SingleHolder.INSTANCE;
}</code></pre><p>}</p>
<pre><code>
### 싱글톤 패턴 우회

1. Reflection 을 활용하여 싱글톤 패턴 우회</code></pre><pre><code>    Single single = Single.getInstance();

    Constructor&lt;Single&gt; declaredConstructor = Single.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    Single reflectedSingle = declaredConstructor.newInstance();

    System.out.println(single == reflectedSingle); // 결과 : false</code></pre><p>````
2. 인스턴스를 직렬화 후 역직렬화 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 사용 팁과 주의사항]]></title>
            <link>https://velog.io/@jaden_94/JPA-%EC%82%AC%EC%9A%A9-%ED%8C%81%EA%B3%BC-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD</link>
            <guid>https://velog.io/@jaden_94/JPA-%EC%82%AC%EC%9A%A9-%ED%8C%81%EA%B3%BC-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD</guid>
            <pubDate>Thu, 27 Jul 2023 10:29:03 GMT</pubDate>
            <description><![CDATA[<h1 id="최대한-단순한-구조로-설계할-것">최대한 단순한 구조로 설계할 것</h1>
<p>JPA 는 많은 기능을 지원한다. 객체간 서로 참조를 하는 부분도 양방향 연관관계를 잘 사용만 하면 큰 문제 없이 사용할 수 있다. 하지만 이런 방법을 잘 사용하는게 최선이라고는 할 수 없다. 객체간의 관계가 복잡해지면, 사용하는 입장에서 관계들을 이해하는게 어려울 수 밖에 없다. 따라서 가능하다면 연관관계를 줄이고, 필요한 경우 단방향 연관관계를 통해서 문제를 해결하도록 하자. </p>
<h2 id="연관관계-tip">연관관계 Tip</h2>
<h3 id="manytoone">@ManyToOne</h3>
<p>실무에서 가장 많이 사용하는 N:1 연관관계이다. 
테이블 입장에서 생각을 해봤을 때도 N 쪽에 FK 가 들어가는 만큼 비즈니스 코드와 그에 따른 SQL이 자연스럽게 이어질 가능성이 높다.</p>
<pre><code>@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
    @ManyToOne
    private Team team;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void joinTeam(Team team) {
        this.team = team;
    }
}

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    public Team(String name) {
        this.name = name;
    }
}

///// Member 의 ManyToOne 활용

    public static void main(String[] args) {
         EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Team teamA = new Team(&quot;teamA&quot;);
              em.persist(teamA);

            Member member = new Member(&quot;jaden&quot;, 29);
            em.persist(member);

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

            Member findMember = em.find(Member.class, member.getId());

            findMember.joinTeam(teamA);
            em.persist(findMember);
            tx.commit();

        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

// mfindMember.joinTeam(teamA) 쿼리 결과는 member 객체를 조작해 
// Member 테이블에 대한 update 쿼리가 발생하여 자연스럽다. 
update
    Member 
        set
            age=?,
            name=?,
            team_id=? 
        where
            id=?</code></pre><h3 id="onetomany">@OneToMany</h3>
<p>불가피하게 사용하게 된다면 @JoinColumn 을 사용한다. 그렇지 않으면 기본적으로 JoinTable 방식을 사용하게 되어 의도하지 않은 테이블을 생성하게 된다. 의도한 경우가 아니라면 @JoinColumn 을 사용한다. 
따라서 1:n 단방향 매핑을 사용해야하는 경우라면 n:1 양방향 매핑을 고려한다.(권장한다)</p>
<pre><code>@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @OneToMany
    @JoinColumn(name = &quot;member_id&quot;)
    private List&lt;Member&gt; members = new ArayList&lt;&gt;();

    public Team(String name) {
        this.name = name;
    }

    public void addMember(Member member) {
        members.add(member);
    }
}

///// team 에 OneToMany 활용
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        Member member = new Member(&quot;jaden&quot;, 29);
        em.persist(member);

        Team teamA = new Team(&quot;teamA&quot;);
        em.persist(teamA);

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

        Member findMember = em.find(Member.class, member.getId());
        Team findTeam = em.find(Team.class, teamA.getId());

        findTeam.addMember(findMember);

// findTeam.addMember(findMember) 쿼리 결과는 team 을 조작하지만
// Member 테이블에 대한 update 쿼리가 발생하여 부자연스럽다. 
// 더불어 업데이트할 member 를 조회해야 해 성능적으로도 불리하다.

Hibernate: 
    select
        members0_.member_id as member_i4_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.name as name3_0_1_ 
    from
        Member members0_ 
    where
        members0_.member_id=?
Hibernate: 
    /* create one-to-many row me.jaden.Team.members */ update
        Member 
    set
        member_id=? 
    where
        id=?</code></pre><h3 id="onetoone">@OneToOne</h3>
<p>1:1 연관관계는 FK 를 원하는 테이블에 둘 수 있다. 
예를들어 회원과 락커라는 엔티티가 있다면 회원 테이블에 락커ID 를 FK 로 둬도, 락커 테이블에 회원ID 를 FK로 두어도 된다. 
비즈니스 적으로 회원이 주도적이라고 생각한다면, FK를 회원에 두고 사용하는 것이 편할 것이다.
예를들어 회원이 락커를 등록할 수 있다고 생각해보자.</p>
<pre><code>// 회원 엔티티
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
    @OneToOne
    @JoinColumn(name = &quot;locker_id&quot;)
    private Locker locker;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void registerPersonalLocker(Locker locker) {
        this.locker = locker;
    }
}

// 락커 엔티티
@Entity
@NoArgsConstructor
@Getter
public class Locker {

    @Id
    @GeneratedValue
    private Long id;
    private int number;

    public Locker(int number) {
        this.number = number;
    }
}

// 회원이 락커 등록
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Locker firstLocker = new Locker(1);
            em.persist(firstLocker);

            Member member = new Member(&quot;jaden&quot;, 29);
            em.persist(member);

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

            Locker getLocker = em.find(Locker.class, firstLocker.getId());
            Member getMember = em.find(Member.class, member.getId());

            # 회원이 락커를 등록한다.
            getMember.registerPersonalLocker(getLocker);

            tx.commit();
       }... 

// getMember.registerPersonalLocker(getLocker); 에 대한 SQL
        update
            Member 
        set
            age=?,
            locker_id=?,
            name=? 
        where
            id=?
</code></pre><p>의도한 대로 동작하고 있는 것을 확인할 수 있었다. 하지만, DB관점에서 생각해도록 하자. 
어떠한 회원은 락커를 등록하지 않을 수 도있다. 그러면 회원 테이블의 FK 에는 null 이 들어갈 것이다.
혹은, 한명의 회원이 여러개의 락커를 사용할 수 있게 비즈니스 로직을 수정해야 한다면 테이블이 수정되어야 한다. 그렇다면 FK를 락커로 옮기고 단방향으로 Member를 통해 락커를 등록할 수 있을까? 아쉽지만 JPA 는 해당 부분을 지원하기 어렵다. 대신 양방향 관계를 맺어 사용할 수 는 있다.</p>
<pre><code>@Entity
@NoArgsConstructor
@Getter
public class Locker {

    @Id
    @GeneratedValue
    private Long id;
    private int number;
    @OneToOne
    @JoinColumn(name = &quot;member_id&quot;)
    private Member owner;

    public Locker(int number) {
        this.number = number;
    }

    public void setOwner(Member owner) {
        this.owner = owner;
    }
}

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
    @OneToOne(mappedBy = &quot;owner&quot;)
    private Locker locker;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void registerPersonalLocker(Locker locker) {
        this.locker = locker;
        locker.setOwner(this);
    }
}

// FK 는 락커테이블에 있지만 양방향관계를 통해 Member 에서 락커등록
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
    try {
            Locker firstLocker = new Locker(1);
            em.persist(firstLocker);

            Member member = new Member(&quot;jaden&quot;, 29);
            em.persist(member);

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


            Locker getLocker = em.find(Locker.class, firstLocker.getId());
            Member getMember = em.find(Member.class, member.getId());

            System.out.println(&quot;====&quot;);
            getMember.registerPersonalLocker(getLocker);

             tx.commit();
       }... 

#  getMember.registerPersonalLocker(getLocker); 에 대한 SQL 쿼리
         update
            Locker 
        set
            number=?,
            member_id=? 
        where
            id=?
</code></pre><p>다만, 이 경우 지연로딩(LazyLoading)을 사용할 수 없게된다. </p>
<pre><code>@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
    @OneToOne(mappedBy = &quot;owner&quot;, fetch = FetchType.LAZY) // lazyLoading 설정
    private Locker locker;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void registerPersonalLocker(Locker locker) {
        this.locker = locker;
        locker.setOwner(this);
    }
}

///// 사용코드

        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Locker firstLocker = new Locker(1);
            em.persist(firstLocker);

            Member member = new Member(&quot;jaden&quot;, 29);
            em.persist(member);

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


            System.out.println(&quot;====&quot;);
            Member getMember = em.find(Member.class, member.getId()); // member 를 조회할 때 locker 를 lazyLoading 해오는지 확인해보자
            tx.commit();

# 쿼리 확인 
====
Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.team_id as team_id4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
Hibernate: 
    /* load me.jaden.Locker */ select
        locker0_.id as id1_0_1_,
        locker0_.number as number2_0_1_,
        locker0_.member_id as member_i3_0_1_,
        member1_.id as id1_1_0_,
        member1_.age as age2_1_0_,
        member1_.name as name3_1_0_,
        member1_.team_id as team_id4_1_0_ 
    from
        Locker locker0_ 
    left outer join
        Member member1_ 
            on locker0_.member_id=member1_.id 
    where
        locker0_.member_id=?
</code></pre><p>쿼리 결과를 보면 알 수 있듯이 lazyLoading 을 설정해도  eagerLoading 을 해버린다. 
이유는 JPA 가 프록시 객체를 생성할 때 Member 의 Locker 가 null 인지 아닌지 확인하기 FK가 Locker 에 있기 때문에 Locker 를 어쩔 수 없이 조회해버리기 때문이다. </p>
<h3 id="manytomany">@ManyToMany</h3>
<p>@ManyToMany 는 실무에서 잘 사용하지 않는다. (연결테이블을 JPA가 관리함으로 연결테이블의 컬럼들을 컨트롤 할 수 없기 떄문) 
관계형 DB 에서는 다대다 관계를 테이블 2개로는 풀어낼 수 없다. 연결테이블을 만들어 일대다+다대일 관계로 풀어내야 한다. 
그래서 @ManyToMany 를 사용하면 JPA 는 매핑정보를 보고 연결테이블을 생성해 사용하게되는데, 단순 열결이 목적이라면 불필요하면 엔티티 클래스를 생성하지 않아도 된다는 장점이 있을 수 있지만, 실무에서 단순 연결만하는 경우가 극히 드물기 떄문에 일반적으로 @ManyToMany 를 사용하기 보다는 연결테이블을 엔티티로 만들어 필요한 기능들을 집어넣어 사용하게 된다.</p>
<h2 id="jpa-와-프록시">JPA 와 프록시</h2>
<p>LazyLoading 과 같은 기법을 사용하기 위해 JPA는 프록시 객체를 활용한다.
JPA 가 프록시를 사용하기에 주의해야할 부분이 몇 가지 있는데, 비즈니스 로직에서 객체의 타입을 체크해야할 때, 해당 타입이 원본 객체일지 프록시 객체일지 명확한 구분이 힘들기에 == 대신 instance of 를 통해 타입을 비교한다. </p>
<h4 id="왜-구분이-어려운가">왜 구분이 어려운가?</h4>
<p>영속성컨택스트에 프록시객체로 저장되면 프록시객체가 반환되고, 실제객체가 저장되면 실제객체가 반환되기 때문이다. 
만약 프록시 객체가 들어간 상태에서  xxxx.getClass() == xxx.class 와 같이 타입을 비교하면 false 가 나오게 되어 원하지 않는 결과를 얻게된다. </p>
<h2 id="jpql-주의사항">JPQL 주의사항</h2>
<p>JPQL 을 통해 연관관계를 가진 경로를 탐색할 시 묵시적 내부 조인이 발생한다. (join 을 명시적으로 사용하지 않아도 join이 된다) 
팀 전체가 JPA와 JPQL에 익숙하다면 괜찮을지 모르겠지만, 그렇지 않다면 묵시적 내부조인이 일어나는 경우에도 명시적으로 join 을 사용하도록 하자.</p>
<h2 id="fetch-join-주의사항">Fetch join 주의사항</h2>
<ol>
<li>페치조인 대상에는 별칭을 줄 수 없다 
하이버네이트를 사용하면 가능하지만 가급적 사용하지 않는다. (데이터 정합성 문제 발생 가능성 있음), 다만 fetch join 대상에서 fetch join 이 일어날 경우는 사용할 수 도 있다.</li>
<li>둘 이상의 컬렉션은 페치조인 할 수 없다. </li>
<li>컬렉션 페치조인 시 페이징을 사용할 수 없다. (데이터 뻥튀기 문제)
일대일, 다대일은 가능하다. (뻥튀기 안일어나니)
하이버네이트를 사용하면 메모리에서 페이징 처리를 애플리케이션 레벨에서 진행하는데 당연히 OOM 위험이 생기게 된다.</li>
</ol>
<p>컬렉션 페치조인 페이징을 해결하는 방법?</p>
<ol>
<li>일대다 관계인 경우 반대방향(다대일)에서 쿼리를 날려 페이징을 한다.</li>
<li>페치조인 없이 조회 한 후 @BatchSize 활용해 lazyLoading 을 한다. (비교적 n+1 문제에서 자유로움)
batch fetch size 는 글로벌 설정도 가능하다. </li>
</ol>
<h2 id="변경감지와-merge">변경감지와 merge()</h2>
<p>엔티티를 수정해야하는 경우 JPA 의 변경감지 기능을 사용한다.
merge() 를 사용할 경우 엔티티 전체를 변경하는 형태이기에, 변경하지 않은 필드에 null 이나 0 과같은 값이 세팅될 수 있기 때문이다. </p>
<h2 id="fetchtype-eager">fetchType EAGER</h2>
<p>n+1 문제를 해결하기 위해 fetchType 을 eager 로 설정한다고 해도 JPQL 이 실행되는 경우에는 n+1 문제가 해결되지 않는다. JPQL 은 그대로 SQL 로 번역되어 DB에 결과를 가져오고 JPA가 eager 패치타입을 확인 후 아직 채워지지 않은 데이터들을 채우기 위해 n+1 select 쿼리가 발생한다. 
entityManager.find() 만을 사용하는 경우라면 원하는대로 join 쿼리를 JPA 가 생성해 보내지만, JPQL 을 사용하는 순간 원하지 않는 N+1 문제가 발생할 수 있음으로 fetchtype 을 EAGER 로 설정하지 않도록 하자</p>
<h2 id="일대다관계-fetch-join">일대다관계 fetch join</h2>
<p>일대다관계(컬렉션)을 조인하여 가져올 때 조회의 주체를 기준으로 데이터가 부풀려진다. (n 관계의 데이터를 조회하니 당연한 일이다.) 이를 해결하기 위해서는 distinct 를 사용해 중복을 제거해야할 필요가 있다. 다만 hibernate6 기준으로는 일대다관계를 fetch join 하는 경우 distinct 를 선언하지 않아도 적용된 데이터를 가져온다.</p>
<ul>
<li>distinct 는 DB 의 distinct 와는 다르다. SQL 의 distinct 는 완전한 중복일 때 중복을 제거하고, fetch join 시 JPA 가 하는 distinct 는 조회 주체의 ID를 기준으로 컬렉션을 만들어주는 개념이라고 보는게 좋다.</li>
<li>컬렉션 fetch join 시 페이징 처리를 해서는 안된다. (하이버네이트는 페이징할 모든 데이터를 메모리에 전부 가져와 어플리케이션 레벨에서 페이징을 시도한다. -&gt; OOM 위험) </li>
<li>컬렉션 패치조인은 1개만 사용가능하다. 
1:n:n -&gt; 불가, 
(1:n) * 2  -&gt; 불가</li>
</ul>
<p>일대다관계(컬렉션)을 가져오면서 페이징이 필요한 경우에는, xxxToOne 관계는 모두 fetchJoin 을 통해 가져온 후, 컬렉션은 지연로딩을 사용하여 가져온다. 다만 이 때 batchSize 를 설정함으로써 성능최적화를 이끌어낸다. </p>
<pre><code>-- yml 을 통한 global 배치사이즈 설정
spring:
 jpa: 
  properties:
   default_batch_fetch_size: 100 # 배치사이즈 100

-- @BatchSize 애노테이션 설정
    @OneToMany(mappedBy = &quot;order&quot;, cascade = CascadeType.ALL)
    @BatchSize(size = 100)
    private List&lt;OrderItem&gt; orderItems = new ArrayList&lt;&gt;();
</code></pre><h2 id="osiv">OSIV</h2>
<p>spring.jpa.open-in-view: true (기본값)
기본값에 따라 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 
-&gt; API controller 나 viewTemplate 에서 지연로딩이 가능해짐
-&gt; 커넥션을 너무 오래물고 있어 커넥션이 부족해지는 상황을 초래한다.</p>
<p>OSIV 설정을 끄면 트랜잭션이 종료될 때 반환한다.</p>
<h2 id="spring-data-jpa-page-활용-dto반환">Spring Data JPA page 활용 (DTO반환)</h2>
<p>page.map 을 활용하면 쉽게 변환 가능 </p>
<pre><code>    public static Page&lt;MemberDto&gt; pageable(Page&lt;Member&gt; members) {
        return members.map(member -&gt; new MemberDto(member.getUsername(), member.getAge()));
    }</code></pre><h2 id="spring-데이터-jpa-사용과-generatedvalue">Spring 데이터 JPA 사용과 @GeneratedValue</h2>
<p>Spring Data Jpa 를 사용하면, JpaRepository 인터페이스의 구현체 SimpleJpaRepository 를 사용하는 경우가 많다. 
이 때, save() 를 확인해보면 새로운 객체인 경우 persist() 를 호출하고 기존에 있는 객체라면 merge() 를 호출하는 다음의 코드를 확인 할 수 있다.</p>
<pre><code>    @Transactional
    @Override
    public &lt;S extends T&gt; S save(S entity) {

        Assert.notNull(entity, &quot;Entity must not be null&quot;);

        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }</code></pre><p>새로운 객체의 판단여부는 id 값이 null인지(래퍼런스타입) 혹은 0인지(프리미티브타입)를 통해 판단하게 되는데, @GeneratedValue 를 사용한다면 persist 이후에 id 가 세팅되지만, 만약 직접 id 를 생성해주는 경우라면 null 이 아니기에 merge 가 호출되고 이는 성능악화(select 이후 save) 를 야기한다.
따라서 직접 id 를 세팅해주는 경우라면 Pesistable 을 엔티티에서 구현하여 isNew() 메소드를 오버라이딩해야한다.
가장 간단한 방법은 createdDate 를 사용하여 해당 필드가 null 이라면 새로운객체라고 판단하는 방법이 있다.</p>
<pre><code>@Entity
public class Item implements Persistable&lt;String&gt; {
    @Id
    private String id;

    @CreatedDate
    private LocalDateTime createdDate;

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public boolean isNew() {
        return createdDate == null;
    }
}
```</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 배치 ]]></title>
            <link>https://velog.io/@jaden_94/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98</link>
            <guid>https://velog.io/@jaden_94/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98</guid>
            <pubDate>Thu, 20 Jul 2023 07:40:54 GMT</pubDate>
            <description><![CDATA[<h1 id="스프링-배치">스프링 배치</h1>
<h1 id="스프링-배치-메타-데이터">스프링 배치 메타 데이터</h1>
<p>스프링 배치의 실행 및 관리 목적의 도메인(Job, Step..) 정보들을 저장, 업데이트, 조회할 수 있는 스키마를 제공
<strong>DB 와 연동할 경우 필수적으로 메타 테이블이 생성</strong>되어있어야 한다. </p>
<p>스키마 파일 위치 : <code>/org/springframework/batch/core/schema-*.sql</code></p>
<ul>
<li>스키마 생성 방법 및 설정 </li>
</ul>
<ol>
<li>수동 생성 - 스키마 파일에서 쿼리 복사 후 직접 실행 </li>
<li>자동 생성 <code>spring.batch.jdbc.initialize-schema</code> 설정
 (always : 스크립트 항상 실행, embedded: 내장 DB 일때만 실행, never: 스크립트 항상 실행 안함) <ul>
<li>운영환경에서는 수동생성할 것</li>
</ul>
</li>
</ol>
<ul>
<li>스프링 배치 스키마<ol>
<li>BATCH_JOB_INSTANCE
job 이 실행될 때 jobInstance 정보가 저장 
job_name 과 job_key 를 합쳐 unique </li>
<li>BATCH_JOB_EXECUTION
job 의 실행정보 저장 
job 생성, 시작, 종료시간, 실행상태 메시지 등을 관리 </li>
<li>BATCH_JOB_EXECUTION_PARAMS
job과 함께 실행되는 jobParameter 정보 저장 </li>
<li>BATCH_JOB_EXECUTION_CONTEXT </li>
</ol>
  <strong>****</strong>job 의 실행동안 여러가지 상태정보, 공유 데이터를 직렬화(json) 해서 저장 
  step 간 서로 공유 가능 <ol start="5">
<li>BATCH_STEP_EXECUTION
step 의 실행정보 저장
step 생성, 시작, 종료시간, 실행상태 메시지 등을 관리 </li>
<li>BATCH_STEP_EXECUTION_CONTEXT
step 의 실행동안 여러가지 상태정보, 공유 데이터를 직렬화 해서 저장.
step 별로 저장되며 step 간 서로 공유할 수 없음</li>
</ol>
</li>
</ul>
<h1 id="스프링-배치-도메인">스프링 배치 도메인</h1>
<ul>
<li><p>JobInstance</p>
<p>  Job 이 실행될 때 생성되는 Job 의 논리적 실행 단위 객체로 고유하게 식별 가능한 작업 실행을 나타낸다.</p>
<p>  Job 의 설정과 구성은 동일하지만 Job 이 실행되는 시점에 처리하는 내용이 다르기 때문에 Job 의 실행을 구분한다.  (하루에 3번씩 Job 이 실행되면 3개의 JobInstance 가 생성)</p>
</li>
<li><p>JobParameter</p>
<p>  Job 을 실행할 때 함께 포함되어 사용되는 파라미터를 가진 도메인 객체</p>
<p>  하나의 Job 에 존재할 수 있는 여러개의 JobInstance 를 구분하기 위한 용도    </p>
<p>  생성 및 바인딩 방법 </p>
<ol>
<li><p>어플리케이션 실행 시 주입 
<code>java -jar LogBatch.jar requestDate=20210101</code></p>
</li>
<li><p>코드로 생성 
<code>JobParameterBuilder</code>, <code>DefaultJobParameterConverter</code></p>
</li>
<li><p>SpEL 이용 
<code>@Value(”#{jobParameter[requestDate]}”)</code>, <code>@JobScope</code>, <code>@StepScope</code> 선언 필수  </p>
<p>JobParameter 를 코드에서 참조방법 </p>
<pre><code class="language-java">@Bean
 public Step step1() {
     return stepBuilderFactory.get(&quot;step1&quot;)
             .tasklet((contribution, chunkContext) -&gt; {
                                     // contribution 에서 참조
                 JobParameters jobParameters = contribution.getStepExecution().getJobParameters();
                                     // chunkContext 에서 참조해서 파라미터 값 확인
                 Map&lt;String, Object&gt; jobParameters1 = chunkContext.getStepContext().getJobParameters();

                 return RepeatStatus.FINISHED;
             })
             .build();
 }</code></pre>
</li>
</ol>
</li>
<li><p>JobExecution</p>
<p>  <code>JobInstance</code> 에 대한 한번의 시도를 의미하는 객체
  <code>Job</code> 실행 중에 발생한 정보들을 저장하고 있는 객체</p>
<p>  <code>JobExecution</code> 은 <code>FAILED</code> 또는 <code>COMPLETED</code> 등의 <code>Job</code> 실행 결과 상태를 가지고 있음
  상태가 <code>COMPLETED</code> 면 재실행이 불가함
  상태가 <code>FAILED</code> 면 재실행이 가능함</p>
<p>  <code>JobExecution</code> 의 실행 결과 상태가 <code>COMPLETED</code> 가 될 때 까지 <code>JobInstance</code> 내에서 여러 번의 시도가 생길 수 있다.</p>
</li>
<li><p>Step</p>
<p>  Batch Job 을 구성하는 독립적인 하나의 단계로 실제 배치 처리를 정의하고 컨트롤하는데 필요한 정보를 가진 객체 </p>
<p>  배치작업을 어떻게 구성하고 실행할 것인지 Job 세부 작업을 Task 기반으로 설정하고 명세해 놓은 객체</p>
<ul>
<li>TaskletStep : 가장 기본이 되는 클래스로서 Tasklet 타입의 구현체들을 제어한다.</li>
<li>PartionStep: 멀티 쓰레드 방식으로 Step 을 여러 개로 분리해서 실행한다.</li>
<li>JobStep: Step 내에서 Job 을 실행하도록 한다.</li>
<li>FlowStep: Step 내에서 Flow 를 실행하도록 한다.</li>
</ul>
</li>
<li><p>StepExecution</p>
<p>  Step 에 대한 한번의 시도를 의미하는 객체
  Step 실행 중에 발생한 정보들을 저장하고 있다.
  Step 이 매번 시도될 때 마다 생성되며 각 Step 별로 생성된다.
  Job 이 재시작 하더라도 이미 성공적으로 완료된 Step 은 재 실행되지 않고 실패한 Step 만 실행된다.</p>
</li>
<li><p>StepContribution</p>
<p>  청크 프로세스 변경 사항을 버퍼링 한 후 StepExecution 상태를 업데이트하는 도메인 객체 
  청크 커밋 직전에 StepExecution 의 apply() 메소드를 호출하여 상태를 업데이트
  ExitStatus 의 기본 종료코드 외 사용자 정의 종료코드를 생성하여 적용할 수 있다.</p>
</li>
<li><p>ExecutionContext</p>
<p>  프레임워크에서 유지 및 관리하는 키/값으로 된 컬렉션으로 StepExecution 또는 JobExecution 객체의 상태를 저장하는 공유 객체</p>
<p>  DB 에 직렬화 한 값으로 저장됨 </p>
<p>  공유 범위 </p>
<ul>
<li><p>Step 범위 : 각 Step 의 StepExecution 에 저장되며 Step 간 서로 공유 안됨</p>
</li>
<li><p>Job 범위 : 각 Job 의 JobExecution 에 저장되며 Job 간 서로 공유 안되며 해당 Job 의 Step 간 서로 공유됨</p>
<p>실패한 Job 재 시작 시 이미 처리한 Row 데이터는 건너뛰고 이후부터 수행할 수 있도록 할 때 ExecutionContext 를 활용할 수 있다.</p>
</li>
</ul>
</li>
<li><p>JobRepository</p>
<p>  배치 작업 중의 정보를 저장하는 저장소 역할 
  배치 작업의 수행과 관련된 모든 meta data 를 저장</p>
<p>  <code>@EnableBatchProcessing</code> 애노테이션 선언시 JobRepository 가 자동으로 빈으로 등록</p>
<p>  <code>BatchConfigurer</code> 인터페이스를 구현하거나 <code>BasicBatchConfgiurer</code> 를 상속하여 JobRepository 설정 커스터마이징 가능</p>
</li>
<li><p>JobLauncher</p>
<p>  배치 Job 을 실행시키는 역할을 한다. 
  Job 과 JobParameters 를 인자로 받아 요청된 배치 작업을 수행한 후 최종 client 에게 JobExecution 을 반환 </p>
<p>  스프링 부트 배치가 구동되면 <code>JobLauncher</code> 빈이 자동 생성된다. </p>
<p>  Job실행 </p>
<ul>
<li><code>JobLauncher.run(Job, Jobparameters)</code></li>
<li>스프링 부트 배치에서는 JobLauncherApplicationRunner 가 자동으로 JobLauncher 실행</li>
<li>동기적 실행<ul>
<li>taskExecutor 를 SyncTaskExecutor 로 설정 (기본값)</li>
<li>JobExecution 을 획득하고 배치 처리를 최종 완료한 이후 Client 에게 JobExecution 반환</li>
<li>스케쥴러에 의한 배치처리에 적합</li>
</ul>
</li>
<li>비동기적 실행<ul>
<li>taskExecutor 를 SimpleAsyncTaskExecutor 로 설정</li>
<li>JobExecution 을 획득한 후 Client 에게 바로 JobExecution 을 반환하고 배치처리를 완료한다.</li>
<li>HTTP 요청에 의한 배치처리에 적합</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="스프링-부트-배치-설정">스프링 부트 배치 설정</h1>
<ol>
<li><code>JobLauncherApplicationRunner</code></li>
</ol>
<ul>
<li><p>BatchAutoConfiguration 에서 생성</p>
</li>
<li><p>애플리케이션 정상적으로 구동될 때 마다 실행됨 </p>
</li>
<li><p>기본적으로 빈으로 등록된 모든 job 을 실행시킴</p>
<pre><code class="language-yaml">  spring:
      batch:
        job:
          enabled: true 
  # (기본값 true 사용하고 싶지 않은경우 false 설정할 것) </code></pre>
</li>
</ul>
<ol start="2">
<li>BatchProperties</li>
</ol>
<ul>
<li>Spring Batch 환경 설정 클래스 </li>
<li>Job 이름, 스키마 초기화 설정, 테이블 prefix 등의 값 설정 가능</li>
<li>application.yml 파일에서 설정</li>
</ul>
<ol start="3">
<li>Job 실행 옵션 </li>
</ol>
<ul>
<li><p>지정한 Batch Job 만 실행하도록 할 수 있다. </p>
<pre><code class="language-yaml">  spring:
      batch:
          job:
              names: ${job.name:NONE}</code></pre>
<ul>
<li>애플리케이션 실행 시 programArguments 로 job 이름 입력
<code>--job.name=helloJob,simpleJob</code></li>
</ul>
</li>
</ul>
<h1 id="스프링-배치가-제공하는-빌더-클래스">스프링 배치가 제공하는 빌더 클래스</h1>
<ul>
<li><p>JobBuilderFactory</p>
<p>  JobBuilder 를 생성하는 팩토릴 클래스로 get(String name) 메소드 제공</p>
<pre><code class="language-java">  jobBuilderFactory.get(&quot;jobName&quot;) // jobName 은 스프링 배치가 job 을 실행할 때 참조하는 job 이름</code></pre>
</li>
<li><p>JobBuilder</p>
<p>  Job 을 구성하는 설정 조건에 따라 하위 빌더 클래스를 생성하고 Job 생성을 위임한다. </p>
<ol>
<li>SimpleJobBuilder </li>
<li>FlowJobBuilder </li>
</ol>
</li>
<li><p>StepBuilderFactory</p>
<p>  StepBuilder 를 생성하는 팩토리 클래스 
  get(String name) 메서드 </p>
</li>
<li><p>StepBuilder</p>
<p>  Step 을 구성하는 설정조건에 따라 5개의 하위 빌더 클래스에 Step 생성을 위임한다. </p>
<ol>
<li>TaskletStepBuilder</li>
<li>SimpleStepBuilder</li>
<li>PartionStepBuilder</li>
<li>JobStepBuilder</li>
<li>FlowStepBuilder</li>
</ol>
</li>
</ul>
<h1 id="simplejob">SimpleJob</h1>
<ul>
<li>simpleJob 은 Step 을 실행시키는 Job 구현체로 SimpleJobBuilder 에 의해 생성=</li>
<li>여러 단계의 Step 으로 구성할 수 있으며 Step 을 순차적으로 실행시킨다.<ul>
<li><strong>실패한 Step 이 생기면 그 이후 Step 은 실행하지 않는다.</strong></li>
</ul>
</li>
<li>모든 Step 의 실행이 성공적으로 완료되어야 Job 이 성공적으로 완료된다.</li>
<li>맨 마지막에 실행된 Step 의 BatchStatus 가 Job 의 최종 BatchStatus</li>
</ul>
<h1 id="taskletstep">TaskletStep</h1>
<ul>
<li>스프링 배치에서 제공하는 Step 구현체, Tasklet 을 실행시키는 도메인 객체</li>
<li>RepeatTemplate 를 사용해서 Tasklet 의 구문을 트랜잭션 경계 내에서 반복 실행</li>
<li>Task 기반과 Chunk 기반으로 나눠 Tasklet 실행</li>
</ul>
<h2 id="task--chunk-비교">Task | Chunk 비교</h2>
<ul>
<li>chunk 기반<ul>
<li>하나의 큰 덩어리를 n 개씩 나눠(chunk 단위) 실행한다는 의미로 대량 처리를 위해 설계됨</li>
<li>ItemReader, ItemProcessor, ItemWriter 를 사용하며 청크 기반 전용 Tasklet 인 <code>ChunkOrientedTasklet</code> 구현체 제공</li>
</ul>
</li>
<li>Task 기반<ul>
<li>청크 기반 작업 보다 단일 작업 기반으로 처리되는 것이 더 효율적인 경우</li>
<li>주로 Tasklet 구현체를 만들어 사용</li>
<li>대량 처리를 하는 경우 chunk 기반에 비해 더 복잡한 구현 필요</li>
</ul>
</li>
</ul>
<h1 id="jobstep">JobStep</h1>
<ul>
<li>Job 에 속하는 Step 중 외부의 Job 을 포함하고 있는 Step</li>
<li>외부의 Job 이 실패하면 해당 Step 이 실패하므로 결국 최종 기본 Job 도 실패</li>
<li>모든 메타데이터는 기본 Job 과 외부 Job 별로 각각 저장</li>
<li>커다른 시스템을 작은 모듈로 쪼개고 job 의 흐름을 관리하고자 할 때 사용</li>
</ul>
<h1 id="flowjob">FlowJob</h1>
<ul>
<li>Step 을 순차적으로만 구성하는 것이 아닌 특정한 상태에 따라 흐름을 전환하도록 구성할 수 있다.<ul>
<li>Step 이 실패 하더라도 Job 이 실패로 끝나지 않도록 해야 하는 경우</li>
<li>Step 이 성공 했을 때 다음에 실행해야 할 Step 을 구분해서 실행해야 하는 경우</li>
<li>특정 Step 은 전혀 실행되지 않게 구성 해야 하는 경우</li>
</ul>
</li>
<li>Flow 와 Job 의 흐름을 구성하는데만 관여하고 실제 비즈니스 로직은 Step 에서 이루어진다.</li>
<li>내부적으로 SimpleFlow 객체를 포함하고 있으며 Job 실행 시 호출한다.</li>
</ul>
<h2 id="트랜지션에-활용하는-배치-상태-유형">트랜지션에 활용하는 배치 상태 유형</h2>
<ul>
<li>BatchStatus<ul>
<li>JobExecution 과 StepExecution 의 속성으로 Job / Step 종료 후 최종 결과 상태 정의</li>
<li>SimpleJob ⇒ 마지막 Step 의 BatchStatus 값을 Job 의 최종 BatchStatus 값으로 반영</li>
<li>FlowJob ⇒ Flow 내 Step 의 ExitStatus 값을 FlowExecutionStatus 값으로 저장</li>
<li>COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED, UNKNOWN</li>
</ul>
</li>
<li>ExitStatus<ul>
<li>JobExecution 과 StepExecution 의 속성으로 Job / Step 실행 후 어떤 상태로 종료됐는지 정의</li>
<li>기본적으로 ExitStatus 는 BatchStatus 와 동일한 값으로 설정된다.</li>
<li>SimpleJob ⇒ 마지막 Step 의 ExitStatus 값을 Job 의 최종 ExitStatus 값으로 반영</li>
<li>FlowJob ⇒ Flow 내 Step 의 ExitStatus 값을 FlowExecution 값으로 저장</li>
<li>UNKNOWN, EXECUTING, COMPLETED, NOOP, FAILED, STOPPED</li>
</ul>
</li>
<li>FlowExecutionStatus<ul>
<li>FlowExecution 의 속성으로 Flow 의 실행 후 최종 결과 상태가 무엇인지 정의</li>
<li>Flow 내 Step 이 실행되고 나서 ExitStatus 값을 FlowExecutionStatus 값으로 저장</li>
<li>FlowJob 의 배치 결과 상태에 관여</li>
<li>COMPLETED, STOPPED, FAILED, UNKNOWN</li>
</ul>
</li>
</ul>
<h2 id="사용자-정의-exitstatus">사용자 정의 ExitStatus</h2>
<ul>
<li><p>StepExecutionListener 의 afterStep() 메서드에서 Custom exitCode 생성 후 새로운 ExitStatus 반환</p>
</li>
<li><p>Step 실행 후 완료 시점에서 현재 exitCode 를 사용자 정의 exitCode로 수정할 수 있음</p>
<pre><code class="language-java">  public class PassCheckingLister implements StepExecutionListener {
      @Override
      public void beforeStep(StepExecution stepExecution) {

      }

      @Override
      public ExitStatus afterStep(StepExecution stepExecution) {
          String exitCode = stepExecution.getExitStatus().getExitCode();
          if(!exitCode.equals(ExitStatus.FAILED.getExitCode())) {
              return new ExitStatus(&quot;PASS&quot;);
          }
          return null;
      }
  }</code></pre>
</li>
</ul>
<h2 id="jobexecutiondecider">JobExecutionDecider</h2>
<pre><code class="language-java">
@Bean
    public Job batchJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .incrementer(new RunIdIncrementer())
                .start(step1())
                .next(decider())
                .from(decider()).on(&quot;ODD&quot;).to(oddStep())
                .from(decider()).on(&quot;EVEN&quot;).to(eventStep())
                .end()
                .build();
    }

@Bean
    public JobExecutionDecider decider() {
        return new CustomDecider();
    }

==== 

public class CustomDecider implements JobExecutionDecider {

    private int count = 0;
    @Override
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        count++;
        if(count % 2 == 0) {
            return new FlowExecutionStatus(&quot;EVEN&quot;);
        }
        return new FlowExecutionStatus(&quot;ODD&quot;);
    }
}</code></pre>
<h1 id="jobscope-stepscope">@JobScope, @StepScope</h1>
<ul>
<li><p>Job 과 Step 의 빈 생성과 실행에 관여하는 스코프</p>
</li>
<li><p>code 예시</p>
<pre><code class="language-java">  // jobConfig 
  @RequiredArgsConstructor
  @Configuration
  public class JobConfiguration {

      private final JobBuilderFactory jobBuilderFactory;
      private final StepBuilderFactory stepBuilderFactory;
      private static final String ANY_STATUS = &quot;*&quot;;

      @Bean
      public Job batchJob() {
          return jobBuilderFactory.get(&quot;job&quot;)
                  .incrementer(new RunIdIncrementer())
                  .start(step1(null))
                  .next(step2())
                  .listener(new JobListener())
                  .build();
      }

      @Bean
      @JobScope
      public Step step1(@Value(&quot;#{jobParameters[&#39;message&#39;]}&quot;) String message) {
          return stepBuilderFactory.get(&quot;step1&quot;)
                  .tasklet((contribution, chunkContext) -&gt; {
                      System.out.println(&quot;message --&gt; &quot; + message);
                      System.out.println(contribution.getStepExecution().getStepName() + &quot; has executed&quot;);
                      return RepeatStatus.FINISHED;
                  }).build();
      }

      @Bean
      public Step step2() {
          return stepBuilderFactory.get(&quot;step2&quot;)
                  .tasklet(tasklet(null, null, null))
                  .listener(new CustomStepExecutionListener())
                  .build();
      }

      @Bean
      @StepScope
      public Tasklet tasklet(@Value(&quot;#{jobExecutionContext[&#39;name&#39;]}&quot;) String name,
                             @Value(&quot;#{stepExecutionContext[&#39;name2&#39;]}&quot;) String name2,
                             @Value(&quot;#{jobParameters[&#39;message&#39;]}&quot;) String message) {
          return (stepContribution, chunkContext) -&gt; {
              System.out.println(&quot;name --&gt; &quot; + name);
              System.out.println(&quot;name2 --&gt; &quot; + name2);
              System.out.println(&quot;message --&gt; &quot; + message);
              System.out.println(&quot;tasklet has executed&quot;);
              return RepeatStatus.FINISHED;
          };
      }
  }

  // JobListener
  public class JobListener implements JobExecutionListener {
      @Override
      public void beforeJob(JobExecution jobExecution) {
          jobExecution.getExecutionContext().put(&quot;name&quot;, &quot;user1&quot;);
      }

      @Override
      public void afterJob(JobExecution jobExecution) {

      }
  }

  // StepListener
  public class CustomStepExecutionListener implements StepExecutionListener {
      @Override
      public void beforeStep(StepExecution stepExecution) {
          stepExecution.getExecutionContext().put(&quot;name2&quot;, &quot;user2&quot;);
      }

      @Override
      public ExitStatus afterStep(StepExecution stepExecution) {
          return null;
      }
  }</code></pre>
</li>
</ul>
<h1 id="chunck-process">Chunck process</h1>
<ul>
<li>chunk 란 여러 개의 아이템을 묶은 하나의 덩어리를 의미한다</li>
<li>한번에 하나씩 아이템을 입력받아 chunk 단위로 트랜잭션을 처리한다.</li>
<li>일반적으로 대용량 데이터를 한번에 처리하는 것이 아닌 청크 단위로 쪼개서 더 이상 처리할 데이터가 없을 때 까지 반복처리</li>
</ul>
<h2 id="chunckprovider">ChunckProvider</h2>
<ul>
<li>ItemReader 를 사용해서 소스로부터 아이템을 chunk size 만큼 읽어서 Chunk 단위로 만들어 제공하는 도메인 객체</li>
<li>반복문 종료 시점<ul>
<li>Chunk size 만큼 item 을 읽으면 반복문 종료하고 ChunkProcess 로 넘어간다</li>
<li>ItemReader 가 읽은 item 이 null 일 경우 반복문 종료 및 해당 Step 반복문까지 종료</li>
</ul>
</li>
</ul>
<h2 id="chunkprocessor">ChunkProcessor</h2>
<ul>
<li>ItemProcessor 를 사용해 Item 을 변형, 가공, 필터링 하고 ItemWriter 를 사용해 Chunk 데이터를 저장,출력</li>
<li>Chunk<O> 를 만들고 앞에서 넘어온 Chunk<I> 의 item 을 한 건씩 처리 후 Chunck<O> 에 저장</li>
<li>ItemProcessor 는 선택사항으로 만약 없는 경우 ItemReader 에서 읽은 item 그대로 Chunk<O> 에 저장</li>
<li>ItemProcessor 처리가 완료되면 Chunck<O> 에 있는 List<Item> 을 ItemWriter 에게 전달</li>
<li>ItemWriter 처리가 완료되면 Chunk 트랜잭션이 종료되고 Step 반복문에서 ChunkOrientedTasklet 이 새롭게 생성된다</li>
</ul>
<h2 id="itemreader">ItemReader</h2>
<ul>
<li>다양한 입력으로부터 데이터를 읽어서 제공하는 인터페이스<ul>
<li>플랫(flat) 파일 - csv, txt ( 고정 위치로 정의된 데이터 필드나 특수만자로 구별된 데이터의 행)</li>
<li>XML, Json</li>
<li>Database</li>
<li>JMS, RabbitMQ 같은 message Queuing 서비스</li>
<li>CustomReader - 구현 시 멀티 스레드 환경에서 스레드에 안전하게 구현할 필요가 있음</li>
</ul>
</li>
</ul>
<h2 id="itemwriter">ItemWriter</h2>
<ul>
<li>Chunk 단위로 데이터를 받아 일괄 출력 작업을 위한 인터페이스<ul>
<li>플랫(flat) 파일 - csv, txt ( 고정 위치로 정의된 데이터 필드나 특수만자로 구별된 데이터의 행)</li>
<li>XML, Json</li>
<li>Database</li>
<li>JMS, RabbitMQ 같은 message Queuing 서비스</li>
<li>CustomWriter - 구현 시 멀티 스레드 환경에서 스레드에 안전하게 구현할 필요가 있음</li>
</ul>
</li>
<li>아이템 하나가 아닌 아이템 리스트를 전달 받는다.</li>
</ul>
<h2 id="itemprocessor">ItemProcessor</h2>
<ul>
<li>데이터를 출력하기 전 데이터를 가공, 변형, 필터링하는 역할</li>
</ul>
<h2 id="itemstream">ItemStream</h2>
<ul>
<li>ItemReader 와 ItemWriter 처리 과정 중 상태를 저장하고 오류가 발생하면 해당 상태를 참조하여 실패한 곳에서 재 시작 하도록 지원</li>
<li>ExecutionContext 를 매개변수로 받아 상태 정보를 업데이트 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Real MySQL 8.0]]></title>
            <link>https://velog.io/@jaden_94/Real-MySQL-8.0</link>
            <guid>https://velog.io/@jaden_94/Real-MySQL-8.0</guid>
            <pubDate>Mon, 19 Jun 2023 05:47:20 GMT</pubDate>
            <description><![CDATA[<h1 id="mysql-아키텍처">MySQL 아키텍처</h1>
<p>MySQL 서버는 크게 <strong>MySQL 엔진</strong>과 <strong>스토리지 엔진</strong>으로 구분할 수 있다.</p>
<h2 id="mysql-엔진">MySQL 엔진</h2>
<p>MySQL 엔진은 요청된 SQL 문장을 분석하거나 최적화하는 등이 역할을 담당한다.</p>
<p>MySQL 엔진은 클라이언트로부터의 접속 및 쿼리 요청을 처리하는 <strong>커넥션 핸들러</strong> 
<strong>SQL 파서</strong> 및 <strong>전 처리기</strong> 쿼리의 최적화된 실행을 위한 <strong>옵티마이저</strong>가 중심을 이룬다.</p>
<ul>
<li>Connection Handler : 커넥션 및 쿼리 요청 처리 담당</li>
<li>SQL 인터페이스 : DML, DDL, Procedure, View 등 SQL 인터페이스 담당</li>
<li>SQL 파서 : SQL 문법 오류 탐지 및 SQL 쿼리를  MySQL 이 처리하기 좋은 토큰 단위로 나눠서 트리 형태로 파싱하는 작업 담당</li>
<li>SQL 옵티마이저 : 쿼리의 최적화된 실행</li>
<li>캐시와 버퍼 : 성능 향상을 위한 보조 저장소 기능</li>
</ul>
<h2 id="스토리지-엔진">스토리지 엔진</h2>
<p>스토리지 엔진은 실제 데이터를 디스크 스토리지에 저장하거나 디스크 스토리지로부터 데이터를 읽어오는 역할을 담당한다. (ex) InnoDB, MyISAM</p>
<p>MySQL 엔진과 달리 복수개의 스토리지 엔진을 동시에 사용할 수 있으며 MySQL 엔진은 핸들러 API 를 통해 스토리지 엔진에 데이터 읽기 및 쓰기를 요청한다. </p>
<p><code>SHOW GLOBAL STATUS LIKE &#39;Handler%&#39;</code> 명령을 통해 각 핸들러 API 를 통한 요청이 얼마나 있었는지 확인할 수 있다</p>
<p><img src="https://velog.velcdn.com/images/jaden_94/post/463ce2f9-60d5-4a42-9ac2-ccbe64c5a813/image.png" alt=""></p>
<h2 id="mysql-스레딩-구조">MySQL 스레딩 구조</h2>
<p>MySQL 서버는 스레드 기반으로 작동하며 포그라운드(foreground)스레드와 백그라운드(background)스레드로 구분할 수 있다. </p>
<ul>
<li>포그라운드 스레드 
포그라운드 스레드는 최소한 MySQL 서버에 접속된 클라이언트 수만큼 존재
주로 각 클라이언트 <strong>사용자가 요청하는 쿼리 문장</strong>을 처리한다. 
작업을 마치고 커넥션을 종료하면 커넥션을 담당하던 스레드는 스레드 캐시로 돌아간다.
스레드 캐시에 유지할 수 있는 최대 스레드 개수는 thread_cache_size 시스템 변수를 통해 설정한다. 
포그라운드 스레드는 MySQL 데이터 버퍼나 캐시로부터 데이터를 가져오며 없는 경우 직접 디스크의 데이터나 인덱스 파일로부터 데이터를 읽어와 작업을 처리한다. </li>
<li>MyISAM 경우 디스크 쓰기 작업까지 포그라운드 스레드가 담당, InnoDB 는  디스크 기록은 백그라운스레드가 처리</li>
<li>백그라운드 스레드
InnoDB의 경우 
인서트 버퍼를 병합하는 스레드,
로그를 디스크로 기록하는 스레드,
InnoDB 버퍼 풀의 데이터를 디스크에 기록하는 스레드,
데이터를 버퍼로 읽어오는 스레드,
잠금이나 데드락을 모니터링하는 스레드가 백그라운드 스레드로 동작한다.</li>
</ul>
<p>MyISAM 은 사용자 스레드가 쓰기 작업까지 함께 처리하도록 설계되어있는 반면 <strong>쓰기작업을 버퍼링해서 일괄처리할 수 있는 InnoDB</strong> 는 CUD 쿼리로 데이터가 변경되는 경우 데이터가 디스크의 데이터 파일로 저장될 때 까지 기다리지 않아도 된다. </p>
<h2 id="메모리-구조">메모리 구조</h2>
<p>MySQL 메모리 공간은 크게 글로벌 메모리 영역과 로컬(세션)메모리 영역으로 구분할 수 있다. </p>
<ul>
<li>글로벌 메모리 영역 
MySQL 서버가 운영체제로 부터 할당받는 메모리는 MySQL 시스템 변수로 설정해 둔 만큼 받는다. 
모든 스레드에 공유되는 영역</li>
<li>로컬 메모리 영역 
클라이언트 스레드가 쿼리를 처리하는데 사용하는 메모리 영역
로컬 메모리는 각 클라이언트 스레드별로 독립적으로 할당되며 공유되지 않는다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[flyway 로 DB 형상관리하기 ]]></title>
            <link>https://velog.io/@jaden_94/flyway-%EB%A1%9C-DB-%ED%98%95%EC%83%81%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jaden_94/flyway-%EB%A1%9C-DB-%ED%98%95%EC%83%81%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 15 Jun 2023 05:33:32 GMT</pubDate>
            <description><![CDATA[<h1 id="flyway-란-무엇인가">flyway 란 무엇인가?</h1>
<p>서비스를 운영하다 보면 기존 DB 구조가 변경되어야 하는 시점이 도래한다. </p>
<p>컬럼이 추가되거나 삭제될 수 있고, 테이블간 관계가 생기거나 끊길 수 도 있는데</p>
<p>데이터베이스 특성상 안정성이 필수적이기 때문에 이러한 작업들이 쉬운일이 아니다.</p>
<p>이럴 때 DB 변경이력을 관리함으로써 안정적으로 DB 구조를 변경할 수 있도록 도와주는 자바 진영 DB 형상관리 툴이라고 생각할 수 있다. </p>
<h1 id="flyway-사용-방법">flyway 사용 방법</h1>
<p>개발환경 : spring boot 2.7.11, gradle, mySQL</p>
<h3 id="1-새로운-프로젝트를-시작할-때-flyway-적용하려는-경우">1. 새로운 프로젝트를 시작할 때 flyway 적용하려는 경우</h3>
<p>기본적으로 flyway 는 스크립트를 기준으로 DB 변경이력들을 관리하게 된다는 점을 인식하고 시작하자.</p>
<ul>
<li>프로젝트 의존성 추가</li>
</ul>
<p>mySQL 을 사용함으로 당연히 mySQL 드라이버에 대한 의존성이 필요하다.</p>
<p>이외에 flyway 의존성을 추가한다. </p>
<pre><code class="language-groovy">implementation &#39;mysql:mysql-connector-java:8.0.33&#39;
implementation &#39;org.flywaydb:flyway-core:9.8.1&#39;
implementation &#39;org.flywaydb:flyway-mysql:9.8.1&#39;</code></pre>
<ul>
<li>flyway 스크립트 작성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jaden_94/post/e41d4dbf-094f-43c5-a932-7259f402639f/image.png" alt=""></p>
<p>default 설정 상 flyway 스크립트는 main.resources.db.migration 패키지 하위에 위치시킨다.<br>커스텀한 위치를 설정하고 싶다면 yml 에서 spring.flyway.locations 에 값을 주어 설정 가능하다. 
→ 커스텀 설정을 통해 프로파일별 스크립트를 별도로 가져갈 수 있을 것이다. 
커스텀 경로는 복수개로도 설정이 가능하다.
→ prod, test 환경에서 동일한 스크립트를 읽고 싶으면서 test 환경에 추가적인 스크립트가 필요하다면 복수개 경로를 설정해 해결 가능하다. </p>
<p><img src="https://velog.velcdn.com/images/jaden_94/post/f6e12b03-63b7-4e36-83e7-1d371959148f/image.png" alt=""></p>
<p>스크립트 파일의 naming 규칙은 <a href="https://flywaydb.org/documentation/concepts/migrations.html#naming-1">다음을 참고</a>하자. </p>
<ul>
<li>YML 에 flyway 설정</li>
</ul>
<pre><code class="language-yaml">spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/study
    username: {userName}
    password: {passWord}
  flyway:
    locations: classpath:db/migration/test
    url: jdbc:mysql://localhost:3306/study
    username: {userName}
    password: {passWord}</code></pre>
<h2 id="2-flyway-를-사용하지-않던-프로젝트에-새로-도입해야하는-경우">2. flyway 를 사용하지 않던 프로젝트에 새로 도입해야하는 경우</h2>
<ul>
<li>YML 에 flyway 설정 추가</li>
</ul>
<pre><code class="language-yaml">flyway:
    baseline-on-migrate: true</code></pre>
<ul>
<li>스크립트 추가 
빈 스크립트를 작성해서 설정한 패키지 하위에 작성해두면 된다. (스크립트를 작성해둬도 괜찮다.)
하지만 빈 스크립트이던 스키마를 작성해놓은 스크립트이던 필수적으로 스크립트 파일은 있어야 한다</li>
</ul>
<h2 id="21-flyway_schema_history-테이블이-있는-경우">2.1 <strong>flyway_schema_history 테이블이 있는 경우</strong></h2>
<ul>
<li>YML 에 flyway 설정 추가</li>
</ul>
<pre><code class="language-yaml">flyway:
    baseline-on-migrate: false</code></pre>
<ul>
<li>히스토리에 있는 내역보다 높은 버전의 스크립트를 작성해서 이어나갈 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[로깅]]></title>
            <link>https://velog.io/@jaden_94/%EB%A1%9C%EA%B9%85</link>
            <guid>https://velog.io/@jaden_94/%EB%A1%9C%EA%B9%85</guid>
            <pubDate>Wed, 03 Aug 2022 00:37:58 GMT</pubDate>
            <description><![CDATA[<h1 id="로깅이란">로깅이란?</h1>
<p>운영체제나 소프트웨어가 실행 중에 발생하는 이벤트를 기록하는 행위 </p>
<h2 id="로깅하는-법">로깅하는 법</h2>
<ul>
<li>System API call
리눅스 시스템 API 의 syslog() </li>
<li>로깅 라이브러리 
JCL, slf4j, log4j, logback 등등 </li>
</ul>
<h2 id="slf4j">slf4j</h2>
<ul>
<li>Simple logging facade for Java</li>
<li>이름에서 볼 수 있듯 퍼사드 패턴을 활용해 다양한 로그라이브러리 들을 통일된 하나의 방법으로 사용할 수 있도록 함</li>
<li>스프링 부트에서는 logback 을 binding 해서 사용</li>
</ul>
<h2 id="로깅레벨">로깅레벨</h2>
<ul>
<li>FATAL : 매우 심각한 에러가 발생한 경우. 프로그램이 종료되는 경우가 많다.</li>
<li>ERROR : 에러가 발생했지만, 프로그램이 종료되지 않는 경우<ul>
<li>외부 API 호출 했는데 응답이 Error 로 오는 경우 </li>
</ul>
</li>
<li>WARN  : 에러가 될 수 있는 잠재적 가능성이 있는 경우, 알람이 오도록 설정하여 에러가 나기 전 조취를 취하거나, 에러가 나면 그 전의 상황을 알 수 있다.<ul>
<li>인메모리 캐시 용량이 다 차가는 경우, DB커넥션이 설정한 접속시간보다 오래걸리는 경우 등</li>
</ul>
</li>
<li>INFO  : 애플리케이션의 상태를 간결하게 보여주는 경우</li>
<li>DEBUG : INFO 레벨보다 더 자세한 정보가 필요한 경우, 권한이 없어 디버깅이 불가능한 경우 유용</li>
<li>TRACE : DEBUG 레벨보다 더 자세한 정보가 필요한 경우, 개발환경에서 버그를 해결하기 위해 사용. <strong>최종 프로덕션이나 커밋에 포함하면 안된다.</strong></li>
</ul>
<h2 id="독자를-고려한-로그메시지">독자를 고려한 로그메시지</h2>
<h3 id="1독자---machine">1독자 - Machine</h3>
<ul>
<li>로그 파일의 인코딩 문제를 최소화하기 위해 되도록 영어로 사용하자</li>
<li>파싱하기 좋도록 작성하자<h3 id="2독자---개발자">2독자 - 개발자</h3>
</li>
<li>로그 메시지에 컨택스를 담을 것</li>
<li>가능하면 해결방법을 같이 적을 것 </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 ORM 표준 JPA 프로그래밍 [6-1]]]></title>
            <link>https://velog.io/@jaden_94/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-6-1</link>
            <guid>https://velog.io/@jaden_94/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-6-1</guid>
            <pubDate>Mon, 25 Jul 2022 14:18:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>자바 ORM 표준 JPA 프로그래밍 - 김영한
책 내용을 정리한 내용입니다.
6.1 다대일
6.2 일대다 
6.3 일대일 </p>
</blockquote>
<h2 id="들어가기-전">들어가기 전</h2>
<p>엔티티 연관관계 매핑시 고려해야할 것</p>
<ol>
<li>다중성</li>
<li>단방향, 양방향</li>
<li>연관관계의 주인</li>
</ol>
<p>연관관계가 있는 엔티티가 일대일 관계인지 일대다 관계인지 다중성을 고려하고,
엔티티 중 한쪽만 참조하는 단방향 관계인지 서로 참조하는 양방향 관계인지 고려하고,
양방향 관계라면 주인을 정해야한다.</p>
<h4 id="다중성">다중성</h4>
<ul>
<li>일대다 (@OneToMany)</li>
<li>다대일 (@ManyToOne)</li>
<li>일대일 (@OneToOne)</li>
<li>다대다 (@ManyToMany)</li>
</ul>
<h4 id="단방향-양방향">단방향, 양방향</h4>
<p>테이블은 외래키를 사용함으로 항상 양방향이다.
객체는 참조용 필드를 통해 연관된 객체를 조회한다. 한 쪽 객체만 참조하는 것을 단방향, 양 쪽 객체가 서로 참조하는 것을 양방향 관계라 한다.</p>
<h4 id="연관관계의-주인">연관관계의 주인</h4>
<p>테이블에서 연관관계를 관리하는 포인트는 외래키 하나지만 엔티티는 양방향 연관관계에서 A.b / B.a 두 곳의 포인트에서 연관관계를 관리한다.
따라서 JPA 는 두 객체 연관관계 중 하나를 정해서 데이터베이스 외래키를 관리해야하며 이를 연관관계의 주인이라 한다.
외래키를 가진 테이블과 매핑한 엔티티가 외래키를 관리하는게 효율적임으로 해당 엔티티를 연관관계의 주인으로 선택하는 것을 권장한다.</p>
<h2 id="61-다대일">6.1 다대일</h2>
<h3 id="다대일-단방향">다대일 단방향</h3>
<pre><code class="language-java">@Entity
public class Member {

    @Id
    private Long id;

    private String memberName;

    @ManyToOne
    private Team team;
}


@Entity
public class Team {

    @Id
    private Long id;

    private String name;
}</code></pre>
<ul>
<li>N 인 Member 엔티티에서는 1 인 Team 을 참조할 수 있지만 Team 에서는 Member 를 참조하지 않는 관계</li>
<li>@JoinColumn 은 생략가능 생략시 :  [참조 필드명(team)] + &quot;_&quot; + [참조엔티티 @Id 의 필드명 (id)] =&gt; team_id </li>
</ul>
<h3 id="다대일-양방향">다대일 양방향</h3>
<pre><code class="language-java">@Entity
public class Member {

    @Id
    private Long id;

    private String memberName;

    @ManyToOne
    private Team team;
}


@Entity
public class Team {

    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members;
}</code></pre>
<ul>
<li>Member 에서도 Team 을 참조할 수 있고 Team 에서도 Member 를 참조할 수 있다.</li>
<li>일대다 혹은 다대일 연관관계에서 외래키는 항상 &#39;다&#39; 쪽에 있다 (테이블 구조상)</li>
<li>mappedBy 속성을 통해 연관관계의 주인을 설정할 수 있다. (Member.team 이 연관관계의 주인) </li>
<li>주인이 아닌쪽은 조회만 가능하다. </li>
</ul>
<h2 id="62-일대다">6.2 일대다</h2>
<h3 id="일대다-단방향">일대다 단방향</h3>
<pre><code class="language-java">@Entity
public class Member {

    @Id
    private Long id;

    private String memberName;
}


@Entity
public class Team {

    @Id
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = &quot;team_id&quot;)
    private List&lt;Member&gt; members;
}</code></pre>
<ul>
<li>일대다 단방향 관계는 실제 외래키 테이블(MEMBER 테이블) 과 외래키관리 엔티티(TEAM)의 위치가 반대편이다.</li>
<li>@JoinColumn 생략시 조인테이블 전략이 사용됨으로 @JoinColumn 을 명시하도록 한다. </li>
<li>일대다 단방향의 경우 매핑한 객체가 관리하는 외래키가 다른테이블에 있음으로 저장과 연관관계 처리시 INSERT 할 떄 UPDATE 를 추가로 실행해야한다.</li>
<li>일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자</li>
</ul>
<h3 id="일대다-양방향">일대다 양방향</h3>
<ul>
<li><p>일대다 양방향은 원칙적으로 존재하지 않는다 -&gt; 다대일 양방향을 사용한다. (@ManyToOne 에는 mappedBy 속성이 없음) </p>
</li>
<li><p>@JoinColumn 을 Member 에 추가하고 insertable, updatable 을 false 로 설정하여 야매로 설정은 할 수 있다. </p>
<pre><code class="language-java">@Entity
public class Member {

  @Id
  private Long id;

  private String memberName;

  @ManyToOne
  @JoinColumn(name = &quot;team_id&quot;, insertable = false, updatable = false) 
  private Team team;
}

</code></pre>
</li>
</ul>
<p>@Entity
public class Team {</p>
<pre><code>@Id
private Long id;

private String name;

@OneToMany
@JoinColumn(name = &quot;team_id&quot;)
private List&lt;Member&gt; members;</code></pre><p>}</p>
<pre><code>
## 6.3 일대일
- 일대일 관계는 반대도 일대일 관계이다
- 테이블 어느 쪽에나 외래키를 넣을 수 있다.
- 외래키에 데이터베이스 유니크 제약조건을 추가해야한다.

### 주 테이블 외래키 단방향
```java
// Member - 주 엔티티
// Locker - 대상 엔티티
@Entity
public class Member {

    @Id
    private Long id;

    private String userName;

    @OneToOne
    private Locker locker;
}

@Entity
public class Locker {

    @Id
    private Long id;

    private int number;
}</code></pre><h3 id="주-테이블-외래키-양방향">주 테이블 외래키 양방향</h3>
<pre><code class="language-java">// Member - 주 엔티티
// Locker - 대상 엔티티
@Entity
public class Member {

    @Id
    private Long id;

    private String userName;

    @OneToOne
    private Locker locker;
}

@Entity
public class Locker {

    @Id
    private Long id;

    private int number;

    @OneToOne(mappedby = &quot;locker&quot;)
    private Member member
}</code></pre>
<h3 id="대상-테이블-외래키-단방향">대상 테이블 외래키 단방향</h3>
<ul>
<li>불가능하다.</li>
</ul>
<h3 id="대상-테이블-외래키-양방향">대상 테이블 외래키 양방향</h3>
<pre><code class="language-java">// Member - 주 엔티티
// Locker - 대상 엔티티
@Entity
public class Member {

    @Id
    private Long id;

    private String userName;

    @OneToOne(mappedby = &quot;member&quot;)
    private Locker locker;
}

@Entity
public class Locker {

    @Id
    private Long id;

    private int number;

    @OneToOne
    private Member member
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 ORM 표준 JPA 프로그래밍 [5-2]]]></title>
            <link>https://velog.io/@jaden_94/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-5-2</link>
            <guid>https://velog.io/@jaden_94/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-5-2</guid>
            <pubDate>Sun, 24 Jul 2022 14:28:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>자바 ORM 표준 JPA 프로그래밍 - 김영한
책 내용을 정리한 내용입니다.
챕터5 - 연관관계 매핑 기초
5.3 양방향 연관관계
5.4 연관관계의 주인
5.5 skip
5.6 양방향 연관관계의 주의점 
5.7 skip</p>
</blockquote>
<h2 id="양방향-연관관계">양방향 연관관계</h2>
<p>양방향 연관관계 ? 관계를 맺는 객체가 서로에 대한 접근이 가능한 관계</p>
<pre><code class="language-java">public class Member {
    @Id
    private Long id;

    private String userName;

    @ManyToOne
    private Team team;
}

public class Team {
    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members;
}</code></pre>
<h2 id="연관관계의-주인">연관관계의 주인</h2>
<p>@OneToMany 의 속성에는 mappedBy 속성이 있다. 해당 속성은 관계의 주인을 설정하는 속성이다. 
JPA 에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야하고 관리하는 쪽을 연관관계의 주인 이라고 한다. 
(주인이 아닌쪽에서 mappedBy 속성을 사용해 주인을 지정한다) </p>
<ul>
<li>연관관계의 주인만이 외래키를 관리(등록, 수정, 삭제) 할 수 있다.</li>
<li>주인이 아닌쪽은 읽기만 가능하다. </li>
<li>연관관계의 주인은 외래키가 있는 곳으로 1:N 관계에서는 항상 N 쪽이 연관관계의 주인이다. </li>
</ul>
<h2 id="양방향-연관관계의-주의점">양방향 연관관계의 주의점</h2>
<p>연관관계의 주인에게는 값을 입력하지 않고 주인이 아닌 곳에만 값을 입력하면 
데이터베이스에 반영되지 않는다. </p>
<pre><code class="language-java">    @Transactional
    public void save(){
        Member member1 = new Member(1l, &quot;member1&quot;);
        em.persist(member1);

        Member member2 = new Member(2l, &quot;member2&quot;);
        em.persist(member2);

        Team team1 = new Team(1l, &quot;team1&quot;);
        team1.getMembers().add(member1);
        team1.getMembers().add(member2);
        em.persist(team1);
    }</code></pre>
<p> <img src="https://velog.velcdn.com/images/jaden_94/post/1e5b69eb-6bd0-463f-b3d1-2a43109a1ee7/image.png" alt=""></p>
<ul>
<li>연관관계의 주인에만 값을 저장하면 JPA 는 테이블에 매핑시켜준다.
하지만 객체 상태를 고려해 양쪽 모두 값을 세팅해주는 것이 맞다. </li>
</ul>
<h4 id="연관관계-편의-메소드">연관관계 편의 메소드</h4>
<p>양방향 연관관계는 결국 양쪽 다 신경 써야함으로 연관관계를 수정하는 부분은 두 쪽 모두 수정이 일어날 수 있도록 로직을 만들어 준다.</p>
<pre><code class="language-java">        /*
        순수한 객체 연관관계를 고려하지 않는 경우
        테이블에는 관계가 생성되지만
        객체는 모르는 상태가 된다. 
        */
        Team team1 = new Team(1l, &quot;team1&quot;);
        em.persist(team1);

        Member member1 = new Member(1l, &quot;member1&quot;);
        member1.setTeam(team1);
        em.persist(member1);

        Member member2 = new Member(2l, &quot;member2&quot;);
        member2.setTeam(team1);
        em.persist(member2);

        List&lt;Member&gt; members = team1.getMembers();
        System.out.println(&quot;size : &quot; + members.size()); // 0 출력


    -----------------

    // setTeam 양방향 관계를 모두 설정하는 연관관계 편의메소드로 변경해보자.
    public void setTeam(Team newTeam) {
        this.team = newTeam;
        newTeam.getMembers().add(this);
    } 

    --&gt; System.out.println(&quot;size : &quot; + members.size()); // 2 출력 </code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 ORM 표준 JPA 프로그래밍 [5-1]]]></title>
            <link>https://velog.io/@jaden_94/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-5-1</link>
            <guid>https://velog.io/@jaden_94/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-5-1</guid>
            <pubDate>Sun, 24 Jul 2022 11:33:29 GMT</pubDate>
            <description><![CDATA[<h1 id="연관관계-매핑-기초">연관관계 매핑 기초</h1>
<blockquote>
<p>자바 ORM 표준 JPA 프로그래밍 - 김영한 
책 내용을 정리한 내용입니다.
챕터5 - 연관관계 매핑 기초 
5.1 단방향 연관관계
5.2 연관관계 사용 </p>
</blockquote>
<blockquote>
<p>객체는 참조를 사용해서 관계를 맺고, 테이블은 외래키 를 사용해서 관계를 맺는다.
이런 차이점이 객체 연관관계와 테이블 연관관계 매핑에 어려움을 준다.
따라서 객체의 참조 - 테이블의 외래키를 매핑하는 방법을 목표한다. </p>
</blockquote>
<ul>
<li><p>방향 
[단방향, 양방향] 이 있다. 회원과 팀이 관계가 있을 때 
회원 -&gt; 팀 또는 팀 -&gt; 회원 둘 중 한쪽만 참조하는 것을 단방향 관계라 하고
회원 &lt; - &gt; 팀 양 쪽 모두 참조하는 것을 양방향 관계라고 한다.
&quot;방향&quot; 이라는 객체관계에만 존재하고 테이블 관계는 항상 양방향 이다.</p>
</li>
<li><p>다중성 
[다대일, 일대다, 일대일, 다대다] 다중성이 있다.
회원과 팀이 관계가 있을 때 여러 회원이 한 팀에 속하므로 회원과 팀은 다대일 관계다. 
반대로 한 팀에 여러 회원이 속할 수 있으므로 팀과 회원은 일대다 관계다 </p>
</li>
<li><p>연관관계의 주인 
객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.</p>
</li>
</ul>
<h2 id="단방향-연관관계">단방향 연관관계</h2>
<h3 id="다대일-단방향-관계">다대일 단방향 관계</h3>
<pre><code class="language-java">class Member {
 private String id;
 private Team team;
 private String userName;
}

class Team {
 private String id;
 private String name;
}</code></pre>
<h4 id="객체연관관계">객체연관관계</h4>
<p>Member 객체는 Member.team 을 통해 Team 객체와 연관관계를 맺는다.
Member 에서 Team 을 조회할 수 있지만 Team 에서 Member 는 조회할 수 있다 (단방향 관계)</p>
<p><img src="https://velog.velcdn.com/images/jaden_94/post/a80e4e35-fc0a-4da8-88ee-81a4fd792fa1/image.png" alt=""></p>
<h4 id="테이블연관관계">테이블연관관계</h4>
<p>회원 테이블의 TEAM_ID 를 통해 팀-회원의 연관관계를 맺는다.
TEAM 도 TEAM_ID 를 통해 팀에 속한 회원을 조회할 수 있고 
MEMBER 도 TEAM_ID 를 통해 회원이 속한 팀을 조회할 수 있다. </p>
<h4 id="차이점">차이점</h4>
<p>객체는 항상 단방향 연관관계이다. 
만약 양방향을 만들고 싶다면 서로가 서로를 참조하도록 2개의 단방향 연관관계를 만들어 주어야 한다. 
테이블은 항상 양방향 연관관계이다. </p>
<h4 id="jpa를-통한-관계매핑">JPA를 통한 관계매핑</h4>
<pre><code class="language-java">@Entity
public class Member {

    @Id
    private Long id;

    private String userName;

    @ManyToOne
    private Team team;
}

@Entity
public class Team {

    @Id
    private Long id;

    private String name;
}
</code></pre>
<p>JPA 를 사용하여 엔티트를 설계하면 아래와 같은 테이블 구조를 만들게 된다.
<img src="https://velog.velcdn.com/images/jaden_94/post/b42cdfbc-1f46-4d4e-9d85-f1fc989a364b/image.png" alt=""></p>
<ul>
<li>@ManyToOne 
다대일 관계라는 매핑 정보.
연관관계를 매핑할 때 다중성을 나타내는 어노테이션은 필수 
ex) @OneToMany, @OneToOne, @ManyToMany</li>
<li>@JoinColumn 
외래키 매핑 정보
생략 가능하며 생략시 [필드명]_[참조하는 테이블 키의 컬럼명]을 [외래키 컬럼명]으로 한다. </li>
</ul>
<h2 id="연관관계-사용">연관관계 사용</h2>
<h3 id="저장">저장</h3>
<pre><code class="language-java">@Service
public class TestSave {

    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void save(){
        Team team1 = new Team(1l, &quot;team1&quot;);
        em.persist(team1);

        Member member1 = new Member(1l, &quot;user1&quot;);
        member1.setTeam(team1);
        em.persist(member1);

        Member member2 = new Member(2l, &quot;user2&quot;);
        member2.setTeam(team1);
        em.persist(member2);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jaden_94/post/c944ddf7-fb4f-4293-82f2-9c2cb60a5d7e/image.png" alt=""></p>
<h3 id="조회">조회</h3>
<pre><code class="language-java">    @Transactional
    public void find() {
        Member member1 = em.find(Member.class, 1l);
        Team team = member1.getTeam();
        System.out.println(team.getName());
    }


    @Transactional
    public void find2() {
        String jpql = &quot;SELECT m FROM Member m JOIN m.team t where t.name =:teamName&quot;;
        List&lt;Member&gt; resultList = em.createQuery(jpql, Member.class)
            .setParameter(&quot;teamName&quot;, &quot;team1&quot;)
            .getResultList();

        for (Member member : resultList) {
            System.out.println(member.getUserName());
    }
</code></pre>
<ul>
<li>객체 그래프 탐색을 통해 조회할 수 있다.</li>
<li>JPQL 을 사용해 조회할 수 있다. </li>
</ul>
<h3 id="수정">수정</h3>
<pre><code class="language-java">    @Transactional
    public void update() {
        Team team2 = new Team(2l, &quot;team2&quot;);
        em.persist(team2);

        Member member1 = em.find(Member.class, 1l);
        member1.setTeam(team2);
    }</code></pre>
<ul>
<li>불러온 엔티티 값만 변경하면 트랜잭션을 커밋할 때 플러시가 일어나면서 &#39;변경 감지 기능&#39; 이 동작</li>
<li>참조하는 대상만 변경하면 &#39;변경 감지 기능&#39; 이 동작한다. </li>
</ul>
<h3 id="제거">제거</h3>
<pre><code class="language-java">    @Transactional
    public void delete() {
        Member member1 = em.find(Member.class, 1l);
        member1.setTeam(null);
    }</code></pre>
<ul>
<li>연관관계를 null 로 설정</li>
</ul>
<h3 id="삭제">삭제</h3>
<pre><code class="language-java">    @Transactional
    public void remove() {
        member1.setTeam(null);
        member2.setTeam(null);
        em.remove(team);
    }</code></pre>
<ul>
<li>관련된 연관관계를 모두 끊은 후 삭제해야 외래키 제약조건 오류가 나지 않는다. </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ElasticSearch 데이터 구조 이해하기 ]]></title>
            <link>https://velog.io/@jaden_94/ElasticSearch-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%A1%B0-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jaden_94/ElasticSearch-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%A1%B0-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 18 Jul 2022 13:16:15 GMT</pubDate>
            <description><![CDATA[<ul>
<li>ElasticSearch 의 시스템 및 데이터 구조</li>
<li>Alias, Rollover, API</li>
<li>Datastream</li>
<li>Integrations, Elastic Agent, Fleet</li>
<li>데모 </li>
</ul>
<p>ElasticSearch 시스템 구조
클러스터 - 독립된 ElasticSearch 시스템 환경. 1개 이상의 노드로 구성
노드 - 실행중인 ElasitcSearch 시스템 프로세스
도큐먼트 - 저장된 단일 데이터 단위
인덱스 - 도큐먼트의 논리적 집합. 1개 이상의 샤드로 구성
샤드 - 색인 &amp; 검색을 진행하는 작업 단위 </p>
<p>샤드는 기본저으로 프라이머리 샤드와 복제본으로 구성
갹 샤드들은 클러스터 내의 노드들에 분산되어 저장
같은 프라이머리 샤드와 복제본은 같은 데이터셋을 담고 있으며 반드시 다른 노드에 저장
데이터 노드가 1개인 경우 복제본은 생성되지 않음 </p>
<p>복재본을 통해 무결성을 유지
노드가 유실되면 남아있는 샤드의 데이터를 다른 노드로 복사 </p>
<p>각 인덱스 별로 프라이머리 샤드와 복제본 세트 수 설정 </p>
<pre><code>PUT books
{ 
 &quot;settings&quot; : {
   &quot;index&quot; : {
        &quot;number_of_shards&quot; : 5, 
     &quot;number_of_replicas&quot; : 1
    }
  }
}</code></pre><blockquote>
<p>프라이머리 샤드는 처음 인덱스를 생성하는 시점에 설정 이후 변경 불가능</p>
</blockquote>
<p>사용자는 어떤 도큐먼트가 어떤 샤드에 적재되는지 알 수 없음 (알 필요 없음) </p>
<p>검색 시, 요청을 받은 노드 (= coordinate node) 는 해당되는 모든 샤드에 검색 명령 전달
-&gt; 검색은 샤드별로 분산되어 실행 된 후 결과는 coordinate node 에서 취합되어 클라이언트로 전달</p>
<h3 id="alias-api">Alias API</h3>
<ul>
<li>Alias 하나에 여러개의 인덱스 연결 가능</li>
<li>여러개의 인덱스가 연결된 경우 조회만 가능</li>
<li>alias 와 인덱스가 1:1 인 경우 색인 가능</li>
<li>입력은 색인 alias, 검색은 조회 alias 로 하면 클라이언트 설정 변경 필요 X </li>
</ul>
<h3 id="rollover-api">Rollover API</h3>
<ul>
<li>날짜를 기준으로 인덱스를 나눌 경우 샤드 용량에 최적화 해서 사용하지 못함</li>
<li>_rollover API 를 이용해 인덱스를 용량을 기준으로 새로 나누어 생성 가능</li>
<li>인덱스가 아닌 alias 또는 datastream 을 대상으로 실행 </li>
</ul>
<pre><code>POST _aliases
{
 &quot;actions&quot; : [
   {
        &quot;add&quot; : {
       &quot;index&quot; : &quot;my-log-0000&quot;,
       &quot;alias&quot; : &quot;my-logs&quot; 
     }
   }
 ]
}


POST my_logs/_rollover
{ 
 &quot;conditions&quot; : {
  &quot;max_age&quot; : &quot;7d&quot;,
  &quot;max_docs&quot; : 1000,
  &quot;max_size&quot; : &quot;10gb&quot;
 }
}</code></pre><p>Rollover 는 조건에 만족될 떄 자동 생성이 아니라 POST 명령을 입력해야 생성됨 
-&gt; ILM (index lifecycle management) 로 제어하는 것이 바람직<img src="https://velog.velcdn.com/images/jaden_94/post/2ebd2894-27a1-4078-9374-0f9bdf410170/image.png" alt=""></p>
<h3 id="datastream">Datastream</h3>
<ul>
<li>색인은 입력 alias, 검색은 조회 alias ... -&gt; 복잡하네?</li>
<li>원래 인덱스 쓰던 것 처럼 한군데서 하면 안되나? </li>
<li>자동화하기 위해 나온 것인 datastream </li>
<li>데이터가 계속 추가(append) 되는 시계열 (로그, 메트릭) 데이터에서 사용</li>
<li>일반 인덱스를 사용하 듯 데이터스티림을 대상으로 데이터 색인, 검색 가능</li>
<li>데이터스트림 뒤에 하나 이상의 숨겨진 인덱스들을 자동으로 구성, 확장</li>
<li>반드시 기준이 되는 date, date_nanos 타입의 @timestamp 필드 필요</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링배치 완벽가이드 [1]]]></title>
            <link>https://velog.io/@jaden_94/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B0%B0%EC%B9%98-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-1</link>
            <guid>https://velog.io/@jaden_94/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B0%B0%EC%B9%98-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-1</guid>
            <pubDate>Sun, 17 Jul 2022 14:10:20 GMT</pubDate>
            <description><![CDATA[<pre><code>@EnableBatchProcessing
@SpringBootApplication
public class Springbatch2Application {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    public Step step(){
        return this.stepBuilderFactory.get(&quot;step1&quot;)
            .tasklet(new Tasklet() {
                @Override
                public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
                    System.out.println(&quot;Hello, world&quot;);
                    return RepeatStatus.FINISHED;
                }
            }).build();
    }

    @Bean
    public Job job() {
        return this.jobBuilderFactory.get(&quot;job&quot;)
            .start(step())
            .build();
    }

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

}</code></pre><p>@EnableBatchProcessing
배치 인프라스트럭처를 부트스트랩 한다.</p>
<ul>
<li>JobRepository : 실행 중인 잡의 상태를 기록하는데 사용  </li>
<li>JobLauncher : 잡을 구동하는데 사용 </li>
<li>JobExplorer : JobRepository 를 사용해 읽기 전용 작업을 수행하는데 사용</li>
<li>JobRegistry : 특정한 런처 구현체를 사용할 떄 잡을 찾는 용도로 사용 </li>
<li>PlatformTransactionManager : 잡 진행과정에서 트랜잭션을 다루는데 사용 </li>
<li>JobBuilderFactory : 잡을 생성하는 빌더</li>
<li>StepBuilderFactory : 스탭을 생성하는 빌더 </li>
</ul>
<h3 id="스탭-생성">스탭 생성</h3>
<p>스탭은 스프링 빈으로 구성되어 있다. 
위 코드에서는 이름 및 태스크릿만으로 스탭을 구성한다.
RepeatStatus.FINISHED 를 반환하면 태스크릿이 완료됐음을 스프링 배치에게 알려준다는 의미이다.
RepeatStatus.CONTINUED 를 반환하면 멈추지 않고 계속 수행된다. </p>
<h3 id="잡-생성">잡 생성</h3>
<p>구성한 스탭을 사용해 잡을 작성할 수 있다.
잡은 하나 이상의 스탭으로 구성된다.
잡 이름과 해당 잡에서 시작할 스탭을 구성한다. </p>
<p><img src="https://velog.velcdn.com/images/jaden_94/post/475653a7-0f64-4d1d-abb2-2e24d46ce983/image.png" alt=""></p>
<h4 id="어떤-일들이-일어났는가">어떤 일들이 일어났는가?</h4>
<p>스프링 부트의 JobLauncherCommandLineRunner 라는 컴포넌트는 스프링 배치가 클래스 경로에 있다면 실행 시에 로딩되어 JobLauncher 를 사용해 ApplicationContext 에서 찾아낸 모든 잡을 실행한다. </p>
<ol>
<li>메인 메서드에서 스프링 부트를 부트스트랩할 때 ApplicationContext 가 생성</li>
<li>JobLauncherCommandLineRunner 가 실행</li>
<li>Job  수행</li>
<li>첫 번째 step 실행</li>
<li>트랜잭션 시작</li>
<li>Tasklet 실행</li>
<li>결과가 JobRepository 에 갱신</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[ElasticSearch 클러스터링]]></title>
            <link>https://velog.io/@jaden_94/ElasticSearch-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81</link>
            <guid>https://velog.io/@jaden_94/ElasticSearch-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81</guid>
            <pubDate>Wed, 13 Jul 2022 15:15:34 GMT</pubDate>
            <description><![CDATA[<p>엘라스틱 서치에서는 하나의 물리 서버안에 여러 개의 노드를 생성하는 것도 가능하며 
여러 개의 물리서버에 각각의 노드를 생성하는 것도 가능하다.</p>
<p>이런 노드들을 클러스터 바인딩하기 위해선 클러스터명을 동일하게 설정해주면 된다.
elasticsearch.yml 에서 cluster.name 을 설정을 함으로써 클러스터명을 설정할 수 있다.
만약 클러스터명이 다르다면 같은 서버에 있더라도 별개의 시스템으로 인식된다. </p>
]]></description>
        </item>
    </channel>
</rss>