<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>min-zi</title>
        <link>https://velog.io/</link>
        <description>개발일지</description>
        <lastBuildDate>Thu, 25 Sep 2025 09:14:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>min-zi</title>
            <url>https://velog.velcdn.com/images/min-zi/profile/03a30163-8e47-4d40-8055-3823d147edcd/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. min-zi. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/min-zi" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring batch] Chunk 방식의 Step 과 트랜잭션 관계 이해하기]]></title>
            <link>https://velog.io/@min-zi/Spring-batch-Chunk-%EB%B0%A9%EC%8B%9D%EC%9D%98-Step-%EA%B3%BC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B4%80%EA%B3%84-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@min-zi/Spring-batch-Chunk-%EB%B0%A9%EC%8B%9D%EC%9D%98-Step-%EA%B3%BC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B4%80%EA%B3%84-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 25 Sep 2025 09:14:54 GMT</pubDate>
            <description><![CDATA[<h2 id="chunk-step-기본-동작">Chunk Step 기본 동작</h2>
<p>트랜잭션은 Step 전체가 아니라 Chunk 단위로 생성/종료된다.
Step 전체가 하나의 트랜잭션인 게 아니고 Step 은 여러 Chunk 트랜잭션의 모음이라고 생각하면 된다.
</br></p>
<pre><code>Chunk 1 (10건) ──[트랜잭션 시작]──&gt; Reader 읽음 → Writer 처리 → [트랜잭션 commit/rollback]──&gt; DB
Chunk 2 (10건) ──[트랜잭션 시작]──&gt; Reader 읽음 → Writer 처리 → [트랜잭션 commit/rollback]──&gt; DB</code></pre><p>Spring Batch 에서 chunk(CHUNK_SIZE, transactionManager) 라고 설정하면 하나의 Chunk 는 하나의 트랜잭션을 말한다. Chunk 는 ItemReader, ItemProcessor, ItemWriter 로 구성 되고 더 이상 처리할 데이터가 없을 때까지 이 과정을 반복한다. Reader 가 CHUNK_SIZE 만큼 NotificationEntity 를 읽어와 하나의 트랜잭션으로 묶어서 Writer 에게 전달한다.
예를 들어 CHUNK_SIZE = 10 이면 Reader 가 DB 에서 10건을 읽어오고 Writer 가 하나의 트랜잭션 안에서 10건 전체를 처리하고 그 트랜잭션이 commit 이 되고 DB 에 반영되는 것이다. </p>
</br>

<h2 id="chunk-step-방식이-db-에-저장-되는-시점">Chunk Step 방식이 DB 에 저장 되는 시점</h2>
<pre><code>// SendNotificationItemWriter 에 전송 후 sent = true 로 업데이트 하는 부분

if (successful) {
    notificationEntity.setSent(true);
    notificationEntity.setSentAt(LocalDateTime.now());
    notificationRepository.save(notificationEntity);
}</code></pre><p>JPA save() 는 영속성 컨텍스트에 엔티티를 저장한다. 실제 DB 에 반영이 되는 건 트랜잭션이 commit 될 때이다. 만약 Chunk 처리 중 하나라도 실패하면 Spring Batch 가 트랜잭션을 rollback 하고 이전 상태로 되돌린다.</p>
</br>

<p>Reader -&gt; NotificationEntity 리스트(10건)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;↓
Writer -&gt; for 문 돌면서 save() 호출 (영속성 컨텍스트에 반영)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;↓
Chunk 종료 -&gt; transactionManager.commit()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;↓
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DB에 최종 반영</p>
</br>

<p>Chunk 처리 도중 중간에 10건 중 한건이라도 실패하면 전체 10건이 rollback 된다. 만약 10건 처리 중 5번째에서 예외가 발생했다면 현재 Chunk 의 이 트랜잭션은 rollback 되면서 종료된다. Chunk 도중 한건이라도 실패하면 기본적으로 Job 실패가 되니까 Step 에 SkipPolicy 등 설정이 되어있다면 실패하는 건은 건너뛰고 새로운 Chunk 트랜잭션을 시작해서 다음 Chunk로 넘어갈 수 있다. 이미 1~4번째에 대해 save() 로 컨텍스트에 들어간 엔티티들도 DB 에는 반영되지 않고(commit 전이라서) 이전 상태로 되돌린다.
</br></p>
<p>핵심은 save() = 영속성 컨텍스트에 기록
Chunk 가 종료되어야 commit 이 되고 DB 반영
중간 오류 시 rollback.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring batch] setDelegate() 메서드]]></title>
            <link>https://velog.io/@min-zi/Spring-batch-setDelegate-%EB%A9%94%EC%84%9C%EB%93%9C</link>
            <guid>https://velog.io/@min-zi/Spring-batch-setDelegate-%EB%A9%94%EC%84%9C%EB%93%9C</guid>
            <pubDate>Thu, 25 Sep 2025 06:31:23 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-setdelegate를-쓰는-이유">📌 setDelegate()를 쓰는 이유</h2>
<p>멀티스레드 Step 환경에서 ItemStreamReader 를 thread-safe 안전하게 감싸주며 여러 스레드가 동시에 read() 를 호출하더라도 동기화(synchronized) 처리되는 동작을 보장한다.</p>
<p>SynchronizedItemStreamReader 는 확장 모듈이 아니라 spring-batch-core 라이브러리에 포함되어 있는 클래스이다. starter-batch 를 사용하고 있다면 바로 사용하면 된다. (batch-core 가 자동으로 포함되어 있음)</p>
<h4 id="delegate-는-실제-데이터-소스에서-읽는-itemreader-이다">delegate 는 실제 데이터 소스에서 읽는 ItemReader 이다.</h4>
<p>JpaCursorItemReader, JdbcCursorItemReader, FlatFileItemReader 등
SynchronizedItemStreamReader 는 직접 데이터를 읽지 않고 delegate 에게 반드시 위임해야 한다. delegate 를 지정하지 않은 null 인 상태에서 read() 를 호출하면 NullPointerException 이 발생하게 된다.</p>
<h2 id="📌-사용방법">📌 사용방법</h2>
<p>SynchronizedItemStreamReader 는 void 반환값이 없으므로 객체 생성 -&gt; setDelegate() -&gt; return 순서로만 사용해줘야 된다.</p>
<h3 id="step-정의">Step 정의</h3>
<pre><code>@Bean
public Step sendNotificationStep() {
    return new StepBuilder(&quot;sendNotificationStep&quot;, jobRepository)
            .&lt;NotificationEntity, NotificationEntity&gt;chunk(CHUNK_SIZE, transactionManager)
            .reader(sendNotificationItemReader())  // 여기서 syncReader 사용
            .writer(sendNotificationItemWriter)    // 알람 전송 로직
            .taskExecutor(new SimpleAsyncTaskExecutor()) // 멀티스레드 실행
            .build();
}</code></pre><h3 id="reader-등록">Reader 등록</h3>
<pre><code>@Bean
public SynchronizedItemStreamReader&lt;NotificationEntity&gt; sendNotificationItemReader() {
    JpaCursorItemReader&lt;NotificationEntity&gt; itemReader =
            new JpaCursorItemReaderBuilder&lt;NotificationEntity&gt;()
                    .name(&quot;sendNotificationItemReader&quot;)
                    .entityManagerFactory(entityManagerFactory)
                    .queryString(&quot;select n from NotificationEntity n where n.event = :event and n.sent = :sent&quot;)
                    .parameterValues(Map.of(&quot;event&quot;, NotificationEvent.BEFORE_CLASS, &quot;sent&quot;, false))
                    .build();

    // Reader 가 읽어온 Chunk 단위의 NotificationEntity 리스트를 차례로 Writer 에 전달하기 위한 래퍼
    SynchronizedItemStreamReader&lt;NotificationEntity&gt; syncReader = new SynchronizedItemStreamReader&lt;&gt;();
    syncReader.setDelegate(itemReader); // 실제 DB 읽기 로직을 delegate 로 연결

    return syncReader;
}</code></pre><h3 id="delegate-실행-흐름">delegate 실행 흐름</h3>
<p>delegate 인 JpaCursorItemReader 가 DB 에서 NotificationEntity 를 순서대로 가져옴 -&gt; Writer 로 데이터가 청크 단위로 전달됨</p>
<p>여러 스레드가 동시에 reader.read() 호출 가능
SynchronizedItemStreamReader 는 내부적으로 이렇게 되어 있어서</p>
<pre><code>public synchronized T read() {
    return delegate.read();
}</code></pre><p> 여러 스레드가 접근해도 하나의 스레드가 각각 delegate.read() 를 실행해서 DB 접근이 꼬이지 않음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Batch] JobLauncherTestUtils.launchJob() 사용 방법]]></title>
            <link>https://velog.io/@min-zi/JobLauncherTestUtils</link>
            <guid>https://velog.io/@min-zi/JobLauncherTestUtils</guid>
            <pubDate>Mon, 22 Sep 2025 10:11:48 GMT</pubDate>
            <description><![CDATA[<p>JobLauncherTestUtils 는 자체적으로 Job Bean 을 주입받아야 한다. 컨텍스트에 테스트용 Job Bean 이 없거나, 여러 Job Bean 이 존재해서 Spring 이 어떤 Job 을 사용해야 할지 모르는 상태에서 launchJob() 을 호출 하면 The Job must not be null 또는 NoUniqueBeanDefinitionException 이 발생할 수 있다.</p>
<p>@SpringBatchTest 를 붙이면 테스트 전용 컨텍스트에 JobLauncherTestUtils 빈이 자동으로 생성된다. Batch 4 에서는 launchJob() 호출 시에 유일한 Job 이 있으면 알아서 그 Job 을 사용했지만, Batch 5 에서는 job 필드가 자동으로 Autowired 되지 않아서(null 상태) 직접 setjob() 으로 지정해주는 것이 필수다. JobLauncherTestUtils 는 job 필드가 null 이면 실행이 불가하다.</p>
</br>

<h2 id="테스트용-job-bean-정의">테스트용 Job Bean 정의</h2>
<p>테스트에서 사용하는 Job(expiredPassesJob) 이 애플리케이션 컨텍스트에 등록되어 있지 않아서 JobLauncherTestUtils 가 Job 을 못 찾는다면 테스트용 Job 을 명시적으로 지정해주는 것이 안전하다.</p>
<pre><code>@TestConfiguration
public class JobConfigTests {
    @Bean
    public Job expiredPassesJob(JobRepository jobRepository, Step expirePassesStep) {
        return new JobBuilder(&quot;expiredPassesJob&quot;, jobRepository)
                .start(expirePassesStep)
                .build();
    }
}</code></pre></br>

<h2 id="사용할-job-지정">사용할 Job 지정</h2>
<p>Job Bean 이 여러개라 못 찾고 있다면 @Qualifier 로 이름 지정해주거나 JobLauncherTestUtils 에 setJob() 으로 Job 을 직접 연결해주면 된다.</p>
<h3 id="importtestjobconfigclass-사용">@Import(TestJobConfig.class) 사용</h3>
<p>Import 로 테스트 클래스가 테스트용 Job Config 를 명시적으로 가져오도록 해준다. main Application.java 에서 정의된 Job 과는 분리된 테스트 전용 Job 을 사용하게 되고 원본 Job 과 충돌을 방지할 수 있다.</p>
<h3 id="setjob">setJob()</h3>
<p>테스트를 진행할 Job 빈을 JobLauncherTestUtils 에 setJob() 으로 연결한다.</p>
<pre><code>@Slf4j
@SpringBatchTest
@SpringBootTest
@Import(JobConfigTests.class)
@ActiveProfiles(&quot;test&quot;)
class ExpirePassesJobConfigTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @BeforeEach
    void setUp(@Qualifier(&quot;expiredPassesJob&quot;) Job expiredPassesJob) {
        jobLauncherTestUtils.setJob(expiredPassesJob);
    }
}</code></pre><p>테스트 전용 Job 을 등록해준 다음에
JobLauncherTestUtils 에 @Autowired 해준 후
@BeforeEach 에서 setJob()으로 연결해주면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] MapStruct - 매핑 자동 생성 라이브러리]]></title>
            <link>https://velog.io/@min-zi/Spring-MapStruct-%EB%A7%A4%ED%95%91-%EC%89%BD%EA%B2%8C-%EA%B0%9D%EC%B2%B4-%EA%B0%84-%EB%B3%80%ED%99%98-%EC%BD%94%EB%93%9C-%EC%9E%90%EB%8F%99-%EC%83%9D%EC%84%B1-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</link>
            <guid>https://velog.io/@min-zi/Spring-MapStruct-%EB%A7%A4%ED%95%91-%EC%89%BD%EA%B2%8C-%EA%B0%9D%EC%B2%B4-%EA%B0%84-%EB%B3%80%ED%99%98-%EC%BD%94%EB%93%9C-%EC%9E%90%EB%8F%99-%EC%83%9D%EC%84%B1-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</guid>
            <pubDate>Fri, 12 Sep 2025 10:48:40 GMT</pubDate>
            <description><![CDATA[<p>MapStruct 는 Java Bean Mapper 라이브러리다.
DTO ↔ Entity 같은 객체 간 변환 코드를 자동으로 생성해주는 도구이다. 구현하다 보면 매핑할 일이 많은데 간단하게 구현할 수 있게 도와준다.</p>
<pre><code>userDto.setId(userEntity.getId());
userDto.setName(userEntity.getName());
userDto.setEmail(userEntity.getEmail());</code></pre><p>필드가 많아지면 매번 이렇게 매핑 코드를 쓰는 게 엄청 귀찮고 실수도 많이 나온다. MapStruct 는 이런 매핑을 자동으로 만들어 주는 도움을 준다.</p>
</br>

<h2 id="mapstruct-를-쓰는-이유">MapStruct 를 쓰는 이유</h2>
<ul>
<li><p>자동 코드 생성
반복되는 setter/getter 매핑 코드를 줄일 수 있다.
우리가 직접 안 써도 되고 MapStruct 가 빌드 타임에 컴파일러가 이해할 수 있는 Java 코드로 변환 코드를 만들어 준다.</p>
</li>
<li><p>컴파일 타임 체크
매핑할 필드명이 잘못되면 컴파일 시점에 에러를 내준다.
(ModelMapper 같은 라이브러리는 런타임에 매핑 실패를 알 수 있는데 MapStruct 는 빌드할 때 잡아줘서 더 안전하다.</p>
</li>
<li><p>커스텀 매핑 가능
매핑 시키는 엔티티들은 대부분 필드명이 같은데 필드명이 다른 경우는 @Mapping 을 써서 직접 선언해 줄 수도 있다.</p>
<pre><code>@Mapping(target = &quot;status&quot;, qualifiedByName = &quot;status&quot;)
@Mapping(target = &quot;remainingCount&quot;, source = &quot;bulkPassEntity.count&quot;)
</code></pre></li>
</ul>
<p>@Named(&quot;status&quot;)
    default PassStatus status(BulkPassStatus status) {
        return PassStatus.READY;
    }</p>
<pre><code>- 성능 빠름
리플렉션(Reflection)을 쓰지 않고, 실제 자바 코드로 변환 코드를 생성해서 실행 속도가 빠르다.

&lt;/br&gt;

## MapStruct의 동작 방식

![](https://velog.velcdn.com/images/min-zi/post/7e16dbc5-4744-4a5f-89a4-969d30e4f0ff/image.png)
</code></pre><p>public interface PassModelMapper {
    PassModelMapper INSTANCE = Mappers.getMapper(PassModelMapper.class);
}</p>
<pre><code>
interface PassModelMapper 만 작성해서 사용하고 있지만 실제적으로는 컴파일 시점에 MapStruct 가 내부에서 자동으로 구현 클래스를 만들어주고 있다. 이름은 보통 PassModelMapperImpl 로 생성된다. 안에 toPassEntity() 같은 메서드의 실제 변환 로직이 들어가고 Mappers.getMapper(PassModelMapper.class)는 그 자동으로 생성된 구현체를 찾아서 리턴해준다.

INSTANCE 는 사실 PassModelMapperImpl 객체를 가지고 있다. 우리가 따로 new PassModelMapperImpl() 해서 만들 필요 없이 이 한 줄로 PassModelMapper 를 그냥 쓸 수 있고 toPassEntity(어쩌구저쩌구) 구현체에 있는 변환 로직을 바로 사용하는게 가능한거다.
</code></pre><p>PassEntity passEntity = PassModelMapper.INSTANCE.toPassEntity(bulkPassEntity, userId);</p>
<pre><code>
&lt;/br&gt;

## 필드 매핑 방법
### ① @Mapping 으로 필드명 연결해주기
필드명이 같으면 자동이지만 필드명이 다르면 @Mapping 으로 “어디 → 어디” 인지 명시적으로 알려줘야 한다.
source: 원본 객체 필드
target: 변환될 객체 필드</code></pre><p>// Entity
public class PackageEntity {
    private Long id;
    private String packageName;
    private int totalCount;
}</p>
<pre><code></code></pre><p>// DTO
public class Package {
    private Long id;
    private String name;
    private int count;
}</p>
<pre><code>요렇게!</code></pre><p>@Mapper
public interface PackageMapper {</p>
<pre><code>@Mapping(source = &quot;packageName&quot;, target = &quot;name&quot;)
@Mapping(source = &quot;totalCount&quot;, target = &quot;count&quot;)
Package map(PackageEntity entity);</code></pre><p>}</p>
<pre><code>&lt;/br&gt;

### ② 중첩 객체 필드 매핑
점(.) 표기법 기억하면 됨</code></pre><p>// Entity
class PackageEntity {
    private Long id;
    private CategoryEntity category;
}</p>
<p>class CategoryEntity {
    private String code;
}</p>
<pre><code></code></pre><p>// DTO
class Package {
    private Long id;
    private String categoryCode;
}</p>
<pre><code>요렇게!</code></pre><p>@Mapper
public interface PackageMapper {</p>
<pre><code>@Mapping(source = &quot;category.code&quot;, target = &quot;categoryCode&quot;)
Package map(PackageEntity entity);</code></pre><p>}</p>
<pre><code>
&lt;/br&gt;

### ③ 상수 값 / 고정 값 넣기</code></pre><p>@Mapping(target = &quot;status&quot;, constant = &quot;ACTIVE&quot;)</p>
<pre><code>항상 status = &quot;ACTIVE&quot;

&lt;/br&gt;

### ④ 특정 필드 매핑 제외
ignore</code></pre><p>@Mapping(target = &quot;createdAt&quot;, ignore = true)</p>
<pre><code>전역 설정</code></pre><p>@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)</p>
<pre><code>
&lt;/br&gt;

### ⑤ enum ↔ String 매핑
커스텀 변환</code></pre><p>@Mapping(
    source = &quot;status&quot;,
    target = &quot;status&quot;,
    qualifiedByName = &quot;statusToString&quot;
)</p>
<pre><code></code></pre><p>@Named(&quot;statusToString&quot;)
default String statusToString(Status status) {
    return status.getCode();
}
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 복합키 매핑 방식 - @IdClass, @EmbeddedId]]></title>
            <link>https://velog.io/@min-zi/JPA-%EB%B3%B5%ED%95%A9%ED%82%A4-%EB%A7%A4%ED%95%91-%EB%B0%A9%EC%8B%9D-IdClass-EmbeddedId</link>
            <guid>https://velog.io/@min-zi/JPA-%EB%B3%B5%ED%95%A9%ED%82%A4-%EB%A7%A4%ED%95%91-%EB%B0%A9%EC%8B%9D-IdClass-EmbeddedId</guid>
            <pubDate>Fri, 12 Sep 2025 10:18:17 GMT</pubDate>
            <description><![CDATA[<p>JPA(Hibernate)에서 @Id 로 지정하는 건 엔티티를 고유하게 식별할 수 있는 단일 키를 의미한다.
근데 어떤 테이블은 하나의 컬럼만으로는 PK 를 못 만들고 여러 컬럼을 합쳐야 유일해지는 경우가 있다. 이때 복합키를 사용하게 된다.</p>
</br>

<h2 id="복합키-방식">복합키 방식</h2>
<h3 id="idclass">@IdClass</h3>
<p>&quot;userGroupId + userId = 이 테이블의 기본키&quot; 라는 사실을 JPA 에게 알려주기 위한 복합키를 담은 클래스라고 이해하면 된다.</p>
<p>@Entity 에서 각 컬럼에 @Id 를 붙이고 별도로 IdClass 식별자 클래스 선언 (implements Serializable 필수) 한다. IdClass 안에는 복합키를 구성하는 필드들을 정의 (userGroupId, userId) 해주면 된다.
JPA 가 내부적으로 equals(), hashCode() 를 이용해서 식별성을 비교한다.</p>
<h4 id="✅-왜-꼭-id-클래스가-필요한가">✅ 왜 꼭 Id 클래스가 필요한가?</h4>
<p>JPA 는 엔티티의 식별성을 명확하게 알아야 한다.
단일 PK 는 그냥 @Id 하나면 끝이지만 컬럼 여러개를 합쳐야 유일해진다면 복합키를 사용하는 것이다. PK 가 여러개 일 때는 JPA 가 이 두개가 합쳐져야 같은 row 인 것을 알 수 있도록 별도의 키 묶음 클래스가 필요하다.</p>
<p>예를 들어
같은 그룹 안에 여러 userId 가 존재할 수 있고 같은 userId 가 다른 그룹에도 속할 수 있다. 그래서 user_group_mapping 테이블의 PK 가 <code>user_group_id</code>, <code>user_id</code> 두개이고 한 줄을 구분할 때 두 컬럼을 합쳐야만 고유하게 식별할 수 있을거다. 이렇게.</p>
<p>(HANBADA, A1000000)
(HANBADA, A1000001)
(TAESAN, B2000001)</p>
<p>각각 다 다른 레코드다.</p>
</br>



<h3 id="embeddedid">@EmbeddedId</h3>
<p>식별자 클래스를 @Embeddable 로 만들고 엔티티 안에서 @EmbeddedId 로 사용</p>
<p>조금 더 객체지향적인 코드가 된다.
필드 접근할 때 entity.getId().getUserGroupId() 처럼 한 단계 더 들어가야 하는 번거로움이 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Batch] 스프링 배치 메타테이블(BATCH_*)이 안 만들어질 때, Job Bean 이 자동 실행이 안될 때]]></title>
            <link>https://velog.io/@min-zi/Spring-Batch-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98-Job-Bean-%EC%9E%90%EB%8F%99-%EC%8B%A4%ED%96%89%EA%B3%BC-%EC%88%98%EB%8F%99-%EC%8B%A4%ED%96%89-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@min-zi/Spring-Batch-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98-Job-Bean-%EC%9E%90%EB%8F%99-%EC%8B%A4%ED%96%89%EA%B3%BC-%EC%88%98%EB%8F%99-%EC%8B%A4%ED%96%89-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 23 Aug 2025 03:43:55 GMT</pubDate>
            <description><![CDATA[<h2 id="✔️-spring-batch-메타테이블">✔️ Spring Batch 메타테이블</h2>
<p>BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_STEP_EXECUTION 같은 테이블은 Spring Batch 가 Job 실행 상태를 관리하기 위해 필요하고, 이 테이블들은 우리가 직접 SQL 로 만들지 않아도 Spring Boot/Spring Batch 가 자동으로 생성해준다.</p>
<h3 id="자동-생성-방법">자동 생성 방법</h3>
<p>application.yml 에서 아래 설정을 추가해주면
애플리케이션 실행 시 BATCH 메타 테이블들이 MySQL 에 자동 생성된다.</p>
<pre><code>spring:
  batch:
    jdbc:
      initialize-schema: always</code></pre><p>always 로 두면 Spring Boot 가 org/springframework/batch/core/schema-mysql.sql 파일을 읽어서 MySQL에 BATCH_ 테이블들을 자동으로 만들어준다.</p>
</br>

<h3 id="메타-테이블-생성-에러">메타 테이블 생성 에러</h3>
<blockquote>
<p>Caused by: org.springframework.jdbc.support.MetaDataAccessException: Could not get Connection for extracting meta-data</p>
</blockquote>
<p>이 에러는 DB 연결이 정상적으로 이루어지지 않아서 메타데이터(테이블, 컬럼 정보 등)를 읽어오지 못할 때 발생한다.</p>
<blockquote>
<p>Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata (please set &#39;jakarta.persistence.jdbc.url&#39; for common cases or &#39;hibernate.dialect&#39; when a custom Dialect implementation must be provided)</p>
</blockquote>
<p>이 에러는 Hibernate 가 어떤 종류의 데이터베이스를 사용하고 있는지 알 수 없을 때 발생한다.</p>
<p>데이터베이스 연결 정보나 Dialect 를 명확하게 지정해주면 해결된다. application.properties 또는 application.yml 파일에 데이터베이스 URL(jakarta.persistence.jdbc.url)을 설정하거나 직접 사용할 Dialect을 지정해주는 hibernate.dialect 속성을 추가해줘야 한다.</p>
<h4 id="db-url-applicationyml-에-추가">DB URL (application.yml 에 추가)</h4>
<pre><code>spring:
          datasource:
            url: jdbc:mysql://localhost:3306/your_database_name?zeroDateTimeBehavior=convertToNull&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Seoul
            username: user 이름 입력
            password: 패스워드 입력</code></pre><p>application.properties 에 추가한다면</p>
<pre><code>spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?zeroDateTimeBehavior=convertToNull&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Seoul
        spring.datasource.username=user 이름 입력
        spring.password=패스워드 입력</code></pre><p>DB URL 을 설정한 후에도 에러가 해결되지 않는다면 Dialect를 직접 지정해야 한다.</p>
<h4 id="dialect-직접-지정-applicationyml-에-추가">Dialect 직접 지정 (application.yml 에 추가)</h4>
<p>사용하는 데이터베이스에 맞는 Dialect 클래스를 선택하면 된다. (예시 MySQL)</p>
<pre><code>spring:
          jpa:
            properties:
              hibernate:
                dialect: org.hibernate.dialect.MySQLDialect</code></pre><p>application.properties 에 추가한다면</p>
<pre><code>spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect</code></pre></br>

<p><img src="https://velog.velcdn.com/images/min-zi/post/8bc93aec-cac4-49e9-ba09-00f936b31665/image.png" alt=""></p>
<p>배치 테이블이 드디어 생겼다 😂
근데 테이블은 만들어졌는데 아무 데이터 값이 들어오지 않는다. Job 이 실행되어야 BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_STEP_EXECUTION 등에 기록이 들어가는데 지금은 Job 이 실행 되지 않았기 때문에 테이블은 비어있는 것이다.</p>
<p></br></br></p>
<h2 id="✔️-job-bean-자동-실행">✔️ Job Bean 자동 실행</h2>
<h3 id="1-job-bean-이름과-springbatchjobname-이-일치해야-함">1. Job Bean 이름과 spring.batch.job.name 이 일치해야 함</h3>
<p>Spring Boot 는 시작할 때 applicationContext 에 등록된 Job 들을 확인한다. 애플리케이션 시작과 동시에 Job 이 실행되도록 하는 방법은 spring.batch.job.enabled=true 를 설정해주는 것이다. spring.batch.job.name 은 실행할 Job 이 하나일 때만 설정해주면 된다. properties 파일 또는 yml 파일에 추가해주면 된다.</p>
<p>application.properties 에 추가한다면</p>
<pre><code>spring.batch.job.name=passJob
spring.batch.job.enabled=true</code></pre><p>application.yml 에 추가한다면</p>
<pre><code>spring:
  batch:
    job:
      enabled: true
      name: passJob</code></pre></br>

<p>Spring Batch 애플리케이션을 그냥 main() 실행만 하면 Spring Boot Context 와 Batch 인프라(JOB REPOSITORY, TRANSACTION 등)는 등록이 되지만, Job 자체는 자동 실행이 되지 않는다. Batch Job 을 자동 실행하려면 JobLauncherApplicationRunner 가 빈으로 등록되어 있어야 한다. JobLauncher 를 통해 Job 을 실행해야 Step 이 돌아가는 구조다. 이 빈이 spring.batch.job.enabled=true 설정을 보고 Job 을 실행시킨다.</p>
</br>

<h3 id="2-bean-은-springbootapplication-클래스-안에-등록되어야-함">2. Bean 은 @SpringBootApplication 클래스 안에 등록되어야 함</h3>
<p>Application 클래스가 @SpringBootApplication 이고 같은 패키지 안에 Bean 등록이 되어 있어야 한다. 만약 패키지 구조가 다르면 컴포넌트 스캔 범위 밖이어서 실행되지 않을 수 있다.</p>
<p>추가적으로 @EnableBatchProcessing 은 기본 Batch 인프라(JobRepository, JobLauncher, JobExplorer, JobRegistry 등)를 자동으로 등록해주는 역할을 한다. Spring Boot 3 부터는 spring-boot-starter-batch 의존성이 기본적으로 자동으로 구성해주기 때문에 꼭 붙이지 않아도 돼서 JobLauncher 충돌이 사라졌다.
Batch Starter 와 spring.batch.job.enabled: true 로 설정해주었다면 자동 실행에 직접 JobLauncher 를 사용할 필요는 없다.</p>
</br>

<h3 id="3-jobparameters-중복-문제-해결">3. JobParameters 중복 문제 해결</h3>
<p>Spring Batch 는 이미 같은 파라미터로 실행된 기록이 있으면 AlreadyCompletedException 발생하거나 Job 이 실행되지 않는다. 자동 실행 시도할 때도 동일하게 JobParameters 가 없으면 실행 안 되는 경우가 생긴다.
이런 경우 Job 에 RunIdIncrementer 를 추가하면 매 실행마다 내부적으로 incrementing ID 를 붙여주어 중복 문제를 회피할 수 있다.</p>
<pre><code>@Bean
public Job passJob(JobRepository jobRepository, Step passStep) {
    return new JobBuilder(&quot;passJob&quot;, jobRepository)
            .incrementer(new RunIdIncrementer()) //여기
            .start(passStep)
            .build();
}</code></pre></br>

<h3 id="4-joblauncherapplicationrunner-명시적-실행">4. JobLauncherApplicationRunner 명시적 실행</h3>
<p>(Spring Boot 3 + Spring Batch 5 환경임)</p>
<ul>
<li>spring-boot-starter-batch 의존성 추가 되어있음</li>
<li>Job 과 Step 을 @Bean 으로 등록 되어있음</li>
<li>JobRepository 와 PlatformTransactionManager 를 직접 주입받아 JobBuilder, StepBuilder 생성했음</li>
<li>spring.batch.job.enabled=true 설정 되어있음</li>
<li>Job 이 하나라 spring.batch.job.name=passJob 도 설정 되어있음</li>
<li>JobParameters 중복일까 싶어 Job 에 RunIdIncrementer() 를 붙임</li>
<li>yml 의 spring.batch.job.name 과 Job Bean 이름 일치함</li>
</ul>
<p>되어 있는 설정으로는 자동 실행돼야 정상인데,</p>
<p>JobLauncherApplicationRunner 는 원래 별도로 클래스를 만들 필요가  없지만 실행되지 않고 실행할 Job 을 못 찾고 있다면 ApplicationRunner 를 통해 직접 JobLauncher 를 실행시켜줘야 된다. Job 이 확실히 실행되고 JobParameters 도 직접 관리 가능하고 배치 재실행, 재시도, 장애 대응이 쉬운 장점이 있다. 오히려 자동 실행보다 안정적일 수 있다.</p>
<pre><code>@Bean
    public ApplicationRunner runJob(Job passJob, JobLauncher jobLauncher) {
        return args -&gt; {
            jobLauncher.run(passJob, new JobParametersBuilder()
                    .addLong(&quot;run.id&quot;, System.currentTimeMillis()) // 매번 유니크한 파라미터
                    .toJobParameters());
        };
    }</code></pre></br>

<p><img src="https://velog.velcdn.com/images/min-zi/post/3a2cb612-d0ab-47fe-8fd7-30c99bcd777d/image.png" alt=""> Application.java 에 JobLauncher 를 추가해줬더니 메타 테이블에 드디어 데이터 값이 들어왔다. 이제 외부 DB 가 붙은 상태이다. 휴</p>
<p></br></br></p>
<h2 id="joblauncherapplicationrunner-가-자동-실행되지-않는-이유">JobLauncherApplicationRunner 가 자동 실행되지 않는 이유</h2>
<h3 id="1-runner-가-실행되기-전에-애플리케이션-종료">1. Runner 가 실행되기 전에 애플리케이션 종료</h3>
<p>Spring Boot 가 ApplicationContext 를 모두 초기화한 후, CommandLineRunner/ApplicationRunner 단계에서 JobLauncherApplicationRunner 가 Job 을 실행한다. 그런데 애플리케이션이 조기 종료되거나 Gradle/IDE 실행 모드에서 main 이 비동기적으로 종료되면 Runner 가 실행 기회를 갖지 못하고 애플리케이션이 종료된다.</p>
<h3 id="2-job-bean-조회-문제">2. Job Bean 조회 문제</h3>
<p>JobLauncherApplicationRunner 는 Job Bean 을 Spring 컨텍스트에서 찾아서 실행한다. Job Bean 이 제대로 등록되지 않았거나 Bean 등록 시점이 늦으면 실행 되지 않는다. @Configuration 클래스가 여러개이면 Runner 가 Job 을 찾지 못하는 경우도 있다.</p>
<h3 id="3-commandline-파라미터와-jobparameters-문제">3. CommandLine 파라미터와 JobParameters 문제</h3>
<p>spring.batch.job.name 을 지정하면 해당 Job 만 실행한다.
이미 DB에 같은 JobParameters 가 존재하면 JobLauncherApplicationRunner 는 중복 실행을 방지하기 위해 Job 실행을 스킵한다. RunIdIncrementer 가 있어도 Runner 가 실행 시점에 새 파라미터를 못 만드는 환경이라면 실행이 안 될 수도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Batch] 스프링 배치 4 와 5 차이점과 예제]]></title>
            <link>https://velog.io/@min-zi/Spring-Batch-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98-4-%EC%99%80-5-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EC%98%88%EC%8B%9C</link>
            <guid>https://velog.io/@min-zi/Spring-Batch-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98-4-%EC%99%80-5-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EC%98%88%EC%8B%9C</guid>
            <pubDate>Sat, 23 Aug 2025 03:27:37 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-spring-batch-4x-주로-spring-boot-2x-와-함께-사용">📌 Spring Batch 4.x (주로 Spring Boot 2.x 와 함께 사용)</h2>
<p>spring-boot-starter-batch 를 의존성에 넣으면 Job이 하나만 있는 경우 별도 코드를 작성하지 않아도 애플리케이션 실행 시 applicationContext 에 등록된 Job이 자동 실행된다.
여러 개의 Job이 있을 경우 spring.batch.job.names (또는 spring.batch.job.name) 설정으로 어떤 Job을 실행할지 지정 가능하다.</p>
<p>@EnableBatchProcessing 은 Spring Batch 핵심 인프라(JobRepository, JobLauncher, JobExplorer, JobRegistry 등)를 자동으로 등록해주는데, 과거 Spring Boot 2.x에서는 이 어노테이션을 꼭 붙여야 Job 자동 실행이 가능했다.</p>
<h3 id="spring-batch-4x-예제">Spring Batch 4.x 예제</h3>
<ul>
<li><p>stepBuilderFactory.get(&quot;stepName&quot;) get() 메서드 사용</p>
</li>
<li><p>@Autowired StepBuilderFactory stepBuilderFactory; 사용</p>
</li>
<li><p>JobBuilder / StepBuilder 필드 주입해서 사용</p>
<pre><code>public class PassBatchApplication {
  private final JobBuilder jobBuilder;
  private final StepBuilder stepBuilder;

  public PassBatchApplication(JobBuilder jobBuilder, StepBuilder stepBuilder) {
      this.jobBuilder = jobBuilder;
      this.stepBuilder = stepBuilder;
  }

  @Bean
  public Step passStep() {
      return this.stepBuilder.get(&quot;passStep&quot;)
          .tasklet(new Tasklet() {
              @Override
              public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                  System.out.println(&quot;Excute PassStep&quot;);
                  return RepeatStatus.FINISHED;
                  }
              }).build();
  }

  @Bean
  public Job passJob() {
      return this.jobBuilder.get(&quot;passJob&quot;)
          .start(passStep())
          .build();
  }</code></pre></li>
</ul>
<p></br></br></p>
<h2 id="📌-spring-batch-5x-주로-spring-boot-3x-와-함께-사용">📌 Spring Batch 5.x (주로 Spring Boot 3.x 와 함께 사용)</h2>
<p>Spring Boot 3.x 이후로는 자동 실행 정책이 조금 더 엄격해졌다. 아무 설정도 없으면 Job을 등록만 하고 실행하지 않는다. 여러 Job이 있을 때는 반드시 spring.batch.job.name=... 지정해야 실행된다.
따라서 JobLauncher 를 직접 쓰거나, spring.batch.job.name을 명시해야 Job 이 실행된다.</p>
<p>Spring Boot 3.x 이상에서는 대부분의 Batch 인프라를 Spring Boot Starter 가 자동으로 구성해 주기 때문에 @EnableBatchProcessing 을 꼭 붙이지 않아도 된다. 다만, 직접 JobBuilder 나 StepBuilder 를 사용할 때는 필요할 수 있다. (직접 Bean 등록 시 인프라가 없으면 예외 발생)</p>
<h3 id="spring-batch-5x-예제">Spring Batch 5.x 예제</h3>
<p>5에서는 JobBuilderFactory와 StepBuilderFactory가 삭제됐다.</p>
<ul>
<li><p>new JobBuilder(&quot;jobName&quot;, jobRepository) / new StepBuilder(&quot;stepName&quot;, jobRepository) 사용</p>
</li>
<li><p>JobRepository와 PlatformTransactionManager를 Bean 메서드 파라미터로 받음</p>
</li>
<li><p>new StepBuilder(&quot;...&quot;, jobRepository) get() 메서드가 제거됨</p>
</li>
<li><p>클래스에 @Configuration 추가. Bean 정의 클래스이므로 명시적으로 설정</p>
<pre><code>@Configuration
public class PassBatchApplication {

  @Bean
  public Step passStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder(&quot;passStep&quot;, jobRepository)
              .tasklet(new Tasklet() {
                  @Override
                  public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                      System.out.println(&quot;Execute PassStep&quot;);
                      return RepeatStatus.FINISHED;
                  }
              }, transactionManager)
              .build();
  }

  @Bean
  public Job passJob(JobRepository jobRepository, Step passStep) {
      return new JobBuilder(&quot;passJob&quot;, jobRepository)
              .start(passStep)
              .build();
  }
}</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Windows 와 WSL 실행 환경에 따른 docker container 차이]]></title>
            <link>https://velog.io/@min-zi/Spring-Windows-%EC%99%80-WSL-%EC%8B%A4%ED%96%89-%ED%99%98%EA%B2%BD%EC%97%90-%EB%94%B0%EB%A5%B8-docker-container-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@min-zi/Spring-Windows-%EC%99%80-WSL-%EC%8B%A4%ED%96%89-%ED%99%98%EA%B2%BD%EC%97%90-%EB%94%B0%EB%A5%B8-docker-container-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Fri, 15 Aug 2025 15:10:35 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-windows-에서-실행">📌 Windows 에서 실행</h2>
<p>터미널이나 PoswerShell, Git bash 등 make 또는 docker-compose 명령을 실행하면 컨테이너는 Windows 용 Docker 엔진을 돌린다. WSL 안에서 만든 컨테이너와는 완전히 별개다. 겉보기에는 Windows 에서 컨테이너를 띄우는 것 같지만, 실제로는 WSL2 Linux 커널 위에서 컨테이너가 돌아가고 있다.</p>
<h3 id="근데-windows-에서-mysql-컨테이너를-작업하는데-왜-wsl-이-쓰이는지">근데 windows 에서 mysql 컨테이너를 작업하는데 왜 wsl 이 쓰이는지</h3>
<p>Docker Desktop 의 작동 구조 때문에 그렇다.
Docker 는 리눅스 컨테이너 기술이 원래 <strong>Linux 커널 기능(Cgroups, Namespaces 등)</strong> 에 의존한다. Windows 에는 Linux 커널이 없으니, 예전엔 Hyper-V VM 으로 리눅스 환경을 흉내 냈었다. 하지만 지금의 Docker Desktop 은 성능 때문에 WSL2 기반 경량 Linux VM 을 사용한다. 즉, Windows 에서 MySQL 컨테이너를 실행해도, 백그라운드에서 WSL2 가 내부 리눅스 OS 를 돌려주고 그 안에서 MySQL 이 실행되는 거다.</p>
<p>흐름은 이렇게 된다.</p>
<pre><code>Windows (명령 실행)
   ↓
Docker CLI / Desktop
   ↓
WSL2 Linux VM (Docker Engine 실행)
   ↓
MySQL 컨테이너 (Linux 기반)</code></pre><br>

<h2 id="📌-wsl-에서-실행">📌 WSL 에서 실행</h2>
<p>WSL 안에서 make 또는 docker-compose 명령을 실행하면 WSL 내부에서 Docker 가 설치되어 있거나, Docker Desktop 이 WSL 통합을 켰을 경우 컨테이너는 WSL 내부 환경에서 돌아간다. WSL 내에서만 접근 가능한 네트워크, 파일 시스템 등을 활용 가능</p>
<br>

<h2 id="1-구조적인-차이">1. 구조적인 차이</h2>
<table border="1">
  <tr>
    <th style="width:20%;">구분</th>
    <th style="width:40%;">Windows에서 실행</th>  
    <th style="width:40%;">WSL에서 실행</th>
  </tr>
  <tr>
    <th>도커 엔진 위치</th>
    <th>Windows용 Docker Desktop 엔진 (Windows 커널 + Linux VM 위)</th>  
    <th>WSL2의 Linux 커널 위 (Docker Desktop 통합 또는 WSL 전용 도커)</th>
  </tr>
  <tr>
    <th>파일 경로</th>
    <th>Windows 파일 시스템(C:\...)</th>  
    <th>Linux 파일 시스템(/home/user/...)</th>
  </tr>
  <tr>
    <th>네트워크 환경</th>
    <th>Windows 네트워크 기준</th>  
    <th>WSL 내부 네트워크 기준</th>
  </tr>
  <tr>
    <th>성능</th>
    <th>Windows ↔ Linux VM 간 파일 I/O가 느릴 수 있음</th>  
    <th>WSL 내부 실행 시 Linux 네이티브와 유사한 속도</th>
  </tr>
  <tr>
    <th>볼륨 마운트</th>
    <th>C:\ 경로 마운트 시 성능 저하</th>  
    <th>/home 등 WSL 내부 경로 마운트 시 빠름</th>
  </tr>
</table>

<br>

<h2 id="2-성능-차이">2. 성능 차이</h2>
<ul>
<li><p>Windows에서 실행 
실제로는 Docker Desktop이 내부적으로 WSL2 VM을 사용하지만, Windows 경로를 마운트하면 I/O가 느려진다.
대규모 코드 변경, DB 데이터 파일 등에서 병목 가능</p>
</li>
<li><p>WSL에서 실행
WSL의 Linux 파일시스템에서 직접 실행하면 거의 리눅스 서버와 같은 속도
파일 접근이 빠르고, 개발 환경이 리눅스와 동일해 배포 테스트가 정확함</p>
<br>

</li>
</ul>
<h3 id="wsl-에서-파일-읽기-쓰기-네트워크-통신-데이터베이스-쿼리-같은-io-서비스가-빠른-이유는">WSL 에서 파일 읽기 쓰기, 네트워크 통신, 데이터베이스 쿼리 같은 I/O 서비스가 빠른 이유는</h3>
<p>Windows 는 파일시스템(C:...) 에 볼륨 마운트 → 리눅스 VM ↔ 윈도우 NTFS 파일시스템 변환 과정 때문에 느림</p>
<p>WSL 은 내부 경로(/home/user/...) 에 볼륨 마운트 → 리눅스 네이티브 ext4 파일시스템으로 변환 과정이 없음 → 속도 빠름</p>
<br>

<h2 id="3-개발-편의성-차이">3. 개발 편의성 차이</h2>
<ul>
<li><p>Windows
윈도우 경로 기반으로 IDE, 툴과 연동이 쉬움
하지만 서버 환경(리눅스)와 미묘하게 다를 수 있음</p>
</li>
<li><p>WSL
서버 환경과 거의 100% 동일
배포 시 발생할 수 있는 경로/권한 문제를 미리 발견 가능
단, IntelliJ에서 WSL 프로젝트를 열 때 설정이 조금 번거로울 수 있음</p>
</li>
</ul>
<p><br><br></p>
<hr>

<h2 id="요약">요약</h2>
<ul>
<li><p>Windows에서 실행 → 편하지만 성능, 환경 차이로 서버와 100% 동일하지 않음.
가벼운 테스트나 빠른 실행은 Windows에서 실행 추천</p>
</li>
<li><p>WSL에서 실행 → 리눅스 서버와 동일, 성능 좋음, 배포 전 테스트에 유리.
로컬에서 DB, 로그, 빌드 아티팩트처럼 I/O가 많은 데이터는 WSL에서 실행 추천</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 도커에서 MySQL 서버 올리기, DBeaver Access denied 에러 해결 방법]]></title>
            <link>https://velog.io/@min-zi/DBeaver-%EA%B3%84%EC%A0%95-%EC%83%9D%EC%84%B1%ED%95%B4%EC%84%9C-MySQL-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0-Access-denied-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@min-zi/DBeaver-%EA%B3%84%EC%A0%95-%EC%83%9D%EC%84%B1%ED%95%B4%EC%84%9C-MySQL-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0-Access-denied-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 06 Aug 2025 13:39:16 GMT</pubDate>
            <description><![CDATA[<p>Windows 에 Docker Desktop 만 설치 되어 있으면 Mysql 계정 생성이 가능하다. Docker 로 MySQL 컨테이너를 띄우는 방식이면 MySQL 계정이 만들어질 수 있다.</p>
<h4 id="전제-조건">전제 조건</h4>
<p>✔️Docker Desktop 이 설치되어 있어야 함
✔️docker-compose.yml 또는 Makefile 안에 MySQL 생성 관련 설정이 있어야 함
✔️make 명령이 Windows 또는 WSL 에서 사용 가능해야 함</p>
</br>

<p>Makefile 에서의 db-up 자체는 단순히</p>
<pre><code>db-up:
    docker-compose up -d --force-recreate</code></pre><p>만 실행할거고 (계정이나 DB가 생성되지 않음)</p>
<p><code>docker-compose.yml</code> 안에 지정된 환경변수(MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD)를 넣으면 컨테이너가 처음 시작될 때 자동으로 MySQL 계정과 DB가 생성된다. 이렇게 만들어진 계정은 컨테이너 최초 실행 시에 pass_local_user@&#39;%&#39; 어디서든 접근 가능한 계정이 자동 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/2d229ca8-9b6e-4a39-a683-4394ddbc73e5/image.png" alt=""></p>
<blockquote>
<p>Access denied for user &#39;pass_local_user&#39;@&#39;localhost&#39; (using password: YES)</p>
</blockquote>
<p>Access denied 는 크게 두 가지 이유가 있다.
비밀번호가 틀리거나 플러그인이 안 맞는 경우와 권한이 없는 경우이다.</p>
</br>

<p>추가적으로 DBeaver 연결 설정을 확인해볼 게 있다.</p>
<h2 id="dbeaver-연결-설정-확인">DBeaver 연결 설정 확인</h2>
<p>Username 과 Password 를 맞게 입력했는지,
Host 는 localhost 가 맞는지, (또는 127.0.0.1 둘 다 시도)
Port 는 3306,
Driver Properties: allowPublicKeyRetrieval = true, useSSL = false (useSSL false 는 로컬 개발 환경일 경우 설정해두면 편함)</p>
<p>다시 Test Connection 해보고 안된다면,</p>
</br>

<p>컨테이너 내부 MySQL 들어가기</p>
<pre><code>docker exec -it mysql_local mysql -u root -p</code></pre><h2 id="1-비밀번호가-틀리거나-플러그인이-안-맞는-경우">1. 비밀번호가 틀리거나 플러그인이 안 맞는 경우</h2>
<p>MySQL 8.0 은 기본이 caching_sha2_password 인데, DBeaver에서 연결 시 이 플러그인을 제대로 못 쓸 수도 있다. 우리가 mysql_native_password 로 바꿨으니 다시 한 번 현재 플러그인과 비밀번호가 제대로 저장됐는지 확인이 필요하다.</p>
<pre><code>SELECT user, host, plugin FROM mysql.user WHERE user = &#39;pass_local_user&#39;;</code></pre><p>여기서
host 가 localhost 인지,
plugin 이 mysql_native_password 인지 체크</p>
<p>만약 플러그인이 caching_sha2_password 라면 다시 바꿔줘야 한다.</p>
<pre><code>ALTER USER &#39;pass_local_user&#39;@&#39;localhost&#39; IDENTIFIED WITH mysql_native_password BY &#39;원하는 비밀번호 입력&#39;;
FLUSH PRIVILEGES;
</code></pre></br>

<h2 id="2-권한이-없는-경우">2. 권한이 없는 경우</h2>
<p>계정이 있어도 해당 DB 에 접근 권한이 없으면 로그인은 되더라도 DB 선택 시 거부된다.</p>
<p>현재 계정 권한 확인</p>
<pre><code>SELECT user, host FROM mysql.user WHERE user = &#39;pass_local_user&#39;;</code></pre><p>권한이 부족한지 확인</p>
<pre><code>SHOW GRANTS FOR &#39;pass_local_user&#39;@&#39;%&#39;;</code></pre><p>GRANT ALL PRIVILEGES ON `pass_local`.* TO &#39;pass_local_user&#39;@&#39;%&#39;` 이런게 있어야한다.</p>
<p>없다면 권한 다시 부여</p>
<pre><code>GRANT ALL PRIVILEGES ON pass_local.* TO &#39;pass_local_user&#39;@&#39;localhost&#39;;
FLUSH PRIVILEGES;</code></pre><p>그래도 안되면..</p>
<p>아예 기존 계정 삭제하고
새로 계정 만든 후 권한 부여도 다시 해보자</p>
<pre><code>-- 1. 기존 계정 삭제
DROP USER IF EXISTS &#39;pass_local_user&#39;@&#39;localhost&#39;;

-- 2. 새 계정 생성 (mysql_native_password로)
CREATE USER &#39;pass_local_user&#39;@&#39;localhost&#39;
IDENTIFIED WITH mysql_native_password BY &#39;새비밀번호 입력&#39;;

-- 3. 권한 부여 (pass_local 데이터베이스 전체 권한)
GRANT ALL PRIVILEGES ON pass_local.* TO &#39;pass_local_user&#39;@&#39;localhost&#39;;

-- 4. 변경 적용
FLUSH PRIVILEGES;</code></pre><br>
이렇게 하면 꼬인 플러그인/비밀번호 문제 싹 정리되고, DBeaver에서 바로 접속될 거다.]]></description>
        </item>
        <item>
            <title><![CDATA[[IntelliJ] 인텔리제이에서 Makefile 편집 시 탭이 스페이스로 바뀌지 않도록 설정하는 방법 - Makefile Language]]></title>
            <link>https://velog.io/@min-zi/IntelliJ-%EC%9D%B8%ED%85%94%EB%A6%AC%EC%A0%9C%EC%9D%B4%EC%97%90%EC%84%9C-Makefile-%ED%8E%B8%EC%A7%91-%EC%8B%9C-%ED%83%AD%EC%9D%B4-%EC%8A%A4%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A1%9C-%EB%B0%94%EB%80%8C%EC%A7%80-%EC%95%8A%EB%8F%84%EB%A1%9D-%EC%84%A4%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@min-zi/IntelliJ-%EC%9D%B8%ED%85%94%EB%A6%AC%EC%A0%9C%EC%9D%B4%EC%97%90%EC%84%9C-Makefile-%ED%8E%B8%EC%A7%91-%EC%8B%9C-%ED%83%AD%EC%9D%B4-%EC%8A%A4%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A1%9C-%EB%B0%94%EB%80%8C%EC%A7%80-%EC%95%8A%EB%8F%84%EB%A1%9D-%EC%84%A4%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 28 Jul 2025 16:59:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Makefile:6: *** missing separator. Stop.</p>
</blockquote>
<p>Makefile 에서는 명령어 실행 줄은 반드시 Tab 문자로 시작해야한다. 근데 이 missing separator 는 거의 100% 명령어 앞에 Tab 이 아니라 Space 가 들어갔을 때 발생한다.</p>
<h3 id="추가-확인">추가 확인</h3>
<ul>
<li>인텔리제이에서 Makefile 열어서 우측 하단 줄바꿈을 LF(Line Endings)로 변경 (CRLF → LF)</li>
<li>인코딩은 UTF-8 (BOM 없음) 으로 저장</li>
</ul>
<p></br></br>
인텔리제이에서 자동으로 스페이스로 바꿔버려서 탭이 깨지는 경우도 많다. 탭 강제 유지 설정 해두면 편하다길래 설정해주겠다.</p>
<h2 id="✅-인텔리제이-탭-유지-설정-플러그인-설치">✅ 인텔리제이 탭 유지 설정 플러그인 설치</h2>
<p><img src="https://velog.velcdn.com/images/min-zi/post/d71b6077-598e-4b1c-9b8e-16c4e9b094b3/image.png" alt="">
File → Settings → Plugins</p>
<p>Makefile Language (JetBrains에서 제공하는 플러그인) 검색해서 설치</p>
<p>설치 후 재시작</p>
<p>File → Settings → Editor → Code Style 목록에 Makefile 항목이 생김 → 여기서 &quot;Use tab character&quot; 체크하고 Indent 조정
<img src="https://velog.velcdn.com/images/min-zi/post/dd1c0bfc-c9fa-48f0-bd32-01ac5d84ee18/image.png" alt=""></p>
<p>안먹히던 Tap 이 이제 되고 Tap 표시도 뜨고 굿!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] WSL Ubuntu 내부에 Docker CLI 와 docker compose 플러그인 설치 방법]]></title>
            <link>https://velog.io/@min-zi/Spring-WSL-Ubuntu-%EB%82%B4%EB%B6%80%EC%97%90-Docker-CLI-%EC%99%80-docker-compose-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@min-zi/Spring-WSL-Ubuntu-%EB%82%B4%EB%B6%80%EC%97%90-Docker-CLI-%EC%99%80-docker-compose-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 28 Jul 2025 11:33:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Command &#39;make&#39; not found, but can be installed with:
sudo apt install make        # version 4.3-4.1build1, or
sudo apt install make-guile  # version 4.3-4.1build1</p>
</blockquote>
<p>Makefile 에 있는 도커 컴포즈 명령을 실행하려는데 make 명령 오류가 났다. Docker 는 Windows 에서 실행 중이고,
WSL Ubuntu 는 배포판만 연결된 상태이다.
그래서 Ubuntu 터미널에서 직접 docker 나 docker compose 같은 명령을 바로 쓸 수 있도록 환경 세팅을 해주려고 한다.</p>
<p>지금은 Windows PowerShell 에서만 docker 명령이 정상 작동.
하지만 WSL Ubuntu 안에서는 기본적으로 Docker CLI가 없어서 docker를 바로 쓸 수 없음.
</br></p>
<h2 id="🔨세팅-방법">🔨세팅 방법</h2>
<h3 id="1-docker-desktop-wsl-통합-확인">1. Docker Desktop WSL 통합 확인</h3>
<p>Docker Desktop → Settings → Resources → WSL Integration
→ Ubuntu 체크 ON → Apply &amp; Restart.
</br></p>
<h3 id="2-ubuntu-터미널에서-docker-cli-docker-compose-설치">2. Ubuntu 터미널에서 Docker CLI, docker compose 설치</h3>
<pre><code># 1. 패키지 업데이트
sudo apt update
sudo apt upgrade -y

# 2. 필수 패키지 설치
sudo apt install -y ca-certificates curl gnupg lsb-release

# 3. Docker GPG 키 등록
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 4. Docker 저장소 추가
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

# 5. 패키지 갱신 &amp; Docker CLI &amp; docker compose 설치
sudo apt update
sudo apt install -y docker-ce-cli</code></pre><p></br></br></p>
<h3 id="3-실행-테스트">3. 실행 테스트</h3>
<pre><code>which docker</code></pre><p>정상적인 WSL-Docker 연동 상태라면
which docker 결과 경로가 /usr/bin/docker 혹은 /usr/local/bin/docker 이어야 한다.
그럼 성공!</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/7decca2a-fe8c-42f1-8698-173d9867062e/image.png" alt=""> 만약 사진과 같다면 PATH를 수정해줘야 된다.
WSL(리눅스) 에 설치된 docker-cli 가 아니라 Windows 쪽 Docker Desktop의 docker.exe 를 WSL 에서 그대로 불러오고 있는 상태이다.
</br></p>
<h3 id="4-path-우선순위-수정">4. PATH 우선순위 수정</h3>
<p>Windows docker.exe 대신 리눅스 docker가 먼저 잡히도록</p>
<pre><code>echo &#39;export PATH=&quot;/usr/bin:$PATH&quot;&#39; &gt;&gt; ~/.bashrc
source ~/.bashrc</code></pre></br>

<h3 id="5-실행-테스트">5. 실행 테스트</h3>
<pre><code>docker version</code></pre><p>버전이 잘 출력되면 성공!</p>
<pre><code>docker compose up -d
docker ps</code></pre><p>컨테이너가 올라가면 성공!  인데</p>
<p>Docker 데몬 어쩌고 오류가 뜬다면</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/5ab73b9a-4925-4ad9-9840-b9d7c3b62ef9/image.png" alt="">  <img src="https://velog.velcdn.com/images/min-zi/post/9db50b27-f75b-4e7f-b79a-6bb63ff71d62/image.png" alt=""></p>
<p>Docker CLI는 설치되어 있지만 Docker 데몬과 연결이 안된 상태 (Cannot connect to the Docker daemon at unix:///var/run/docker.sock 오류 )
Docker Desktop 의 데몬을 WSL 내부에서 못 쓰고 있는 상태이다.
</br></p>
<h3 id="6-docker-데몬-연결">6. Docker 데몬 연결</h3>
<p><strong>1. Docker Desktop 이 WSL 내부에 소켓을 마운트했는지 확인</strong>
WSL Ubuntu 에서</p>
<pre><code>ls -l /var/run/docker.sock</code></pre><p><img src="https://velog.velcdn.com/images/min-zi/post/7bc94c4d-6121-4269-9600-ccdef90514a2/image.png" alt=""> Docker 데몬 소켓이 존재하고 권한도 설정됨
이렇게 나와야 정상.</p>
<h3 id="7연결-테스트">7.연결 테스트</h3>
<pre><code>docker info
docker ps</code></pre><p>정상 연결되면 Docker 클라이언트가 Docker Desktop 데몬과 정상적으로 통신되는 정보와 컨테이너 목록이 정상 작동한다.</p>
<p>이제 인텔리제이 WSL Ubuntu 터미널에서 바로 make, docker compose 명령 실행이 가능해진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] WSL 업데이트가 안돼서 기존 WSL 제거 후 수동 재설치 방법]]></title>
            <link>https://velog.io/@min-zi/Spring-Docker-WSL-needs-updating-WSL-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%98%A4%EB%A5%98%EC%99%80-%EA%B8%B0%EC%A1%B4-WSL-%EC%A0%9C%EA%B1%B0-%ED%9B%84-%EC%9E%AC%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@min-zi/Spring-Docker-WSL-needs-updating-WSL-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%98%A4%EB%A5%98%EC%99%80-%EA%B8%B0%EC%A1%B4-WSL-%EC%A0%9C%EA%B1%B0-%ED%9B%84-%EC%9E%AC%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 19 Jul 2025 17:44:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/min-zi/post/cfbce47d-5d17-4cba-b8ef-8ffa547446bb/image.png" alt=""></p>
<p>Docker WSL needs updating
도커 데스크탑에 windows 에 설치된 WSL 버전이 너무 오래되어서 Docker가 작동하지 않는 상태라고 메세지가 뜬다. 그래서 Docker 엔진도 멈춰 있고(왼쪽 아래 Engine stopped), Ubuntu랑 통합도 제대로 작동하지 못하는 상황이다.</p>
</br>

<h2 id="✅-wsl-업데이트">✅ WSL 업데이트</h2>
<h3 id="1-관리자-권한으로-powershell-실행">1. 관리자 권한으로 PowerShell 실행</h3>
<p>시작 메뉴 → PowerShell 검색 → 오른쪽 클릭 → 관리자 권한으로 실행</p>
<h3 id="2-아래-명령어-입력">2. 아래 명령어 입력</h3>
<p>WSL의 커널(kernel) 을 최신 버전으로 업데이트해준다.</p>
<pre><code>wsl --update</code></pre><h3 id="3-완료되면-컴퓨터-재시작">3. 완료되면 컴퓨터 재시작</h3>
<p><img src="https://velog.velcdn.com/images/min-zi/post/8b66e6a3-4503-43ee-83bd-373ea2edbf37/image.png" alt=""></p>
<p>똑같은 에러..
wsl2 커널 업데이트가 누락되는 것 같다.</p>
<h2 id="✅-wsl-완전히-제거-후-재설치">✅ WSL 완전히 제거 후 재설치</h2>
<p>wsl 이 꼬여 있나 싶어서 완전히 제거하고 다시 설치하기로 했다.</p>
<h3 id="1-관리자-권한인-powershell-에서-제거">1. 관리자 권한인 PowerShell 에서 제거</h3>
<p>docker 용 가상 wsl 리소스 삭제하는거다.</p>
<pre><code>wsl --unregister docker-desktop
wsl --unregister docker-desktop-data</code></pre><h3 id="2-wsl-전체-기능-재설치">2. WSL 전체 기능 재설치</h3>
<pre><code>dism.exe /online /disable-feature /featurename:Microsoft-Windows-Subsystem-Linux /norestart
dism.exe /online /disable-feature /featurename:VirtualMachinePlatform /norestart</code></pre><p>그 다음 다시 설치</p>
<pre><code>dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart</code></pre><h3 id="3-wsl_update_x64msi-실행">3. wsl_update_x64.msi 실행</h3>
<p>👉👉 <a href="https://learn.microsoft.com/ko-kr/windows/wsl/install-manual#step-4---download-the-linux-kernel-update-package"><code>wsl_update_x64.msi</code> 파일 다운로드</a></p>
<p>설치 후 다시 명령어 확인</p>
<pre><code>wsl --update</code></pre><p><img src="https://velog.velcdn.com/images/min-zi/post/72a1c241-d351-4a38-8168-0326e2fef394/image.png" alt="">
커널 문제보다 WSL 실행 파일(wsl.exe) 자체가 구버전이라 그런가 그냥 wsl 을 최신 버전으로 수동 재설치를 해주겠다.</p>
<h2 id="✅-wsl--ubuntu-수동-설치--등록-방법-windows-10-기준">✅ WSL + Ubuntu 수동 설치 &amp; 등록 방법 (Windows 10 기준)</h2>
<ul>
<li>필요한 설치 파일들
<a href="https://github.com/microsoft/WSL/releases">wsl.exe 최신 버전</a> (깃헙에서 <code>.msixbundle</code> 최신 버전 다운로드) 예) WSL_2.6.0.0_x64_ARM64.msixbundle
<a href="https://aka.ms/wslubuntu2204">Ubuntu 22.04 설치 파일</a> (Ubuntu.appx 확장자의 파일)</li>
</ul>
<h3 id="1-최신-wsl-실행파일-설치">1. 최신 WSL 실행파일 설치</h3>
<p>관리자 권한으로 PowerShell 실행해서 파일 설치 명령어 입력</p>
<pre><code>Add-AppxPackage .\wsl-2.6.0.0_x64_ARM64.msixbundle</code></pre><h3 id="2-ubuntu-appx-에서-appxbundle-파일과-리눅스-루트-파일-추출">2. Ubuntu Appx 에서 .AppxBundle 파일과 리눅스 루트 파일 추출</h3>
<p>이미 받은 Ubuntu2204-221101.AppxBundle 파일을 7-Zip 압축 프로그램으로 열면 안에 여러 .appx 파일이 들어있는데 Ubuntu_2204_XXXX_x64.appx 파일을 찾는다. 이 .appx 파일을 다른 폴더로 끌어내준다.
예) C:\Users\사용자\Downloads\UbuntuUnpack 등으로 풀기</p>
<p>추출한 .appx 파일을 다시 7-Zip 으로 열면 안에
install.tar.gz (구버전) 혹은 ext4.vhdx (신버전) 같은 루트 파일이 있다. 이 파일을 적당한 다른폴더로 꺼내둔다.
예) D:\WSL\Ubuntu\install.tar.gz 위치에 저장.</p>
<p><a href="https://www.7-zip.org/">7-zip 설치</a></p>
<h3 id="3-wsl-배포판으로-등록">3. WSL 배포판으로 등록</h3>
<p>관리자 권한으로 PowerShell 실행해서</p>
<pre><code>wsl --import Ubuntu D:\WSL\Ubuntu D:\WSL\Ubuntu\install.tar.gz --version 2</code></pre><h3 id="4-확인">4. 확인</h3>
<pre><code>wsl --list --verbose</code></pre><p>Ubuntu 가 Version 2로 뜨면 성공!</p>
<h3 id="5-실행-및-초기-설정">5. 실행 및 초기 설정</h3>
<pre><code>wsl -d Ubuntu</code></pre><p>이제 사용자 계정과 비밀번호를 설정하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/6ac9add6-637f-49af-baf6-beb0a85c9f18/image.png" alt=""></p>
<h2 id="ubuntu-새-계정-추가">Ubuntu 새 계정 추가</h2>
<h3 id="1-새-사용자-계정-추가">1. 새 사용자 계정 추가</h3>
<p>wsl Ubuntu 터미널에서 입력</p>
<pre><code>adduser 사용자이름</code></pre><p>비밀번호와 사용자 정보 입력하라는 메시지가 나오는데 비밀번호는 꼭 입력하고 추가 사용자 정보(Full Name, Room Number 등) 그냥 ENTER 눌러서 건너 뛰어도 된다. 마지막에 &quot;Is the information correct? [Y/N]&quot; 물으면 Y 입력하면 완료!</p>
<h3 id="2-sudo-권한-부여">2. sudo 권한 부여</h3>
<p>root 권한이 있는 상태에서</p>
<pre><code>usermod -aG sudo 사용자이름</code></pre><h3 id="3-기본-로그인-계정-변경">3. 기본 로그인 계정 변경</h3>
<p>wsl 기본 계정을 새로 만든 계정으로 바꿔준다.</p>
<pre><code>nano /etc/wsl.conf</code></pre><p>들어가서 아래 내용을 추가해준다.</p>
<pre><code>[user]
default=사용자이름</code></pre><p>저장 후 닫기 (nano는 Ctrl+O → Enter → Ctrl+X)</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/1b2c6a7a-0bc8-4203-b2be-c1c5b6e308b4/image.png" alt=""></p>
<h3 id="4-wsl-재시작">4. WSL 재시작</h3>
<p>powershell 새 창 열어서</p>
<pre><code>wsl --shutdown</code></pre><p>그리고 다시</p>
<pre><code>wsl -d Ubuntu</code></pre><p>새로 만든 계정으로 로그인이 된다.</p>
<pre><code>whoami</code></pre><p>새로 만든 계정 이름이 나오면 성공!
<img src="https://velog.velcdn.com/images/min-zi/post/7d152166-896c-46f8-ac7b-301b036b5cb9/image.png" alt=""></p>
<h2 id="✅-docker-desktop과-wsl-ubuntu-연결">✅ Docker Desktop과 WSL Ubuntu 연결</h2>
<ol>
<li>Docker Desktop 실행</li>
<li>오른쪽 위 톱니바퀴⚙️(Settings) → Resources → WSL Integration</li>
<li>Enable integration with my default WSL distro 체크</li>
<li>Ubuntu (방금 설치한 배포판) 옆 체크박스 ON</li>
<li>Apply 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/min-zi/post/ff1b8c8f-4949-45f9-a818-8fdb3436d6ef/image.png" alt=""></p>
<p>docker 명령어를 확인해보면 버전 정보가 잘 나온다.</p>
<p>여기까지 되면
이제 docker-compose 도 바로 사용 가능
IntelliJ 에서도 도커 연결해서 DB 컨테이너 띄울 수 있음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] QueryDsl Qclass 사용 이유]]></title>
            <link>https://velog.io/@min-zi/Spring-QueryDsl-Qclass-%EC%82%AC%EC%9A%A9-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@min-zi/Spring-QueryDsl-Qclass-%EC%82%AC%EC%9A%A9-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 17 Oct 2024 10:56:51 GMT</pubDate>
            <description><![CDATA[<p>jpa 가 기본적으로 제공해주는 crud 와 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 jpql 을 작성하게 된다.</p>
<p>간단한 로직을 작성하는데는 큰 문제가 없다. 하지만 복잡한 로직의 경우엔 쿼리 열이 상당히 길어지고 jpql 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생한다.</p>
<p>이러한 문제를 어느 정도 해소하고자 안정성에 많은 노력을 한 프레임워크가 바로 QueryDsl 이다.</p>
<br>

<h1 id="querydsl">QueryDsl</h1>
<p>: 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크</p>
<h2 id="qclass">QClass</h2>
<p>기본적으로 QueryDsl 을 사용할때 QClass 가 생성이 된다.
엔티티 클래스의 메타 정보를 담고 있는 클래스로 Querydsl 은 이를 이용하여 타입 안정성(Type safe) 을 보장하면서 쿼리를 작성할 수 있게 된다.</p>
<p>Qclass 는 엔티티 클래스와 대응되며 엔티티의 속성을 나타내고 있다. 이러한 Qclass 를 사용하여 쿼리를 작성하면 엔티티 속성을 직접 참조하고 조합하여 쿼리를 구성할 수 있다. Qclass 를 사용하면 컴파일 시점에 오류를 확인할 수 있고 IDE 의 자동 완성 기능을 활용하여 쿼리 작성을 보다 편리하게 할 수 있다.</p>
<p>그냥 Entity 를 사용해도 되는데 굳이 QClass 를 만들어서 사용하는 이유는 뭘까? JPA_APT(JPAAnnotationProcessorTool)가 @Entity 와 같은 특정 어노테이션을 찾고 해당 클래스를 분석해서 QClass 를 만들어 준다.
엔티티 클래스는 데이터베이스 테이블의 매핑을 담당하고, QClass는 쿼리 작성을 위한 편의성과 안전성을 제공을 해주면서 유지보수의 편의성 및 실수 방지를 하지 않도록 해준다고 생각한다.</p>
<h3 id="apt">APT</h3>
<p>Annotation 이 있는 기존코드를 바탕으로 새로운 코드와 새로운 파일들을 만들 수 있고, 이들을 이용한 클래스에서 compile 하는 기능도 지원해준다.
쉬운 예시로는 Lombok의 @Getter, @Setter가 있다. 해당 어노테이션을 사용하는 경우 apt가 컴파일 시점에 해당 어노테이션을 기준으로 getter 와 setter를 만들어 주기 때문에 코드를 작성하지 않고 사용이 가능해진다.</p>
<h2 id="qclass를-사용하는-이유">Qclass를 사용하는 이유</h2>
<ul>
<li><p>Qclass 는 엔티티 속성을 정적인 방식으로 표현하므로 자동 완성 기능 등 IDE의 도움을 받을 수 있고 속성 이름을 직접 기억하거나 확인하지 않아도 된다.</p>
</li>
<li><p>엔티티 속성의 타입을 정확하게 표현하므로 타입에 맞지 않는 연산이나 비교를 시도하면 컴파일 단계에서 에러를 쉽게 잡을 수 있다.</p>
</li>
<li><p>동적인 쿼리 작성이 편리하다.</p>
</li>
<li><p>쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] QueryDsl QClass 파일 생성이 안될 때, QClass 생성 오류 해결 방법]]></title>
            <link>https://velog.io/@min-zi/Spring-QueryDsl-QClass-%ED%8C%8C%EC%9D%BC-%EC%83%9D%EC%84%B1-%EC%95%88%EB%90%A0-%EB%95%8C-%EC%83%9D%EC%84%B1-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@min-zi/Spring-QueryDsl-QClass-%ED%8C%8C%EC%9D%BC-%EC%83%9D%EC%84%B1-%EC%95%88%EB%90%A0-%EB%95%8C-%EC%83%9D%EC%84%B1-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Tue, 15 Oct 2024 08:37:02 GMT</pubDate>
            <description><![CDATA[<p>Querydsl을 사용하기 위해서는 다소 번거로운 Gradle 설정 및 사용법 등을 익혀야한다.</p>
<p>Q클래스가 import 자체가 안돼서 생성이 안되는 오류 해결 방법들을 정리해두려고 한다. Q클래스 import 오류는 보통 Querydsl 설정 문제로 인해 발생한다.
</br></p>
<hr>
</br>

<h2 id="gradle-설정으로-들어가기-전-확인사항">Gradle 설정으로 들어가기 전 확인사항</h2>
<p><strong>IDE: IntelliJ, Build Tool: Gradle 사용</strong></p>
<h3 id="✅-자바-버전-확인">✅ 자바 버전 확인</h3>
<p>Querydsl 5.0 이상에서는 Java 17 이상을 권장한다.
터미널에서 명령어로 프로젝트의 Java 버전 확인해보기.
<code>java -version</code>
만약 Java 8, 11이라면 17 이상으로 변경 후 다시 빌드해보자.</p>
<h3 id="✅-annotation-processing-활성화-여부-확인">✅ Annotation Processing 활성화 여부 확인</h3>
<p>사용 중인 IDE(IntelliJ, Eclipse 등)에서 annotation processing 기능이 활성화되어 있는지 확인해보자
File - Settings - Build, Execution, Deployment - Compiler - Annotation Processors
<strong>&quot;Enable annotation processing&quot; 옵션 체크</strong></p>
<h3 id="✅-gradle-빌드-시스템-설정">✅ Gradle 빌드 시스템 설정</h3>
<p>File - Settings - Build, Execution, Deployment - Build Tools - Gradle
<strong>&quot;Build and run using&quot; 을 Gradle 로 설정</strong></p>
<h3 id="✅-q클래스-생성-경로-직접-설정했을-때만-해당-ide-에서-소스-폴더로-등록되어-있는지-확인-intellij-idea-기준">✅ (Q클래스 생성 경로 직접 설정했을 때만 해당) IDE 에서 소스 폴더로 등록되어 있는지 확인 (IntelliJ IDEA 기준)</h3>
<p>Q클래스가 생성되었는데도 import가 안 된다면, IntelliJ에서 소스 폴더로 인식되지 않았을 수 있다.
File - Project Structure - Modules - Sources 탭 이동
좌측의 폴더 트리에서 Q클래스 생성 경로로 지정한(ex: src/main/generated) 폴더가 파란색으로 표시되는지 확인한다. 만약 폴더가 일반 폴더(회색)로 표시된다면 해당 폴더를 오른쪽 커서로 클릭 후 &quot;Sources&quot; 버튼을 클릭해서 소스 폴더로 지정한다.</p>
<p>Apply 후 File - Invalidate Caches/Restart 옵션을 사용해 캐시를 정리하고 재빌드를 해보자.</p>
</br>

<h1 id="1-buildgradle-groovy-dsl-설정-확인">1. build.gradle (Groovy DSL) 설정 확인</h1>
<h3 id="querydsl-관련-의존성을-추가했는지-확인-의존성-버전-확인">Querydsl 관련 의존성을 추가했는지 확인, 의존성 버전 확인</h3>
<pre><code>implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39;
implementation &quot;com.querydsl:querydsl-core&quot;
implementation &quot;com.querydsl:querydsl-collections&quot;
annotationProcessor &quot;com.querydsl:querydsl-apt:${dependencyManagement.importedProperties[&#39;querydsl.version&#39;]}:jpa&quot; // querydsl JPAAnnotationProcessor 사용 지정
annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot; // java.lang.NoClassDefFoundError (javax.annotation.Generated) 에러 대응 코드
annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot; // java.lang.NoClassDefFoundError (javax.annotation.Entity) 에러 대응 코드</code></pre><p><strong>dependencyManagement.importedProperties[&#39;querydsl.version&#39;]</strong> 값이 올바르게 설정되어 있는지 확인해볼까?
터미널에서 이 명령어를 실행하면 querydsl 관련 의존성 버전을 확인할 수 있다.
<code>./gradlew dependencyInsight --dependency querydsl-apt</code>
이 명령어는 실제로 어떤 버전이 적용되고 있는지 상세하게 보여준다.
<img src="https://velog.velcdn.com/images/min-zi/post/bf1caab2-498b-4d60-8d0e-ac21d2e61069/image.png" alt="">
터미널 출력 결과 일부를 캡쳐해왔다. 보면 com.querydsl:querydsl-apt의 버전은 5.0.0으로 올바르게 사용되고 있는 것이 맞다.</p>
<h3 id="q클래스-생성-위치-및-경로-annotation-processor-및-source-set-설정">Q클래스 생성 위치 및 경로 (Annotation Processor 및 Source Set 설정)</h3>
<p>Querydsl이 Q클래스를 생성하려면, annotation processor가 정상적으로 동작해야 한다.</p>
<p>build.gradle 파일에서 생성 디렉토리를</p>
<pre><code>// Querydsl 설정부
def generated = &#39;src/main/generated&#39;

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
    main.java.srcDirs += [ generated ]
}</code></pre><p>로 지정했으므로 Q 클래스는 src/main/generated 폴더에 생성되어야 한다.</p>
<p>현재 build.gradle에서 Querydsl JPA 의존성은</p>
<pre><code>implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39;</code></pre><p>로 jakarta 버전을 사용하고 있지만,</p>
<pre><code>annotationProcessor &quot;com.querydsl:querydsl-apt:${dependencyManagement.importedProperties[&#39;querydsl.version&#39;]}:jpa&quot;</code></pre><p>는 jakarta 버전이 아니라 javax 버전을 참조할 수 있다.
Querydsl APT 의존성도 jakarta 버전으로 일치시켜야 한다.</p>
<p>이렇게 하면 APT가 jakarta.persistence.Entity를 참조하게 되어 NoClassDefFoundError 문제가 해결될 가능성이 높다.
즉, 모든 Querydsl 관련 의존성이 동일하게 jakarta 버전을 사용하도록 일관되게 맞춰주면 문제 해결에 도움이 된다.</p>
<h2 id="2-엔티티-스캔-범위-설정">2. 엔티티 스캔 범위 설정</h2>
<p>main 함수가 있는 &lt;프로젝트명&gt;Application 클래스에
<code>@SpringBootApplication(scanBasePackages = &quot;com.ming.projectboard&quot;)</code>
를 추가하는 것을 권장한다. 이는 Spring Boot가 해당 패키지 하위의 모든 컴포넌트와 엔티티를 제대로 스캔하게 도와준다.</p>
<h2 id="3-빌드-후-q클래스-생성-확인">3. 빌드 후 Q클래스 생성 확인</h2>
<p>터미널에서 아래 명령어로 Gradle 빌드 수행
<code>./gradlew clean compileJava</code> 또는 <code>./gradlew clean build</code> 
</br></p>
<p>생성된 파일 확인해보기.
빌드 후 프로젝트 디렉토리에서 src/main/generated 폴더를 열어보고 Q&lt;엔티티명&gt;.java 파일이 정상적으로 생성되었는지 확인해본다.</p>
<p>캐시 문제를 배제하기 위해 클린 빌드도 해보기
<code>./gradlew clean compileJava --refresh-dependencies</code></p>
</br>
</br>

<h4 id="그래도-파일이-없으면">그래도 파일이 없으면?</h4>
<p>Q클래스가 생성되지 않는 주요 원인은 Querydsl이 엔티티를 인식하지 못하는 경우일 것이다.
Querydsl 설정이 잘못되었는지 다시 확인해보기.
@Entity 를 선언한 JPA 엔티티 클래스가 없는건 아닌지, @Entity가 올바르게 선언은 되어 있는지 확인해보기.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] SpringBoot의 MockMvc 단위 테스트]]></title>
            <link>https://velog.io/@min-zi/Spring-SpringBoot%EC%9D%98-MockMvc</link>
            <guid>https://velog.io/@min-zi/Spring-SpringBoot%EC%9D%98-MockMvc</guid>
            <pubDate>Wed, 05 Jun 2024 09:22:32 GMT</pubDate>
            <description><![CDATA[<h2 id="mockmvc-">mockMvc ?</h2>
<p>실제 객체와 비슷하지만 Controller 테스트에 필요한 기능만 가지는 가짜 객체를 만들어서 애플리케이션 배포하지 않아도 스프링 MVC 패턴 및 동작을 테스트 할 수 있는 클래스이다.</p>
<p>단위 테스트를 위해서는 MockMvc 라는 객체가 사용된다.
MockMvc 는 테스트를 위해 브라우저나 WAS 의 동작을 똑같이 처리해 줄 수 있는 환경이라고 생각하면 된다. MockMvc 를 이용해 브라우저에서 발생하는 요청을 가상으로 만들고 Controller 가 응답하는 내용을 기반으로 검증을 수행한다.</p>
<h3 id="단위-테스트를-작성할-때-확인-할-내용들">단위 테스트를 작성할 때 확인 할 내용들</h3>
<ul>
<li>요청 경로에 대해 적절한 handler method 가 호출되었는지?</li>
<li>입력 파라미터는 handler method 에 잘 전달되는지?</li>
<li>model 에 설정한 값은 잘 참조되는지?</li>
<li>요청 결과 페이지는 잘 연결되는지?</li>
</ul>
<br>



<h1 id="mockmvc의-메소드">mockMvc의 메소드</h1>
<h2 id="요청-만들기">요청 만들기</h2>
<h3 id="1-perform--요청-가상의-request-를-처리함">1. perform : 요청, 가상의 request 를 처리함</h3>
<p>요청은 MockMvcRequestBuilders 의 static 메소드인 get, post, put, delete 등을 이용해서 MockHttpServletRequestBuilder 객체를 생성하는 것에서 시작한다.</p>
<p>MockHttpServletRequestBuilder 는 ServletRequest 를 구성하기에 필요한 메소드를 제공한다.</p>
<table border="1" width=700>
<tr>
  <td>get</td>
  <td>요청을 전송한다.
결과로 ResultActions 객체를 받으며, <br>
인자로는 경로를 보내주는데 HTTP 메소드를 결정할 수 있다. (get(), post(), put(), delete())</td>
</tr>
<tr>
  <td>param / params</td>
  <td>요청 파라미터를 설정한다.</td>
</tr>
<tr>
  <td>header / headers</td>
  <td>요청 헤더를 설정한다.</td>
</tr>
<tr>
  <td>cookie</td>
  <td>쿠키를 설정한다.</td>
</tr>
<tr>
  <td>contentType</td>
  <td>Enum 인 MediaType 으로 요청의 컨텐트 타입을 설정한다.</td>
</tr>
<tr>
  <td>file</td>
  <td>fileUpload 로 ServletRequestBuilder 를 생성한 경우 업로드 파일을 지정한다.</td>
</tr>
</table>

<pre><code>private final MockMvc mvc;

mvc.perform(get(&quot;/add&quot;))</code></pre><p><br><br></p>
<h2 id="검증하기">검증하기</h2>
<h3 id="2-expect--결과-가상의-response-에-대해-검증함">2. expect : 결과, 가상의 response 에 대해 검증함</h3>
<p>perform 의 결과로 받은 ResultActions 객체는 andExcpect() 메소드에 RequltMatcher 를 넘겨줘서 리턴 값을 검증하고 확인할 수 있다.</p>
<table border="1" width=700>
<tr>
  <td>handler</td>
  <td>요청에 매핑된 컨트롤러를 검증한다.</td>
</tr>
<tr>
  <td>header</td>
  <td>응답 헤더의 값을 검증한다.</td>
</tr>
<tr>
  <td>cookie</td>
  <td>응답을 통해 전달된 쿠키를 검증한다.</td>
</tr>
<tr>
  <td>content</td>
  <td>응답의 본문 내용을 검증한다.</td>
</tr>
<tr>
  <td>view</td>
  <td>Controller 의 handler method 가 반환한 view 의 이름을 검증한다. <br>
  view().name("forward:/add") : 리턴하는 뷰 이름이 add 가 맞나?</td>
</tr>
<tr>
  <td>model</td>
  <td>model 에 담긴 attribute 값을 검증한다.</td>
</tr>
<tr>
  <td>forwardedUrl / forwardedUrlPattern</td>
  <td>forward 로 이동하는 대상의 경로를 검증한다.</td>
</tr>
<tr>
  <td>redirectedUrl / redirectedUrlPattern</td>
  <td>redirect 로 이동하는 대상의 경로를 검증한다.</td>
</tr>
<tr>
  <td>status</td>
  <td>Http 상태 코드를 이용해 검증한다. <br>
  isOk() : 200 <br>
isNotFound() : 404 <br>
isMethodNotAllowed() : 405 <br>
isInternalServerError() : 500 <br>
is(int status) : status 상태 코드
  </td>
</tr>
</table>

<pre><code>private final MockMvc mvc;

mvc.perform(get(&quot;/add&quot;))
        .andExpect(status().isOk())
        .andExpect(view().name(&quot;forward:/add&quot;))
        .andExpect(forwardedUrl(&quot;/add&quot;))
        .andExpect(model().attribute(&quot;message&quot;, &quot;어쩌고저쩌고&quot;))</code></pre><p><br><br></p>
<h2 id="실행하기">실행하기</h2>
<h3 id="3-do--테스트-과정에서-콘솔-출력-등-직접-처리할-일을-작성함">3. do : 테스트 과정에서 콘솔 출력 등 직접 처리할 일을 작성함</h3>
<p>ResultActions 객체의 andDo 메소드를 이용한다. 파라미터로 ResultHandler 를 전달하는데 MockMvcResultHandlers 에 static 메소드로 정의되어있다.</p>
<table border="1" width=700>
<tr>
  <td>print</td>
  <td>실행 결과를 출력한다. <br>
    요청/응답 전체 메세지를 확인할 수 있다.</td>
</tr>
</table>

<pre><code>private final MockMvc mvc;

mvc.perform(get(&quot;/add&quot;))
        .andExpect(status().isOk())
        .andDo(MockMvcResultHandlers.print());</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Servlet] Forward 와 Redirect 차이]]></title>
            <link>https://velog.io/@min-zi/Selvlet-Forward-%EC%99%80-Redirect-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@min-zi/Selvlet-Forward-%EC%99%80-Redirect-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Tue, 04 Jun 2024 16:53:15 GMT</pubDate>
            <description><![CDATA[<p>JSP 환경에서 현재 작업중인 페이지에서 다른 페이지로 이동하는 페이지 전환 기능에 두가지 방식이 있다.</p>
<h1 id="forward">Forward</h1>
<p><strong>(URL 변화 X , 객체 재사용 O)</strong>
forward 방식은 이동한 URL 로 요청 정보를 그대로 전달한다. 말 그대로 forward(건네주기) 하는 것이다. 그렇기 때문에 클라이언트가 최초로 요청한 총 1번의 브라우저 요청 정보는 다음 URL 에서도 유효하다.</p>
<ul>
<li>웹 컨테이너에서의 페이지의 이동, 웹 브라우저는 다른 페이지로 이동했는지 알 수 없다.</li>
<li>웹 브라우저는 URL만 표시되고, 이동한 페이지의 URL 정보는 볼 수가 없다.</li>
<li>현재 실행중인 페이지와 forward 에 의해 호출 될 페이지는 request, response 객체를 공유한다.</li>
</ul>
<br>

<h1 id="redirect">Redirect</h1>
<p><strong>URL 변화 O , 객체 재사용 X</strong>
redirect 방식의 경우 최초 요청을 받은 URL1 에서 클라이언트에 redirect 할 URL2 를 리턴하고, 클라이언트에게 새로운 요청을 생성하여 URL2 에 다시 요청을 보낸다. 따라서 처음 보냈던 요청 정보는 더 이상 유효하지 않게 된다.
새로운 페이지에서는 request, response 객체가 새롭게 생성된다.</p>
<ul>
<li>웹 컨테이너는 redirect 명령이 들어오면 웹 브라우저에게 다른 페이지로 이동하라는 명령을 내린다.</li>
<li>웹 브라우저는 URL 을 지시된 주소로 바꾸고 그 주소로 이동한다.</li>
<li>다른 웹 컨테이너에 있는 주소로 이동이 가능하다.</li>
</ul>
<p><br><br></p>
<h2 id="forward-와-redirect-요청-처리-방식">Forward 와 Redirect 요청 처리 방식</h2>
<p>예를 들어 브라우저 URL 요청 순서가 URL1-&gt;URL2-&gt;URL3 이면,</p>
<p>forward 는 스프링 내부에서 알아서 진행하고 URL3 이라는 결과를 보내준다.
redirect 는 브라우저가 URL1 요청을 보냄 -&gt; URL2 를 결과값으로 브라우저가 받음 -&gt; 그럼 다시 URL2 로 (GET)요청을 보냄 -&gt; URL3 이라는 결과를 받음</p>
<h2 id="사용-방법">사용 방법</h2>
<p>예를 들어 브라우저가 POST 요청을 보낸 경우
forward 를 사용하면 응답페이지에서 새로고침을 하게 됐을 때 문제가 된다. 서버에서 알아서 GET 응답 페이지를 준 것이므로 새로고침을 하면 POST 요청을 한 번 더 하게 되는 것이다.
redirect 를 사용하면 새로고침을 해도 두번째 보낸 요청을 새로고침 한 것이기 때문에 응답페이지 GET 요청(redirect는 GET 요청)을 새로고침 한 것이라 괜찮다.</p>
<p><strong>시스템에 변화가 생기지 않는 단순 조회(리스트 목록 보기, 검색)의 경우는 forward 방식을 사용함</strong></p>
<p><strong>시스템(Session, DB)에 변화가 생기는 요청 DELETE, PUT(로그인, 회원가입, 글쓰기)의 경우 redirect 방식을 사용함</strong></p>
<p>redirect는 어쨌든 다시 요청을 보내야 하고 HTTP request 를 다시 보내야 하니까 <strong>forward는 redirect보다 빠르다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IntellJ] SpringBoot 포트 번호 변경]]></title>
            <link>https://velog.io/@min-zi/IntellJ-SpringBoot-%ED%8F%AC%ED%8A%B8-%EB%B2%88%ED%98%B8-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@min-zi/IntellJ-SpringBoot-%ED%8F%AC%ED%8A%B8-%EB%B2%88%ED%98%B8-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Sat, 01 Jun 2024 16:41:36 GMT</pubDate>
            <description><![CDATA[<p>이미 열려 있는 포트를 다른 프로세스에서 또 열려고 하면 포트 번호 중복 오류가 발생한다.</p>
<h2 id="중복된-포트-번호-변경하는-방법">중복된 포트 번호 변경하는 방법</h2>
<p><em>(intellij community 사용중)</em></p>
<p>Web server failed to start. Port 8080 was already in use.
8080 포트 번호는 이미 사용 중이라는 오류가 발생했다.</p>
<p>프로젝트의 src - main - resources - application.properties 파일에서
server.port=원하는 포트 번호 작성</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/d8c1b3fd-9590-436e-8a51-8571e49d202c/image.PNG" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IntelliJ] 인텔리제이 community Git 연동 설정]]></title>
            <link>https://velog.io/@min-zi/IntelliJ-%EC%9D%B8%ED%85%94%EB%A6%AC%EC%A0%9C%EC%9D%B4-community-%EC%97%90%EC%84%9C-Git-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@min-zi/IntelliJ-%EC%9D%B8%ED%85%94%EB%A6%AC%EC%A0%9C%EC%9D%B4-community-%EC%97%90%EC%84%9C-Git-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Thu, 09 May 2024 07:13:02 GMT</pubDate>
            <description><![CDATA[<p>인텔리제이 ultimate 버전을 30일 무료 체험판으로 갖고있는 이메일들로 연명해왔었다..... 돌고돌아 다시 무료버전인 community Editiond 을 설치했다..</p>
<h3 id="1-설치했었던-git의-실행파일-경로를-지정">1. 설치했었던 Git의 실행파일 경로를 지정</h3>
<p>file - Settings - Version Control - Git 클릭
Path to Git executable 에 설치되어있는 Git의 실행파일 경로를 지정</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/ece0a611-908a-4908-acd9-a17503eab972/image.png" alt=""></p>
<h3 id="2-github-계정-연동">2. GitHub 계정 연동</h3>
<p>이제 Version Control - Github 클릭
Add account - Log In with Token 클릭
개인 토큰(Personal Acesss Token) 을 발급받고 해당 토큰을 통해 인증하는 방식이다. 토큰 인증방식으로 접근하여 커밋과 푸쉬를 진행할 수 있다.
<img src="https://velog.velcdn.com/images/min-zi/post/ddd31620-d69f-4719-9af3-cbd976ad3c60/image.png" alt="">Generate  클릭<img src="https://velog.velcdn.com/images/min-zi/post/64420df2-7356-403a-91a3-6edbc9c17679/image.png" alt="">
깃허브 사이트에서 본인 settings - Developer settings 로 접속해도 된다.
Personal access tokens - Tokens - Generate new token 
<img src="https://velog.velcdn.com/images/min-zi/post/8538dc19-397c-4b11-85d2-9aeef3342883/image.png" alt=""> </p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/1d05fb3b-fb94-4216-a2dc-e3b7d3a5efc9/image.png" alt="">
<strong>Note</strong>
발급 받으려는 토큰이 무슨 용도의 토큰인지 이름을 지어주는 것</p>
<p><strong>Expiration</strong>
토큰의 만료기간을 설정
기본적으로 1주일, 30일, 60일, 90일 단위로 만료기간의 설정이 가능</p>
<p><strong>Select scopes</strong>
해당 토큰에 어느 범위(scope)까지 접근 허용을 할 것인지 부여할 권한을 선택
이 부분은 각자 상황에 맞게 설정</p>
<p>해당 부분을 체크 하고</p>
<ul>
<li>workflow, admin:org, gist, user
Generate toke 버튼을 눌러 토큰을 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/min-zi/post/a41cb85c-0582-4f26-af71-07016704ced4/image.png" alt=""> 생성된 토큰을 복사(생성된 토큰은 재확인이 어렵기 때문에 따로 메모해두기)</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/89090caa-dec5-4857-b2b4-df57803c85be/image.png" alt=""> 복사해 온 토큰을 인텔리제이에 붙여넣기</p>
<p><img src="https://velog.velcdn.com/images/min-zi/post/d3876338-91a2-458d-b949-60cb70611a06/image.png" alt=""> 연동 성공 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] Merge conflict 병합 충돌 해결 방법]]></title>
            <link>https://velog.io/@min-zi/Git-Merge-conflict-%EB%B3%91%ED%95%A9-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@min-zi/Git-Merge-conflict-%EB%B3%91%ED%95%A9-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 19 Mar 2024 16:03:57 GMT</pubDate>
            <description><![CDATA[<p>feature 를 따서 작업을 하고 main branch 에 merge 하는 과정에서 한 파일에서의 수정한 부분들이 충돌이 발생했다.
conflicts 편집기를 사용해서 this branch has conflicts that must be resolved 해결할 수 있다.</p>
<h2 id="merge-conflict-해결-방법">Merge conflict 해결 방법</h2>
<ol>
<li><p>[Resolve conflicts] 버튼을 눌러 충돌이 있는 파일들을 확인한다.
<img src="https://velog.velcdn.com/images/min-zi/post/703632f3-f9aa-4a53-bb02-4c6114cb6b4c/image.png" alt=""></p>
</li>
<li><p>branch 의 변경 내용만 유지하거나, 다른 branch 의 변경 내용만 유지하거나, 두 branch 의 변경 내용을 통합할 수 있는 완전히 새로운 변경을 수행할지 결정한다.
충돌 표식 &lt;&lt;&lt;&lt;&lt;&lt;&lt;, =======, &gt;&gt;&gt;&gt;&gt;&gt;&gt;를 삭제하고 최종 merge 에서 원하는 대로 변경한다.</p>
</li>
<li><p>충돌을 해결하고 [Mark as resolved] 버튼을 눌러준다.
<img src="https://velog.velcdn.com/images/min-zi/post/5873e03a-e1d9-4414-b167-7a9ff7144c6d/image.png" alt=""></p>
</li>
<li><p>모든 merge conflict 이 해결됐으면 [Commit merge] 버튼을 눌러 main branch 에 merge 시켜준다.
<img src="https://velog.velcdn.com/images/min-zi/post/cc839bff-0d05-4bfb-a8f2-fa86b24292b8/image.png" alt=""></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring-Lombok] @RequiredArgsConstructor란?]]></title>
            <link>https://velog.io/@min-zi/Spring-Lombok-RequiredArgsConstructor%EB%9E%80</link>
            <guid>https://velog.io/@min-zi/Spring-Lombok-RequiredArgsConstructor%EB%9E%80</guid>
            <pubDate>Wed, 21 Feb 2024 11:10:44 GMT</pubDate>
            <description><![CDATA[<h3 id="1-requiredargsconstructor-를-사용한-예시">1. @RequiredArgsConstructor 를 사용한 예시</h3>
<pre><code>@RequiredArgsConstructor
@Service
public class ArticleController {

    private final ArticleService articleService;

    ...
}</code></pre><h3 id="2-requiredargsconstructor-를-사용하지-않고-생성자-주입-코드를-모두-작성한-경우의-예시">2. @RequiredArgsConstructor 를 사용하지 않고 생성자 주입 코드를 모두 작성한 경우의 예시</h3>
<pre><code>public class ArticleController {

    private final ArticleService articleService;

  @Autowired
  public ArticleController(ArticleService articleService) {
    this.ArticleRepository = ArticleRepository;
  }</code></pre><p>보통 DI(의존성 주입)을 방식에는 필드 주입(Field Injection), 수정자 주입(Setter Injection), 생성자 주입(Constructor Injection)의 3가지의 방법이 있는데 이중에서 가장 권장하는 의존성 주입은 생성자 주입 방식이다.</p>
<p>final 로 DI(의존성 주입)를 할 때 필요한 객체를 선언하여 생성자를 만들어주어야 한다. 하지만 생성자 주입을 위한 코드를 직접 작성하는 부분에서 번거로움이 존재한다.</p>
<p>그래서 많이들 사용하는 Lombok에서 @Getter, @Setter 어노테이션 처럼 <code>@RequiredArgsConstructor</code> 어노테이션은 클래스에 선언된 final 변수들, 필드들을 매개변수로 하는 생성자를 자동으로 생성해준다. 코드도 간결해지고 의존성 주입을 자동으로 처리하여 객체 생성 시 필요한 의존성을 보다 편리하게 주입할 수 있는 장점이 있다.</p>
]]></description>
        </item>
    </channel>
</rss>