<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_gyeongmin.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 23 Jul 2025 05:51:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. dev_gyeongmin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_gyeongmin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[스트림(Stream)]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%8A%A4%ED%8A%B8%EB%A6%BCStream</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%8A%A4%ED%8A%B8%EB%A6%BCStream</guid>
            <pubDate>Wed, 23 Jul 2025 05:51:04 GMT</pubDate>
            <description><![CDATA[<h2 id="기존-방식의-한계">기존 방식의 한계</h2>
<p>자바에서 배열이나 컬렉션을 다루기 위해 흔히 for문이나 Iterator를 사용하곤 했습니다. 하지만 다음과 같은 문제점이 존재합니다.</p>
<table>
<thead>
<tr>
<th>기존 방식의 문제점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>코드 가독성 저하</td>
<td>반복문이 중첩되거나 조건문이 섞이면 코드가 길고 복잡해짐</td>
</tr>
<tr>
<td>데이터 소스별 API 차이</td>
<td>List, Set, 배열 등 각 데이터 소스마다 다른 방식으로 접근해야 함</td>
</tr>
<tr>
<td>재사용 어려움</td>
<td>반복문 기반 로직은 추상화가 어려워 재사용성이 낮음</td>
</tr>
</tbody></table>
<p>이러한 문제를 해결하고자 도입된 것이 바로 <strong>Stream API</strong>입니다.</p>
<h2 id="스트림stream이란">스트림(Stream)이란?</h2>
<p>스트림은 자바 8부터 도입된 기능으로, <strong>데이터 소스를 추상화하여 일관된 방식으로 처리</strong>할 수 있도록 도와주는 API입니다. 배열, 컬렉션, 파일 등 다양한 소스를 대상으로 동일한 연산이 가능하도록 합니다.</p>
<ul>
<li><p>데이터 소스: 처리 대상 (예: 배열, 컬렉션, 파일 등)</p>
</li>
<li><p>중간 연산: map(), filter(), sorted() 등. 연산 결과로 새로운 스트림 반환</p>
</li>
<li><p>최종 연산: forEach(), collect(), count() 등. 연산을 종료하고 결과를 반환</p>
</li>
</ul>
<h3 id="스트림의-주요-특징">스트림의 주요 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 변경 없음</td>
<td>스트림은 원본 데이터를 변경하지 않고 읽기만 함</td>
</tr>
<tr>
<td>일회성 사용</td>
<td>스트림은 한 번 소비되면 재사용 불가</td>
</tr>
<tr>
<td>내부 반복</td>
<td>반복 로직을 메서드 내부로 숨겨 코드 간결화</td>
</tr>
<tr>
<td>지연 연산(Lazy Evaluation)</td>
<td>최종 연산이 수행되기 전까지 중간 연산은 실행되지 않음</td>
</tr>
<tr>
<td>기본형 스트림 제공</td>
<td>오토박싱/언박싱 비용을 줄이기 위해 IntStream, LongStream 등 지원</td>
</tr>
<tr>
<td>병렬 처리 지원</td>
<td>parallel() 호출 시 병렬 스트림 생성 (Fork/Join 프레임워크 기반)</td>
</tr>
</tbody></table>
<h2 id="스트림-만들기">스트림 만들기</h2>
<h3 id="컬렉션">컬렉션</h3>
<p>컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다. 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.</p>
<pre><code class="language-java">List&lt;Integer&gt; list = Arrays.asList(1,2,3,4,5);
Stream&lt;Integer&gt; intStream = list.stream();</code></pre>
<h3 id="배열">배열</h3>
<p>배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 static 메서드로 정의되어 있다.</p>
<pre><code class="language-java">Stream&lt;String&gt; strStream = Stream.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;);
Stream&lt;String&gt; strStream = Stream.of(new String[]{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;});
Stream&lt;String&gt; strStream = Arrays.stream(new String[]{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;});
Stream&lt;String&gt; strStream = Arrays.stream(new String[]{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;}, 0, 3);</code></pre>
<p>int, long, double과 같은 기본형 배열을 소스로 하는 스트림을 생성하는 메서드도 있다. (IntStream, LongStream, DoubleStream)</p>
<pre><code class="language-java">IntStream IntStream.of(int ...values)
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)</code></pre>
<h3 id="임의의-수">임의의 수</h3>
<p>난수를 생성하는데 사용하는 Random 클래스에는 해당 타입의 난수들로 이루어진 스트림을 반환하는 인스턴스 메서드들이 포함되어 있다.</p>
<ul>
<li><p>ints() : Integer.MIN_VALUE ~ Integer.MAX_VALUE</p>
</li>
<li><p>longs() : Long.MIN_VALUE ~ Long.MAX_VALUE</p>
</li>
<li><p>doubles() : 0.0 ~ 1.0</p>
</li>
</ul>
<pre><code class="language-java">// 무한 스트림
IntStream ints(); 
LongStream longs();
DoubleStream doubles();

IntStream intStream = new Random().ints();
intStream.limit(5).forEach(System.out::println); // 무한 스트림을 5개로 제한

// 유한 스트림
IntStream ints(long streamSize);
LongStream longs(long streamSize);
DoubleStream doubles(long streamSize);

IntStream intStream = new Random().ints(5);</code></pre>
<h3 id="특정-범위의-정수">특정 범위의 정수</h3>
<p>IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.</p>
<pre><code class="language-java">IntStream IntStream.range(int begin, int end) // end 포함 x
IntStream IntStream.rangeClosed(int begin, int end) // end 포함 o

IntStream.range(1, 5) // 1, 2, 3, 4
IntStream.rangeClosed(1, 5) // 1, 2, 3, 4, 5</code></pre>
<p>지정된 범위의 난수를 발생시키는 스트림을 얻는 메서드도 있다.</p>
<pre><code class="language-java">IntStream ints(int begin, int end); 
LongStream longs(long begin, long end);
DoubleStream doubles(double begin, double end);

IntStream ints(long streamSize, int begin, int end); 
LongStream longs(long streamSize, long begin, long end);
DoubleStream doubles(long streamSize, double begin, double end);</code></pre>
<h3 id="람다식-iterate-generate">람다식 iterate(), generate()</h3>
<p>Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.</p>
<ul>
<li>iterate()는 seed로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed로 해서 계산을 반복한다.</li>
</ul>
<pre><code class="language-java">static &lt;T&gt; Stream&lt;T&gt; iterate(T seed, UnaryOperator&lt;T&gt; f)

Stream&lt;Integer&gt; eventStream = Stream.iterate(0, n -&gt; n + 2); // 0, 2, 4, 6, ...</code></pre>
<ul>
<li>generate()는 이전 결과를 이용해서 다음 요소를 계산하지 않는다.</li>
</ul>
<pre><code class="language-java">static &lt;T&gt; Stream&lt;T&gt; generate(Supplier&lt;T&gt; s)

Stream&lt;Double&gt; randomStream = Stream.generate(Math::random);
Stream&lt;Integer&gt; oneStream = Stream.generate(() -&gt; 1);</code></pre>
<p>한 가지 주의할 점은 iterate()와 generate()에 의해 생성된 스트림을 아래와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없다는 것이다. 굳이 필요하다면, mapToInt()와 같은 메서드로 변환을 해야 한다.</p>
<pre><code class="language-java">IntStream eventStream = Stream.iterate(0, n -&gt; n + 2).mapToInt(Integer::valueOf);
Stream&lt;Integer&gt; stream = eventStream.boxed(); // IntStream -&gt; Stream&lt;Integer&gt;</code></pre>
<h3 id="빈-스트림">빈 스트림</h3>
<p>요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다. 스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.</p>
<pre><code class="language-java">Stream emptyStream = Stream.empty();
long count = emptyStream.count(); // 0</code></pre>
<h2 id="스트림의-연산">스트림의 연산</h2>
<p>스트림이 제공하는 다양한 연산을 이용하면 복잡한 작업들을 간단히 처리할 수 있다. 스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것을 <strong>연산(operation)</strong>이라고 한다. 스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있다.</p>
<ul>
<li><p><strong>중간 연산</strong> : 연산결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다.</p>
</li>
<li><p><strong>최종 연산</strong> : 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.</p>
</li>
</ul>
<pre><code class="language-java">stream.distinct().limit(5).sorted().forEach(System.out::println)

// 중간 연산 : distinct, limit, sorted
// 최종 연산 : forEach</code></pre>
<h2 id="스트림의-중간-연산">스트림의 중간 연산</h2>
<h3 id="sorted">sorted()</h3>
<p>스트림을 정렬할 때 사용한다.</p>
<pre><code class="language-java">Stream&lt;T&gt; sorted()
Stream&lt;T&gt; sorted(Comparator&lt;? super T&gt; comparator)</code></pre>
<ul>
<li><p>지정된 Comparator로 스트림을 정렬하는데, Comparator 대신 int 값을 반환하는 람다식을 사용하는 것도 가능하다.</p>
</li>
<li><p>Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다. 단, 스트림의 요소가  Comparable을 구현한 클래스가 아니면 예외가 발생한다.</p>
</li>
</ul>
<pre><code class="language-java">Stream&lt;string&gt; strStream = Stream.of(&quot;dd&quot;, &quot;aaa&quot;, &quot;CC&quot;, &quot;cc&quot;, &quot;b&quot;);
strStream.sorted().forEach(System.out::print) // CCaaabccdd</code></pre>
<h3 id="comparator의-메서드">Comparator의 메서드</h3>
<p>JDK1.8부터 Comparator 인터페이스에 정렬 관련 static 메서드와 디폴트 메서드가 많이 추가되었다. 이 메서드들은 모두 <code>Comparator&lt;T&gt;</code> 를 반환한다. 가장 기본적인 메서드는 comparing()이다.</p>
<pre><code class="language-java">// 스트림의 요소가 Comparable을 구현한 경우
comparing(Function&lt;T, U&gt; keyExtractor)
// 스트림의 요소가 Comparable을 구현하지 않은 경우
comparing(Function&lt;T, U&gt; keyExtractor, Comparator&lt;U&gt; keyComparator)</code></pre>
<p>비교대상이 기본형인 경우, 오토박싱과 언박싱 과정이 없어서 더 효율적인 메서드를 사용한다.</p>
<pre><code class="language-java">comparingInt(ToIntFunction&lt;T&gt; keyExtractor);
comparingLong(ToLongFunction&lt;T&gt; keyExtractor);
comparingDouble(ToDoubleFunction&lt;T&gt; keyExtractor);</code></pre>
<p>정렬 조건을 추가할 때는 thenComparing()을 사용한다. Comparator.naturalOrder()은 기본 정렬(Comparable)을 사용한다.</p>
<pre><code class="language-java">studentStream.sorted(Comparator.comparing(Student::getBan)
                .thenComparing(Comparator.naturalOrder()))
                .forEach(System.out::println);</code></pre>
<h3 id="peek">peek()</h3>
<p>연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶을 때 사용한다. forEach()와 달리 스트림 요소를 소모하지 않는다.</p>
<pre><code class="language-java">fileStream.map(File::getName)
    .filter(s -&gt; s.indexOf(&#39;.&#39;) != -1)
    .peek(s -&gt; System.out.printf(&quot;filename=%s%n&quot;, s))
    .map(s -&gt; s.substring(s.indexOf(&#39;.&#39;) + 1))
    .peek(s -&gt; System.out.printf(&quot;extension=%s%n&quot;, s))
    .forEach(System.out::println);</code></pre>
<h3 id="flatmap">flatMap()</h3>
<p>스트림의 타입이 <code>Stream&lt;T[]&gt;</code> 인 경우, <code>Stream&lt;T&gt;</code> 로 변환해야 할 때 사용한다. (평탄화)</p>
<pre><code class="language-java">Stream&lt;String[]&gt; strArrStrm = Stream.of(
    new String[]{&quot;abc&quot;, &quot;def&quot;, &quot;jkl&quot;},
    new String[]{&quot;ABC&quot;, &quot;GHI&quot;, &quot;JKL&quot;}
);

Stream&lt;String&gt; strStrm = strArrStrm.flatMap(Arrays::stream);</code></pre>
<h2 id="optional">Optional</h2>
<p><code>Optional&lt;T&gt;</code> 은 T타입의 객체를 감싸는 래퍼 클래스이다. 제네릭 타입이므로 Optional 타입의 객체에는 모든 타입의 객체를 담을 수 있다. java.util.Optional은 JDK1.8부터 추가되었다. 최종 연산을 Optional 객체에 담아서 반환하면 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리할 수 있다.</p>
<pre><code class="language-java">public final class Optional&lt;T&gt; {
    private final T value;
}</code></pre>
<h3 id="optional-객체-생성하기">Optional 객체 생성하기</h3>
<p>Optional 객체를 생성할 때는 of() 또는 ofNullable()을 사용한다. 참조변수의 값이 null일 가능성이 있으면 ofNullable()을 사용한다. of()는 매개변수의 값이 null이면 NullPointerException이 발생한다.</p>
<pre><code class="language-java">Optional&lt;String&gt; optVal = Optional.of(&quot;abc&quot;);
Optional&lt;String&gt; optVal = Optional.ofNullable(null);</code></pre>
<p><code>Optional&lt;T&gt;</code> 타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다.</p>
<pre><code class="language-java">Optional&lt;String&gt; optVal = null // 가능하나 바람직하지 않음
Optional&lt;String&gt; optVal = Optional.empty();</code></pre>
<h3 id="optional-객체의-값-가져오기">Optional 객체의 값 가져오기</h3>
<p>Optional 객체에 지정된 값을 가져올 때는 get()을 사용한다. 값이 null일 때는 NoSuchElementException이 발생하며, 이를 대비해서 orElse()로 대체할 값을 지정할 수 있다.</p>
<pre><code class="language-java">optVal.get().orElse(&quot;&quot;);</code></pre>
<p>orElse()의 변형으로는 null을 대체할 값을 반환하는 람다식을 지정할 수 있는 orElseGet()과 null일 때 지정된 예외를 발생시키는 orElseThrow()가 있다.</p>
<pre><code class="language-java">optVal.get().orElseGet(String::new);
optVal.get().orElseThrow(NullPointerException::new);</code></pre>
<p>isPresent()는 Optional 객체의 값이 null이면 false를, 아니면 true를 반환한다. ifPresent()은 값이 있으면 주어진 람다식을 실행하고, 없으면 아무 일도 하지 않는다.</p>
<pre><code class="language-java">// isPresent()
if (Optional.ofNullable(str).isPresent()) {
    System.out.println(str);
}

// ifPresent()
Optional.ofNullable(str).ifPresent(System.out::println);</code></pre>
<h3 id="optionalint-optionallong-optionaldouble">OptionalInt, OptionalLong, OptionalDouble</h3>
<p>IntStream과 같은 기본형 스트림의 최종 연산의 일부는 Optional 대신 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.</p>
<pre><code class="language-java">// IntStream에 정의된 메서드 일부
OptionalInt findAny()
OptionalInt findFirst()
OptionalInt reduce(IntBinaryOperator op)
OptionalInt max()
OptionalInt min()
OptionalDouble average()</code></pre>
<p>기본형 Optional에서 값을 가져올 때는 다음과 같은 메서드를 사용한다.</p>
<table>
<thead>
<tr>
<th>Optional 클래스</th>
<th>값을 반환하는 메서드</th>
</tr>
</thead>
<tbody><tr>
<td>OptionalInt</td>
<td>int getAsInt()</td>
</tr>
<tr>
<td>OptionalLong</td>
<td>long getAsLong()</td>
</tr>
<tr>
<td>OptionalDouble</td>
<td>double getAsDouble()</td>
</tr>
</tbody></table>
<p>기본형 int의 기본값이 0 이라고 해도 0을 지정한 것과 empty()를 통해 기본값을 지정한 것과는 다르다.</p>
<pre><code class="language-java">// OptionalInt 코드 일부
public final class OptionalInt {
    ...
    private final boolean isPresent;
    private final int value;

OptionalInt opt = OptionalInt.of(0); // 0을 저장
OptionalInt opt2 = OptionalInt.empty(); // 0을 저장

opt.isPresent() // true;
opt2.isPresent() // false;</code></pre>
<h2 id="스트림의-최종연산">스트림의 최종연산</h2>
<h3 id="조건검사">조건검사</h3>
<p>조건검사 관련 메서드들은 아래와 같다.</p>
<pre><code class="language-java">boolean allMatch (Predicate&lt;? super T&gt; predicate) // 모든 요소가 일치하면 참
boolean anyMatch (Predicate&lt;? super T&gt; predicate) // 하나의 요소라도 일치하면 참
boolean noneMatch (Predicate&lt;? super T&gt; predicate) // 모든 요소가 불일치하면 참

// 예시
boolean noFailed = stuStream.anyMatch(s -&gt; s.getTotalScore() &lt;= 100);</code></pre>
<p>스트림의 요소 중에서 조건에 일치하는 첫 번째 요소를 반환하는 findFirst()가 있는데, 주로 filter()와 함께 사용되어 조건에 맞는 스트림의 요소가 있는지 확인하는데 사용된다. 병렬 스트림인 경우에는 findFirst()대신 findAny()를 사용해야 한다.</p>
<pre><code class="language-java">Optional&lt;Student&gt; stu = stuStream.filter(s -&gt; s.getTotalScore() &lt; 100)
    .findFirst();
Optional&lt;Student&gt; stu = parallelStream.filter(s -&gt; s.getTotalScore() &lt; 100)
    .findAny();</code></pre>
<h3 id="reduce">reduce()</h3>
<p>매개변수가 1개일 때는 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 반환 타입은 <code>Optional&lt;T&gt;</code> 이다.</p>
<pre><code class="language-java">Optional&lt;T&gt; reduce(BinaryOperator&lt;T&gt; accumulator)

// 예시
String[] strArr = {
    &quot;Inheritance&quot;, &quot;Java&quot;, &quot;Lambda&quot;, &quot;stream&quot;,
    &quot;OptionalDouble&quot;, &quot;IntStream&quot;, &quot;count&quot;, &quot;sum&quot;
};

IntStream intStream = Stream.of(strArr).mapToInt(String::length);
OptionalInt max = intStream.reduce(Integer::max);</code></pre>
<p>매개변수가 2개일 때는 초기값(identity)를 지정할 수 있으며, 초기값과 스트림의 첫 번째 요소로 연산을 시작한다. 이 경우 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로, 반환 타입이 T이다.</p>
<pre><code class="language-java">T reduce(T identity, BinaryOperator&lt;T&gt; accumulator)

// 예시
int count = intStream.reduce(0, (a,b) -&gt; a + 1);</code></pre>
<h3 id="collect와-collectors">collect()와 Collectors</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
<th>형태</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>collect()</code></td>
<td>스트림의 <strong>최종 연산</strong>으로, 요소들을 가공해 하나의 결과로 수집</td>
<td>메서드</td>
<td><code>Stream&lt;T&gt;</code>에서 호출 가능</td>
</tr>
<tr>
<td><code>Collector</code></td>
<td><code>collect()</code>에서 사용할 수 있는 <strong>수집 전략</strong>을 정의한 인터페이스</td>
<td>인터페이스</td>
<td>직접 구현 가능 (고급 활용 시)</td>
</tr>
<tr>
<td><code>Collectors</code></td>
<td><code>Collector</code> 구현체들을 <strong>정적 메서드 형태로 제공</strong>하는 유틸리티 클래스</td>
<td>클래스</td>
<td><code>toList()</code>, <code>joining()</code>, <code>groupingBy()</code> 등 제공</td>
</tr>
</tbody></table>
<p>스트림의 모든 요소를 컬렉션에 수집하려면, Collectors 클래스의 toList()와 같은 메서드를 사용하면 된다. List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 원하는 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.</p>
<pre><code class="language-java">// toList
List&lt;String&gt; names = stuStream.map(Student::getName)
    .collect(Collectors.toList());

// toCollection
ArrayList&lt;String&gt; list = names.stream()
    .collect(Collectors.toCollection(ArrayList::new));</code></pre>
<p>Map은 키와 값의 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 한다.</p>
<pre><code class="language-java">// toMap
Map&lt;String, Person&gt; map = personStream
    .collect(Collectors.toMap(p -&gt; p.getRegId(), p -&gt; p));</code></pre>
<p>스트림에 저장된 요소들을 <code>T[]</code> 타입의 배열로 변환하려면, toArray()를 사용하면 된다. 해당 타입의 생성자 참조를 매개변수로 지정해줘야 하며, 지정하지 않으면 반환되는 배열의 타입은 <code>Object[]</code> 이다.</p>
<pre><code class="language-java">// 매개변수 지정
Student[] stuNames = studentStream.toArray(Student[]::new);

// 매개변수 지정 X
Object[] stuNames = studentStream.toArray();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[람다식(Lambda Expression)]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda-Expression</link>
            <guid>https://velog.io/@dev_gyeongmin/%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda-Expression</guid>
            <pubDate>Mon, 21 Jul 2025 06:56:44 GMT</pubDate>
            <description><![CDATA[<h2 id="람다식이란">람다식이란?</h2>
<ul>
<li>간단히 말해서 메서드를 하나의 식(expression)으로 표현한 것이다. 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 익명 함수(anonymous function)라고도 한다.</li>
</ul>
<pre><code class="language-java">int[] arr = new int[5];
Arrays.setAll(arr, (i) -&gt; (int)(Math.random() * 5) + 1);</code></pre>
<ul>
<li><p>자바에서는 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 메서드를 호출할 수 있다. 그러나 람다식은 이 모든 과정이 필요없다.</p>
</li>
<li><p>람다식은 값처럼 다룰 수 있어 메서드의 매개변수, 반환값이 될 수 있다. (함수형 프로그래밍)</p>
</li>
<li><p>람다식은 익명 클래스의 객체와 동등하다. (MyFunction은 함수형 인터페이스)</p>
</li>
</ul>
<pre><code class="language-java">(a, b) -&gt; a &gt; b ? a : b

new MyFunction() {
    int max(int a, int b) {
        return a &gt; b ? a : b;
    }
}</code></pre>
<h2 id="함수형-인터페이스functional-interface">함수형 인터페이스(Functional Interface)</h2>
<ul>
<li>함수형 인터페이스를 구현한 익명 객체를 람다식으로 대체 가능하다. 대체가 가능한 이유는, 람다식도 실제로는 익명 객체이고, 함수형 인터페이스를 구현한 익명 객체의 메서드와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다.</li>
</ul>
<pre><code class="language-java">// 함수형 인터페이스
@FunctionalInterface
interface MyFunction {
  int max(int a, int b); // 추상 메서드
}

// 1. 함수형 인터페이스를 구현한 익명 객체
MyFunction f = new MyFunction() {
    public int max(int a, int b) {
        return a &gt; b ? a : b;
    }
};
int big = f.max(5, 3); // 익명 객체의 메서드 호출

// 2. 람다식
MyFunction f = (int a, int b) -&gt; a &gt; b ? a : b;
int big = f.max(5, 3) // 익명 객체의 메서드 호출</code></pre>
<ul>
<li><p>하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다. 그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 <strong>함수형 인터페이스</strong>라고 부르기로 했다.</p>
</li>
<li><p>함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있다.</p>
</li>
</ul>
<h3 id="매개변수와-반환-타입">매개변수와 반환 타입</h3>
<ul>
<li>메서드의 매개변수가 함수형 인터페이스 타입이면 매개변수에 람다식을 전달할 수 있다.</li>
</ul>
<pre><code class="language-java">@FunctionalInterface
interface MyFunction {
    void run();
}

static void execute(MyFunction function) {
    function.run();
}

MyFunction f1 = () -&gt; System.out.println(&quot;f1.run()&quot;);
execute(f1);</code></pre>
<ul>
<li>메서드의 반환타입이 함수형 인터페이스 타입이면 람다식을 반환할 수 있다.</li>
</ul>
<pre><code class="language-java">static MyFunction getMyFunction() {
    MyFunction function = () -&gt; System.out.println(&quot;f3.run()&quot;);
    return function;
}</code></pre>
<h2 id="javautilfunction-패키지">java.util.function 패키지</h2>
<ul>
<li><p>java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 정의되어 있다.</p>
</li>
<li><p>매번 새로운 함수형 인터페이스를 정의하지 말고, 가능하면 이 패키지의 인터페이스를 활용하는 것이 좋다. 그래야 함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서 좋다.</p>
</li>
</ul>
<pre><code class="language-java">Supplier&lt;Integer&gt; s = () -&gt; (int)(Math.random() * 100) + 1;
Consumer&lt;Integer&gt; c = i -&gt; System.out.print(i + &quot;, &quot;);
Predicate&lt;Integer&gt; p = i -&gt; i % 2 == 0;
Function&lt;Integer, Integer&gt; f = i -&gt; i / 10 * 10;</code></pre>
<table>
<thead>
<tr>
<th>함수형 인터페이스</th>
<th>메서드 시그니처</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>Runnable</code></td>
<td><code>void run()</code></td>
<td>매개변수와 반환값이 모두 없음</td>
</tr>
<tr>
<td><code>Supplier&lt;T&gt;</code></td>
<td><code>T get()</code></td>
<td>매개변수는 없고, 반환값만 있음</td>
</tr>
<tr>
<td><code>Consumer&lt;T&gt;</code></td>
<td><code>void accept(T t)</code></td>
<td>매개변수만 있고, 반환값은 없음</td>
</tr>
<tr>
<td><code>Function&lt;T, R&gt;</code></td>
<td><code>R apply(T t)</code></td>
<td>하나의 매개변수를 받아 결과를 반환</td>
</tr>
<tr>
<td><code>Predicate&lt;T&gt;</code></td>
<td><code>boolean test(T t)</code></td>
<td>하나의 매개변수를 받아 조건을 판별 (<code>boolean</code> 반환)</td>
</tr>
<tr>
<td><code>BiConsumer&lt;T, U&gt;</code></td>
<td><code>void accept(T t, U u)</code></td>
<td>두 개의 매개변수를 받아 처리하고 반환값은 없음</td>
</tr>
<tr>
<td><code>BiFunction&lt;T, U, R&gt;</code></td>
<td><code>R apply(T t, U u)</code></td>
<td>두 개의 매개변수를 받아 하나의 결과를 반환</td>
</tr>
<tr>
<td><code>BiPredicate&lt;T, U&gt;</code></td>
<td><code>boolean test(T t, U u)</code></td>
<td>두 개의 매개변수를 받아 조건을 판별 (<code>boolean</code> 반환)</td>
</tr>
<tr>
<td><code>UnaryOperator&lt;T&gt;</code></td>
<td><code>T apply(T t)</code></td>
<td><code>Function</code>의 특수 형태. 입력과 출력의 타입이 동일</td>
</tr>
<tr>
<td><code>BinaryOperator&lt;T&gt;</code></td>
<td><code>T apply(T t1, T t2)</code></td>
<td><code>BiFunction</code>의 특수 형태. 두 입력과 결과의 타입이 동일</td>
</tr>
</tbody></table>
<ul>
<li>다른 함수형 인터페이스가 필요하다면 직접 만들어서 써야한다.</li>
</ul>
<h3 id="predicate의-결합">Predicate의 결합</h3>
<ul>
<li>여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.</li>
</ul>
<pre><code class="language-java">Predicate&lt;Integer&gt; p = i -&gt; i &lt; 100;
Predicate&lt;Integer&gt; q = i -&gt; i &lt; 200;
Predicate&lt;Integer&gt; r = i -&gt; i % 2 == 0;
Predicate&lt;Integer&gt; notP = p.negate();

System.out.println(notP.and(q.or(r)).test(150)); // true</code></pre>
<ul>
<li>static 메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다.</li>
</ul>
<pre><code class="language-java">String str1 = &quot;abc&quot;;
String str2 = &quot;abc&quot;;

Predicate&lt;String&gt; p2 = Predicate.isEqual(str1);
System.out.println(p2.test(str2)); // true</code></pre>
<h2 id="메서드-참조">메서드 참조</h2>
<ul>
<li>하나의 메서드만 호출하는 람다식은 ‘클래스이름::메서드이름’ 또는 ‘참조변수::메서드이름’으로 바꿀 수 있다.</li>
</ul>
<pre><code class="language-java">Function&lt;String, Integer&gt; f = (String s) -&gt; Integer.parseInt(s); // 람다식
Function&lt;String, Integer&gt; f = Integer::parseInt // 메서드 참조

// 생성자의 메서드 참조
// 매개변수 없을 때
Supplier&lt;MyClass&gt; s = () -&gt; new MyClass(); // 람다식
Supplier&lt;MyClass&gt; s = MyClass::new; // 메서드 참조

// 매개변수 1개일 때
Function&lt;Integer, MyClass&gt; f = (i) -&gt; new MyClass(i); // 람다식
Function&lt;Integer, MyClass&gt; f = MyClass::new; // 메서드 참조

// 매개변수 2개일 때
BiFunction&lt;Integer, String, MyClass&gt; bf = (i, s) -&gt; new MyClass(i, s); // 람다식
BiFunction&lt;Integer, String, MyClass&gt; bf = MyClass::new; // 메서드 참조

// 배열 생성
Function&lt;Integer, int[]&gt; f = x -&gt; new int[x]; // 람다식
Function&lt;Integer, int[]&gt; f = int[]::new // 메서드 참조</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트 3 백엔드 개발자 되기 책 후기]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-3-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%90%98%EA%B8%B0-%EC%B1%85-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-3-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%90%98%EA%B8%B0-%EC%B1%85-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Fri, 18 Jul 2025 09:33:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>젊은 개발자가 쓴 스프링 부트 입문서</p>
</blockquote>
<h2 id="스프링이-궁금해">스프링이 궁금해</h2>
<p>현재 작은 스타트업에서 풀스택 개발자로 재직 중이며, 규모에 비해 적지 않은 연봉을 받고 있다. 처음에는 프론트엔드 개발자로 입사하였으나, 현재는 업무 영역을 넓혀 백엔드 개발까지 담당하고 있다. 회사의 백엔드는 주로 Node.js를 기반으로 하고 있지만, 대학 시절부터 &quot;백엔드 분야에서는 스프링이 최고다&quot;라는 말을 많이 접한 탓에 스프링에 대한 호기심이 자연스럽게 생겼다.</p>
<p>이러한 관심으로 무작정 자바 입문서를 구매하여 기초부터 학습을 시작했다. 기본적인 자바 학습을 마친 후 본격적으로 스프링을 배우기 위해 마음에 드는 책을 찾던 중, 스프링과 스프링 부트가 다르다는 사실을 뒤늦게 알게 되었다. 특히 스프링을 활용한 클론 코딩을 진행하는 서적을 찾던 중 『스프링 부트 3 백엔드 개발자 되기』라는 책을 구매하게 되었다.</p>
<h2 id="젊음이-느껴지는-코드">젊음이 느껴지는 코드</h2>
<p>프론트엔드 분야는 기술과 트렌드의 변화가 빠른 반면, 백엔드 분야는 상대적으로 안정적인 기술 생태계를 유지하는 경향이 있다. 이로 인해 백엔드를 학습하면서 오래된 정보들을 접할 기회가 많았는데, 이는 최신 정보가 풍부하고 체계적으로 정리된 프론트엔드 생태계와 비교했을 때 다소 생소한 경험이었다. 특히 백엔드 자료의 외래어 표기법과 같은 세부적인 부분에서 시대적 차이를 느끼기도 했다.</p>
<p>이 책의 가장 큰 장점은 코드에서 최신 개발 트렌드를 적극적으로 반영하고 있다는 점이다. 젊은 개발자가 저술한 덕분인지 코드 전반에서 신선함이 느껴진다. 개인적으로는 최신 코드로 먼저 학습하고, 이후 현업에서 레거시 코드를 점진적으로 경험하며 배우는 방식을 선호하는데, 그런 측면에서 이 책의 젊고 현대적인 코드 스타일이 매우 만족스러웠다.</p>
<h2 id="전체적인-개발-프로세스-경험">전체적인 개발 프로세스 경험</h2>
<p>이 책의 또 다른 장점은 단순한 기능 구현에 그치지 않고, 전체적인 개발 프로세스를 폭넓게 경험할 수 있도록 구성되어 있다는 점이다. 더불어 AWS 배포와 GitHub Actions를 활용한 CI/CD 방법도 함께 다루고 있어 실무에 더욱 유용하다.</p>
<h2 id="부족한-설명">부족한 설명</h2>
<p>한편, 다른 스프링 부트 입문서에 비해 설명이 다소 부족하다는 느낌도 받았다. 이는 장점이자 단점이 될 수 있는데, 이 책을 통해 빠르게 스프링 부트의 전체적인 흐름을 경험하고, 부족한 부분은 다른 서적을 통해 보충하면 되기 때문이다. 나에게는 이러한 점이 오히려 장점으로 작용했다.</p>
<p>책의 전체적인 구성은 코드를 먼저 작성하고 간단한 설명을 거친 뒤, 작동 및 테스트를 진행한 후 다음 단계로 넘어가는 방식이다.</p>
<p>처음 공부할 때부터 완벽을 추구하는 사람 입장에서는 자칫 타자 연습을 한다고 느낄 수도 있을거 같다.</p>
<h2 id="앞으로">앞으로</h2>
<p>아직 부록 부분을 읽지 않았는데, 부록에 중요한 내용이 많다고 느껴져서 우선 부록을 마저 읽을 계획이다. 그리고 네이버 및 카카오 로그인을 추가하려고 한다. 이후에는 자바의 람다와 스트림을 공부하고, 『스프링 부트 3 핵심 가이드』라는 책을 통해 부족한 부분을 채울 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 시큐리티]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0</guid>
            <pubDate>Wed, 16 Jul 2025 12:28:52 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-시큐리티란">스프링 시큐리티란?</h2>
<p>스프링 시큐리티(Spring Security)는 <strong>스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당</strong>하는 스프링 하위 프레임워크이다.</p>
<ul>
<li><p>스프링 스큐리티는 <strong>필터 기반</strong>으로 동작한다. 각 필터에서 인증, 인가와 관련된 작업을 처리한다.</p>
</li>
<li><p>기본적으로 세션 &amp; 쿠키 방식으로 인증을 처리한다.</p>
</li>
<li><p>OAuth2와 JWT를 사용해 인증, 인가를 구현할 수도 있다.</p>
</li>
</ul>
<h3 id="인증과-인가">인증과 인가</h3>
<ul>
<li><p><strong>인증(Authentication)</strong> : 보호된 리소스에 접근하는 것을 허용하기 이전에 등록한 사용자의 신원을 입증하는 과정</p>
</li>
<li><p><strong>인가(Authorization)</strong> : 특정 부분에 접근할 수 있는지 확인하는 작업</p>
</li>
</ul>
<h3 id="의존성-추가">의존성 추가</h3>
<pre><code class="language-java">dependencies {
    ...
    // 스프링 시큐리티를 사용하기 위한 스타터
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    // 타임리프에서 스프링 시큐리티를 사용하기 위한 의존성 추가
    implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity6&#39;
    // 스프링 시큐리티를 테스트하기 위한 의존성 추가
    testImplementation &#39;org.springframework.security:spring-security-test&#39;
}</code></pre>
<h2 id="userdetails-인터페이스">UserDetails 인터페이스</h2>
<p>스프링 시큐리티에서 사용자의 인증 정보를 저장하는 인터페이스이다. 스프링 시큐리티에서 해당 객체를 통해 인증 정보를 가져오려면 필수 오버라이드 메서드들을 여러 개 사용해야 한다.</p>
<pre><code class="language-java">@Table(name = &quot;users&quot;)
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User implements UserDetails {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = &quot;id&quot;, updatable = false)
  private Long id;

  @Column(name = &quot;email&quot;, nullable = false, unique = true)
  private String email;

  @Column(name = &quot;password&quot;)
  private String password;

  @Builder
  public User(String email, String password, String auth) {
    this.email = email;
    this.password = password;
  }

  // 권한 반환
  @Override
  public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
    return List.of(new SimpleGrantedAuthority(&quot;user&quot;));
  }

  // 사용자의 id를 반환(고유한 값)
  @Override
  public String getUsername() {
    return email;
  }

  // 사용자의 패스워드 반환
  @Override
  public String getPassword() {
    return password;
  }

  // 계정 만료 여부 반환
  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  // 계정 잠금 여부 반환
  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  // 패스워드의 만료 여부 반환
  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  // 계정 사용 가능 여부 반환
  @Override
  public boolean isEnabled() {
    return true;
  }
}</code></pre>
<ul>
<li>User 클래스(엔티티)가 구현한 UserDetails는 스프링 스큐리티에서 사용자의 인증 정보를 담아두는 인터페이스이다.</li>
</ul>
<h2 id="userdetailsservice-인터페이스">UserDetailsService 인터페이스</h2>
<p>UserDetailsService 인터페이스 구현을 통해 필수로 구현해야 하는 loadUserByUsername() 메서드를 오버라이딩해서 사용자 정보를 가져오는 로직을 작성한다. </p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class UserDetailService implements UserDetailsService {

  private final UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String email) {
    return userRepository.findByEmail(email).orElseThrow(() -&gt; new IllegalArgumentException((email)));
  }
}</code></pre>
<h2 id="시큐리티-설정">시큐리티 설정</h2>
<h3 id="스프링-시큐리티-기능-비활성화">스프링 시큐리티 기능 비활성화</h3>
<pre><code class="language-java">// 스프링 시큐리티 기능 비활성화
@Bean
public WebSecurityCustomizer configure() {
  return (web) -&gt; web.ignoring()
      .requestMatchers(toH2Console())
      .requestMatchers(new AntPathRequestMatcher(&quot;/static/**&quot;));
}</code></pre>
<ul>
<li><p>인증, 인가 서비스를 적용하지 않는 부분에 스프링 스큐리티의 모든 기능을 사용하지 않게 설정하는 코드이다.</p>
</li>
<li><p>정적 리소스와 h2-console 하위 url을 대상으로 <code>ignoring()</code> 메서드를 사용했다.</p>
</li>
<li><p><code>requestMatchers()</code> : 특정 요청과 일치하는 url에 대한 액세스를 설정한다.</p>
</li>
</ul>
<h3 id="특정-http-요청에-대한-웹-기반-보안-구성">특정 HTTP 요청에 대한 웹 기반 보안 구성</h3>
<pre><code class="language-java">// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  return http
      .authorizeRequests(auth -&gt; auth
          .requestMatchers(
              new AntPathRequestMatcher(&quot;/login&quot;),
              new AntPathRequestMatcher(&quot;/signup&quot;),
              new AntPathRequestMatcher(&quot;/user&quot;)
          ).permitAll()
          .anyRequest()
          .authenticated()
      )
      .formLogin(formLogin -&gt; formLogin.loginPage(&quot;/login&quot;).defaultSuccessUrl(&quot;/articles&quot;))
      .logout(logout -&gt; logout.logoutSuccessUrl(&quot;/login&quot;).invalidateHttpSession(true))
      .csrf(AbstractHttpConfigurer::disable)
      .build();
}</code></pre>
<ul>
<li><p>특정 HTTP 요청에 대해 웹 기반 보안을 구성한다. 이 메서드에서 인증/인가 및 로그인, 로그아웃 관련 설정을 할 수 있다.</p>
</li>
<li><p><code>formLogin()</code> : 폼 기반 로그인 설정</p>
</li>
<li><p><code>permitAll()</code> : 누구나 접근이 가능하게 설정한다. 즉, “/login”, “/signup”, “/user”로 요청이 오면 인증/인가 없이도 접근할 수 있다.</p>
</li>
<li><p><code>anyRequest()</code> : 위에서 설정한 url 이외의 요청에 대해서 설정</p>
</li>
<li><p><code>authenticated()</code> : 별도의 인가는 필요하지 않지만 인증이 성공된 상태여야 접근할 수 있다.</p>
</li>
<li><p><code>invalidateHttpSession()</code> : 로그아웃 이후에 세션을 전체 삭제할지 여부를 설정</p>
</li>
<li><p>CSRF 설정이 비활성화되어 있는데 실무에서는 활성화하는 게 좋다.</p>
</li>
</ul>
<h3 id="인증-관리자-관련-설정">인증 관리자 관련 설정</h3>
<pre><code class="language-java">// 인증 관리자 관련 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
  DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
  authProvider.setUserDetailsService(userDetailService); // 사용자 정보 서비스 설정
  authProvider.setPasswordEncoder(bCryptPasswordEncoder);

  return new ProviderManager(authProvider);
}</code></pre>
<ul>
<li>사용자 정보를 가져올 서비스를 재정의하거나, 인증 방법, 예를 들어 LDAP, JDBC 기반 인증 등을 설정할 때 사용합니다.</li>
</ul>
<h3 id="패스워드-인코더로-사용할-빈-등록">패스워드 인코더로 사용할 빈 등록</h3>
<pre><code class="language-java">// 패스워드 인코더로 사용할 빈 등록
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
  return new BCryptPasswordEncoder();
}</code></pre>
<ul>
<li>패스워드 인코더를 빈으로 등록한다.</li>
</ul>
<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

  private final UserDetailService userDetailService;

  // 스프링 시큐리티 기능 비활성화
  @Bean
  public WebSecurityCustomizer configure() {
    return (web) -&gt; web.ignoring()
        .requestMatchers(toH2Console())
        .requestMatchers(new AntPathRequestMatcher(&quot;/static/**&quot;));
  }

  // 특정 HTTP 요청에 대한 웹 기반 보안 구성
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeRequests(auth -&gt; auth
            .requestMatchers(
                new AntPathRequestMatcher(&quot;/login&quot;),
                new AntPathRequestMatcher(&quot;/signup&quot;),
                new AntPathRequestMatcher(&quot;/user&quot;)
            ).permitAll()
            .anyRequest()
            .authenticated()
        )
        .formLogin(formLogin -&gt; formLogin.loginPage(&quot;/login&quot;).defaultSuccessUrl(&quot;/articles&quot;))
        .logout(logout -&gt; logout.logoutSuccessUrl(&quot;/login&quot;).invalidateHttpSession(true))
        .csrf(AbstractHttpConfigurer::disable)
        .build();
  }

  // 인증 관리자 관련 설정
  @Bean
  public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailService); // 사용자 정보 서비스 설정
    authProvider.setPasswordEncoder(bCryptPasswordEncoder);

    return new ProviderManager(authProvider);
  }

  // 패스워드 인코더로 사용할 빈 등록
  @Bean
  public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
  }
}</code></pre>
<h2 id="토큰-기반-인증">토큰 기반 인증</h2>
<p>인증 정보를 서버나 세션에 저장하지 않고 클라이언트에서 발급받은 토큰을 저장하여 요청시 토큰을 전송하는 방식을 <strong>토큰 기반 인증</strong>이라고 한다. 대표적으로 JWT가 있다.</p>
<h3 id="특징">특징</h3>
<ul>
<li><p><strong>무상태성</strong> : 사용자의 인증 정보가 담겨 있는 토큰이 서버가 아닌 클라이언트에 있으므로 서버에서 인증 정보를 관리할 필요가 없다.</p>
</li>
<li><p><strong>확장성</strong> : 토큰을 클라이언트에서 관리하기 때문에 결제 서버와 주문 서버를 분리해서 운영해도 두 곳 모두에서 토큰 인증을 할 수 있다. 서버에서 관리를 하면 별도의 작업이 필요하다.</p>
</li>
<li><p><strong>무결성</strong> : 토큰을 발급한 이후에는 토큰 정보를 변경하는 행위를 할 수 없다.</p>
</li>
</ul>
<h3 id="jwt">JWT</h3>
<ul>
<li><p><strong>헤더(header)</strong> : 토큰의 타입(typ)과 해싱 알고리즘(alg)을 지정하는 정보를 담는다.</p>
</li>
<li><p><strong>내용(payload)</strong> : 토큰과 관련된 정보를 담는다. 내용의 한 덩어리를 클레임(claim)이라고 하며, 클레임은 키값의 한 쌍으로 이루어져 있다. 클레임은 등록된 클레임, 공개 클레임, 비공개 클레임으로 나눌 수 있다. 아래는 등록된 클레임 종류이다.</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>이름</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>iss</td>
<td>토큰 발급자(issuer)</td>
</tr>
<tr>
<td>sub</td>
<td>토큰 제목(subject)</td>
</tr>
<tr>
<td>aud</td>
<td>토큰 대상자(audience)</td>
</tr>
<tr>
<td>exp</td>
<td>토큰의 만료 시간(expiration)</td>
</tr>
<tr>
<td>nbf</td>
<td>토큰의 활성 날짜와 비슷한 개념으로 nbf는 Not Before를 의미한다.</td>
</tr>
<tr>
<td>iat</td>
<td>토큰이 발급된 시간(issued at)</td>
</tr>
<tr>
<td>jti</td>
<td>JWT의 고유 식별자로서 주로 일회용 토큰에 사용한다.</td>
</tr>
</tbody></table>
<ul>
<li><strong>서명(signature)</strong> : 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용하며, 헤더의 인코딩값과 내용의 인코딩값을 합친 후에 주어진 비밀키를 사용해 해시값을 생성한다.</li>
</ul>
<h3 id="의존성-추가-1">의존성 추가</h3>
<pre><code class="language-java">dependencies {
    ...
    // 자바 JWT 라이브러리
    implementation &#39;io.jsonwebtoken:jjwt:0.9.1&#39;
    // XML 문서와 Java 객체 간 매핑 자동화
    implementation &#39;javax.xml.bind:jaxb-api:2.3.1&#39;
}</code></pre>
<h2 id="jwtproperties">JwtProperties</h2>
<p>application.yml 파일에 설정한 환경 변수 값을 클래스의 프로퍼티값으로 가져오기 위해 <code>@ConfigurationProperties</code> 애너테이션을 사용한다.</p>
<pre><code class="language-java">@Setter
@Getter
@Component
@ConfigurationProperties(&quot;jwt&quot;)
public class JwtProperties {
  private String issuer;
  private String secretKey;
}</code></pre>
<h2 id="tokenprovider">TokenProvider</h2>
<h3 id="jwt-토큰-생성-메서드">JWT 토큰 생성 메서드</h3>
<pre><code class="language-java">// JWT 토큰 생성 메서드
private String makeToken(Date expiry, User user) {
  Date now = new Date();

  return Jwts.builder()
      .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
      .setIssuer(jwtProperties.getIssuer())
      .setIssuedAt(now)
      .setExpiration(expiry)
      .setSubject(user.getEmail())
      .claim(&quot;id&quot;, user.getId())
      .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
      .compact();
}</code></pre>
<ul>
<li><p>헤더에는 typ(타입), 내용에는 iss(발급자), iat(발급일시), exp(만료일시), sub(토큰 제목), 클레임에는 유저 ID를 지정</p>
</li>
<li><p>토큰을 만들 때는 프로퍼티즈 파일에 선언해둔 비밀키와 함께 HS256 방식으로 암호화한다.</p>
</li>
</ul>
<h3 id="jwt-토큰-유효성-검증-메서드">JWT 토큰 유효성 검증 메서드</h3>
<pre><code class="language-java">// JWT 토큰 유효성 검증 메서드
public boolean validToken(String token) {
  try {
    Jwts.parser()
        .setSigningKey(jwtProperties.getSecretKey())
        .parseClaimsJws(token);

    return true;
  } catch (Exception e) {
    return false;
  }
}</code></pre>
<ul>
<li><p>비밀키와 함께 토큰 복호화를 진행한다.</p>
</li>
<li><p>복호화 과정에서 에러가 발생하면 유효하지 않은 토큰이므로 false를 반환한다.</p>
</li>
</ul>
<h3 id="토큰-기반으로-인증-정보를-가져오는-메서드">토큰 기반으로 인증 정보를 가져오는 메서드</h3>
<pre><code class="language-java">// 토큰 기반으로 인증 정보를 가져오는 메서드
public Authentication getAuthentication(String token) {
  Claims claims = getClaims(token);
  Set&lt;SimpleGrantedAuthority&gt; authorities = Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;));

  return new UsernamePasswordAuthenticationToken(
      new org.springframework.security.core.userdetails.User(
          claims.getSubject(), &quot;&quot;, authorities
      ), token, authorities
  );
}</code></pre>
<ul>
<li><code>getClaims()</code> 를 호출해서 클레임 정보를 반환받아 사용자 이메일이 들어 있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성한다.</li>
</ul>
<h3 id="토큰-기반으로-유저-id를-가져오는-메서드">토큰 기반으로 유저 ID를 가져오는 메서드</h3>
<pre><code class="language-java">// 토큰 기반으로 유저 ID를 가져오는 메서드
public Long getUserId(String token) {
  Claims claims = getClaims(token);
  return claims.get(&quot;id&quot;, Long.class);
}</code></pre>
<ul>
<li><code>getClaims()</code> 를 호출해서 클레임 정보를 반환받고 클레임에서 id 키로 저장된 값을 가져와 반환한다.</li>
</ul>
<h3 id="전체-코드-1">전체 코드</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class TokenProvider {

  private final JwtProperties jwtProperties;

  public String generateToken(User user, Duration expireAt) {
    Date now = new Date();
    return makeToken(new Date(now.getTime() + expireAt.toMillis()), user);
  }

  // JWT 토큰 생성 메서드
  private String makeToken(Date expiry, User user) {
    Date now = new Date();

    return Jwts.builder()
        .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
        .setIssuer(jwtProperties.getIssuer())
        .setIssuedAt(now)
        .setExpiration(expiry)
        .setSubject(user.getEmail())
        .claim(&quot;id&quot;, user.getId())
        .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
        .compact();
  }

  // JWT 토큰 유효성 검증 메서드
  public boolean validToken(String token) {
    try {
      Jwts.parser()
          .setSigningKey(jwtProperties.getSecretKey())
          .parseClaimsJws(token);

      return true;
    } catch (Exception e) {
      return false;
    }
  }

  // 토큰 기반으로 인증 정보를 가져오는 메서드
  public Authentication getAuthentication(String token) {
    Claims claims = getClaims(token);
    Set&lt;SimpleGrantedAuthority&gt; authorities = Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;));

    return new UsernamePasswordAuthenticationToken(
        new org.springframework.security.core.userdetails.User(
            claims.getSubject(), &quot;&quot;, authorities
        ), token, authorities
    );
  }

  // 토큰 기반으로 유저 ID를 가져오는 메서드
  public Long getUserId(String token) {
    Claims claims = getClaims(token);
    return claims.get(&quot;id&quot;, Long.class);
  }

  private Claims getClaims(String token) {
    return Jwts.parser()
        .setSigningKey(jwtProperties.getSecretKey())
        .parseClaimsJws(token)
        .getBody();
  }
}</code></pre>
<h2 id="토큰-필터">토큰 필터</h2>
<p>필터는 실제로 요청이 전달되기 전과 후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공한다.</p>
<ul>
<li><p><strong>시큐리티 컨텍스트</strong>는 인증 객체가 저장되는 보관소이다. 이 클래스는 쓰레드마다 공간을 할당하는 즉, 쓰레드 로컬에 저장되므로 코드 아무 곳에서나 참조할 수 있고, 다른 쓰레드와 공유하지 않으므로 독립적으로 사용할 수 있다.</p>
</li>
<li><p>스큐리티 컨텍스트 객체를 저장하는 객체가 <strong>시큐리티 컨텍스트 홀더</strong>이다.</p>
</li>
<li><p>요청이 오면 헤더값을 비교해서 토큰이 있는지 확인하고 유효 토큰이라면 시큐리티 콘텍스트 홀더에 인증 정보를 저장한다.</p>
</li>
</ul>
<pre><code class="language-java">@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {

  private final TokenProvider tokenProvider;
  private final static String HEADER_AUTHORIZATION = &quot;Authorization&quot;;
  private final static String TOKEN_PREFIX = &quot;Bearer&quot;;

  @Override
  protected void doFilterInternal(
      HttpServletRequest request, 
      HttpServletResponse response, 
      FilterChain filterChain) throws ServletException, IOException {

    String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
    String token = getAccessToken(authorizationHeader);

    if (tokenProvider.validToken(token)) {
      Authentication authentication = tokenProvider.getAuthentication(token);
      SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    filterChain.doFilter(request, response);
  }

  private String getAccessToken(String authorizationHeader) {
    if (authorizationHeader != null &amp;&amp; authorizationHeader.startsWith(TOKEN_PREFIX)) {
      return authorizationHeader.substring(TOKEN_PREFIX.length());
    }

    return null;
  }
}</code></pre>
<h2 id="oauthusercustomservice">OAuthUserCustomService</h2>
<p>리소스 서버에서 보내주는 사용자 정보를 불러오는 메서드인 <code>loadUser()</code> 를 통해 사용자를 조회한다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class OAuth2UserCustomService extends DefaultOAuth2UserService {

  private final UserRepository userRepository;

  @Override
  public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    OAuth2User user = super.loadUser(userRequest);
    saveOrUpdate(user);
    return user;
  }

  private User saveOrUpdate(OAuth2User oAuth2User) {
    Map&lt;String, Object&gt; attributes = oAuth2User.getAttributes();
    String email = (String) attributes.get(&quot;email&quot;);
    String name = (String) attributes.get(&quot;name&quot;);
    User user = userRepository.findByEmail(email)
        .map(entity -&gt; entity.update(name))
        .orElse(User.builder()
            .email(email)
            .nickname(name)
            .build());

    return userRepository.save(user);
  }
}</code></pre>
<ul>
<li><p>부모 클래스인 DefaultOAuth2UserService에서 제공하는 OAuth 서비스에서 제공하는 정보를 기반으로 유저 객체를 만들어주는 <code>loadUser()</code> 메서드를 사용해 사용자 객체를 불러온다.</p>
</li>
<li><p>사용자 객체는 식별자, 이름, 이메일, 프로필 사진 링크 등의 정보를 담고 있다.</p>
</li>
</ul>
<h2 id="weboauthsecurityconfig">WebOAuthSecurityConfig</h2>
<pre><code class="language-java">@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  return http
      .csrf(AbstractHttpConfigurer::disable)
      .httpBasic(AbstractHttpConfigurer::disable)
      .formLogin(AbstractHttpConfigurer::disable)
      .logout(AbstractHttpConfigurer::disable)
      .sessionManagement(management -&gt; management.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
      .authorizeRequests(auth -&gt; auth
          .requestMatchers(new AntPathRequestMatcher(&quot;/api/token&quot;)).permitAll()
          .requestMatchers(new AntPathRequestMatcher(&quot;/api/**&quot;)).authenticated()
          .anyRequest().permitAll())
      .oauth2Login(oauth2 -&gt; oauth2
          .loginPage(&quot;/login&quot;)
          .authorizationEndpoint(authorizationEndpoint -&gt;
              authorizationEndpoint.authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository()))
          .userInfoEndpoint(userInfoEndpoint -&gt;
              userInfoEndpoint.userService(oAuth2UserCustomService))
          .successHandler(oAuth2SuccessHandler())
      ).exceptionHandling(exceptionHandling -&gt;
          exceptionHandling.defaultAuthenticationEntryPointFor(
              new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
              new AntPathRequestMatcher(&quot;/api**&quot;)
          ))
      .build();
}</code></pre>
<ul>
<li><p>토큰 방식으로 인증을 하기 때문에 기존에 사용하던 폼 로그인, 세션 비활성화</p>
</li>
<li><p>토큰 재발급 URL은 인증 없이 접근 가능하도록 설정, 나머지 API URL은 인증 필요</p>
</li>
<li><p>OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소 설정</p>
</li>
</ul>
<h2 id="oauth2authorizationrequestbasedoncookierepository">OAuth2AuthorizationRequestBasedOnCookieRepository</h2>
<p>OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소를 구현한다. 권한 인증 흐름에서 클라이언트의 요청을 유지하는 데 사용하는 AuthorizationRequestRepository 클래스를 구현해 쿠키를 사용해 OAuth의 정보를 가져오고 저장하는 로직을 작성한다.</p>
<pre><code class="language-java">public class OAuth2AuthorizationRequestBasedOnCookieRepository implements AuthorizationRequestRepository&lt;OAuth2AuthorizationRequest&gt; {

  public final static String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = &quot;oauth2_auth_request&quot;;
  private final static int COOKIE_EXPIRE_SECONDS = 18000;

  @Override
  public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
    Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
    return CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class);
  }

  @Override
  public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
    if (authorizationRequest == null) {
      removeAuthorizationRequestCookies(request, response);
      return;
    }

    CookieUtil.addCookie(
        response,
        OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
        CookieUtil.serialize(authorizationRequest),
        COOKIE_EXPIRE_SECONDS
    );
  }

  @Override
  public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
    return this.loadAuthorizationRequest(request);
  }

  public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
    CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
  }
}</code></pre>
<h2 id="oauth2successhandler">OAuth2SuccessHandler</h2>
<pre><code class="language-java">// 리프레시 토큰 생성 -&gt; 저장 -&gt; 쿠키에 저장
  String refreshToken = tokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
  saveRefreshToken(user.getId(), refreshToken);
  addRefreshTokenToCookie(request, response, refreshToken);

// 생성된 리프레시 토큰을 전달받아 데이터베이스에 저장
private void saveRefreshToken(Long userId, String newRefreshToken) {
  RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId)
      .map(entity -&gt; entity.update(newRefreshToken))
      .orElse(new RefreshToken(userId, newRefreshToken));

  refreshTokenRepository.save(refreshToken);
}

// 생성된 리프레시 토큰을 쿠키에 저장
private void addRefreshTokenToCookie(HttpServletRequest request, HttpServletResponse response, String refreshToken) {
  int cookieMaxAge = (int)REFRESH_TOKEN_DURATION.toSeconds();
  CookieUtil.deleteCookie(request, response, REFRESH_TOKEN_COOKIE_NAME);
  CookieUtil.addCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, cookieMaxAge);
}</code></pre>
<ul>
<li>토큰 제공자를 사용해 리프레시 토큰을 만든 뒤 데이터베이스와 쿠키에 저장한다.</li>
</ul>
<pre><code class="language-java">// 액세스 토큰 생성 -&gt; 패스에 액세스 토큰 추가
String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
String targetUrl = getTargetUrl(accessToken);

// 액세스 토큰을 path에 추가
private String getTargetUrl(String token) {
  return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
      .queryParam(&quot;token&quot;, token)
      .build()
      .toUriString();
}</code></pre>
<ul>
<li>토큰 제공자를 사용해 액세스 토큰을 만든 뒤 쿼리 파라미터에 엑세스 토큰을 추가한다.</li>
</ul>
<pre><code class="language-java">// 인증 관련 설정값, 쿠키 제거
clearAuthenticationAttributes(request, response);

private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
  super.clearAuthenticationAttributes(request);
  authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}</code></pre>
<ul>
<li><p>인증 프로세스를 진행하면서 세션과 쿠키에 임시로 저장해둔 인증 관련 데이터를 제거한다.</p>
</li>
<li><p><code>removeAuthorizationRequestCookies()</code> 를 추가로 호출해 OAuth 인증을 위해 저장된 정보도 삭제한다.</p>
</li>
</ul>
<pre><code class="language-java">// 리다이렉트
getRedirectStrategy().sendRedirect(request, response, targetUrl);</code></pre>
<ul>
<li>엑세스 토큰을 쿼리 파라미터로 가진 URL로 리다이렉트한다.</li>
</ul>
<h3 id="전체-코드-2">전체 코드</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
  public static final String REFRESH_TOKEN_COOKIE_NAME = &quot;refresh_token&quot;;
  public static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(14);
  public static final Duration ACCESS_TOKEN_DURATION = Duration.ofDays(1);
  public static final String REDIRECT_PATH = &quot;/articles&quot;;

  private final TokenProvider tokenProvider;
  private final RefreshTokenRepository refreshTokenRepository;
  private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
  private final UserService userService;

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
    User user = userService.findByEmail((String)oAuth2User.getAttributes().get(&quot;email&quot;));

    // 리프레시 토큰 생성 -&gt; 저장 -&gt; 쿠키에 저장
    String refreshToken = tokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
    saveRefreshToken(user.getId(), refreshToken);
    addRefreshTokenToCookie(request, response, refreshToken);
    // 액세스 토큰 생성 -&gt; 패스에 액세스 토큰 추가
    String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
    String targetUrl = getTargetUrl(accessToken);
    // 인증 관련 설정값, 쿠키 제거
    clearAuthenticationAttributes(request, response);
    // 리다이렉트
    getRedirectStrategy().sendRedirect(request, response, targetUrl);
  }

  // 생성된 리프레시 토큰을 전달받아 데이터베이스에 저장
  private void saveRefreshToken(Long userId, String newRefreshToken) {
    RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId)
        .map(entity -&gt; entity.update(newRefreshToken))
        .orElse(new RefreshToken(userId, newRefreshToken));

    refreshTokenRepository.save(refreshToken);
  }

  // 생성된 리프레시 토큰을 쿠키에 저장
  private void addRefreshTokenToCookie(HttpServletRequest request, HttpServletResponse response, String refreshToken) {
    int cookieMaxAge = (int)REFRESH_TOKEN_DURATION.toSeconds();
    CookieUtil.deleteCookie(request, response, REFRESH_TOKEN_COOKIE_NAME);
    CookieUtil.addCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, cookieMaxAge);
  }

  // 인증 관련 설정값, 쿠키 제거
  private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
    super.clearAuthenticationAttributes(request);
    authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
  }

  // 액세스 토큰을 path에 추가
  private String getTargetUrl(String token) {
    return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
        .queryParam(&quot;token&quot;, token)
        .build()
        .toUriString();
  }
}</code></pre>
<ul>
<li><p>스프링 시큐리티의 기본 로직에서는 별도의 authenticationSuccessHandler를 지정하지 않으면 로그인 성공 이후 SimpleUrlAuthenticationSuccessHandler를 사용한다.</p>
</li>
<li><p>일반적인 로직은 동일하게 사용하고, 토큰과 관련된 작업만 추가로 처리하기 위해 SimpleUrlAuthenticationSuccessHandler을 상속받은 뒤에 onAuthenticationSuccess() 메서드를 오버라이딩 했다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[thymeleaf]]></title>
            <link>https://velog.io/@dev_gyeongmin/thymeleaf</link>
            <guid>https://velog.io/@dev_gyeongmin/thymeleaf</guid>
            <pubDate>Mon, 14 Jul 2025 09:46:34 GMT</pubDate>
            <description><![CDATA[<p>타임리프는 템플릿 엔진이다. 템플릿 엔진은 서버에서 데이터를 받아 우리가 보는 웹 페이지, 즉 HTML 상에 데이터를 넣어 보여주는 도구이다.</p>
<h2 id="의존성-추가">의존성 추가</h2>
<p>build.gradle 파일에 의존성을 추가해야 타임리프를 사용할 수 있다.</p>
<pre><code class="language-java">dependencies {
    ...
    implementation &#39;org.springframework.boot:spring-boot-starter-thymeleaf&#39;
}</code></pre>
<h2 id="표현식과-문법">표현식과 문법</h2>
<p>■ 타임리프 표현식</p>
<table>
<thead>
<tr>
<th>표현식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>${…}</td>
<td>변수의 값 표현식</td>
</tr>
<tr>
<td>#{…}</td>
<td>속성 파일 값 표현식</td>
</tr>
<tr>
<td>@{…}</td>
<td>URL 표현식</td>
</tr>
<tr>
<td>*{…}</td>
<td>선택한 변수의 표현식. th:object에서 선택한 객체에 접근</td>
</tr>
</tbody></table>
<p>■ 타임리프 문법</p>
<table>
<thead>
<tr>
<th>표현식</th>
<th>설명</th>
<th>예제</th>
</tr>
</thead>
<tbody><tr>
<td>th:text</td>
<td>텍스트를 표현할 때 사용</td>
<td>th:text=${person.name}</td>
</tr>
<tr>
<td>th:each</td>
<td>컬렉션을 반복할 때 사용</td>
<td>th:each=”person : ${persons}”</td>
</tr>
<tr>
<td>th:if</td>
<td>조건이 true인 때만 표시</td>
<td>th:if=”${person.age} ≥ 20”</td>
</tr>
<tr>
<td>th:unless</td>
<td>조건이 false인 때만 표시</td>
<td>th:unless=”${person.age} ≥ 20”</td>
</tr>
<tr>
<td>th:href</td>
<td>이동 경로</td>
<td>th:href=”@{/person/{id}(id=${person.id})}”</td>
</tr>
<tr>
<td>th:with</td>
<td>변숫값으로 지정</td>
<td>th:with=”name=${person.name}”</td>
</tr>
<tr>
<td>th:object</td>
<td>선택한 객체로 지정</td>
<td>th:object=”${person}”</td>
</tr>
</tbody></table>
<h2 id="뷰-컨트롤러">뷰 컨트롤러</h2>
<p>API 컨트롤러는 데이터를 직렬화한 JSON 문자열을 반환하며 <code>@RestController</code> 애너테이션을 사용한다. 뷰 컨트롤러는 모델 객체 값을 지정 및 뷰의 이름을 반환하며 <code>@Controller</code> 애너테이션을 사용한다.</p>
<pre><code class="language-java">@Controller
public class ExampleController {

  @GetMapping(&quot;/thymeleaf/example&quot;)
  public String thymeleafExample(Model model) {
    Person examplePerson = new Person();
    examplePerson.setId(1L);
    examplePerson.setName(&quot;홍길동&quot;);
    examplePerson.setAge(11);
    examplePerson.setHobbies(List.of(&quot;운동&quot;, &quot;독서&quot;));

    model.addAttribute(&quot;person&quot;, examplePerson);
    model.addAttribute(&quot;today&quot;, LocalDate.now());

    return &quot;example&quot;;
  }

  @Setter
  @Getter
  class Person {
    private Long id;
    private String name;
    private int age;
    private List&lt;String&gt; hobbies;
  }
}</code></pre>
<ul>
<li><p><code>@Controller</code> 이므로 뷰의 이름을 반환한다. 스프링은 반환하는 뷰의 이름을 보고 <code>/resources/templates</code> 디렉터리에서 파일을 찾아 웹 브라우저에 해당 파일을 보여준다.</p>
</li>
<li><p><code>Model</code> 객체는 뷰인 HTML 쪽으로 값을 넘겨주는 객체이다. <code>addAttribute()</code> 메서드로 모델에 값을 저장한다. 모델을 컨트롤러와 뷰의 중간다리 역할이라고 생각하면 된다.</p>
</li>
</ul>
<h3 id="뷰-작성">뷰 작성</h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;Title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;타임리프 익히기&lt;/h1&gt;
  &lt;p th:text=&quot;${#temporals.format(today, &#39;yyyy-MM-dd&#39;)}&quot;&gt;&lt;/p&gt;
  &lt;div th:object=&quot;${person}&quot;&gt;
    &lt;p th:text=&quot;|이름 : *{name}|&quot;&gt;&lt;/p&gt;
    &lt;p th:text=&quot;|나이 : *{age}|&quot;&gt;&lt;/p&gt;
    &lt;p&gt;취미&lt;/p&gt;
    &lt;ul th:each=&quot;hobby : *{hobbies}&quot;&gt;
      &lt;li th:text=&quot;${hobby}&quot;&gt;&lt;/li&gt;
      &lt;span th:if=&quot;${hobby == &#39;운동&#39;}&quot;&gt;(대표 취미)&lt;/span&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;a th:href=&quot;@{/api/articles/{id}(id=${person.id})}&quot;&gt;글 보기&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<ul>
<li><p>타임리프의 문법과 표현식을 사용해서 HTML 파일을 작성한다.</p>
</li>
<li><p><code>#temporals.format()</code> 함수로 LocalDate 타입인 오늘 날짜를 String 타입으로 포매팅한다.</p>
</li>
</ul>
<h2 id="엔티티에-생성-수정-시간-추가">엔티티에 생성, 수정 시간 추가</h2>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = &quot;id&quot;, updatable = false)
  private Long id;

  @Column(name = &quot;title&quot;, nullable = false)
  private String title;

  @Column(name = &quot;content&quot;, nullable = false)
  private String content;

  @CreatedDate
  @Column(name = &quot;created_at&quot;)
  private LocalDateTime createdAt;

  @LastModifiedDate
  @Column(name = &quot;updated_at&quot;)
  private LocalDateTime updatedAt;

  @Builder
  public Article(String title, String content) {
    this.title = title;
    this.content = content;
  }

  public void update(String title, String content) {
    this.title = title;
    this.content = content;
  }
}</code></pre>
<ul>
<li><p><code>@CreatedDate</code> 애너테이션을 사용하면 엔티티의 생성 시간을 특정 컬럼에 저장할 수 있다.</p>
</li>
<li><p><code>@LastModifiedDate</code> 애너테이션을 사용하면 엔티티가 수정될 때 마지막으로 수정된 시간을 특정 컬럼에 저장할 수 있다.</p>
</li>
<li><p>엔티티의 생성 및 수정 시간을 자동으로 감시하고 기록하기 위해 <code>@EntityListeners(AuditingEntityListener.class)</code> 애너테이션을 추가한다.</p>
</li>
</ul>
<pre><code class="language-java">@EnableJpaAuditing
@SpringBootApplication
public class SpringBootDeveloperApplication {
  public static void main(String[] args) {
    SpringApplication.run(SpringBootDeveloperApplication.class, args);
  }
}</code></pre>
<ul>
<li><code>@CreatedDate</code> , <code>@LastModifiedDate</code> 애너테이션이 동작하려면 <code>@EnableJpaAuditing</code> 애너테이션을 추가해 JPA Auditing을 활성화해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[쓰레드]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%93%B0%EB%A0%88%EB%93%9C</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%93%B0%EB%A0%88%EB%93%9C</guid>
            <pubDate>Sat, 12 Jul 2025 05:35:05 GMT</pubDate>
            <description><![CDATA[<h2 id="프로세스와-쓰레드">프로세스와 쓰레드</h2>
<ul>
<li><p>프로세스란 실행 중인 프로그램, 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.</p>
</li>
<li><p>프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.</p>
</li>
<li><p>모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 “멀티쓰레드 프로세스”라고 한다.</p>
</li>
<li><p>쓰레드를 프로세스라는 작업공간에서 작업을 처리하는 일꾼이라고 생각할 수 있다.</p>
</li>
<li><p>프로세스를 생성하는 것은 쓰레드를 생성하는 것에 비해 더 많은 시간과 메모리 공간이 필요하다. 쓰레드를 경량 프로세스(light-weight process)라고 부른다.</p>
</li>
</ul>
<h2 id="멀티쓰레딩">멀티쓰레딩</h2>
<ul>
<li><p>메신저로 채팅하면서 파일을 다운로드 받거나 음성대화를 나눌 수 있는 것이 가능한 이유는 멀티쓰레드로 작성되어 있기 때문이다.</p>
</li>
<li><p>여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것은 필수적이어서 하나의 서버 프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍해야 한다.</p>
</li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li><p>CPU의 사용률을 향상시킨다.</p>
</li>
<li><p>자원을 보다 효율적으로 사용할 수 있다.</p>
</li>
<li><p>사용자에 대한 응답성이 향상된다.</p>
</li>
<li><p>작업이 분리되어 코드가 간결해진다.</p>
</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li><p>여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 발생하는 문제가 있다.</p>
</li>
<li><p>동기화, 교착상태 등</p>
</li>
</ul>
<h2 id="쓰레드의-구현과-실행">쓰레드의 구현과 실행</h2>
<ul>
<li><p>쓰레드를 구현하는 방법은 <strong>Thread 클래스</strong>를 상속받는 방법과 <strong>Runnable 인터페이스</strong>를 구현하는 방법이 있다.</p>
</li>
<li><p>Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 방법이 일반적이다.</p>
</li>
</ul>
<h3 id="thread-클래스-상속">Thread 클래스 상속</h3>
<pre><code class="language-java">class MyThread extends Thread {
    public void run() { /* 작업내용 */ } // Thread 클래스의 run()을 오버라이딩
}</code></pre>
<h3 id="runnable-인터페이스-구현">Runnable 인터페이스 구현</h3>
<pre><code class="language-java">class MyThread implements Runnable {
    public void run() { /* 작업내용 */ } // Runnable 인터페이스의 run()을 구현
}</code></pre>
<h3 id="thread-클래스-실행">Thread 클래스 실행</h3>
<pre><code class="language-java">class Ex13_1 {
    public static void main(String args[]) {
        Thread1 thread1 = new Thread1();

        thread1.start();
    }
}

class Thread1 extends Thread {
    public void run() {
        for(int i = 0; i &lt; 5; i++) {
            // Thread 클래스의 getName()
            // 쓰레드의 이름을 반환
            System.out.println(getName());
        }
    }
}</code></pre>
<h3 id="runnable-인터페이스-실행">Runnable 인터페이스 실행</h3>
<pre><code class="language-java">class Ex13_1 {
    public static void main(String args[]) {
        Runnable runnable = new Thread2();
        Thread thread2 = new Thread(runnable);

        thread2.start();
    }
}

class Thread2 implements Runnable {
    public void run() {
        for(int i = 0; i &lt; 5; i++) {
            // Thread.currentThread()는 현재 실행중인 쓰레드의 참조를 반환
            System.out.println(Thread.currentThread().getName());
        }
    }
}</code></pre>
<ul>
<li><p>Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다.</p>
</li>
<li><p>Runnable 인터페이스는 run()만 정의되어 있는 간단한 인터페이스라 현재 실행중인 Thread를 반환하기 위해 Thread.currentThread()를 사용했다.</p>
</li>
</ul>
<h3 id="쓰레드의-실행---start">쓰레드의 실행 - start()</h3>
<ul>
<li><p>start()가 호출되면 쓰레드는 실행대기 상태에 있다가 자신의 차례가 되어야 실행된다. 물론 실행대기중인 쓰레드가 없으면 곧바로 실행 상태가 된다. 쓰레드의 실행순서는 OS의 스케쥴러가 작성한 스케쥴에 의해 결정된다.</p>
</li>
<li><p>한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다. 그래서 쓰레드의 작업을 한 번 더 수행해야 한다면 새로운 쓰레드를 생성한 다음에 start()를 호출해야 한다.</p>
</li>
</ul>
<pre><code class="language-java">Thread1 t1 = new Thread1();
t1.start();

t1 = new Thread1();
t1.start();</code></pre>
<ul>
<li>만일 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시에 IllegalThreadStateException이 발생한다.</li>
</ul>
<pre><code class="language-java">Thread1 t1 = new Thread1();
t1.start();
...
t1.start(); // 예외 발생</code></pre>
<h3 id="start와-run">start()와 run()</h3>
<ul>
<li><p>main 메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것이다.</p>
</li>
<li><p>start()는 새로운 쓰레드가 작업을 실행하는데 필요한 콜스택을 생성한 다음에 run()을 호출해서, 생성된 콜스택에 run()이 첫 번째로 올라가게 한다.</p>
</li>
<li><p>모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 콜스택을 필요로 한다. 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 콜스택이 생성되고 쓰레드가 종료되면 작업에 사용된 콜스택은 소멸된다.</p>
</li>
</ul>
<h3 id="main-쓰레드">main 쓰레드</h3>
<ul>
<li><p>main 메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main 쓰레드라고 한다.</p>
</li>
<li><p>프로그램이 실행되면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업을 수행한다.</p>
</li>
<li><p>쓰레드는 “사용자 쓰레드”와 “데몬 쓰레드” 두 종류가 있다. 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.</p>
</li>
</ul>
<h2 id="싱글쓰레드와-멀티쓰레드">싱글쓰레드와 멀티쓰레드</h2>
<ul>
<li><p><strong>하나의 쓰레드</strong>로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, <strong>두 개의 쓰레드</strong>로 작업 하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것처럼 보인다.</p>
</li>
<li><p>멀티쓰레드로 작업시 <strong>컨텍스트 스위칭</strong> 시간이 소요된다. 단순히 CPU만을 사용하는 계산작업이라면 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 효율적이다.</p>
</li>
</ul>
<h3 id="싱글쓰레드로-작업">싱글쓰레드로 작업</h3>
<pre><code class="language-java">class Ex13_2 {
    public static void main(String args[]) {
        long startTime = System.currentTimeMillis();

        for(int i=0; i &lt; 300; i++)
            System.out.printf(&quot;%s&quot;, new String(&quot;-&quot;));        

        // 15
        System.out.print(&quot;소요시간1:&quot; +(System.currentTimeMillis()- startTime)); 

        for(int i=0; i &lt; 300; i++) 
            System.out.printf(&quot;%s&quot;, new String(&quot;|&quot;));        

        // 17
         System.out.print(&quot;소요시간2:&quot;+(System.currentTimeMillis() - startTime));
    }
}</code></pre>
<ul>
<li>수행시간을 측정하기 쉽게 “-” 대신 new String(”-”)을 사용해서 수행 속도를 늦췄다.</li>
</ul>
<h3 id="멀티쓰레드로-작업">멀티쓰레드로 작업</h3>
<pre><code class="language-java">class Ex13_3 {
    static long startTime = 0;

    public static void main(String args[]) {
        ThreadEx3_1 th1 = new ThreadEx3_1();
        th1.start();
        startTime = System.currentTimeMillis();

        for(int i=0; i &lt; 300; i++)
            System.out.printf(&quot;%s&quot;, new String(&quot;-&quot;));    

        // 19
        System.out.print(&quot;소요시간1:&quot; + (System.currentTimeMillis() - Ex13_3.startTime));
    } 
}

class ThreadEx3_1 extends Thread {
    public void run() {
        for(int i=0; i &lt; 300; i++)
            System.out.printf(&quot;%s&quot;, new String(&quot;|&quot;));    

        // 19
        System.out.print(&quot;소요시간2:&quot; + (System.currentTimeMillis() - Ex13_3.startTime));
    }
}</code></pre>
<ul>
<li><p>두 작업이 아주 짧은 시간동안 번갈아가면서 실행되었으며 거의 동시에 작업이 완료되었다.</p>
</li>
<li><p>컨텍스트 스위칭와 한 쓰레드가 화면에 출력하고 있는 동안 다른 쓰레드는 출력이 끝나기를 기다려야 하는 대기시간 때문에 멀티쓰레드로 작업할 때 더 오래 걸린다.</p>
</li>
<li><p>싱글 코어인 경우에는 멀티쓰레드라도 하나의 코어가 번갈아가면서 작업을 수행하므로 두 작업이 절대 겹치지 않는다. 그러나, 멀티 코어에서는 멀티쓰레드로 두 작업을 수행하면, 동시에 두 쓰레드가 수행될 수 있다. 그래서 화면(console)이라는 자원을 놓고 두 쓰레드가 경쟁하게 되는 것이다.</p>
</li>
<li><p>위의 결과는 실행할 때마다 다른 결과를 얻을 수 있는데 그 이유는 실행 중인 예제프로그램(프로세스)이 OS의 프로세스 스케줄러의 영향을 받기 때문이다. (쓰레드는 OS 종속적이다.)</p>
</li>
</ul>
<h2 id="쓰레드의-io블로킹">쓰레드의 I/O블로킹</h2>
<ul>
<li><p>쓰레드가 입출력(I/O)처리를 위해 기다리는 것을 I/O블로킹이라고 한다.</p>
</li>
<li><p>두 쓰레드가 서로 다른 자원을 사용하는 작업을 수행하는 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다. 예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와 입출력을 필요로 하는 경우가 이에 해당한다.</p>
</li>
<li><p>싱글쓰레드의 경우 사용자로부터 입력을 받는 경우 입력이 완료될 때까지 대기해야 하지만 멀티쓰레드의 경우 대기하지 않고 다른 작업을 수행할 수 있다.</p>
</li>
</ul>
<h3 id="싱글쓰레드로-작업-1">싱글쓰레드로 작업</h3>
<pre><code class="language-java">import javax.swing.JOptionPane;

class Ex13_4 {
    public static void main(String[] args) throws Exception {
        String input = JOptionPane.showInputDialog(&quot;아무 값이나 입력하세요.&quot;); 
        System.out.println(&quot;입력하신 값은 &quot; + input + &quot;입니다.&quot;);

        for(int i=10; i &gt; 0; i--) {
            System.out.println(i);
            try {
                Thread.sleep(1000);  // 1초간 시간을 지연한다.
            } catch(Exception e ) {}
        }
    }
}</code></pre>
<ul>
<li>하나의 쓰레드로 사용자의 입력을 받는 작업과 화면에 숫자를 출력하는 작업을 처리하기 때문에 사용자가 입력을 마치기 전까지는 화면에 숫자가 출력되지 않는다.</li>
</ul>
<h3 id="멀티쓰레드로-작업-1">멀티쓰레드로 작업</h3>
<pre><code class="language-java">import javax.swing.JOptionPane;

class Ex13_5 {
    public static void main(String[] args) throws Exception  {
        ThreadEx5_1 th1 = new ThreadEx5_1();
        th1.start();

        String input = JOptionPane.showInputDialog(&quot;아무 값이나 입력하세요.&quot;); 
        System.out.println(&quot;입력하신 값은 &quot; + input + &quot;입니다.&quot;);
    }
}

class ThreadEx5_1 extends Thread {
    public void run() {
        for(int i=10; i &gt; 0; i--) {
            System.out.println(i);
            try {
                sleep(1000);
            } catch(Exception e ) {}
        }
    }
}</code></pre>
<ul>
<li>사용자로부터 입력받는 부분과 화면에 숫자를 출력하는 부분을 두 개의 쓰레드로 나누어서 처리했기 때문에 사용자 입력을 기다리지 않는다.</li>
</ul>
<h2 id="쓰레드의-우선순위">쓰레드의 우선순위</h2>
<ul>
<li><p>쓰레드는 우선순위(priority)라는 속성(멤버변수)를 가지고 있다. 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. </p>
</li>
<li><p>쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다. 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.</p>
</li>
</ul>
<h3 id="우선순위-지정하기">우선순위 지정하기</h3>
<pre><code class="language-java">void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority() // 쓰레드의 우선순위를 반환한다.

public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1  // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위</code></pre>
<ul>
<li><p>쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다.</p>
</li>
<li><p>쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다. main 메서드를 수행하는 쓰레드는 우선순위가 5이므로 main 메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.</p>
</li>
<li><p>쓰레드를 실행하기 전에만 우선순위를 변경할 수 있다.</p>
</li>
<li><p>주의할 점은 쓰레드에 높은 우선순위를 준다고 더 많은 실행시간과 실행기회를 갖는 것은 아니다. 자바는 쓰레드가 우선순위에 따라 어떻게 다르게 처리되어야 하는지에 대해 강제하지 않으므로 쓰레드의 우선순위과 관련된 구현이 JVM마다 다를 수 있다.</p>
</li>
<li><p>굳이 우선순위에 차등을 두어 쓰레드를 실행하려면, 특정 OS의 스케쥴링 정책과 JVM의 구현을 직접 확인해봐야 한다. 만일 확인한다 하더라도 OS의 스케쥴러에 종속적이라서 어느 정도 예측만 가능한 정도일 뿐 정확히 알 수 없다.</p>
</li>
</ul>
<h2 id="쓰레드-그룹">쓰레드 그룹</h2>
<ul>
<li><p>쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 관리할 수 있다. 쓰레드 그룹은 보안상의 이유로 도입된 개념으로, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수 없다.</p>
</li>
<li><p>쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용하면 된다.</p>
</li>
</ul>
<pre><code class="language-java">Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)</code></pre>
<ul>
<li><p>모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.</p>
</li>
<li><p>자바 애플리케이션이 실행되면, JVM은 main와 system이라는 쓰레드 그룹을 만들고 JVM 운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다. 예를 들어 main 메서드를 수행하는 main이라는 이름의 쓰레드는 main 쓰레드 그룹에 속하고, 가비지컬렉션을 수행하는 Finalizer 쓰레드는 system 쓰레드 그룹에 속한다.</p>
</li>
<li><p>우리가 생성하는 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹이 되며, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main 쓰레드 그룹에 속하게 된다.</p>
</li>
</ul>
<h2 id="데몬-쓰레드">데몬 쓰레드</h2>
<ul>
<li><p>데몬 쓰레드는 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 한다. 데몬 쓰레드의 예로는 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신 등이 있다.</p>
</li>
<li><p>데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.</p>
</li>
<li><p>데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료된다.</p>
</li>
<li><p>보통 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.</p>
</li>
</ul>
<pre><code class="language-java">public void run() {
    while(true) {
        try {
            Thread.sleep(3 * 1000); // 3초
        } catch(InterruptedException e) {}

        if(autoSave) autoSave();
    }
}</code></pre>
<ul>
<li>데몬 쓰레드는 일반 쓰레드의 작성방법과 실행방법이 같으며 다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하기만 하면 된다.</li>
</ul>
<pre><code class="language-java">class Ex13_7 implements Runnable  {
    static boolean autoSave = false;

    public static void main(String[] args) {
        Thread t = new Thread(new Ex13_7());
        t.setDaemon(true);        // 이 부분이 없으면 종료되지 않는다.
        t.start();

        for(int i=1; i &lt;= 10; i++) {
            try{
                Thread.sleep(1000);
            } catch(InterruptedException e) {}
            System.out.println(i);

            if(i==5) autoSave = true;
        }

        System.out.println(&quot;프로그램을 종료합니다.&quot;);
    }

    public void run() {
        while(true) {
            try { 
                Thread.sleep(3 * 1000); // 3초 마다
            } catch(InterruptedException e) {}

            if(autoSave) autoSave();
        }
    }

    public void autoSave() {
        System.out.println(&quot;작업파일이 자동저장되었습니다.&quot;);
    }
}

1
2
3
4
5
작업파일이 자동저장되었습니다.
6
7
8
작업파일이 자동저장되었습니다.
9
10
프로그램을 종료합니다.</code></pre>
<ul>
<li>main 쓰레드가 종료되면 데몬 쓰레드도 종료된다.</li>
</ul>
<h2 id="쓰레드의-상태">쓰레드의 상태</h2>
<table>
<thead>
<tr>
<th>상태 이름</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>NEW</code></td>
<td>스레드 객체가 생성되었지만 <code>start()</code> 메서드가 호출되지 않은 상태이다. 아직 운영체제의 스레드 스케줄러에 등록되지 않았다.</td>
</tr>
<tr>
<td><code>RUNNABLE</code></td>
<td>스레드가 실행 중이거나 CPU 할당을 기다리는 상태이다. 실제 실행 여부는 JVM의 스케줄러가 결정한다.</td>
</tr>
<tr>
<td><code>BLOCKED</code></td>
<td>다른 스레드가 소유하고 있는 **동기화 블럭(lock)**에 진입하려고 할 때 대기 중인 상태이다. 해당 lock이 해제될 때까지 기다린다.</td>
</tr>
<tr>
<td><code>WAITING</code></td>
<td>스레드가 작업을 일시 중지하고 <strong>다른 스레드의 작업 완료를 기다리는 상태</strong>이다. 명시적으로 지정된 시간이 없기 때문에 무기한 대기하며, <code>notify()</code> 또는 <code>interrupt()</code>에 의해 깨어난다.<br>예: <code>Object.wait()</code>, <code>Thread.join()</code></td>
</tr>
<tr>
<td><code>TIMED_WAITING</code></td>
<td><code>WAITING</code>과 유사하지만, <strong>지정된 시간만큼만 대기</strong>하는 상태이다. 시간이 경과되면 자동으로 <code>RUNNABLE</code> 상태로 전환된다.<br>예: <code>Thread.sleep(1000)</code>, <code>join(1000)</code></td>
</tr>
<tr>
<td><code>TERMINATED</code></td>
<td>스레드의 작업이 완료되어 종료된 상태이다. 예외가 발생하거나 정상적으로 종료되었을 때 이 상태로 전환된다.</td>
</tr>
</tbody></table>
<h3 id="생명주기">생명주기</h3>
<ol>
<li><p>쓰레드를 생성하고 start()를 호출하면 실행대기열에 저장되어 자신의 차례를 기다린다. 실행대기열은 큐와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.</p>
</li>
<li><p>실행대기 상태에 있다가 자신의 차례가 되면 실행 상태가 된다.</p>
</li>
<li><p>주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기 상태가 되고 다음 차례의 쓰레드가 실행 상태가 된다.</p>
</li>
<li><p>실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. 예를 들어 사용자 입력을 기다리는 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 된다.</p>
</li>
<li><p>지정된 일시정지 시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장된다.</p>
</li>
<li><p>실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.</p>
</li>
</ol>
<h2 id="쓰레드의-실행제어">쓰레드의 실행제어</h2>
<ul>
<li><p>쓰레드 프로그래밍이 어려운 이유는 동기화(synchronization)과 스케줄링(scheduling) 때문이다.</p>
</li>
<li><p>효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.</p>
</li>
<li><p>쓰레드의 스케줄링을 잘하기 위해서는 쓰레드의 상태와 관련 메서드를 잘 알아야 한다.</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>메서드 시그니처</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>static void sleep(long millis)</code><br><code>static void sleep(long millis, int nanos)</code></td>
<td>지정된 시간 동안 스레드를 <strong>일시정지(TIMED_WAITING 상태)</strong> 시킨다. 시간이 지나면 자동으로 <strong>실행대기(RUNNABLE)</strong> 상태로 전환된다. <br>⚠ <code>InterruptedException</code> 예외가 발생할 수 있으므로 예외 처리가 필요하다.</td>
</tr>
<tr>
<td><code>void join()</code><br><code>void join(long millis)</code><br><code>void join(long millis, int nanos)</code></td>
<td><strong>다른 스레드가 작업을 마칠 때까지 대기</strong>하게 만든다. 시간 제한을 줄 수도 있으며, 해당 스레드가 종료되거나 시간이 경과되면 다시 실행된다. 주로 메인 스레드가 다른 작업 스레드의 완료를 기다릴 때 사용된다.</td>
</tr>
<tr>
<td><code>void interrupt()</code></td>
<td><code>sleep()</code>이나 <code>join()</code> 등에 의해 일시정지된 스레드를 <strong>깨워서 실행대기 상태로 만든다</strong>. 이때 <code>InterruptedException</code>이 발생하며, 적절한 처리 로직이 필요하다.</td>
</tr>
<tr>
<td><code>void stop()</code> ❌</td>
<td>스레드를 <strong>즉시 강제 종료</strong>한다. 현재는 <strong>비추천(Deprecated)</strong> 되었으며, 자원 해제나 일관성 문제로 인해 <strong>사용 지양</strong>해야 한다.</td>
</tr>
<tr>
<td><code>void suspend()</code> ❌</td>
<td>스레드를 <strong>무기한 일시정지</strong>시킨다. <code>resume()</code>을 호출해야 재개된다. 하지만 <strong>데드락 가능성</strong> 때문에 역시 <strong>비추천(Deprecated)</strong> 상태이다.</td>
</tr>
<tr>
<td><code>void resume()</code> ❌</td>
<td><code>suspend()</code>에 의해 중지된 스레드를 <strong>다시 실행 가능하게 만든다</strong>. 하지만 위와 같은 이유로 <strong>현업에서는 사용하지 않는 것이 원칙</strong>이다.</td>
</tr>
<tr>
<td><code>static void yield()</code></td>
<td><strong>실행 중인 스레드가 CPU를 양보</strong>하고 다시 실행대기 상태로 전환된다. 스케줄러가 다음에 어떤 스레드를 실행할지는 보장되지 않는다.</td>
</tr>
</tbody></table>
<ul>
<li>resume(), stop(), suspend()는 쓰레드를 교착상태로 만들기 쉽게 때문에 deprecated 되었다.</li>
</ul>
<h3 id="sleep">sleep()</h3>
<ul>
<li><p>지정된 시간동안 쓰레드를 멈추게 한다.</p>
</li>
<li><p>밀리세컨드(millis, 1000분의 일초)와 나노세컨드(nanos, 10억분의 일초)의 시간단위로 세밀하게 값을 지정할 수 있지만 어느 정도 오차가 발생할 수 있다.</p>
</li>
</ul>
<pre><code class="language-java">try {
    Thread.sleep(1, 500000);
} catch(InterruptedException e) {}</code></pre>
<ul>
<li>sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면, InterruptedException이 발생되어 실행대기 상태가 된다. 그래서 항상 try-catch문으로 예외 처리를 해야 한다.</li>
</ul>
<h3 id="interrupt">interrupt()</h3>
<ul>
<li><p>진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야할 때가 있다. 예를 들어 큰 파일을 다운로드받을 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 한다.</p>
</li>
<li><p>interrupt()는 쓰레드의 interrupted 상태(인스턴스 변수)를 바꿔 작업을 멈추도록 요청할 수 있다.</p>
</li>
</ul>
<pre><code class="language-java">Thread th = new Thread();
th.start();

th.interrupt()

class MyThread extends Thread {
    public void run() {
        while(!interrupted()) {
            ...
        }
    }
}

void interrupt() // 쓰레드의 interrupted 상태를 false -&gt; true
boolean isInterrupted() // 쓰레드의 interrupted 상태를 반환
static boolean interrupted() // 현재 쓰레드의 interrupted 상태를 반환 후, false로 변경</code></pre>
<ul>
<li><p>interrupted()는 쓰레드에 대해 interrupt()가 호출되었는지 알려준다. (boolean 값)</p>
</li>
<li><p>한 쓰레드가 sleep(), wait(), join()에 의해 일시정지 상태에 있을 때, interrupt()를 호출하면 sleep(), wait(), join()에서 InterruptedException이 발생하고 이 쓰레드는 실행대기 상태로 바뀐다.</p>
</li>
</ul>
<h3 id="suspend-resume-stop">suspend(), resume(), stop()</h3>
<ul>
<li><p>suspend(), resume(), stop()은 쓰레드의 실행을 제어하는 가장 손쉬운 방법이지만, suspend()와 stop()이 교착상태를 일으키기 쉽게 작성되어있으므로 사용이 권장되지 않는다.</p>
</li>
<li><p>해당 메서드들은 모두 deprecated 되었다.</p>
</li>
</ul>
<h3 id="join">join()</h3>
<ul>
<li><p>쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 한다. 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다.</p>
</li>
<li><p>작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 join()을 사용한다.</p>
</li>
</ul>
<pre><code class="language-java">try {
    th1.join();
} catch(InterruptedException e) {}</code></pre>
<ul>
<li>interrupt()에 의해 InterruptedException이 발생해 대기상태에서 벗어나 실행대기 상태로 바뀔 수 있다.</li>
</ul>
<h3 id="yield">yield()</h3>
<ul>
<li>쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다. 예를 들어 스케쥴러에 의해 1초의 실행시간을 할당받은 쓰레드가 0.5초의 시간동안 작업한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기 상태가 된다.</li>
</ul>
<pre><code class="language-java">try {
    th1.join(); // main 쓰레드가 th1의 작업을 끝날 때까지 기다린다.
    th2.join(); // main 쓰레드가 th2의 작업을 끝날 때까지 기다린다.
} catch (InterruptedException e) {}</code></pre>
<h2 id="쓰레드의-동기화">쓰레드의 동기화</h2>
<ul>
<li><p>한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것</p>
</li>
<li><p>멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.</p>
</li>
<li><p>쓰레드A가 작업하던 도중에 다른 쓰레드B에게 제어권이 넘어갔을 때, 쓰레드A가 작업하던 공유데이터를 쓰레드B가 임의로 변경하였다면, 다시 쓰레드A가 제어권을 받아서 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.</p>
</li>
<li><p>이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 바로 “임계 영역(critical section)”과 “잠금(락, lock)”이다.</p>
</li>
<li><p>공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정하여 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다.</p>
</li>
</ul>
<h3 id="synchronized-사용">synchronized 사용</h3>
<ul>
<li>메서드 전체를 임계 영역으로 지정하는 방법과 특정한 영역을 임계 영역으로 지정하는 방법이 있다.</li>
</ul>
<pre><code class="language-java">// 메서드 전체를 임계 영역으로 지정
public synchronized void caclSum() {
    // 임계 영역
}

// 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) {
    // 임계 영역
}</code></pre>
<ul>
<li><p>“객체의 참조변수”는 락(lock)을 걸고자하는 객체를 참조하는 것이어야 한다. (this 등)</p>
</li>
<li><p>임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 락을 거는 것보다는 특정 영역으로 임계 영역을 최소화하는 것이 좋다.</p>
</li>
</ul>
<h3 id="wait과-notify">wait()과 notify()</h3>
<ul>
<li><p>wait(), notify(), notifyAll()은 모두 Object 클래스에 정의되어 있으며 동기화 블럭(synchronized) 내에서만 사용할 수 있다.</p>
</li>
<li><p>synchronized로 동기화해서 공유 데이터를 보호할 때 특정 쓰레드가 객체의 락을 오랜 시간 유지하지 않도록 해야 한다. 특정 쓰레드가 락을 오랜 시간 유지하는 상황을 개선하기 위해 wait()과 notify()를 사용한다.</p>
</li>
<li><p>동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 한다. 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 수행할 수 있게 한다.</p>
</li>
<li><p>wait()이 호출되면, 실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 notify()를 기다린다. notify()가 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 락을 얻을 수 있다.</p>
</li>
<li><p>notifyAll()은 기다리고 있는 모든 쓰레드에게 통보를 하지만, 락을 얻을 수 있는 것은 하나의 쓰레드일 뿐이다.</p>
</li>
<li><p>waiting pool은 객체마다 존재한다. 따라서 특정 객체에서 notify()를 호출하면 해당 객체의 waiting pool에서 대기하는 쓰레드에만 통보할 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트와 JPA]]></title>
            <link>https://velog.io/@dev_gyeongmin/3%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@dev_gyeongmin/3%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Fri, 11 Jul 2025 08:43:11 GMT</pubDate>
            <description><![CDATA[<h2 id="orm">ORM</h2>
<ul>
<li><p>ORM은 Object-Relational Mapping의 약어로, 객체(Object)와 관계형 데이터베이스(Relational Database) 사이의 데이터를 자동으로 매핑해주는 기술이다.</p>
</li>
<li><p>자바의 객체와 관계형 데이터베이스의 테이블 사이의 구조적 차이를 매핑하여, 개발자가 SQL을 직접 작성하지 않고도 데이터베이스를 조작할 수 있도록 지원한다.</p>
</li>
<li><p>즉, 객체 지향 프로그래밍 언어로만 데이터베이스를 다룰 수 있도록 도와주는 추상화 도구이다.</p>
</li>
</ul>
<h3 id="핵심-개념">핵심 개념</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>객체(Object)</td>
<td>자바 등 OOP 언어에서 사용하는 클래스 기반의 데이터 구조</td>
</tr>
<tr>
<td>관계형 테이블(Relational Table)</td>
<td>DB에서 데이터를 행(row)과 열(column)로 표현하는 구조</td>
</tr>
<tr>
<td>매핑(Mapping)</td>
<td>클래스 ↔ 테이블, 필드 ↔ 컬럼, 객체 ↔ 레코드 간 변환 작업</td>
</tr>
</tbody></table>
<h3 id="장점">장점</h3>
<ul>
<li><p>비즈니스 로직에 집중할 수 있음 (SQL 분리)</p>
</li>
<li><p>생산성 향상 및 유지보수 용이</p>
</li>
<li><p>객체지향적 설계와 데이터 접근 방식의 일관성 유지</p>
</li>
<li><p>DBMS에 대한 의존도 감소 (DB 추상화)</p>
</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li><p>복잡한 SQL 쿼리 처리에는 한계가 있음 (성능 이슈)</p>
</li>
<li><p>예기치 않은 SQL이 생성될 수 있음 (튜닝 어려움)</p>
</li>
</ul>
<h2 id="jpa와-하이버네이트">JPA와 하이버네이트</h2>
<ul>
<li><p>ORM에는 여러 종류가 있다. 자바에서는 JPA(java persistence API)를 표준으로 사용한다.</p>
</li>
<li><p>JPA는 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다. 인터페이스이므로 실제 사용을 위해서는 ORM 프레임워크를 추가로 선택해야 한다.</p>
</li>
<li><p>하이버네이트는 JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크이다. 내부적으로는 JDBC API를 사용한다.</p>
</li>
<li><p>애플리케이션 ↔ 스프링 데이터 JPA ↔ JPA ↔ 하이버네이트 ↔ JDBC ↔ 데이터베이스</p>
</li>
</ul>
<h3 id="엔티티">엔티티</h3>
<ul>
<li><p>데이터베이스의 테이블과 매핑되는 객체를 의미</p>
</li>
<li><p>자바의 일반 객체와 다르지 않지만 데이터베이스에 영향을 미치는 쿼리를 실행하는 객체</p>
</li>
</ul>
<h3 id="엔티티-매니저">엔티티 매니저</h3>
<ul>
<li><p>엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 등의 역할을 한다.</p>
</li>
<li><p>엔티티 매니저를 만드는 곳은 엔티티 매니저 팩토리</p>
</li>
<li><p>스프링 부트는 내부에서 엔티티 매니저 팩토리를 하나만 생성해서 관리하고 @PersistenceContext 또는 @Autowired 애너테이션을 사용해서 엔티티 매니저를 사용한다.</p>
</li>
</ul>
<pre><code class="language-java">@PersistenceContext
EntityManager em; // 프록시 엔티티 매니저</code></pre>
<ul>
<li><p>스프링 부트는 기본적으로 빈을 하나만 생성해서 공유하므로 동시성 문제가 발생할 수 있다. 그래서 실제로는 엔티티 매니저가 아닌 실제 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용한다. 그리고 필요할 때 데이터베이스 트랙잭션과 관련된 실제 엔티티 매니저를 호출한다.</p>
</li>
<li><p>엔티티 매니저는 Spring Data JPA에서 관리</p>
</li>
</ul>
<h3 id="엔티티의-상태">엔티티의 상태</h3>
<ol>
<li>영속성 컨텍스트가 관리하고 있지 않는 분리(detached) 상태</li>
<li>관리하는 관리(managed) 상태</li>
<li>영속성 컨텍스트와 전혀 관계가 없는 비영속(transient) 상태</li>
<li>삭제된(removed) 상태</li>
</ol>
<p>특정 메서드를 호출해 상태는 변경 가능하다. 필요에 따라 엔티티의 상태를 조절해 데이터를 올바르게 유지하고 관리할 수 있다. 엔티티를 처음 만들면 엔티티는 비영속 상태가 된다.</p>
<pre><code class="language-java">public class EntityManagerTest {

  @Autowired
  EntityManager em;

  public void example() {
    // 비영속 상태
    Member member = new Member(1L, &quot;홍길동&quot;);

    // 관리 상태
    em.persist(member);
    // 분리 상태
    em.detach(member);
    // 삭제 상태
    em.remove(member);
  }
}</code></pre>
<h2 id="영속성-컨텍스트">영속성 컨텍스트</h2>
<ul>
<li><p>엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장한다는 특징이 있다.</p>
</li>
<li><p>영속성 컨텍스트는 JPA의 중요한 특징 중 하나로, 엔티티를 관리하는 가상의 공간이다.</p>
</li>
<li><p>데이터베이스의 접근을 최소화해 성능을 높일 수 있다.</p>
</li>
</ul>
<h3 id="1차-캐시">1차 캐시</h3>
<ul>
<li><p>영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다. 이때 캐시의 키는 엔티티의 @Id 애너테이션이 달린 기본키 역할을 하는 식별자이며 값은 엔티티이다.</p>
</li>
<li><p>엔티티를 조회하면 1차 캐시에서 데이터를 조회하고 값이 있으면 반환한다. 값이 없으면 데이터베이스에서 조회해 1차 캐시에 저장한 다음 반환한다.</p>
</li>
</ul>
<h3 id="쓰기-지연">쓰기 지연</h3>
<ul>
<li><p>쿼리를 모았다가 트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것을 의미한다.</p>
</li>
<li><p>적당한 묶음으로 쿼리를 요청할 수 있어 데이터베이스 시스템의 부담을 줄일 수 있다.</p>
</li>
</ul>
<h3 id="변경-감지">변경 감지</h3>
<ul>
<li>트랜잭션을 커밋하면 1차 캐시에 저장되어 있는 엔티티의 값과 현재 엔티티의 값을 비교해서 변경된 값이 있다면 변경 사항을 감지해 변경된 값을 데이터베이스에 자동으로 반영한다.</li>
</ul>
<h3 id="지연-로딩">지연 로딩</h3>
<ul>
<li><p>쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라 필요할 때 쿼리를 날려 데이터를 조회하는 것을 의미한다.</p>
</li>
<li><p>반대로 조회할 때 쿼리를 보내 연관된 모든 데이터를 가져오는 즉시 로딩도 있다.</p>
</li>
</ul>
<h2 id="스프링-데이터">스프링 데이터</h2>
<ul>
<li><p>스프링 데이터는 비즈니스 로직에 더 집중할 수 있게 데이터베이스 사용 기능을 클래스 레벨에서 추상화했다.</p>
</li>
<li><p>스프링 데이터에서 제공하는 인터페이스를 통해서 스프링 데이터를 사용할 수 있다. CRUD 포함 여러 메서드, 페이징 처리 기능, 쿼리 메서드 등을 제공</p>
</li>
<li><p>spring data JPA, spring data MongoDB 등 데이터베이스의 특성에 맞춰 기능을 확장해 제공하는 기술도 제공한다.</p>
</li>
</ul>
<h2 id="스프링-데이터-jpa">스프링 데이터 JPA</h2>
<ul>
<li><p>스프링 데이터 JPA는 스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술이다.</p>
</li>
<li><p>스프링 데이터의 인터페이스인 PagingAndSortingRepository를 상속받아 JpaRepository 인터페이스를 만들었고 JPA를 더 편리하게 사용하는 메서드를 제공한다.</p>
</li>
<li><p>스프링 데이터 JPA를 사용하면 리포지토리 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있다.</p>
</li>
<li><p>JpaRepository 인터페이스를 우리가 만든 인터페이스에 상속받고, 제네릭에는 관리할 &lt;엔티티 이름, 엔티티 기본키의 타입&gt;을 작성하면 기본 CRUD 메서드를 사용할 수 있다.</p>
</li>
</ul>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
}</code></pre>
<h3 id="쿼리-메서드">쿼리 메서드</h3>
<ul>
<li><p>JPA는 메서드 이름으로 쿼리를 작성하는 기능을 제공</p>
</li>
<li><p>쿼리 메서드는 JPA가 정해준 메서드 이름 규칙을 따르면 쿼리문을 특별히 구현하지 않아도 메서드처럼 사용할 수 있다.</p>
</li>
</ul>
<pre><code class="language-java">@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
  Optional&lt;Member&gt; findByName(String name);
}</code></pre>
<h3 id="update">update</h3>
<ul>
<li>JPA는 트랜잭션 내에서 데이터를 수정해야 한다. 따라서 데이터를 수정할 때는 그냥 메서드만 사용하면 안 되고 @Transactional 애너테이션을 메서드에 추가해야 한다.</li>
</ul>
<pre><code class="language-java">public class Member {
  ...
  public void changeName(String name) {
    this.name = name;
  }
}</code></pre>
<ul>
<li>위 메서드가 @Transactional 애너테이션이 포함된 메서드에서 호출되면 JPA는 변경 감지 기능을 통해 엔티티의 필드값이 변경될 때 그 변경 사항을 데이터베이스에 자동으로 반영한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트와 테스트]]></title>
            <link>https://velog.io/@dev_gyeongmin/2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@dev_gyeongmin/2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 10 Jul 2025 09:06:36 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-부트-구조">스프링 부트 구조</h2>
<ul>
<li><p>스프링 부트는 각 계층이 양 옆의 계층과 통신하는 구조를 따른다. 계층은 각자의 역할과 책임이 있는 어떤 소프트웨어의 구성 요소를 의미한다. 각 계층은 소통할 수 있지만 다른 계층에 직접 간섭하거나 영향을 미치지 않는다.</p>
</li>
<li><p>스프링 부트에는 프레젠테이션(컨트롤러), 비즈니스(서비스), 퍼시스턴스(리포지토리) 계층이 있다.</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>계층 구분</th>
<th>역할 설명</th>
</tr>
</thead>
<tbody><tr>
<td>프레젠테이션 계층</td>
<td>- 클라이언트의 <strong>HTTP 요청을 수신</strong><br>- 요청을 <strong>비즈니스 계층에 전달</strong>하고 결과를 응답</td>
</tr>
<tr>
<td>비즈니스 계층</td>
<td>- <strong>비즈니스 로직 처리</strong> 전담<br>- 서비스 목적에 맞는 핵심 기능 수행</td>
</tr>
<tr>
<td>퍼시스턴스 계층</td>
<td>- <strong>데이터베이스와의 연동</strong> 담당<br>- DAO(Data Access Object)를 통해 데이터 조작 수행</td>
</tr>
</tbody></table>
<h2 id="스프링-부트-요청-응답-과정">스프링 부트 요청-응답 과정</h2>
<ol>
<li><p>톰캣에 HTTP 요청을 한다. 이 요청은 스프링 부트 내로 이동한다. 이때 스프링 부트의 <strong>디스패처 서블릿</strong>이 URL을 분석하고, 이 요청을 처리할 수 있는 컨트롤러를 찾는다.</p>
</li>
<li><p>컨트롤러가 비즈니스 계층과 퍼시스턴스 계층을 통하면서 필요한 데이터를 가져온다.</p>
</li>
<li><p><strong>뷰 리졸버</strong>는 템플릿 엔진을 사용해 HTML 문서를 만들거나 JSON, XML 등의 데이터를 생성한다.</p>
</li>
<li><p>디스패처 서블릿에 의해 응답으로 클라이언트에게 반환된다.</p>
</li>
</ol>
<h2 id="스프링-부트와-테스트">스프링 부트와 테스트</h2>
<ul>
<li><p>스프링 부트는 애플리케이션을 테스트하기 위한 도구와 애너테이션을 제공한다. </p>
</li>
<li><p>spring-boot-starter-test 스타터에 테스트를 위한 도구가 모여 있다. 대표적으로 :</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>JUnit</strong></td>
<td>자바 프로그래밍 언어용 단위 테스트 프레임워크</td>
</tr>
<tr>
<td><strong>Spring Test &amp; Spring Boot Test</strong></td>
<td>스프링 및 스프링 부트 애플리케이션을 위한 통합 테스트 지원 도구</td>
</tr>
<tr>
<td><strong>AssertJ</strong></td>
<td>가독성 높고 풍부한 기능을 제공하는 어설션(검증문) 작성 라이브러리</td>
</tr>
</tbody></table>
<h2 id="junit">JUnit</h2>
<ul>
<li><p>자바 언어를 위한 단위 테스트 프레임워크</p>
</li>
<li><p>단위 테스트란, 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것을 의미합니다. 이때 단위는 보통 메서드가 된다.</p>
</li>
<li><p>JUnit은 테스트 클래스에 정의된 각 테스트 메서드(@Test)를 실행할 때마다 <strong>새로운 인스턴스(객체)</strong>를 생성해서 테스트를 수행하고, 테스트가 끝나면 그 인스턴스를 즉시 폐기합니다.</p>
</li>
</ul>
<pre><code class="language-java">import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class JUnitTest {
  @DisplayName(&quot;1 + 2는 3이다&quot;)
  @Test
  public void junitTest() {
    int a = 1;
    int b = 2;

    int sum = 3;

    Assertions.assertEquals(sum, a + b);
  }
}</code></pre>
<ul>
<li><p>@DisplayName 애너테이션은 테스트 이름을 명시해준다.</p>
</li>
<li><p>@Test 애너테이션을 붙인 메서드는 테스트를 수행하는 메서드가 된다.</p>
</li>
<li><p>assertEquals() 메서드의 첫 번째 인수에는 기대하는 값, 두 번째 인수에는 실제로 검증할 값을 넣어준다.</p>
</li>
</ul>
<h3 id="cycle">Cycle</h3>
<pre><code class="language-java">import org.junit.jupiter.api.*;

public class JUnitCycleTest {
  @BeforeAll
  static void beforeAll() {
    System.out.println(&quot;@BeforeAll&quot;);
  }

  @BeforeEach
  public void beforeEach() {
    System.out.println(&quot;@BeforeEach&quot;);
  }

  @Test
  public void test1() {
    System.out.println(&quot;test1&quot;);
  }

  @Test
  public void test2() {
    System.out.println(&quot;test2&quot;);
  }

  @Test
  public void test3() {
    System.out.println(&quot;test3&quot;);
  }

  @AfterAll
  static void afterAll() {
    System.out.println(&quot;@AfterAll&quot;);
  }

  @AfterEach
  public void afterEach() {
    System.out.println(&quot;@AfterEach&quot;);
  }
}</code></pre>
<h4 id="beforeall">BeforeAll</h4>
<ul>
<li><p>전체 테스트를 시작하기 전에 처음으로 한 번만 실행</p>
</li>
<li><p>전체 테스트 주기에서 한 번만 호출되어야 하기 때문에 static으로 선언</p>
</li>
</ul>
<h4 id="beforeeach">BeforeEach</h4>
<ul>
<li><p>테스트 케이스를 시작하기 전에 매번 실행</p>
</li>
<li><p>각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 함</p>
</li>
</ul>
<h4 id="afterall">AfterAll</h4>
<ul>
<li><p>전체 테스트를 마치고 종료하기 전에 한 번만 실행</p>
</li>
<li><p>전체 테스트 주기에서 한 번만 호출되어야 하기 때문에 static으로 선언</p>
</li>
</ul>
<h4 id="aftereach">AfterEach</h4>
<ul>
<li><p>각 테스트 케이스를 종료하기 전 매번 실행</p>
</li>
<li><p>각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 함</p>
</li>
</ul>
<h2 id="assertj">AssertJ</h2>
<ul>
<li>AssertJ는 JUnit과 함께 사용해 검증문의 가독성을 확 높여주는 라이브러리</li>
</ul>
<pre><code class="language-java">import static org.assertj.core.api.Assertions.assertThat;

assertThat(a + b).isEqualTo(sum);</code></pre>
<ul>
<li><p>assertThat을 사용시 주로 static import 사용</p>
</li>
<li><p>다양한 검증 메서드 제공 (isEqualTo, isNotEqualTo, contains, doesNotContain, startsWith, endsWith, isEmpty, isNotEmpty, isPositive, isNegative, isGreaterThan, isLessThan 등)</p>
</li>
</ul>
<h2 id="컨트롤러-테스트-코드-작성">컨트롤러 테스트 코드 작성</h2>
<h3 id="springboottest">@SpringBootTest</h3>
<ul>
<li>메인 애플리케이션 클래스에 추가하는 애너테이션 @SpringBootApplication 이 있는 클래스를 찾고 그 클래스에 포함되어 있는 빈을 찾은 다음 테스트용 애플리케이션 컨텍스트라는 것을 만든다.</li>
</ul>
<h3 id="autoconfiguremockmvc">@AutoConfigureMockMvc</h3>
<ul>
<li><p>MockMvc를 생성하고 자동으로 구성하는 애너테이션</p>
</li>
<li><p>MockMvc는 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스이다. 즉, 컨트롤러를 테스트할 때 사용되는 클래스</p>
</li>
</ul>
<h3 id="예제1">예제1</h3>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureMockMvc
class TestControllerTest {
  @Autowired
  protected MockMvc mockMvc;

  @Autowired
  private WebApplicationContext context;

  @Autowired
  private MemberRepository memberRepository;

  @BeforeEach
  public void mockMvcSetUp() {
    mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
  }

  @AfterEach
  public void cleanUp() {
    memberRepository.deleteAll();
  }

  @DisplayName(&quot;getAllMembers: 아티클 조회에 성공한다.&quot;)
  @Test
  public void getAllMembers() throws Exception {
    // given
    final String url = &quot;/test&quot;;
    Member savedMember = memberRepository.save(new Member(1L, &quot;홍길동&quot;));

    // when
    final ResultActions result = mockMvc.perform(get(url)
        .accept(MediaType.APPLICATION_JSON));

    // then
    result
        .andExpect(status().isOk())
        .andExpect(jsonPath(&quot;$[0].id&quot;).value(savedMember.getId()))
        .andExpect(jsonPath(&quot;$[0].name&quot;).value(savedMember.getName()));
  }
}</code></pre>
<ul>
<li><p>perform() 메서드는 요청을 전송하는 역할을 하는 메서드이다. 그 결과로 ResultActions 객체를 받으며, ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드를 제공해준다.</p>
</li>
<li><p>accept() 메서드는 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드이다.</p>
</li>
<li><p>jsonPath() 메서드는 JSON 응답값의 값을 가져오는 역할을 하는 메서드이다.</p>
</li>
</ul>
<h3 id="예제2">예제2</h3>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureMockMvc
class QuizControllerTest {

  @Autowired
  protected MockMvc mockMvc;

  @Autowired
  private WebApplicationContext context;

  @Autowired
  private ObjectMapper objectMapper;

  @BeforeEach
  public void mockMvcSetUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
        .build();
  }

  @DisplayName(&quot;quiz(): GET /quiz?code=1 이면 응답 코드는 201, 응답 본문은 Created!를 리턴한다.&quot;)
  @Test
  public void getQuiz1() throws Exception {
    String url = &quot;/quiz&quot;;

    final ResultActions result = mockMvc.perform(get(url)
        .param(&quot;code&quot;, &quot;1&quot;));

    result
        .andExpect(status().isCreated())
        .andExpect(content().string(&quot;Created!&quot;));
  }

  @DisplayName(&quot;quiz(): GET /quiz?code=2 이면 응답 코드는 400, 응답 본문은 Bad Request!를 리턴한다.&quot;)
  @Test
  public void getQuiz2() throws Exception {
    String url = &quot;/quiz&quot;;

    final ResultActions result = mockMvc.perform(get(url)
        .param(&quot;code&quot;, &quot;2&quot;));

    result
        .andExpect(status().isBadRequest())
        .andExpect(content().string(&quot;Bad Request!&quot;));
  }

  @DisplayName(&quot;quiz(): POST /quiz 이면 응답 코드는 403, 응답 본문은 Forbidden!를 리턴한다.&quot;)
  @Test
  public void postQuiz1() throws Exception {
    String url = &quot;/quiz&quot;;

    final ResultActions result = mockMvc.perform(post(url)
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(new Code(1)))
    );

    result
        .andExpect(status().isForbidden())
        .andExpect(content().string(&quot;Forbidden!&quot;));
  }

  @DisplayName(&quot;quiz(): POST /quiz 이면 응답 코드는 200, 응답 본문은 OK!를 리턴한다.&quot;)
  @Test
  public void postQuiz2() throws Exception {
    String url = &quot;/quiz&quot;;

    final ResultActions result = mockMvc.perform(post(url)
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(new Code(13)))
    );

    result
        .andExpect(status().isOk())
        .andExpect(content().string(&quot;OK!&quot;));
  }

  record Code(int value) {}
}</code></pre>
<ul>
<li>ObjectMapper는 jackson 라이브러리에서 제공하는 클래스로 객체와 JSON 간의 변환을 처리해준다. (객체 직렬화)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링과 스프링 부트]]></title>
            <link>https://velog.io/@dev_gyeongmin/1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@dev_gyeongmin/1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 09 Jul 2025 07:34:36 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-프레임워크란">스프링 프레임워크란?</h2>
<p>스프링은 <strong>자바 기반의 엔터프라이즈 애플리케이션 개발을 손쉽게 만들어주는 프레임워크</strong>입니다. 객체 지향적인 설계 원칙을 자연스럽게 따르도록 지원함으로써 대규모 애플리케이션 개발에 효과적입니다.</p>
<h3 id="제어의-역전-ioc-inversion-of-control">제어의 역전 (IoC: Inversion of Control)</h3>
<h4 id="기존-방식-개발자가-객체를-직접-생성">기존 방식: 개발자가 객체를 직접 생성</h4>
<pre><code class="language-java">public class A {
    B b = new B();
}</code></pre>
<p>위와 같이 클래스 A에서 직접 B 객체를 생성할 경우, 클래스 간 결합도가 높아지고 테스트나 유지보수가 어려워집니다.</p>
<h4 id="스프링-방식-객체-제어-권한을-프레임워크에-위임">스프링 방식: 객체 제어 권한을 프레임워크에 위임</h4>
<pre><code class="language-java">public class A {
    private B b;
}</code></pre>
<p>스프링에서는 객체 생성 및 관리 책임을 <strong>스프링 컨테이너</strong>에 위임합니다. 개발자는 단지 객체를 사용할 준비만 하고, 실제 생성은 프레임워크가 담당합니다. 이러한 구조를 제어의 역전이라 부릅니다.</p>
<h3 id="의존성-주입-di-dependency-injection">의존성 주입 (DI: Dependency Injection)</h3>
<p>의존성 주입은 IoC의 구현 방식 중 하나로, 객체 간의 의존성을 외부에서 주입해주는 방식입니다.</p>
<pre><code class="language-java">public class A {
    @Autowired
    B b;
}</code></pre>
<p><code>@Autowired</code> 애너테이션을 통해 스프링 컨테이너가 관리하는 Bean을 자동으로 주입받을 수 있습니다. 개발자는 객체의 생성 방식을 몰라도 사용할 수 있어 코드가 훨씬 유연해집니다.</p>
<h3 id="스프링-컨테이너">스프링 컨테이너</h3>
<p>스프링 컨테이너는 애플리케이션 구동 시 필요한 객체(Bean)를 생성하고, 이들의 생명주기를 관리합니다. 또한, DI 기능을 통해 의존 객체를 주입해주는 역할을 수행합니다.</p>
<table>
<thead>
<tr>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>객체 생성 (Bean 관리)</td>
<td>@Component, 자동 구성 등을 통해 등록된 클래스 인스턴스를 생성</td>
</tr>
<tr>
<td>의존성 주입 (DI 지원)</td>
<td>@Autowired 등을 통해 필요한 객체를 주입</td>
</tr>
<tr>
<td>생명주기 관리</td>
<td>생성 → 초기화 → 사용 → 소멸 단계까지 관리</td>
</tr>
</tbody></table>
<h3 id="빈bean">빈(Bean)</h3>
<p>스프링 컨테이너가 관리하는 객체를 <strong>Bean(빈)</strong>이라고 합니다. 빈으로 등록하는 방법에는 여러 가지가 있지만, 대표적으로 아래와 같이 <code>@Component</code> 애너테이션을 사용합니다.</p>
<pre><code class="language-java">@Component
public class MyBean {}</code></pre>
<p>위 클래스는 myBean이라는 이름으로 스프링 컨테이너에 등록되며, 다른 클래스에서 DI를 통해 사용할 수 있습니다.</p>
<h3 id="관점-지향-프로그래밍-aop-aspect-oriented-programming">관점 지향 프로그래밍 (AOP: Aspect-Oriented Programming)</h3>
<p>AOP는 로직을 <strong>핵심 관점(core concern)</strong>과 <strong>부가 관점(cross-cutting concern)</strong>으로 나누어 모듈화하는 방법론입니다.</p>
<ul>
<li><p>핵심 관점: 계좌 이체, 회원 가입 등 주요 비즈니스 로직</p>
</li>
<li><p>부가 관점: 로깅, 트랜잭션, 보안 등 반복되는 부가 기능</p>
</li>
</ul>
<p>AOP를 적용하면 이러한 부가 기능을 별도로 분리하여 재사용성과 유지보수성을 향상시킬 수 있습니다.</p>
<h3 id="이식-가능한-서비스-추상화-psa-portable-service-abstraction">이식 가능한 서비스 추상화 (PSA: Portable Service Abstraction)</h3>
<p>PSA는 다양한 기술을 사용하는 환경에서도 <strong>일관된 프로그래밍 모델을 제공하기 위한 스프링의 추상화 전략</strong>입니다.</p>
<p>예를 들어, 데이터 접근 기술을 JDBC에서 JPA로 변경하더라도 코드 수정이 최소화됩니다. 마찬가지로 서블릿 컨테이너를 톰캣에서 언더토우로 교체해도 기존 비즈니스 로직에는 영향이 없습니다.</p>
<h3 id="핵심-개념-요약">핵심 개념 요약</h3>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>IoC (제어의 역전)</td>
<td>객체의 생성과 제어를 프레임워크가 담당</td>
</tr>
<tr>
<td>DI (의존성 주입)</td>
<td>외부에서 객체를 주입받아 사용하는 방식</td>
</tr>
<tr>
<td>AOP (관점 지향)</td>
<td>핵심/부가 관점을 나누어 모듈화하는 프로그래밍</td>
</tr>
<tr>
<td>PSA (서비스 추상화)</td>
<td>다양한 기술에 대한 일관된 접근 방식 제공</td>
</tr>
</tbody></table>
<h2 id="스프링-부트란">스프링 부트란?</h2>
<p>전통적인 스프링 프레임워크는 강력한 기능을 제공하지만, 설정이 복잡하고 러닝 커브가 비교적 높다는 단점이 있습니다. 이러한 불편함을 해결하고자 등장한 것이 <strong>스프링 부트(Spring Boot)</strong>입니다.</p>
<blockquote>
<p>스프링 부트는 스프링 애플리케이션을 더 빠르고 간편하게 개발할 수 있도록 도와주는 도구입니다.</p>
</blockquote>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li><p><strong>자동 구성(Auto Configuration)</strong>: 복잡한 설정 파일 없이 기본 설정 자동 적용</p>
</li>
<li><p><strong>스타터 제공</strong>: 기능별 의존성 패키지를 묶어 관리</p>
</li>
<li><p><strong>내장 WAS 지원</strong>: 톰캣, 제티, 언더토우 등 내장, 별도 설치 없이 실행 가능</p>
</li>
<li><p><strong>JAR 실행 지원</strong>: java -jar 명령어로 바로 실행 및 배포 가능</p>
</li>
<li><p><strong>액츄에이터 제공</strong>: 모니터링 및 관리 기능 통합 제공</p>
</li>
<li><p><strong>XML 설정 불필요</strong>: 대부분의 설정을 자바 코드로 처리</p>
</li>
</ul>
<h3 id="스프링-부트-프로젝트-만들기-gradle-기반">스프링 부트 프로젝트 만들기 (Gradle 기반)</h3>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;3.2.0&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.1.0&#39;
}

group = &#39;me.park&#39;
version = &#39;1.0&#39;

repositories {
    mavenCentral()
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
}

test {
    useJUnitPlatform()
}</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>plugins</code></td>
<td>스프링 부트 플러그인 및 의존성 자동 관리 플러그인 등록</td>
</tr>
<tr>
<td><code>repositories</code></td>
<td>외부 의존성 다운로드를 위한 저장소 설정</td>
</tr>
<tr>
<td><code>dependencies</code></td>
<td>프로젝트 기능에 필요한 의존성 지정</td>
</tr>
</tbody></table>
<h3 id="스프링-부트-스타터">스프링 부트 스타터</h3>
<p><strong>스타터는 특정 기능에 필요한 의존성들을 묶어놓은 라이브러리 집합</strong>입니다. 일일이 라이브러리를 개별로 추가할 필요 없이 스타터 하나로 일괄 적용할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>스타터명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>spring-boot-starter-web</code></td>
<td>REST API 및 Spring MVC 기반 웹 개발을 위한 구성</td>
</tr>
<tr>
<td><code>spring-boot-starter-test</code></td>
<td>JUnit 기반 테스트 기능 제공</td>
</tr>
<tr>
<td><code>spring-boot-starter-validation</code></td>
<td>유효성 검사(Validation) 기능 제공</td>
</tr>
<tr>
<td><code>spring-boot-starter-actuator</code></td>
<td>애플리케이션 모니터링 및 헬스 체크 기능</td>
</tr>
<tr>
<td><code>spring-boot-starter-data-jpa</code></td>
<td>ORM 기반 데이터 접근(JPA) 기능 지원</td>
</tr>
</tbody></table>
<h3 id="스프링-부트-3와-자바-17-기능">스프링 부트 3와 자바 17 기능</h3>
<p>스프링 부트 3는 <strong>자바 17 이상을 요구</strong>하며, 자바의 최신 문법 기능과 매우 잘 통합됩니다.</p>
<h4 id="텍스트-블록-text-blocks">텍스트 블록 (Text Blocks)</h4>
<pre><code class="language-java">String query = &quot;&quot;&quot;
    SELECT * FROM &quot;items&quot;
    WHERE &quot;status&quot; = &quot;ON_SALE&quot;
    ORDER BY &quot;price&quot;;
    &quot;&quot;&quot;;</code></pre>
<h4 id="문자열-포맷-formatted">문자열 포맷 .formatted()</h4>
<pre><code class="language-java">String result = &quot;&quot;&quot;
{
  &quot;id&quot;: %d,
  &quot;name&quot;: &quot;%s&quot;
}
&quot;&quot;&quot;.formatted(2, &quot;juice&quot;);</code></pre>
<h4 id="레코드-record">레코드 (Record)</h4>
<pre><code class="language-java">record Item(String name, int price) {}

Item item = new Item(&quot;juice&quot;, 3000);
System.out.println(item.price()); // 3000</code></pre>
<ul>
<li><p>데이터 전달을 목적으로 하는 객체를 더 빠르고 간편하게 만들기 위한 기능</p>
</li>
<li><p>레코드는 상속을 할 수 없고 파라미터에 정의한 필드는 private final로 정의됨</p>
</li>
<li><p>getter 자동 생성</p>
</li>
</ul>
<h4 id="패턴-매칭-pattern-matching">패턴 매칭 (Pattern Matching)</h4>
<pre><code class="language-java">if (obj instanceof Integer i) {
    System.out.println(&quot;정수 값: &quot; + i);
}</code></pre>
<ul>
<li>instanceof 키워드와 형변환을 동시 수행</li>
</ul>
<h4 id="switch-문에서-자료형-분기-처리">switch 문에서 자료형 분기 처리</h4>
<pre><code class="language-java">static double getIntegerValue(Object o) {
    return switch(o) {
        case Double d -&gt; d.intValue();
        case Float f -&gt; f.intValue();
        case String s -&gt; Integer.parseInt(s);
        default -&gt; 0d;
    }
}</code></pre>
<h4 id="스프링-부트-관련-변경점">스프링 부트 관련 변경점</h4>
<ul>
<li><p>Servlet, JPA의 네임 스페이스가 Jakarta로 대체</p>
</li>
<li><p>GraalVM 기반의 스프링 네이티브 공식 지원</p>
</li>
</ul>
<h3 id="springbootapplication-애너테이션">@SpringBootApplication 애너테이션</h3>
<pre><code class="language-java">@SpringBootApplication
public class SpringBootDeveloperApplication {
  public static void main(String[] args) {
    SpringApplication.run(SpringBootDeveloperApplication.class, args);
  }
}</code></pre>
<ul>
<li><p>이 클래스는 자바의 main() 메서드와 같은 역할을 합니다. (여기서 스프링 부트가 시작됨)</p>
</li>
<li><p>이 애너테이션 하나로 스프링 부트 애플리케이션 구동에 필요한 대부분의 설정이 자동 구성됩니다.</p>
</li>
</ul>
<pre><code class="language-java">@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
  excludeFilters = {@Filter(
  type = FilterType.CUSTOM,
  classes = {TypeExcludeFilter.class}
), @Filter(
  type = FilterType.CUSTOM,
  classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...
}</code></pre>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>@SpringBootConfiguration</code></td>
<td>스프링 부트 설정 클래스 표시 (@Configuration 상속)</td>
</tr>
<tr>
<td><code>@EnableAutoConfiguration</code></td>
<td>자동 설정 활성화, 자동 설정으로 등록되는 빈을 읽고 등록</td>
</tr>
<tr>
<td><code>@ComponentScan</code></td>
<td>사용자 정의 빈 탐색 및 등록</td>
</tr>
</tbody></table>
<h3 id="restcontroller-애너테이션">@RestController 애너테이션</h3>
<pre><code class="language-java">@RestController
public class HelloController {
    @GetMapping(&quot;/hello&quot;)
    public String hello() {
        return &quot;Hello, Spring Boot!&quot;;
    }
}</code></pre>
<ul>
<li><p><code>@RestController</code> 는 <code>@Controller</code> + <code>@ResponseBody</code> 를 포함한 애너테이션</p>
</li>
<li><p>HTTP 요청을 해당 메서드와 연결하는 라우팅 역할을 수행</p>
</li>
<li><p><code>@Controller</code>가 <code>@Component</code> 를 포함하고 있어 빈으로 자동 등록됨</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[애너테이션(Annotation)]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%95%A0%EB%84%88%ED%85%8C%EC%9D%B4%EC%85%98Annotation</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%95%A0%EB%84%88%ED%85%8C%EC%9D%B4%EC%85%98Annotation</guid>
            <pubDate>Mon, 07 Jul 2025 07:53:29 GMT</pubDate>
            <description><![CDATA[<h2 id="개념과-등장-배경">개념과 등장 배경</h2>
<p>자바 언어의 설계자들은 소스코드와 그에 대한 문서를 따로 관리하는 것보다 소스코드 자체에 문서 정보를 포함시키는 방식이 더욱 효율적이라고 판단하였습니다. 이에 따라 자바에서는 주석(comment)을 이용해 문서화할 수 있는 기능을 제공하며, 이를 위해 javadoc.exe라는 문서 생성 도구가 함께 제공됩니다.</p>
<h3 id="주석-기반-문서화의-예시">주석 기반 문서화의 예시</h3>
<p>다음은 JDK의 Annotation 인터페이스의 일부 소스코드입니다.</p>
<pre><code class="language-java">/**
 * The common interface extended by all annotation interfaces.  Note that an
 * interface that manually extends this one does &lt;i&gt;not&lt;/i&gt; define
 * an annotation interface.  Also note that this interface does not itself
 * define an annotation interface.
 *
 * More information about annotation interfaces can be found in section
 * {@jls 9.6} of &lt;cite&gt;The Java Language Specification&lt;/cite&gt;.
 *
 * The {@link java.lang.reflect.AnnotatedElement} interface discusses
 * compatibility concerns when evolving an annotation interface from being
 * non-repeatable to being repeatable.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
public interface Annotation {
    ...</code></pre>
<p>위 예제에서 볼 수 있듯이, @author, @since 등의 <strong>미리 정의된 태그(@ 태그)</strong>를 활용하여 소스코드에 대한 다양한 정보를 기술할 수 있습니다.</p>
<p>이러한 정보는 javadoc.exe 도구에 의해 HTML 문서로 변환되어 API 문서화에 활용됩니다.</p>
<h3 id="애너테이션의-개념">애너테이션의 개념</h3>
<p>이러한 문서화 주석 기능을 확장한 개념이 바로 <strong>애너테이션(annotation)</strong>입니다.
애너테이션은 다음과 같은 특징을 지니며, 자바 소스코드에 기계가 이해할 수 있는 정보를 부가적으로 포함시킬 수 있도록 해줍니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>정의</strong></td>
<td>주석과 유사하지만, 컴파일러나 다른 프로그램이 인식하여 처리할 수 있는 메타데이터</td>
</tr>
<tr>
<td><strong>문법적 특징</strong></td>
<td><code>@</code> 기호로 시작</td>
</tr>
<tr>
<td><strong>실행 영향</strong></td>
<td>일반적인 주석처럼 프로그램 실행에는 영향을 주지 않음</td>
</tr>
<tr>
<td><strong>주요 활용 예시</strong></td>
<td>컴파일러 지시, 코드 자동 생성, 프레임워크 설정 등</td>
</tr>
</tbody></table>
<h3 id="애너테이션의-분류">애너테이션의 분류</h3>
<p>자바에서 사용되는 애너테이션은 크게 다음과 같이 나눌 수 있습니다.</p>
<table>
<thead>
<tr>
<th>분류</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>표준 애너테이션</strong></td>
<td>JDK에서 기본 제공</td>
<td><code>@Override</code>, <code>@Deprecated</code>, <code>@SuppressWarnings</code></td>
</tr>
<tr>
<td><strong>메타 애너테이션</strong></td>
<td>애너테이션을 정의할 때 사용하는 애너테이션</td>
<td><code>@Target</code>, <code>@Retention</code>, <code>@Inherited</code> 등</td>
</tr>
<tr>
<td><strong>사용자 정의 애너테이션</strong></td>
<td>개발자가 직접 정의</td>
<td>프로젝트 또는 프레임워크에서 커스텀 용도로 사용</td>
</tr>
<tr>
<td><strong>외부 라이브러리 애너테이션</strong></td>
<td>프레임워크나 도구에서 제공</td>
<td>Lombok의 <code>@Getter</code>, JUnit의 <code>@Test</code>, Spring의 <code>@Autowired</code> 등</td>
</tr>
</tbody></table>
<p>이 중 표준 애너테이션 및 메타 애너테이션은 java.lang.annotation 패키지에 포함되어 있으며, 자바 컴파일러나 런타임 환경에서 직접 인식하여 처리하는 용도로 사용됩니다.</p>
<h2 id="표준-애너테이션-정리">표준 애너테이션 정리</h2>
<p>자바에서는 애너테이션을 통해 코드에 다양한 의미와 제약 조건을 선언적으로 표현할 수 있습니다. 특히 JDK에서 기본적으로 제공하는 표준 애너테이션은 주로 컴파일러에게 정보를 전달하거나, 코드의 품질을 향상시키는 데 사용됩니다.</p>
<h3 id="컴파일러용-애너테이션">컴파일러용 애너테이션</h3>
<table>
<thead>
<tr>
<th>애너테이션</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Override</code></td>
<td>메서드가 상위 클래스나 인터페이스의 메서드를 오버라이딩하고 있음을 컴파일러에게 알림</td>
<td><code>@Override public String toString() { ... }</code></td>
</tr>
<tr>
<td><code>@Deprecated</code></td>
<td>더 이상 사용되지 않거나 사용이 권장되지 않는 API임을 명시</td>
<td><code>@Deprecated public void oldMethod() { ... }</code></td>
</tr>
<tr>
<td><code>@SuppressWarnings</code></td>
<td>컴파일 시 특정 경고 메시지를 억제</td>
<td><code>@SuppressWarnings(&quot;unchecked&quot;)</code></td>
</tr>
<tr>
<td><code>@SafeVarargs</code></td>
<td>제네릭 가변 인자(varargs) 사용 시 발생할 수 있는 타입 안정성 경고를 제거 (JDK 1.7+)</td>
<td><code>@SafeVarargs public final &lt;T&gt; void print(T... args) { ... }</code></td>
</tr>
<tr>
<td><code>@FunctionalInterface</code></td>
<td>해당 인터페이스가 함수형 인터페이스임을 명시 (JDK 1.8+)</td>
<td><code>@FunctionalInterface interface MyFunc { void run(); }</code></td>
</tr>
<tr>
<td><code>@Native</code></td>
<td><code>static final</code> 상수를 네이티브 코드에서 참조할 수 있도록 함 (JDK 1.8+)</td>
<td><code>@Native public static final int ERROR_CODE = 42;</code></td>
</tr>
</tbody></table>
<ul>
<li><p>@Override는 반드시 붙이는 습관을 들이면 실수로 메서드 이름을 잘못 쓰는 오류를 줄일 수 있습니다.</p>
</li>
<li><p>@SuppressWarnings는 최소한의 범위에만 적용하고, 경고 메시지를 무시하기 전에 반드시 원인을 확인하는 습관이 필요합니다.</p>
</li>
<li><p>@FunctionalInterface는 컴파일러가 해당 인터페이스가 정확히 하나의 추상 메서드를 가지는지 확인해주므로, 람다식 사용 전 확인용으로 좋습니다.</p>
</li>
<li><p>@Retention(RetentionPolicy.RUNTIME)을 지정한 애너테이션은 리플렉션(reflection)을 통해 런타임에 정보를 읽어들일 수 있으므로 프레임워크에서 자주 활용됩니다.</p>
</li>
</ul>
<h3 id="메타-애너테이션">메타 애너테이션</h3>
<p>메타 애너테이션은 애너테이션 자체에 부가 정보를 제공할 때 사용하는 애너테이션입니다. 사용자 정의 애너테이션을 만들 때 자주 사용됩니다.</p>
<table>
<thead>
<tr>
<th>애너테이션</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Target</code></td>
<td>애너테이션이 적용될 수 있는 대상을 지정 (예: 클래스, 메서드, 필드 등)</td>
<td><code>@Target(ElementType.METHOD)</code></td>
</tr>
<tr>
<td><code>@Retention</code></td>
<td>애너테이션의 유지 범위를 지정 (SOURCE, CLASS, RUNTIME)</td>
<td><code>@Retention(RetentionPolicy.RUNTIME)</code></td>
</tr>
<tr>
<td><code>@Documented</code></td>
<td>해당 애너테이션이 javadoc에 포함되도록 지정</td>
<td><code>@Documented</code></td>
</tr>
<tr>
<td><code>@Inherited</code></td>
<td>애너테이션이 자손 클래스에 자동으로 상속되도록 지정</td>
<td><code>@Inherited</code></td>
</tr>
<tr>
<td><code>@Repeatable</code></td>
<td>하나의 대상을 여러 애너테이션으로 반복 적용 가능 (JDK 1.8+)</td>
<td><code>@Repeatable(Roles.class)</code></td>
</tr>
</tbody></table>
<h2 id="메타-애너테이션-1">메타 애너테이션</h2>
<h3 id="target--애너테이션-적용-대상-지정">@Target – 애너테이션 적용 대상 지정</h3>
<p>@Target은 애너테이션을 적용할 수 있는 위치를 명시합니다.
여러 위치에 적용하려면 배열 형태로 중괄호 {}를 사용합니다.</p>
<pre><code class="language-java">@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyAnnotation {
    ...
}</code></pre>
<p>예를 들어, JDK의 @SuppressWarnings 애너테이션의 정의는 다음과 같습니다.</p>
<pre><code class="language-java">@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}</code></pre>
<h4 id="지정-가능한-위치-종류">지정 가능한 위치 종류:</h4>
<table>
<thead>
<tr>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ANNOTATION_TYPE</code></td>
<td>애너테이션 타입</td>
</tr>
<tr>
<td><code>CONSTRUCTOR</code></td>
<td>생성자</td>
</tr>
<tr>
<td><code>FIELD</code></td>
<td>필드(멤버변수, enum 상수 포함)</td>
</tr>
<tr>
<td><code>LOCAL_VARIABLE</code></td>
<td>지역 변수</td>
</tr>
<tr>
<td><code>METHOD</code></td>
<td>메서드</td>
</tr>
<tr>
<td><code>PACKAGE</code></td>
<td>패키지 선언</td>
</tr>
<tr>
<td><code>PARAMETER</code></td>
<td>매개변수</td>
</tr>
<tr>
<td><code>TYPE</code></td>
<td>클래스, 인터페이스, enum 등</td>
</tr>
<tr>
<td><code>TYPE_PARAMETER</code></td>
<td>타입 매개변수 (JDK 1.8+)</td>
</tr>
<tr>
<td><code>TYPE_USE</code></td>
<td>타입이 사용되는 모든 위치 (JDK 1.8+)</td>
</tr>
</tbody></table>
<blockquote>
<p>이 값들은 java.lang.annotation.ElementType 열거형에 정의되어 있습니다.</p>
</blockquote>
<h3 id="retention--애너테이션-유지-범위-지정">@Retention – 애너테이션 유지 범위 지정</h3>
<p>@Retention은 애너테이션이 어느 시점까지 유지되는지를 지정합니다. 기본값은 CLASS입니다.</p>
<table>
<thead>
<tr>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>SOURCE</code></td>
<td>소스 코드에만 존재, 컴파일 후 제거됨</td>
</tr>
<tr>
<td><code>CLASS</code></td>
<td>클래스 파일(.class)에 존재하지만, 런타임엔 읽히지 않음 (기본값)</td>
</tr>
<tr>
<td><code>RUNTIME</code></td>
<td>클래스 파일에 존재하고, 런타임에도 리플렉션을 통해 참조 가능</td>
</tr>
</tbody></table>
<pre><code class="language-java">@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeAnnotation {
    ...
}</code></pre>
<p>프레임워크나 런타임 리플렉션 기반 처리 (예: Spring, Jackson 등)에 사용될 애너테이션은 반드시 RUNTIME으로 지정해야 합니다.</p>
<h3 id="documented--javadoc-포함-여부-지정">@Documented – Javadoc 포함 여부 지정</h3>
<p>@Documented는 해당 애너테이션이 Javadoc API 문서에 포함되도록 지정합니다.</p>
<pre><code class="language-java">@Documented
public @interface PublicAPI {
    ...
}</code></pre>
<ul>
<li><p>기본적으로 애너테이션은 Javadoc에 표시되지 않습니다.</p>
</li>
<li><p>API 문서에 애너테이션 존재 여부를 명확히 하고 싶을 때 유용합니다.</p>
</li>
</ul>
<h3 id="inherited--자손-클래스에-상속-여부-지정">@Inherited – 자손 클래스에 상속 여부 지정</h3>
<p>@Inherited는 부모 클래스에 선언된 애너테이션이 자식 클래스에도 자동으로 적용되도록 지정합니다.</p>
<pre><code class="language-java">@Inherited
public @interface Role {
    String value();
}

@Role(&quot;ADMIN&quot;)
class Parent {}

class Child extends Parent { }
// Child 클래스도 @Role(&quot;ADMIN&quot;)으로 인식됨
</code></pre>
<blockquote>
<p>단, 클래스 간의 상속에만 적용되며, 인터페이스에는 적용되지 않습니다.</p>
</blockquote>
<h3 id="repeatable--애너테이션-반복-사용-허용">@Repeatable – 애너테이션 반복 사용 허용</h3>
<p>기본적으로 하나의 대상에는 동일한 애너테이션을 한 번만 사용할 수 있습니다.
그러나 @Repeatable을 사용하면 같은 애너테이션을 여러 번 적용할 수 있습니다.</p>
<pre><code class="language-java">@Repeatable(ToDos.class)
public @interface ToDo {
    String value();
}

@ToDo(&quot;delete test codes&quot;)
@ToDo(&quot;override inherited methods&quot;)
class MyClass {
    ...
}</code></pre>
<p>이때 컨테이너 애너테이션도 함께 정의해야 하며, value()라는 이름을 반드시 사용해야 합니다.</p>
<pre><code class="language-java">public @interface ToDos {
    ToDo[] value(); // 반드시 value 이름이어야 함
}</code></pre>
<h2 id="사용자-정의-애너테이션">사용자 정의 애너테이션</h2>
<p>자바에서는 @Override나 @Deprecated처럼 JDK에서 제공하는 표준 애너테이션 외에도, 개발자가 직접 애너테이션을 정의하여 사용할 수 있습니다.
이러한 사용자 정의 애너테이션은 다양한 상황에서 프레임워크 구성, 코드 메타데이터 관리, 커스텀 유효성 검증 등에 유용하게 활용됩니다.</p>
<h3 id="애너테이션-정의-기본-문법">애너테이션 정의 기본 문법</h3>
<p>새로운 애너테이션은 @interface 키워드를 사용하여 정의합니다.</p>
<pre><code class="language-java">@interface 애너테이션이름 {
    타입 요소이름();
    ...
}</code></pre>
<ul>
<li><p>애너테이션 내에 선언된 메서드를 애너테이션의 요소라고 합니다.</p>
</li>
<li><p>애너테이션에도 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 없습니다.</p>
</li>
</ul>
<h3 id="복합-애너테이션-정의">복합 애너테이션 정의</h3>
<pre><code class="language-java">@interface DateTime {
    String yymmdd();
    String hhmmss();
}

enum TestType { FIRST, FINAL }

@interface TestInfo {
    int count();
    String testedBy();
    String[] testTools();
    TestType testType();
    DateTime testDate(); // 다른 애너테이션 포함 가능
}
</code></pre>
<h3 id="애너테이션-적용-예시">애너테이션 적용 예시</h3>
<pre><code class="language-java">@TestInfo(
    count = 3,
    testedBy = &quot;Kim&quot;,
    testTools = {&quot;JUnit&quot;, &quot;AutoTester&quot;},
    testType = TestType.FIRST,
    testDate = @DateTime(yymmdd = &quot;160101&quot;, hhmmss = &quot;235959&quot;)
)
public class NewClass {
    ...
}
</code></pre>
<ul>
<li><p>요소 이름과 값을 모두 지정해야 하며, 값이 없는 경우 컴파일 오류가 발생합니다.</p>
</li>
<li><p>이처럼 애너테이션은 다양한 타입의 요소를 포함할 수 있으며, 다른 애너테이션이나 enum 타입도 요소로 포함 가능합니다.</p>
</li>
</ul>
<h3 id="요소에-기본값-지정하기">요소에 기본값 지정하기</h3>
<p>요소에는 기본값을 지정할 수 있으며, 이를 통해 애너테이션 적용 시 값 생략이 가능합니다.</p>
<pre><code class="language-java">@interface TestInfo {
    int count() default 1;
}

@TestInfo // = @TestInfo(count = 1)
public class NewClass {
    ...
}</code></pre>
<blockquote>
<p>기본값으로는 null을 제외한 모든 리터럴(숫자, 문자, 문자열, boolean 등)이 허용됩니다.</p>
</blockquote>
<h3 id="value-요소-단축-표기">value 요소 단축 표기</h3>
<p>애너테이션의 요소가 단 하나이고 그 이름이 value인 경우에는 요소 이름 생략이 가능합니다.</p>
<pre><code class="language-java">@interface TestInfo {
    String value();
}

@TestInfo(&quot;passed&quot;) // = @TestInfo(value = &quot;passed&quot;)
public class NewClass {
    ...
}</code></pre>
<h3 id="애너테이션-요소의-규칙-정리">애너테이션 요소의 규칙 정리</h3>
<table>
<thead>
<tr>
<th>규칙 항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>허용 타입</td>
<td>기본형(primitive), <code>String</code>, <code>enum</code>, 애너테이션, <code>Class</code>만 허용</td>
</tr>
<tr>
<td>매개변수 선언</td>
<td>불가능 (매개변수가 있는 메서드는 요소로 정의할 수 없음)</td>
</tr>
<tr>
<td>예외 선언</td>
<td>불가능 (<code>throws</code> 사용 불가)</td>
</tr>
<tr>
<td>타입 매개변수</td>
<td>불가능 (제네릭 타입 요소 선언 불가)</td>
</tr>
</tbody></table>
<h2 id="리플렉션을-활용한-접근">리플렉션을 활용한 접근</h2>
<p>자바의 애너테이션은 단순히 선언하는 것뿐만 아니라, 프로그램 실행 중에 해당 정보를 읽어 동적으로 처리할 수 있다는 점에서 강력한 확장성을 제공합니다.
이를 위해 사용하는 기술이 바로 <strong>리플렉션(reflection)</strong>이며, 클래스에 선언된 애너테이션 정보를 런타임에 조회할 수 있습니다.</p>
<h3 id="애너테이션-정보-읽기--예제-코드">애너테이션 정보 읽기 – 예제 코드</h3>
<pre><code class="language-java">import java.lang.annotation.*;

@Deprecated
@TestInfo(
    testedBy = &quot;aaa&quot;,
    testDate = @DateTime(yymmdd = &quot;160101&quot;, hhmmss = &quot;235959&quot;)
)
class Ex12_8 {
    public static void main(String[] args) {
        Class&lt;Ex12_8&gt; cls = Ex12_8.class;

        // 단일 애너테이션 정보 가져오기
        TestInfo anno = cls.getAnnotation(TestInfo.class);
        System.out.println(anno.testedBy());
        System.out.println(anno.testDate().yymmdd());
        System.out.println(anno.testDate().hhmmss());

        // 모든 애너테이션 가져오기
        Annotation[] annoArr = cls.getAnnotations();
        for (Annotation a : annoArr) {
            System.out.println(a);
        }
    }
}

// 애너테이션 정의
@Retention(RetentionPolicy.RUNTIME)
@interface TestInfo {
    int count() default 1;
    String testedBy();
    String[] testTools() default &quot;JUnit&quot;;
    TestType testType() default TestType.FINAL;
    DateTime testDate();
}

@Retention(RetentionPolicy.RUNTIME)
@interface DateTime {
    String yymmdd();
    String hhmmss();
}

enum TestType { FIRST, FINAL }</code></pre>
<ul>
<li><p>모든 클래스 파일은 클래스 로더에 의해 메모리에 올라갈 때, 클래스에 대한 정보가 담긴 객체를 생성하는데 이 객체를 클래스 객체라고 합니다. 이 객체를 참조할 때는 <strong>클래스이름.class</strong>의 형식으로 사용합니다.</p>
</li>
<li><p>클래스 객체에는 해당 클래스에 대한 모든 정보를 가지고 있는데, 애너테이션의 정보도 포함되어 있습니다.</p>
</li>
<li><p>getAnnotation()에 매개변수로 정보를 얻고자하는 애너테이션을 지정해주거나 getAnnotations()로 모든 애너테이션을  배열로 받아올 수 있습니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[제네릭]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%A0%9C%EB%84%A4%EB%A6%AD</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%A0%9C%EB%84%A4%EB%A6%AD</guid>
            <pubDate>Fri, 04 Jul 2025 07:51:56 GMT</pubDate>
            <description><![CDATA[<h2 id="제네릭">제네릭</h2>
<p>Java에서 제네릭(Generic)은 다양한 타입의 객체들을 다루는 클래스나 메서드에서 컴파일 시점의 타입 체크(compile-time type check) 를 가능하게 해주는 강력한 기능입니다. 제네릭을 올바르게 사용하면 타입 안정성을 높이고, 불필요한 형변환(casting)을 줄이는 동시에 코드의 가독성과 재사용성도 향상시킬 수 있습니다.</p>
<h3 id="제네릭-도입-전과-후의-비교">제네릭 도입 전과 후의 비교</h3>
<p>Java의 컬렉션 클래스(ArrayList, HashMap 등)는 내부적으로 Object 타입을 사용하여 다양한 객체를 저장할 수 있도록 설계되어 있습니다. 하지만 이 경우, 객체를 꺼낼 때는 원래의 타입으로 형변환이 필요하며, 잘못된 형변환은 런타임 에러로 이어질 수 있습니다.</p>
<p>제네릭은 이러한 문제를 <strong>컴파일 시점에 미리 방지</strong>할 수 있도록 도와줍니다.</p>
<pre><code class="language-java">ArrayList&lt;Tv&gt; tvList = new ArrayList&lt;Tv&gt;();

tvList.add(new Tv());        // OK
tvList.add(new Audio());     // 컴파일 에러

Tv t = tvList.get(0);        // 형변환 불필요</code></pre>
<h3 id="제네릭의-장점-요약">제네릭의 장점 요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>타입 안정성 확보</strong></td>
<td>잘못된 타입의 객체 저장을 컴파일 시점에 차단하여 안정성을 높임</td>
</tr>
<tr>
<td><strong>형변환 생략 가능</strong></td>
<td>불필요한 <code>cast</code> 문법 제거로 코드가 간결해짐</td>
</tr>
<tr>
<td><strong>재사용성 향상</strong></td>
<td>다양한 타입에 대해 하나의 클래스 또는 메서드로 대응 가능</td>
</tr>
</tbody></table>
<blockquote>
<p>타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체를 저장하는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻입니다.</p>
</blockquote>
<h3 id="제네릭의-용어">제네릭의 용어</h3>
<pre><code class="language-java">class Box&lt;T&gt; {}</code></pre>
<table>
<thead>
<tr>
<th>용어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>Box&lt;T&gt;</code></strong></td>
<td>제네릭 클래스(Generic Class) <br> &quot;T의 Box&quot; 또는 &quot;T Box&quot;라고 읽으며, <strong>타입 매개변수 T를 사용하는 클래스</strong>를 의미합니다.</td>
</tr>
<tr>
<td><strong>T</strong></td>
<td>타입 변수(Type Variable) 또는 타입 매개변수(Type Parameter) <br> 실제 타입이 지정되기 전까지 <strong>임시적으로 쓰이는 타입 기호</strong>입니다. 관례적으로 <code>T</code>, <code>E</code>, <code>K</code>, <code>V</code>, <code>N</code> 등의 대문자 한 글자를 사용합니다.</td>
</tr>
<tr>
<td><strong>Box</strong></td>
<td>원시 타입(Raw Type)</td>
</tr>
<tr>
<td><strong>컴파일 이후 처리</strong></td>
<td>Java 컴파일러는 제네릭 타입 정보를 컴파일 시에 검사한 뒤, <strong>바이트코드에서는 타입 정보를 제거</strong>합니다.</td>
</tr>
</tbody></table>
<h2 id="제네릭-타입과-다형성">제네릭 타입과 다형성</h2>
<p>Java에서 <strong>다형성(polymorphism)</strong>은 객체지향 프로그래밍의 핵심 개념 중 하나로, 상위 타입의 참조변수로 하위 타입의 객체를 참조할 수 있도록 허용합니다. 그러나 제네릭(Generic)에서는 이러한 다형성이 타입 매개변수 간에는 적용되지 않으며, 제네릭 타입의 일치가 필수적이라는 점을 이해해야 합니다.</p>
<p>제네릭 클래스의 객체를 생성할 때, <strong>참조변수에 지정해준 타입과 생성자에 지정해준 제네릭 타입은 일치</strong>해야 합니다. 클래스 Tv와 Product가 서로 상속관계에 있어도 일치해야 합니다.</p>
<pre><code class="language-java">class Product {}
class Tv extends Product {}
class Audio extends Product {}

ArrayList&lt;Tv&gt; list = new ArrayList&lt;Tv&gt;();       // ✅ OK: 타입 일치
ArrayList&lt;Product&gt; list = new ArrayList&lt;Tv&gt;();  // ❌ 컴파일 에러</code></pre>
<p>제네릭 타입이 아닌 클래스의 타입 간에 다형성을 적용하는 것은 가능합니다. 이 경우에도 제네릭 타입은 일치해야 합니다.</p>
<pre><code class="language-java">List&lt;Tv&gt; list1 = new ArrayList&lt;Tv&gt;();    // OK: ArrayList는 List 구현체
List&lt;Tv&gt; list2 = new LinkedList&lt;Tv&gt;();   // OK: LinkedList도 List 구현체
</code></pre>
<p>Product 타입을 제네릭 타입으로 지정하면, Product를 상속하는 모든 객체를 저장할 수 있습니다. 단, 꺼낼 때는 구체적인 타입으로 명시적 형변환이 필요합니다.</p>
<pre><code class="language-java">ArrayList&lt;Product&gt; list = new ArrayList&lt;Product&gt;();
list.add(new Product());
list.add(new Tv());
list.add(new Audio());

// 꺼낼 때, 형변환이 필요하다.
Tv t = (Tv)list.get(1);</code></pre>
<h2 id="제한된-제네릭-클래스">제한된 제네릭 클래스</h2>
<p>Java 제네릭의 유연성은 장점이지만, 경우에 따라 특정 계층의 타입만 허용하도록 제한할 필요가 있습니다. 예를 들어, FruitBox라는 클래스를 만들 때, 오직 Fruit의 자손 클래스만 담을 수 있도록 해야 의미가 명확해지고 타입 안정성도 확보됩니다.</p>
<p>이러한 제한을 적용하기 위해 extends 키워드를 사용하여 제네릭 타입에 경계(bound)를 설정할 수 있습니다.</p>
<h3 id="타입-제한이-없는-제네릭-클래스">타입 제한이 없는 제네릭 클래스</h3>
<pre><code class="language-java">class FruitBox&lt;T&gt; {
    ArrayList&lt;T&gt; list = new ArrayList&lt;T&gt;();
}</code></pre>
<p>이 경우 <code>FruitBox&lt;Toy&gt;</code> 와 같이 아무 타입이나 제네릭 타입으로 지정할 수 있습니다.</p>
<pre><code class="language-java">FruitBox&lt;Toy&gt; fruitBox = new FruitBox&lt;Toy&gt;();
fruitBox.list.add(new Toy()); // 문제 없음</code></pre>
<p>타입 안정성은 유지되지만, 의미적으로 어색한 타입도 허용된다는 점에서 제약이 필요할 수 있습니다.</p>
<h3 id="특정-클래스의-자손으로-제한하기">특정 클래스의 자손으로 제한하기</h3>
<pre><code class="language-java">class Fruit {}

class Apple extends Fruit {}
class Grape extends Fruit {}
class Toy {}

class FruitBox&lt;T extends Fruit&gt; {
    ArrayList&lt;T&gt; list = new ArrayList&lt;T&gt;();
}

FruitBox&lt;Apple&gt; appleBox = new FruitBox&lt;Apple&gt;();  // ✅ OK
FruitBox&lt;Grape&gt; grapeBox = new FruitBox&lt;Grape&gt;();  // ✅ OK
FruitBox&lt;Toy&gt; toyBox = new FruitBox&lt;Toy&gt;();        // ❌ 에러: Toy는 Fruit의 자손이 아님</code></pre>
<p><code>T extends Fruit</code> 은 제네릭 타입 T가 Fruit 또는 그 자손 클래스만 가능하다는 뜻입니다. 이를 통해 컴파일 시점에 잘못된 타입 대입을 방지할 수 있습니다.</p>
<h3 id="인터페이스-구현-제한하기">인터페이스 구현 제한하기</h3>
<p>extends는 클래스뿐 아니라 인터페이스에도 사용할 수 있습니다.</p>
<pre><code class="language-java">interface Eatable {}

class FruitBox&lt;T extends Eatable&gt; {
    ArrayList&lt;T&gt; list = new ArrayList&lt;T&gt;();
}</code></pre>
<p>제네릭 타입 T는 Eatable 인터페이스를 구현한 타입만 가능합니다.</p>
<h3 id="다중-제한-클래스--인터페이스">다중 제한: 클래스 + 인터페이스</h3>
<p>하나의 타입이 특정 클래스의 자손이면서 동시에 인터페이스도 구현해야 할 경우, 아래와 같이 <code>&amp;</code> 연산자를 활용합니다.</p>
<pre><code class="language-java">class Fruit implements Eatable {}
class Apple extends Fruit {}

class FruitBox&lt;T extends Fruit &amp; Eatable&gt; {
    ArrayList&lt;T&gt; list = new ArrayList&lt;T&gt;();
}</code></pre>
<ul>
<li><p>T는 Fruit의 자손이면서 Eatable 인터페이스를 반드시 구현해야 합니다.</p>
</li>
<li><p>다중 제한 시, 클래스는 반드시 첫 번째에 작성해야 하며, 그 이후에 인터페이스들을 나열합니다.</p>
</li>
</ul>
<h2 id="제네릭의-제약">제네릭의 제약</h2>
<p>Java 제네릭(Generic)은 타입 안정성과 유연한 설계를 동시에 제공하지만, 몇 가지 기술적 제약도 함께 존재합니다. 이러한 제약은 대부분 <strong>타입 소거(type erasure)</strong> 라는 Java 컴파일러의 제네릭 처리 방식에서 비롯됩니다.</p>
<h3 id="static-멤버에는-제네릭-타입-변수를-사용할-수-없음">static 멤버에는 제네릭 타입 변수를 사용할 수 없음</h3>
<p>제네릭 타입 매개변수는 인스턴스 수준에서만 유효합니다. 따라서 static 영역에서는 사용할 수 없습니다.</p>
<pre><code class="language-java">class Box&lt;T&gt; {
    static T item;                      // ❌ 컴파일 에러
    static int compare(T t1, T t2) {    // ❌ 컴파일 에러
        return 0;
    }
}</code></pre>
<h4 id="📌-이유">📌 이유</h4>
<ul>
<li><p>T는 인스턴스가 생성될 때 지정되는 타입입니다.</p>
</li>
<li><p>하지만 static 멤버는 클래스 수준에서 공유되므로, 인스턴스가 생성되기도 전에 T의 구체적인 타입을 알 수 없습니다.</p>
</li>
</ul>
<h3 id="제네릭-배열-생성-불가">제네릭 배열 생성 불가</h3>
<p>제네릭 타입 배열의 참조 변수는 선언할 수 있지만, 실제로 배열을 생성하는 것은 허용되지 않습니다.</p>
<pre><code class="language-java">class Box&lt;T&gt; {
    T[] itemArr;  // ✅ 참조 변수 선언은 가능

    T[] toArray() {
        T[] tmpArr = new T[itemArr.length];  // ❌ 컴파일 에러
        return tmpArr;
    }
}</code></pre>
<h4 id="📌-이유-1">📌 이유</h4>
<ul>
<li><p>Java의 new 연산자는 배열의 요소 타입을 컴파일 시점에 명확히 알아야 합니다.</p>
</li>
<li><p>하지만 제네릭 타입은 <strong>컴파일 후 타입 정보가 제거(타입 소거)</strong>되므로, new T[]와 같은 표현은 사용할 수 없습니다.</p>
</li>
</ul>
<h2 id="와일드-카드">와일드 카드</h2>
<p>앞서 설명한 바와 같이, 제네릭 타입 간에는 다형성이 적용되지 않는다는 점이 Java 제네릭의 중요한 특성입니다. 예를 들어 <code>List&lt;Product&gt;</code> 와 <code>List&lt;Tv&gt;</code> 는 서로 아무 관계가 없는 타입으로 간주됩니다.</p>
<p>이러한 제한 속에서 제네릭 타입 간에도 유연한 관계를 허용하고 싶다면, <strong>와일드 카드(?)</strong>를 사용해야 합니다. 와일드 카드는 제네릭 타입의 경계를 유동적으로 만들어, 특정 범위 내의 타입을 받아들이도록 허용합니다.</p>
<table>
<thead>
<tr>
<th>문법</th>
<th>설명</th>
<th>허용 범위</th>
</tr>
</thead>
<tbody><tr>
<td><code>&lt;?&gt;</code></td>
<td>모든 타입 허용</td>
<td><code>Object</code>를 상한으로 하는 모든 타입</td>
</tr>
<tr>
<td><code>&lt;? extends T&gt;</code></td>
<td>상한 제한 (Upper Bound)</td>
<td><code>T</code> 또는 <code>T</code>의 자손 타입만 허용</td>
</tr>
<tr>
<td><code>&lt;? super T&gt;</code></td>
<td>하한 제한 (Lower Bound)</td>
<td><code>T</code> 또는 <code>T</code>의 조상 타입만 허용</td>
</tr>
</tbody></table>
<pre><code class="language-java">ArrayList&lt;? extends Product&gt; list = new ArrayList&lt;Tv&gt;(); // OK
ArrayList&lt;? extends Product&gt; list = new ArrayList&lt;Audio&gt;(); // OK

static Juice makeJuice(FruitBox&lt;? extends Fruit&gt; box) {
    String tmp = &quot;&quot;;
    for(Fruit f : box.getList()) tmp += f + &quot; &quot;;
    return new Juice(tmp);
}

Juicer.makeJuice(new FruitBox&lt;Fruit&gt;());
Juicer.makeJuice(new FruitBox&lt;Apple&gt;()); </code></pre>
<h2 id="제네릭-메서드">제네릭 메서드</h2>
<p>Java에서 <strong>제네릭 메서드(Generic Method)</strong>는 메서드 수준에서 타입 매개변수를 선언하여, 다양한 타입에 유연하게 대응할 수 있도록 설계된 메서드입니다. 제네릭 클래스와는 별도로, 특정 메서드에 한해 타입을 일반화하고자 할 때 매우 유용하게 사용됩니다.</p>
<p>타입 매개변수의 선언 위치는 반환 타입 바로 앞에 위치하며, 대표적으로 <code>Collections.sort()</code> 가 바로 제네릭 메서드이다.</p>
<pre><code class="language-java">static &lt;T&gt; void sort(List&lt;T&gt; list, Comparator&lt;? super T&gt; c)</code></pre>
<h3 id="예제1">예제1</h3>
<pre><code class="language-java">class FruitBox&lt;T&gt; {
    ...
    static &lt;T&gt; void sort(List&lt;T&gt; list, Comparator&lt;? super T&gt; c) {
        ...
    }
    ...
}</code></pre>
<ul>
<li><p>제네릭 클래스에 정의된 타입 매개변수가 T이고 제네릭 메서드에 정의된 타입 매개변수가 T이어도 이 둘은 전혀 별개의 것입니다.</p>
</li>
<li><p>static 멤버에는 타입 매개변수를 사용할 수 없지만, 이처럼 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능합니다.</p>
</li>
<li><p>참고로 제네릭 메서드는 제네릭 클래스가 아닌 클래스에도 정의될 수 있습니다.</p>
</li>
</ul>
<h3 id="예제2">예제2</h3>
<pre><code class="language-java">static &lt;T extends Fruit&gt; Juice makeJuice(FruitBox&lt;T&gt; box) {
    String tmp = &quot;&quot;;
    for(Fruit f : box.getList()) tmp += f + &quot; &quot;;
    return new Juice(tmp);
}

FruitBox&lt;Fruit&gt; fruitBox = new FruitBox&lt;Fruit&gt;();
FruitBox&lt;Apple&gt; appleBox = new FruitBox&lt;Apple&gt;();

Juicer.&lt;Fruit&gt;makeJuice(fruitBox);
Juicer.&lt;Apple&gt;makeJuice(appleBox);</code></pre>
<ul>
<li>메서드를 호출할 때는 타입 변수에 타입을 대입해야 합니다. 그러나 대부분의 경우 컴파일러가 대입된 타입을 추정할 수 있기 때문에 생략해도 됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[컬렉션 프레임워크]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</guid>
            <pubDate>Thu, 03 Jul 2025 03:31:18 GMT</pubDate>
            <description><![CDATA[<h2 id="컬렉션-프레임워크란-무엇인가">컬렉션 프레임워크란 무엇인가?</h2>
<p>프로그래밍을 하다 보면 데이터를 하나만 다루는 경우보다, 여러 개의 데이터를 그룹으로 묶어 처리해야 하는 상황이 훨씬 더 많습니다. Java에서는 이러한 다수의 데이터를 효과적으로 저장하고 관리할 수 있도록 <strong>컬렉션 프레임워크(Collection Framework)</strong>를 제공합니다.</p>
<h3 id="컬렉션--프레임워크">컬렉션 + 프레임워크</h3>
<ul>
<li><p>컬렉션(Collection): 다수의 데이터, 즉 데이터의 집합 또는 데이터 군을 의미합니다.</p>
</li>
<li><p>프레임워크(Framework): 단순히 기능을 제공하는 데 그치지 않고, 표준화된 구조와 방식을 통해 개발 생산성과 유지보수성을 향상시키는 일종의 개발 틀입니다.</p>
</li>
</ul>
<p>따라서 컬렉션 프레임워크란, 데이터를 그룹으로 저장하고 다루는 클래스들을 <strong>표준화된 설계 방식에 따라 구현한 구조</strong>라고 할 수 있습니다.</p>
<blockquote>
<p>📌 공식 Java API 문서에서는 컬렉션 프레임워크를 다음과 같이 정의합니다.
&quot;A unified architecture for representing and manipulating collections.&quot;
→ 즉, 데이터 군을 표현하고 처리하기 위한 통일된 아키텍처를 의미합니다.</p>
</blockquote>
<h3 id="컬렉션-프레임워크의-등장-배경">컬렉션 프레임워크의 등장 배경</h3>
<p>JDK 1.2 이전까지 Java에는 Vector, Hashtable, Properties 등 일부 데이터 집합을 다루는 클래스들이 존재했지만, 이들 간에 일관된 구조나 사용 방식이 없어 개발자 입장에서는 클래스마다 <strong>각기 다른 방식으로 데이터를 다뤄야 하는 불편함</strong>이 있었습니다.</p>
<p>그러나 <strong>JDK 1.2부터 컬렉션 프레임워크가 도입</strong>되면서 다음과 같은 변화가 이루어졌습니다.</p>
<ul>
<li><p>다양한 컬렉션 클래스(ArrayList, HashSet, HashMap 등)가 추가되었고,</p>
</li>
<li><p>이들은 <strong>공통된 인터페이스</strong>를 기반으로 구현되어 일관된 방식으로 데이터 조작이 가능해졌습니다.</p>
</li>
</ul>
<p>이러한 표준화는 생산성과 재사용성 향상이라는 큰 장점을 제공했지만, 완전한 성공이라고 보기는 어려웠습니다. 이후 <strong>JDK 1.8에 도입된 람다 표현식과 스트림(Stream)</strong> 기능이 결합되면서, 컬렉션 프레임워크는 진정한 의미의 표준화된 데이터 처리 방식을 완성하게 됩니다.</p>
<h3 id="라이브러리와-프레임워크의-차이">라이브러리와 프레임워크의 차이</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>라이브러리</strong></td>
<td>유용한 기능을 묶어놓은 모듈. 필요할 때 호출하여 사용.</td>
</tr>
<tr>
<td><strong>프레임워크</strong></td>
<td>단순한 기능 제공을 넘어, <strong>전체적인 개발 방식과 구조를 정형화</strong>하여 생산성과 유지보수성을 향상시킴.</td>
</tr>
</tbody></table>
<p>따라서, 컬렉션 프레임워크는 단순한 라이브러리의 개념을 넘어서 일관된 설계 철학을 가진 프로그램 구조라고 볼 수 있습니다.</p>
<h2 id="컬렉션-프레임워크의-핵심-인터페이스">컬렉션 프레임워크의 핵심 인터페이스</h2>
<p>컬렉션 프레임워크는 설계적으로 <strong>인터페이스를 중심</strong>으로 구성되어 있으며, 다음의 세 가지 주요 인터페이스가 핵심입니다:</p>
<table>
<thead>
<tr>
<th>인터페이스</th>
<th>주요 특징</th>
<th>구현 클래스 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>List</strong></td>
<td>순서가 있는 데이터의 집합<br>중복된 요소 허용</td>
<td><code>ArrayList</code>, <code>LinkedList</code>, <code>Vector</code>, <code>Stack</code></td>
</tr>
<tr>
<td><strong>Set</strong></td>
<td>순서를 유지하지 않음<br>중복된 요소 허용하지 않음</td>
<td><code>HashSet</code>, <code>TreeSet</code>, <code>LinkedHashSet</code></td>
</tr>
<tr>
<td><strong>Map</strong></td>
<td>키(key)와 값(value)의 쌍으로 구성<br>키는 중복 불가, 값은 중복 가능</td>
<td><code>HashMap</code>, <code>TreeMap</code>, <code>Hashtable</code>, <code>Properties</code></td>
</tr>
</tbody></table>
<blockquote>
<p>💡 추가로, List와 Set의 공통적인 기능을 묶어 <strong>Collection 인터페이스</strong>가 정의되어 있으며, Map은 Collection의 하위 인터페이스는 아닙니다.</p>
</blockquote>
<h3 id="클래스-이름으로-특징을-유추할-수-있다">클래스 이름으로 특징을 유추할 수 있다?</h3>
<p>컬렉션 클래스들의 네이밍은 대체로 <strong>구현한 인터페이스의 이름을 포함</strong>하고 있기 때문에, 클래스 이름만 보더라도 해당 클래스가 어떤 특성을 가지는지 쉽게 파악할 수 있습니다.</p>
<p>예시:</p>
<p>ArrayList → 배열 기반의 List</p>
<p>HashSet → 해시 기반의 Set</p>
<p>TreeMap → 정렬 기능이 있는 Map</p>
<h2 id="arraylist-vs-linkedlist">ArrayList vs LinkedList</h2>
<p>Java 컬렉션 프레임워크에서 가장 많이 사용되는 List 구현체로는 ArrayList와 LinkedList가 있습니다. 두 클래스 모두 List 인터페이스를 구현하고 있지만, 내부 구조와 성능 특성에서 뚜렷한 차이를 가지고 있어, 상황에 따라 적절한 선택이 중요합니다.</p>
<h3 id="arraylist-배열-기반의-리스트">ArrayList: 배열 기반의 리스트</h3>
<ul>
<li><p>ArrayList는 기존의 Vector를 개선한 클래스입니다. Vector와 구현 원리나 동작 방식은 유사하지만, <strong>동기화를 지원하지 않아 성능이 더 우수</strong>합니다.</p>
</li>
<li><p>Java에서는 <strong>호환성을 위해 Vector를 여전히 유지</strong>하고 있지만, 새로운 코드에서는 일반적으로 ArrayList를 사용하는 것이 권장됩니다.</p>
</li>
<li><p>내부적으로는 Object 배열을 사용하여 데이터를 순차적으로 저장합니다. <strong>배열의 공간이 부족할 경우, 더 큰 배열을 새로 생성하고 기존 데이터를 복사한 후 추가 작업을 수행</strong>합니다.</p>
</li>
</ul>
<pre><code class="language-java">public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable {
    ...
    transient Object[] elementData;
    ...
}</code></pre>
<ul>
<li>elementData는 Object 타입 배열로, Java의 모든 객체를 저장할 수 있습니다.</li>
</ul>
<h4 id="요소-추가-및-삭제의-동작-방식">요소 추가 및 삭제의 동작 방식</h4>
<ul>
<li><p><strong>삭제 시</strong>: 삭제된 요소 아래의 모든 데이터를 한 칸씩 앞으로 이동시킵니다. 단, 마지막 요소 삭제 시에는 단순히 해당 요소를 null로 설정하면 됩니다.</p>
</li>
<li><p><strong>추가 시</strong>: 중간 위치에 요소를 삽입하는 경우, 이후의 요소들을 한 칸씩 뒤로 이동시킨 후 값을 삽입합니다.</p>
</li>
</ul>
<blockquote>
<p>📌 배열의 구조상, <strong>맨 끝에서 추가/삭제</strong>하는 경우에는 데이터 이동이 필요 없기 때문에 성능이 상대적으로 우수합니다. 반대로, <strong>중간 위치에서 삽입/삭제가 빈번한 경우에는 성능 저하</strong>가 발생할 수 있습니다.</p>
</blockquote>
<h3 id="linkedlist-연결-기반의-리스트">LinkedList: 연결 기반의 리스트</h3>
<ul>
<li><p>LinkedList는 배열의 단점을 보완하기 위해 고안된 연결 리스트 기반의 자료구조입니다.</p>
</li>
<li><p>배열은 메모리 상에 데이터가 연속적으로 존재하지만, 링크드 리스트는 <strong>불연속적인 메모리 공간에 존재하는 노드들이 포인터(주소값)를 통해 연결된 구조</strong>입니다.</p>
</li>
<li><p>각 노드(Node)는 다음 요소의 주소와 데이터를 함께 가지고 있습니다.</p>
</li>
</ul>
<pre><code class="language-java">class Node {
    Node next;   // 다음 노드의 참조
    Object obj;  // 실제 데이터
}</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>ArrayList</th>
<th>LinkedList</th>
</tr>
</thead>
<tbody><tr>
<td>내부 구조</td>
<td>Object 배열</td>
<td>노드(Node) 연결</td>
</tr>
<tr>
<td>데이터 접근 속도</td>
<td>빠름 (인덱스를 통한 접근)</td>
<td>느림 (순차 탐색 필요)</td>
</tr>
<tr>
<td>중간 삽입/삭제</td>
<td>느림 (데이터 이동 필요)</td>
<td>빠름 (포인터만 수정)</td>
</tr>
<tr>
<td>메모리 사용</td>
<td>상대적으로 적음</td>
<td>포인터 정보로 인해 더 큼</td>
</tr>
<tr>
<td>랜덤 액세스</td>
<td>지원 (인덱스로 접근 가능)</td>
<td>지원하지 않음</td>
</tr>
</tbody></table>
<h2 id="컬렉션을-순회하는-세-가지-방식-iterator-listiterator-enumeration">컬렉션을 순회하는 세 가지 방식: Iterator, ListIterator, Enumeration</h2>
<p>Java 컬렉션 프레임워크는 데이터를 저장하는 것뿐 아니라, <strong>저장된 데이터를 어떻게 효율적이고 일관된 방식으로 순회(iterate)할 수 있을지</strong>에 대해서도 깊이 있는 설계를 제공합니다. 이러한 순회 기능은 Iterator, ListIterator, Enumeration이라는 세 가지 주요 인터페이스를 통해 구현됩니다.</p>
<h3 id="왜-iterator가-필요한가">왜 Iterator가 필요한가?</h3>
<p>컬렉션에 저장된 요소들을 순차적으로 읽어오는 기능은 거의 모든 프로그램에서 반복적으로 사용됩니다. Java는 이러한 작업을 표준화하기 위해 <strong>Iterator 인터페이스를 정의</strong>하고, 이를 통해 컬렉션 순회를 위한 통일된 프로그래밍 방식을 제공합니다.</p>
<pre><code class="language-java">public interface Iterator {
    boolean hasNext();   // 다음 요소가 있는지 확인
    Object next();       // 다음 요소 반환
    void remove();       // 현재 요소 삭제 (선택적 기능)
}</code></pre>
<p>모든 Collection 계열 클래스(List, Set 등)는 iterator() 메서드를 통해 Iterator 객체를 반환합니다. 즉, 모든 컬렉션 클래스는 공통된 방식으로 순회가 가능합니다.</p>
<pre><code class="language-java">List list = new ArrayList();
Iterator it = list.iterator();

while(it.hasNext()) {
    System.out.println(it.next());
}</code></pre>
<blockquote>
<p>이러한 공통된 순회 방식을 통해 코드의 일관성, 재사용성, 유지보수성을 높일 수 있다는 점은 객체지향 설계에서 매우 중요한 가치입니다.</p>
</blockquote>
<h3 id="listiterator">ListIterator</h3>
<p>ListIterator는 Iterator의 기능을 확장한 인터페이스로, 다음과 같은 추가 기능을 제공합니다:</p>
<ul>
<li><p>양방향 탐색 (hasPrevious(), previous())</p>
</li>
<li><p>요소의 인덱스 조회</p>
</li>
<li><p>요소의 수정, 추가 (set(), add())</p>
</li>
</ul>
<pre><code class="language-java">public interface ListIterator extends Iterator {
    boolean hasPrevious();
    Object previous();
    int nextIndex();
    int previousIndex();
    void set(Object e);
    void add(Object e);
}</code></pre>
<blockquote>
<p>단, ListIterator는 <strong>List 인터페이스를 구현한 컬렉션(ArrayList, LinkedList 등)</strong>에서만 사용할 수 있습니다. Set과 같은 비순차 컬렉션에서는 사용할 수 없습니다.</p>
</blockquote>
<h3 id="enumeration">Enumeration</h3>
<ul>
<li><p>Enumeration은 Iterator의 구버전으로, 주로 Vector, Hashtable 같은 JDK 1.0 시절의 레거시 컬렉션 클래스에서 사용됩니다.</p>
</li>
<li><p>기능은 hasMoreElements()와 nextElement() 정도로 제한적이며, 삭제 기능(remove)이 없는 등 유연성이 부족합니다.</p>
</li>
<li><p>가능하면 Iterator를 사용할 것이 권장됩니다.</p>
</li>
</ul>
<pre><code class="language-java">Enumeration e = vector.elements();
while(e.hasMoreElements()) {
    System.out.println(e.nextElement());
}</code></pre>
<h2 id="comparable-vs-comparator">Comparable vs Comparator</h2>
<p>Java 컬렉션 프레임워크에서는 객체 간의 정렬이 필요한 경우가 자주 발생합니다. 예를 들어, 이름순으로 학생 목록을 정렬하거나, 점수순으로 랭킹을 정렬하는 상황이 대표적입니다. 이러한 사용자 정의 객체의 정렬 기준을 명확히 지정하기 위해 Java는 두 가지 인터페이스를 제공합니다.</p>
<ul>
<li><p><strong>Comparable</strong>: 객체에 기본 정렬 기준을 정의</p>
</li>
<li><p><strong>Comparator</strong>: 객체의 기본 정렬과 다른 기준을 동적으로 지정</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>Comparable</th>
<th>Comparator</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>객체 자체에 기본 정렬 기준 부여</td>
<td>외부에서 정렬 기준 지정</td>
</tr>
<tr>
<td>패키지</td>
<td><code>java.lang</code></td>
<td><code>java.util</code></td>
</tr>
<tr>
<td>주요 메서드</td>
<td><code>compareTo(T o)</code></td>
<td><code>compare(T o1, T o2)</code></td>
</tr>
<tr>
<td>구현 위치</td>
<td><strong>정렬 대상 클래스 내부에 구현</strong></td>
<td><strong>별도의 클래스로 구현</strong></td>
</tr>
<tr>
<td>사용 예</td>
<td><code>Arrays.sort(arr)</code></td>
<td><code>Arrays.sort(arr, comparator)</code></td>
</tr>
</tbody></table>
<h3 id="인터페이스-정의">인터페이스 정의</h3>
<pre><code class="language-java">public interface Comparable&lt;T&gt; {
    int compareTo(T o); // this와 o를 비교
}

public interface Comparator&lt;T&gt; {
    int compare(T o1, T o2); // o1과 o2를 비교
}</code></pre>
<p>각 메서드는 두 객체의 상대적 크기를 판단하여 다음의 값을 반환해야 합니다:</p>
<ul>
<li><p>0: 두 객체가 동일함</p>
</li>
<li><p>양수: 왼쪽 객체가 더 큼</p>
</li>
<li><p>음수: 오른쪽 객체가 더 큼</p>
</li>
</ul>
<h3 id="🔍-예제-문자열-배열-정렬하기">🔍 예제: 문자열 배열 정렬하기</h3>
<pre><code class="language-java">import java.util.*;

public class Ex11_7 {
    public static void main(String[] args) {
        String[] strArr = { &quot;cat&quot;, &quot;Dog&quot;, &quot;lion&quot;, &quot;tiger&quot; };

        Arrays.sort(strArr); // 기본 정렬: Comparable(String 클래스 내부 구현)
        System.out.println(&quot;기본 정렬: &quot; + Arrays.toString(strArr));

        Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER); // 대소문자 무시
        System.out.println(&quot;대소문자 무시: &quot; + Arrays.toString(strArr));

        Arrays.sort(strArr, new Descending()); // 역순 정렬
        System.out.println(&quot;역순 정렬: &quot; + Arrays.toString(strArr));
    }
}

class Descending implements Comparator {
    public int compare(Object o1, Object o2) {
        if (o1 instanceof Comparable &amp;&amp; o2 instanceof Comparable) {
            Comparable c1 = (Comparable) o1;
            return c1.compareTo(o2) * -1; // 기본 정렬 기준의 반대 (역순)
        }
        return -1;
    }
}</code></pre>
<blockquote>
<p>위 예제에서 String 클래스는 이미 Comparable을 구현하고 있기 때문에 Arrays.sort()만으로도 기본 정렬이 가능합니다. Comparator는 이 기본 정렬 기준을 변경하고 싶을 때 사용됩니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[형식화 클래스]]></title>
            <link>https://velog.io/@dev_gyeongmin/%ED%98%95%EC%8B%9D%ED%99%94-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@dev_gyeongmin/%ED%98%95%EC%8B%9D%ED%99%94-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Tue, 24 Jun 2025 14:07:20 GMT</pubDate>
            <description><![CDATA[<p>Java 애플리케이션에서는 사용자에게 데이터를 보여줄 때, 숫자나 날짜, 통화 등 다양한 형식으로 데이터를 표현하는 경우가 많습니다. 이러한 <strong>데이터 표현의 일관성과 유연성</strong>을 위해 Java에서는 java.text 패키지를 통해 <strong>형식화(formatting) 클래스</strong>를 제공합니다.</p>
<p>형식화 클래스는 데이터를 <strong>패턴 기반의 형식 문자열로 변환</strong>하는 기능뿐만 아니라, 형식화된 문자열로부터 <strong>원래의 데이터 객체를 복원</strong>하는 기능도 함께 제공합니다. 이는 단순한 변환을 넘어 <strong>입출력 양방향에 걸쳐 신뢰성 있는 데이터 처리를 가능하게</strong> 해주며, 객체지향적으로 잘 설계된 API 덕분에 다양한 상황에 유연하게 대응할 수 있습니다.</p>
<hr>
<h2 id="decimalformat">DecimalFormat</h2>
<p>Java에서 숫자를 일정한 형식으로 표현하거나, 형식화된 문자열을 다시 숫자로 되돌리고자 할 때 가장 유용하게 사용할 수 있는 클래스가 바로 DecimalFormat입니다. 이 클래스는 java.text.NumberFormat의 하위 클래스이며, <strong>정수, 소수, 통화, 퍼센트, 지수 표기 등 다양한 숫자 표현을 패턴 기반으로 처리할 수 있는 기능</strong>을 제공합니다.</p>
<h3 id="decimalformat의-기본-개념">DecimalFormat의 기본 개념</h3>
<p>DecimalFormat은 문자열 패턴을 통해 숫자의 출력 형식을 지정합니다. 예를 들어 <code>&quot;#,###.##&quot;</code> 와 같은 패턴은 숫자를 천 단위 구분자(,)와 소수점 둘째 자리까지 표현하도록 지정합니다. 또한 문자열에 포함된 숫자 형식에서 원래의 Number 객체로 복원할 수 있는 <strong>역변환(parse)</strong> 기능도 지원합니다.</p>
<h3 id="패턴-기호-정리">패턴 기호 정리</h3>
<p>아래 표는 DecimalFormat에서 사용되는 주요 패턴 기호와 예시를 정리한 것입니다.</p>
<table>
<thead>
<tr>
<th>기호</th>
<th>의미</th>
<th>예시 패턴</th>
<th>결과 (입력값: 1234567.89)</th>
</tr>
</thead>
<tbody><tr>
<td><code>0</code></td>
<td>10진수 (빈 자리는 0으로 채움)</td>
<td><code>0000000000.0000</code></td>
<td><code>0001234567.8900</code></td>
</tr>
<tr>
<td><code>#</code></td>
<td>10진수 (빈 자리는 표시 안함)</td>
<td><code>##########.####</code></td>
<td><code>1234567.89</code></td>
</tr>
<tr>
<td><code>.</code></td>
<td>소수점 구분자</td>
<td><code>#.#</code></td>
<td><code>1234567.9</code></td>
</tr>
<tr>
<td><code>-</code></td>
<td>음수 기호</td>
<td><code>-#.#</code></td>
<td><code>-1234567.9</code></td>
</tr>
<tr>
<td><code>,</code></td>
<td>천 단위 구분자</td>
<td><code>#,###.##</code></td>
<td><code>1,234,567.89</code></td>
</tr>
<tr>
<td><code>E</code></td>
<td>지수 표기</td>
<td><code>#.#E0</code></td>
<td><code>1.2E6</code></td>
</tr>
<tr>
<td><code>;</code></td>
<td>양수/음수 패턴 구분</td>
<td><code>#,###.##+;#,###.##-</code></td>
<td><code>1,234,567.89+</code> 또는 <code>1,234,567.89-</code></td>
</tr>
<tr>
<td><code>%</code></td>
<td>퍼센트 (100을 곱함)</td>
<td><code>#.#%</code></td>
<td><code>123456789%</code></td>
</tr>
<tr>
<td><code>\u2030</code></td>
<td>퍼밀 (1000을 곱함)</td>
<td>#.#\u2030</td>
<td><code>1234567890‰</code></td>
</tr>
<tr>
<td><code>\u00A4</code></td>
<td>통화 기호</td>
<td>`\u00A4 #,###</td>
<td><code>₩ 1,234,568</code></td>
</tr>
<tr>
<td><code>&#39;</code></td>
<td>특수 문자 escape</td>
<td><code>&#39;#&#39;#,###</code></td>
<td><code>#1,234,568</code></td>
</tr>
</tbody></table>
<h3 id="기본-사용-예제">기본 사용 예제</h3>
<p>아래는 DecimalFormat을 사용하여 숫자를 지수 표기로 변환하는 간단한 예시입니다.</p>
<pre><code class="language-java">double number = 1234567.89;
DecimalFormat df = new DecimalFormat(&quot;#.#E0&quot;);
String result = df.format(number);
System.out.println(result); // 1.2E6</code></pre>
<h3 id="문자열을-숫자로-역변환-parse">문자열을 숫자로 역변환 (parse)</h3>
<p>parse(String source)는 DecimalFormat의 조상인 NumberFormat에 정의된 메서드입니다.</p>
<pre><code class="language-java">public Number parse(String source) throws ParseException</code></pre>
<p>이 메서드는 형식화된 텍스트를 Number 객체로 변환하며, 필요에 따라 intValue(), doubleValue() 등으로 구체적인 타입으로 꺼낼 수 있습니다.</p>
<pre><code class="language-java">import java.text.*;

class Ex10_7 {
    public static void main(String[] args) {
        DecimalFormat df  = new DecimalFormat(&quot;#,###.##&quot;);
        DecimalFormat df2 = new DecimalFormat(&quot;#.###E0&quot;);

        try {
            Number num = df.parse(&quot;1,234,567.89&quot;);
            System.out.print(&quot;1,234,567.89&quot; + &quot; -&gt; &quot;);

            double d = num.doubleValue(); 
            System.out.print(d + &quot; -&gt; &quot;);

            System.out.println(df2.format(num));
        } catch(Exception e) {}
    }
}

// 1,234,567.89 -&gt; 1234567.89 -&gt; 1.235E6</code></pre>
<hr>
<h2 id="simpledateformat">SimpleDateFormat</h2>
<p>날짜와 시간은 다양한 형식으로 출력되어야 하는 데이터 중 하나입니다. 사용자의 로케일, 시스템 설정, 표현 목적에 따라 “2025-06-24”, “2025년 6월 24일”, “June 24, 2025” 등 매우 다양하게 표현되죠. 이러한 형식의 제어를 위해 Java에서는 java.text 패키지의 SimpleDateFormat 클래스를 제공합니다.</p>
<p>이 클래스는 원하는 출력 형식을 문자열 패턴으로 정의하고, 이를 바탕으로 Date 객체를 문자열로 <strong>형식화(format)</strong> 하거나, 문자열을 다시 Date 객체로 <strong>파싱(parse)</strong> 하는 기능을 제공합니다.</p>
<h3 id="날짜시간-패턴-기호-정리">날짜/시간 패턴 기호 정리</h3>
<p>아래는 SimpleDateFormat에서 사용 가능한 주요 포맷 기호와 그 의미입니다.</p>
<table>
<thead>
<tr>
<th>기호</th>
<th>의미</th>
<th>보기</th>
</tr>
</thead>
<tbody><tr>
<td>G</td>
<td>연대(BC, AD)</td>
<td>AD</td>
</tr>
<tr>
<td>y</td>
<td>년도</td>
<td>2006</td>
</tr>
<tr>
<td>M</td>
<td>월(1~12 또는 1월 ~ 12월)</td>
<td>10 또는 10월, OCT</td>
</tr>
<tr>
<td>w</td>
<td>년의 몇 번째 주(1~53)</td>
<td>50</td>
</tr>
<tr>
<td>W</td>
<td>월의 몇 번째 주(1~5)</td>
<td>4</td>
</tr>
<tr>
<td>D</td>
<td>년의 몇 번째 일(1~366)</td>
<td>100</td>
</tr>
<tr>
<td>d</td>
<td>월의 몇 번째 일(1~31)</td>
<td>15</td>
</tr>
<tr>
<td>F</td>
<td>월의 몇 번째 요일(1~5)</td>
<td>1</td>
</tr>
<tr>
<td>E</td>
<td>요일</td>
<td>월</td>
</tr>
<tr>
<td>a</td>
<td>오전/오후(AM, PM)</td>
<td>PM</td>
</tr>
<tr>
<td>H</td>
<td>시간(0~23)</td>
<td>20</td>
</tr>
<tr>
<td>k</td>
<td>시간(1~24)</td>
<td>13</td>
</tr>
<tr>
<td>K</td>
<td>시간(0~11)</td>
<td>10</td>
</tr>
<tr>
<td>h</td>
<td>시간(1~12)</td>
<td>11</td>
</tr>
<tr>
<td>m</td>
<td>분(0~59)</td>
<td>35</td>
</tr>
<tr>
<td>s</td>
<td>초(0~59)</td>
<td>55</td>
</tr>
<tr>
<td>S</td>
<td>천분의 일초(0~999)</td>
<td>253</td>
</tr>
<tr>
<td>z</td>
<td>Time zone(General time zone)</td>
<td>GMT+9:00</td>
</tr>
<tr>
<td>Z</td>
<td>Time zone(RFC 822 time zone)</td>
<td>+0900</td>
</tr>
<tr>
<td>‘</td>
<td>escape문자(특수문자를 표현하는데 사용)</td>
<td>없음</td>
</tr>
</tbody></table>
<h3 id="날짜를-문자열로-변환--format">날짜를 문자열로 변환 – format()</h3>
<p>아래는 현재 날짜를 원하는 형식으로 출력하는 예시입니다.</p>
<pre><code class="language-java">import java.text.SimpleDateFormat;
import java.util.Date;

public class DateFormatExample {
    public static void main(String[] args) {
        Date today = new Date();
        SimpleDateFormat df = new SimpleDateFormat(&quot;yyyy-MM-dd&quot;);

        String result = df.format(today);
        System.out.println(result);  // 2025-06-24
    }
}</code></pre>
<p>여기서 &quot;yyyy-MM-dd&quot;는 4자리 연도-2자리 월-2자리 일의 형식을 지정하는 패턴입니다. &quot;MM&quot;을 &quot;MMM&quot; 또는 &quot;MMMM&quot;으로 바꾸면 각각 &quot;6월&quot;, &quot;June&quot; 등으로 출력됩니다.</p>
<h3 id="문자열을-날짜로-변환--parse">문자열을 날짜로 변환 – parse()</h3>
<p>parse() 메서드를 사용하면 문자열을 Date 객체로 변환할 수 있습니다. 단, 입력 형식과 패턴이 정확히 일치해야 하며, 일치하지 않을 경우 ParseException이 발생하므로 예외 처리가 필요합니다.</p>
<pre><code class="language-java">import java.util.*;
import java.text.*;

class Ex10_9 {
    public static void main(String[] args) {
        DateFormat df  = new SimpleDateFormat(&quot;yyyy년 MM월 dd일&quot;);
        DateFormat df2 = new SimpleDateFormat(&quot;yyyy/MM/dd&quot;);

        try {
            Date d = df.parse(&quot;2019년 11월 23일&quot;);
            System.out.println(df2.format(d)); // 2019/11/23
        } catch(Exception e) {}
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Calendar 클래스]]></title>
            <link>https://velog.io/@dev_gyeongmin/Calendar-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@dev_gyeongmin/Calendar-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Sun, 22 Jun 2025 14:18:35 GMT</pubDate>
            <description><![CDATA[<p>소프트웨어 개발에서 <strong>날짜와 시간의 처리</strong>는 생각보다 까다로운 문제입니다. 시간대(Time Zone), 윤년(leap year), 월의 길이 차이 등 다양한 예외 상황이 존재하기 때문입니다. Java는 이러한 복잡성을 감안하여 일찍이 날짜와 시간을 다루기 위한 표준 클래스를 제공해 왔습니다.</p>
<p>가장 먼저 등장한 클래스는 Date입니다. Date 클래스는 JDK 1.0부터 제공된 기본 클래스이며, 날짜와 시간의 표현 및 연산을 위한 수단을 제공하였습니다. 그러나 Date는 설계상의 한계로 인해 다음과 같은 문제점을 안고 있었습니다:</p>
<ul>
<li><p>내부적으로 날짜를 표현하는 방식이 직관적이지 않고, 월(month)의 인덱스가 0부터 시작하는 등 혼란을 유발</p>
</li>
<li><p>불변(immutable)하지 않아 동시성 처리에 적합하지 않음</p>
</li>
<li><p>생성자 및 메서드 중 일부가 deprecated 처리되며 유지보수에 비효율적</p>
</li>
</ul>
<p>이러한 한계를 극복하기 위해, <strong>JDK 1.1</strong>부터 Calendar 클래스가 도입되었습니다. Calendar는 다양한 국가와 문화권의 달력 시스템을 지원하며, 시간 계산과 조작을 보다 유연하게 수행할 수 있도록 설계되었습니다. 예를 들어 특정 날짜에 일(day)을 더하거나 빼는 작업은 Calendar를 통해 훨씬 수월하게 구현할 수 있습니다. 하지만 Calendar 역시 완벽하지는 않았습니다.</p>
<p>이러한 문제점을 개선하기 위해 <strong>JDK 1.8</strong>부터 java.time 패키지가 새롭게 도입되었습니다. 이 패키지는 LocalDate, LocalTime, ZonedDateTime 등 불변 객체 기반의 현대적인 날짜/시간 API를 제공하며, 기존 Date 및 Calendar의 단점을 효과적으로 보완하였습니다.</p>
<p>그럼에도 불구하고, Calendar는 여전히 많은 레거시 코드와 시스템에서 사용되고 있으며, 과거와의 호환성 측면에서도 여전히 학습할 가치가 있는 클래스입니다. 이번 글에서는 Calendar 클래스의 기본 구조와 주요 기능들을 중심으로 그 활용 방법을 알아보겠습니다.</p>
<hr>
<h2 id="calendar-클래스--날짜와-시간을-구성하고-조작">Calendar 클래스 – 날짜와 시간을 구성하고 조작</h2>
<p>자바에서 날짜와 시간을 보다 유연하게 다루기 위해 도입된 Calendar 클래스는 Date 클래스의 한계를 보완하기 위해 JDK 1.1부터 제공된 <strong>추상 클래스</strong>입니다. Calendar는 시간 연산, 구성 요소 분리, 시간대 설정 등 다양한 기능을 제공하지만, 추상 클래스이기 때문에 직접 객체를 생성할 수는 없습니다.</p>
<h3 id="calendar-인스턴스-생성-방법">Calendar 인스턴스 생성 방법</h3>
<p>Calendar 객체를 생성하려면, <strong>정적 메서드 Calendar.getInstance()</strong>를 사용해야 합니다. 이 메서드는 현재 시스템의 기본 로케일과 시간대를 기준으로 적절한 하위 클래스의 인스턴스를 반환합니다.</p>
<pre><code class="language-java">Calendar cal = Calendar.getInstance();</code></pre>
<p>이때 반환되는 실제 클래스는 상황에 따라 다릅니다.</p>
<table>
<thead>
<tr>
<th>반환되는 클래스</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>GregorianCalendar</code></td>
<td>대부분의 시스템에서 기본적으로 반환됨. 서양식 그레고리력을 따름</td>
</tr>
<tr>
<td><code>BuddhistCalendar</code></td>
<td>태국처럼 불기(Buddhist Era)를 사용하는 경우 반환</td>
</tr>
</tbody></table>
<h4 id="📌-그레고리력이란">📌 그레고리력이란?</h4>
<p>그레고리력(Gregorian calendar)은 <strong>1582년 교황 그레고리오 13세</strong>가 율리우스력의 오차를 보완하여 제정한 <strong>현재 전 세계에서 가장 널리 쓰이는 달력 체계</strong>입니다. 윤년 계산법과 월의 길이가 명확히 정의되어 있어 컴퓨터 시스템에서도 표준 달력으로 채택됩니다.</p>
<h3 id="날짜-및-시간-필드-얻기--get-메서드">날짜 및 시간 필드 얻기 – get() 메서드</h3>
<p>Calendar 객체는 다양한 날짜/시간 필드를 포함하고 있으며, 이 값들을 <code>get(int field)</code> 메서드와 함께 <strong>정적 상수</strong>를 통해 읽을 수 있습니다.</p>
<pre><code class="language-java">Calendar today = Calendar.getInstance();
System.out.println(&quot;현재 연도: &quot; + today.get(Calendar.YEAR));
System.out.println(&quot;현재 월 (0~11): &quot; + today.get(Calendar.MONTH));</code></pre>
<h4 id="자주-사용하는-필드-목록">자주 사용하는 필드 목록</h4>
<table>
<thead>
<tr>
<th>상수명</th>
<th>설명</th>
<th>반환값 예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>YEAR</code></td>
<td>연도</td>
<td>2025</td>
</tr>
<tr>
<td><code>MONTH</code></td>
<td>월 (0부터 시작)</td>
<td>0: 1월, 11: 12월</td>
</tr>
<tr>
<td><code>DATE</code>, <code>DAY_OF_MONTH</code></td>
<td>해당 월의 일</td>
<td>1 ~ 31</td>
</tr>
<tr>
<td><code>DAY_OF_YEAR</code></td>
<td>연 중 몇 번째 날</td>
<td>1 ~ 366</td>
</tr>
<tr>
<td><code>DAY_OF_WEEK</code></td>
<td>요일 (1: 일요일 ~ 7: 토요일)</td>
<td>1 ~ 7</td>
</tr>
<tr>
<td><code>HOUR</code></td>
<td>시간 (12시간제)</td>
<td>0 ~ 11</td>
</tr>
<tr>
<td><code>HOUR_OF_DAY</code></td>
<td>시간 (24시간제)</td>
<td>0 ~ 23</td>
</tr>
<tr>
<td><code>MINUTE</code></td>
<td>분</td>
<td>0 ~ 59</td>
</tr>
<tr>
<td><code>SECOND</code></td>
<td>초</td>
<td>0 ~ 59</td>
</tr>
<tr>
<td><code>MILLISECOND</code></td>
<td>밀리초</td>
<td>0 ~ 999</td>
</tr>
<tr>
<td><code>ZONE_OFFSET</code></td>
<td>표준 시간대 오프셋 (밀리초 단위)</td>
<td>-43200000 ~ +43200000</td>
</tr>
</tbody></table>
<h4 id="예제">예제</h4>
<pre><code class="language-java">import java.util.*;

class Ex10_1 {
    public static void main(String[] args) 
    { 
        // 기본적으로 현재날짜와 시간으로 설정된다. 
        Calendar today = Calendar.getInstance();
        System.out.println(&quot;이 해의 년도 : &quot; + today.get(Calendar.YEAR));
        System.out.println(&quot;월(0~11) : &quot; + today.get(Calendar.MONTH));
        System.out.println(&quot;이 해의 몇 째  : &quot; + today.get(Calendar.WEEK_OF_YEAR));
        System.out.println(&quot;이 달의 몇 째  : &quot; + today.get(Calendar.WEEK_OF_MONTH));

        // DATE와 DAY_OF_MONTH는 같다.
        System.out.println(&quot;이 달의 몇 일: &quot; + today.get(Calendar.DATE));
        System.out.println(&quot;이 달의 몇 일: &quot; + today.get(Calendar.DAY_OF_MONTH));
        System.out.println(&quot;이 해의 몇 일: &quot; + today.get(Calendar.DAY_OF_YEAR));
        System.out.println(&quot;요일(1~7, 1:일요일): &quot; + today.get(Calendar.DAY_OF_WEEK));
        System.out.println(&quot;이 달의 몇 째 요일: &quot; + today.get(Calendar.DAY_OF_WEEK_IN_MONTH));

        System.out.println(&quot;오전_오후(0:오전, 1:오후): &quot; + today.get(Calendar.AM_PM));
        System.out.println(&quot;시간(0~11): &quot; + today.get(Calendar.HOUR));
        System.out.println(&quot;시간(0~23): &quot; + today.get(Calendar.HOUR_OF_DAY));
        System.out.println(&quot;분(0~59): &quot; + today.get(Calendar.MINUTE));
        System.out.println(&quot;초(0~59): &quot; + today.get(Calendar.SECOND));
        System.out.println(&quot;1000분의 1초(0~999): &quot; + today.get(Calendar.MILLISECOND));

        // 천분의 1초를 시간으로 표시하기 위해 3600000으로 나누었다.(1시간 = 60 * 60초)
        System.out.println(&quot;TimeZone(-12~+12): &quot; + (today.get(Calendar.ZONE_OFFSET)/(60*60*1000)));
        System.out.println(&quot;이 달의 마지막 : &quot; + today.getActualMaximum(Calendar.DATE));
    }
}</code></pre>
<h3 id="원하는-날짜-및-시간-설정--set-메서드">원하는 날짜 및 시간 설정 – set() 메서드</h3>
<p>Calendar는 기본적으로 현재 시각으로 초기화되지만, 필요에 따라 특정 날짜나 시간으로 변경할 수 있습니다. 이때는 set() 메서드를 활용합니다.</p>
<h4 id="오버로딩된-set-메서드-시그니처">오버로딩된 set 메서드 시그니처</h4>
<pre><code class="language-java">void set(int field, int value)
void set(int year, int month, int date)
void set(int year, int month, int date, int hourOfDay, int minute)
void set(int year, int month, int date, int hourOfDay, int minute, int second)</code></pre>
<h4 id="예제-두-날짜-간의-차이-구하기">예제: 두 날짜 간의 차이 구하기</h4>
<pre><code class="language-java">import java.util.*;

class Ex10_2 {
    public static void main(String[] args) {
        // 요일은 1부터 시작
        final String[] DAY_OF_WEEK = {&quot;&quot;, &quot;일&quot;, &quot;월&quot;, &quot;화&quot;, &quot;수&quot;, &quot;목&quot;, &quot;금&quot;, &quot;토&quot;};

        Calendar date1 = Calendar.getInstance();
        Calendar date2 = Calendar.getInstance();

        // Calendar.APRIL = 3, 2019년 4월 29일
        date1.set(2019, Calendar.APRIL, 29);
        System.out.println(&quot;date1은 &quot; + toString(date1) 
        + DAY_OF_WEEK[date1.get(Calendar.DAY_OF_WEEK)] + &quot;요일이고,&quot;);
        System.out.println(&quot;오늘(date2)은 &quot; + toString(date2) 
        + DAY_OF_WEEK[date2.get(Calendar.DAY_OF_WEEK)] + &quot;요일입니다.&quot;);

        long difference = (date2.getTimeInMillis() - date1.getTimeInMillis()) / 1000;
        System.out.println(&quot;그 날(date1)부터 지금(date2)까지 &quot; + difference + &quot;초가 지났습니다.&quot;);
        System.out.println(&quot;일(day)로 계산하면 &quot; + difference/(24 * 60 * 60) + &quot;일입니다.&quot;);
    }

    public static String toString(Calendar date) {
        return date.get(Calendar.YEAR) + &quot;년 &quot;+ (date.get(Calendar.MONTH) + 1) + &quot;월 &quot;
                + date.get(Calendar.DATE) + &quot;일 &quot;;
    }
}</code></pre>
<h3 id="실무-팁">실무 팁</h3>
<ul>
<li><p>Calendar는 <strong>가변 객체(Mutable Object)</strong>이므로, 공유되는 환경에서는 주의해서 사용해야 합니다. 가능하다면 java.time 패키지의 LocalDateTime과 같은 불변 클래스 사용을 권장합니다.</p>
</li>
<li><p>날짜 간 차이를 계산하거나 포맷팅하려면 SimpleDateFormat이나 DateFormat을 함께 사용하는 것이 좋습니다.</p>
</li>
<li><p>로케일이나 시간대(TimeZone) 변경이 필요한 경우, <code>Calendar.getInstance(TimeZone, Locale)</code> 메서드를 사용할 수 있습니다.</p>
</li>
</ul>
<hr>
<h2 id="add-vs-roll-메서드의-차이점">add() vs roll() 메서드의 차이점</h2>
<p>실무에서는 특정 날짜를 기준으로 며칠 전, 혹은 몇 개월 후의 날짜를 계산해야 하는 경우가 자주 발생합니다. 예를 들어, “오늘로부터 7일 뒤 마감일 설정”이나 “6개월 전 가입자의 데이터 조회” 같은 시나리오가 해당합니다. Java의 Calendar 클래스는 이러한 날짜 연산을 쉽게 처리할 수 있도록 <code>add()</code> 와 <code>roll()</code> 이라는 메서드를 제공합니다.</p>
<h3 id="addint-field-int-amount--날짜-필드의-연쇄적-변화">add(int field, int amount) – 날짜 필드의 연쇄적 변화</h3>
<p><code>add()</code> 메서드는 지정한 날짜/시간 필드를 원하는 만큼 증가시키거나 감소시킵니다. 이 메서드의 특징은 <strong>필드 간의 연관성을 반영</strong>하여 변화가 전파된다는 것입니다.</p>
<p>예를 들어, 1월 31일에서 일(day) 필드를 1만큼 증가시키면, 결과는 2월 1일이 됩니다. 즉, <strong>일 필드의 변화가 월 필드에도 영향을 미칩니다.</strong></p>
<pre><code class="language-java">Calendar date = Calendar.getInstance();
date.set(2019, Calendar.JULY, 31); // 2019년 8월 31일
date.add(Calendar.DATE, 1);        // 하루 후</code></pre>
<h3 id="rollint-field-int-amount--독립적인-단위-변화">roll(int field, int amount) – 독립적인 단위 변화</h3>
<p>반면, roll() 메서드는 지정한 필드만을 독립적으로 증가 또는 감소시킵니다. <strong>다른 필드에는 영향을 미치지 않는다는 점이 가장 큰 차이점입니다.</strong></p>
<p>예를 들어, 8월 31일에서 DATE 필드를 1만큼 roll하면 결과는 <strong>9월 1일이 아닌 8월 내에서의 날짜로 롤링</strong>됩니다.</p>
<pre><code class="language-java">date.roll(Calendar.DATE, 31);  // 월은 그대로, 날짜만 순환</code></pre>
<h3 id="add와-roll-비교-표">add()와 roll() 비교 표</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th><code>add()</code></th>
<th><code>roll()</code></th>
</tr>
</thead>
<tbody><tr>
<td>필드 간 영향</td>
<td>해당 필드 변화가 상위 필드(월, 연도 등)에 영향을 줌</td>
<td>지정한 필드만 영향을 받고 다른 필드는 그대로 유지</td>
</tr>
<tr>
<td>월말 처리</td>
<td>1월 31일 + 1일 → 2월 1일</td>
<td>1월 31일 roll +1 → 1월 내 날짜로 롤링</td>
</tr>
<tr>
<td>사용 목적</td>
<td>정확한 날짜 계산이 필요한 경우</td>
<td>단순 반복(예: UI 달력 표시 등)에서 날짜를 순환시킬 때 적합</td>
</tr>
<tr>
<td>예외 상황</td>
<td>필드 간 자연스러운 연산 수행</td>
<td>월말 처리 시 예상치 못한 결과가 나올 수 있음</td>
</tr>
</tbody></table>
<h3 id="예제-add와-roll의-차이-체험">예제: add()와 roll()의 차이 체험</h3>
<pre><code class="language-java">Calendar date = Calendar.getInstance();
date.set(2019, 7, 31); // 2019년 8월 31일

System.out.println(toString(date));

date.add(Calendar.DATE, 1);
System.out.println(&quot;= 1일 후(add) = &quot; + toString(date)); // 2019년 9월 1일

date.add(Calendar.MONTH, -6);
System.out.println(&quot;= 6달 전(add) = &quot; + toString(date)); // 2019년 3월 1일

date.roll(Calendar.DATE, 31);
System.out.println(&quot;= 31일 후(roll) = &quot; + toString(date)); // 2019년 3월 1일

date.add(Calendar.DATE, 31);
System.out.println(&quot;= 31일 후(add) = &quot; + toString(date)); // 2019년 4월 1일

public static String toString(Calendar date) {
    return date.get(Calendar.YEAR)+&quot;년 &quot; + (date.get(Calendar.MONTH)+1) 
        + &quot;월 &quot; + date.get(Calendar.DATE) + &quot;일&quot;;
}</code></pre>
<p>실행 결과를 통해 add()는 실제 날짜 기준으로 계산되어 월과 연도가 변경되지만, roll()은 같은 월 내에서만 날짜가 순환되는 것을 확인할 수 있습니다.</p>
<h3 id="⚠️-실무에서-주의할-점">⚠️ 실무에서 주의할 점</h3>
<ul>
<li><p><code>roll()</code> 은 단독으로 사용할 때 유용하지만, 월말(end of month) 처리 시 예외가 발생할 수 있습니다. 예를 들어, DATE 필드가 31일인데 MONTH를 roll하면 해당 월의 말일에 따라 DATE 값도 변경될 수 있습니다.</p>
</li>
<li><p>날짜 간 계산이 정확하게 필요하다면 가급적 <code>add()</code> 를 사용하는 것이 안전합니다.</p>
</li>
<li><p>java.time 패키지의 <code>LocalDate.plusDays()</code> , <code>minusMonths()</code> 등은 이와 같은 불확실성을 줄인 더 직관적인 대안이 될 수 있습니다.</p>
</li>
</ul>
<hr>
<h2 id="date와-calendar-간의-변환">Date와 Calendar 간의 변환</h2>
<p>자바에서는 시간이 지남에 따라 날짜와 시간을 다루는 방식이 진화해 왔으며, 그 과정에서 Date와 Calendar라는 두 가지 대표적인 클래스가 공존하게 되었습니다. Date는 JDK 1.0부터, Calendar는 JDK 1.1부터 제공되면서 역할이 분화되었지만, 레거시 코드 또는 외부 API와의 호환성 때문에 여전히 두 클래스를 오가는 상황이 자주 발생합니다.</p>
<h3 id="calendar-→-date-변환">Calendar → Date 변환</h3>
<p>Calendar 객체는 내부적으로 시간을 <code>long</code> 값으로 저장하고 있으며, <code>getTimeInMillis()</code> 메서드를 통해 해당 값을 추출할 수 있습니다. 이 값을 사용하여 Date 객체를 생성할 수 있습니다.</p>
<pre><code class="language-java">Calendar cal = Calendar.getInstance();
Date date = new Date(cal.getTimeInMillis()); // Date(long date)</code></pre>
<blockquote>
<p>✅ Date(long date) 생성자는 1970년 1월 1일 00:00:00 GMT로부터의 밀리초 값을 기준으로 Date 객체를 생성합니다.</p>
</blockquote>
<h3 id="date-→-calendar-변환">Date → Calendar 변환</h3>
<p>반대로, Date 객체를 Calendar로 변환하려면 <code>Calendar.getInstance()</code> 로 객체를 생성한 후, <code>setTime(Date date)</code> 메서드를 사용하여 날짜 값을 설정하면 됩니다.</p>
<pre><code class="language-java">Date date = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(date);</code></pre>
<blockquote>
<p>✅ Calendar는 Date 객체의 시간을 기반으로 내부 필드를 자동으로 갱신합니다 (YEAR, MONTH, DATE 등).</p>
</blockquote>
<h3 id="💡-실무-팁">💡 실무 팁</h3>
<ul>
<li><p><strong>외부 API 사용 시 호환성 확보</strong>: JDBC, JSON 직렬화 등 많은 라이브러리가 여전히 Date 객체를 사용하기 때문에 변환이 필요할 수 있습니다.</p>
</li>
<li><p><strong>시간 정밀도 주의</strong>: Calendar는 밀리초 단위까지 포함하지만, 일부 시스템은 초 단위까지만 처리하기 때문에 정밀도가 손실될 수 있습니다.</p>
</li>
<li><p><strong>java.time 패키지와의 변환</strong>도 가능하며, JDK 8 이상에서는 Date, Calendar를 LocalDateTime 또는 ZonedDateTime으로 변환해 사용하는 것이 점점 더 일반적입니다.</p>
</li>
</ul>
<hr>
<p>이번 글에서는 Calendar 클래스의 구조와 주요 메서드들을 중심으로 날짜와 시간을 다루는 방법을 살펴보았습니다. Date 클래스의 한계를 보완하기 위해 등장한 Calendar는 다양한 날짜 필드 접근, 연산 메서드(add, roll) 및 유연한 설정 기능을 제공함으로써 자바의 날짜/시간 처리를 보다 강력하게 만들어 주었습니다.</p>
<p>비록 JDK 8 이후 java.time 패키지(LocalDate, ZonedDateTime 등)의 도입으로 인해 새로운 API가 주류로 자리잡았지만, 여전히 많은 레거시 코드와 외부 라이브러리에서는 Calendar와 Date 기반의 API가 사용되고 있습니다. 이러한 상황에서 Calendar 클래스의 원리를 정확히 이해하고 자유롭게 활용할 수 있는 능력은 실무에서 큰 자산이 됩니다.</p>
<p>앞으로는 SimpleDateFormat을 이용한 날짜 포맷 처리, 또는 Calendar와 java.time API 간의 변환 방법 등을 함께 익혀두면 더욱 강력하고 유연한 시간 처리가 가능해질 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[java.lang 패키지]]></title>
            <link>https://velog.io/@dev_gyeongmin/java.lang-%ED%8C%A8%ED%82%A4%EC%A7%80</link>
            <guid>https://velog.io/@dev_gyeongmin/java.lang-%ED%8C%A8%ED%82%A4%EC%A7%80</guid>
            <pubDate>Wed, 18 Jun 2025 15:29:20 GMT</pubDate>
            <description><![CDATA[<p>Java 프로그래밍을 시작하면서 가장 먼저 접하게 되는 패키지 중 하나가 바로 java.lang입니다. 이 패키지는 자바 애플리케이션의 기본적인 동작에 필수적인 클래스들로 구성되어 있으며, 별도의 import 문 없이도 사용할 수 있도록 특별히 설계되어 있습니다.</p>
<p>예외 처리, 문자열 처리, 수치 연산, 객체 비교 등 일상적인 프로그래밍 작업 대부분에 필요한 기능들이 이 패키지 안에 포함되어 있으며, 자바의 철학과 설계 사상을 가장 잘 보여주는 패키지이기도 합니다.</p>
<p>이번 글에서는 java.lang 패키지의 주요 클래스들과 그 역할에 대해 체계적으로 정리하고, 실무에서 자주 마주치는 대표 메서드들을 예제와 함께 살펴보겠습니다. Java의 기반을 탄탄하게 다지고자 하는 개발자라면 반드시 익혀야 할 내용입니다.</p>
<hr>
<h2 id="object-클래스--모든-클래스의-뿌리">Object 클래스 – 모든 클래스의 뿌리</h2>
<p>자바의 모든 클래스는 자동으로 Object 클래스를 상속받습니다. 이는 개발자가 명시적으로 상속을 지정하지 않더라도, 자바 컴파일러가 자동으로 extends Object를 삽입하기 때문입니다.
그만큼 Object 클래스는 Java 언어의 가장 기본적인 클래스이며, 자바 객체가 공통적으로 가져야 할 최소한의 동작들을 정의합니다.</p>
<h3 id="object-클래스의-특징">Object 클래스의 특징</h3>
<ul>
<li><p>Object는 <strong>모든 클래스의 최상위 조상 클래스</strong>입니다.</p>
</li>
<li><p>단 하나의 멤버 변수도 없고, 오직 <strong>11개의 메서드</strong>만 정의되어 있습니다.</p>
</li>
<li><p>이 메서드들은 모든 객체가 갖추어야 할 기본 동작(비교, 문자열 표현, 복제 등)을 담당합니다.</p>
</li>
</ul>
<h3 id="equals--객체-비교의-기준">equals() – 객체 비교의 기준</h3>
<p>기본적으로 <code>equals()</code> 메서드는 두 객체의 <strong>주소값</strong>(즉, 참조가 동일한지)을 비교합니다.</p>
<pre><code class="language-java">public boolean equals(Object obj) {
    return (this == obj);
}
</code></pre>
<h4 id="🔍-예제-1---주소값-비교">🔍 예제 1 - 주소값 비교</h4>
<pre><code class="language-java">class Value {
    int value;

    Value(int value) {
        this.value = value;
    }
}

class Ex1 {
    public static void main(String[] args) {
        Value v1 = new Value(10);
        Value v2 = new Value(10);

        System.out.println(v1.equals(v2)); // false
    }
}</code></pre>
<ul>
<li>위 예제에서 v1과 v2는 같은 값을 가지고 있지만, 서로 다른 메모리 주소를 참조하므로 <code>false</code> 를 반환합니다.</li>
</ul>
<h4 id="✔-equals-오버라이딩--내용-비교로-변경">✔ equals() 오버라이딩 – 내용 비교로 변경</h4>
<p>내용을 기준으로 비교하고자 한다면 <code>equals()</code> 를 오버라이딩해야 합니다.</p>
<pre><code class="language-java">class Person {
    long id;

    public Person(long id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {
        // obj의 상속계층도에 Person이 있는지 확인
        if (obj instanceof Person)
            return this.id == ((Person) obj).id; // 형변환
        return false;
    }
}

class Ex2 {
    public static void main(String[] args) {
        Person p1 = new Person(123L);
        Person p2 = new Person(123L);

        System.out.println(p1.equals(p2)); // true
    }
}</code></pre>
<h4 id="✅-실무-팁">✅ 실무 팁</h4>
<ul>
<li><code>equals()</code> 를 오버라이딩할 경우에는 반드시 <code>hashCode()</code> 도 함께 오버라이딩해야 합니다. 둘의 계약(Contract)을 위반하면 컬렉션에서의 검색이나 비교 연산이 잘못 작동할 수 있습니다.</li>
</ul>
<h3 id="hashcode--해시-기반-자료구조의-핵심">hashCode() – 해시 기반 자료구조의 핵심</h3>
<p>이 메서드는 해싱(hashing)기법에 사용되는 <strong>해시함수(hash function)</strong>를 구현한 것입니다. 해싱은 데이터관리기법 중의 하나인데 다량의 데이터를 저장하고 검색하는 데 유용합니다. 해시함수는 찾고자하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시코드(hashcode)를 반환합니다.</p>
<p>일반적으로 해시코드가 같은 두 객체가 존재하는 것이 가능하지만, Object 클래스에 정의된 hashCode 메서드는 <strong>객체의 주소값을 이용해서 해시코드를 만들어 반환하기 때문에 서로 다른 두 객체는 결코 같은 해시코드를 가질 수 없습니다.</strong> 단, 64 bit JVM에서는 주소가 64 bit이므로 주소를 해시코드(32 bit)로 변환하면 중복된 값이 나올 수 있습니다.</p>
<p><code>hashCode()</code> 메서드는 객체의 해시코드를 반환합니다. 기본 구현은 객체의 메모리 주소를 기반으로 정수값을 반환합니다.</p>
<h4 id="🔍-예제-2--문자열-비교와-해시코드">🔍 예제 2 – 문자열 비교와 해시코드</h4>
<pre><code class="language-java">class Ex3 {
    public static void main(String[] args) {
        String s1 = new String(&quot;abc&quot;);
        String s2 = new String(&quot;abc&quot;);

        System.out.println(s1.equals(s2)); // true
        System.out.println(s1.hashCode()); // 96354
        System.out.println(s2.hashCode()); // 96354

        System.out.println(System.identityHashCode(s1)); // 942731712
        System.out.println(System.identityHashCode(s2)); // 971848845
    }
}</code></pre>
<ul>
<li><p>String 클래스는 <code>equals()</code> 와 <code>hashCode()</code> 를 모두 오버라이딩하여 문자열 내용이 같으면 같은 해시코드를 반환하도록 설계되어 있습니다.</p>
</li>
<li><p>반면 <code>System.identityHashCode()</code> 는 객체의 <strong>실제 주소기반 해시코드</strong>를 반환합니다.</p>
</li>
</ul>
<h4 id="✅-실무-팁-1">✅ 실무 팁</h4>
<ul>
<li><p>해시 기반 자료구조(HashMap, HashSet, Hashtable 등)를 사용할 때는 <code>equals()</code> 와 <code>hashCode()</code> 의 일관성을 반드시 보장해야 합니다.</p>
</li>
<li><p>동일한 객체라고 판단되면 <strong>동일한 해시코드</strong>를 반환해야 검색과 저장이 정확히 작동합니다.</p>
</li>
</ul>
<h3 id="tostring--객체-정보의-문자열-표현">toString() – 객체 정보의 문자열 표현</h3>
<p><code>toString()</code> 은 객체를 문자열로 표현할 때 호출됩니다. 예를 들어 <code>System.out.println(obj)</code> 와 같이 객체를 출력하면 내부적으로 obj.toString()이 호출됩니다.</p>
<pre><code class="language-java">public String toString() {
    return getClass().getName() + &quot;@&quot; + Integer.toHexString(hashCode());
}</code></pre>
<h4 id="🔍-예제-3--기본-tostring-출력">🔍 예제 3 – 기본 toString() 출력</h4>
<p>클래스를 작성할 때 <code>toString()</code> 을 오버라이딩하지 않는다면, 클래스이름과 16진수의 해시코드를 얻을 수 있습니다.</p>
<pre><code class="language-java">class Card {
    String kind;
    int number;

    Card(String kind, int number) {
        this.kind = kind;
        this.number = number;
    }
}

class Ex4 {
    public static void main(String[] args) {
        Card c1 = new Card(&quot;SPADE&quot;, 1);
        System.out.println(c1); // Card@762efe5d
    }
}</code></pre>
<h4 id="✔-tostring-오버라이딩--가독성-향상">✔ toString() 오버라이딩 – 가독성 향상</h4>
<pre><code class="language-java">@Override
public String toString() {
    return &quot;kind : &quot; + kind + &quot;, number : &quot; + number;
}

class Ex5 {
    public static void main(String[] args) {
        Card c1 = new Card(&quot;SPADE&quot;, 1);
        Card c2 = new Card(&quot;HEART&quot;, 10);

        System.out.println(c1); // kind : SPADE, number : 1
        System.out.println(c2); // kind : HEART, number : 10
    }
}</code></pre>
<h4 id="✅-실무-팁-2">✅ 실무 팁</h4>
<ul>
<li><p>객체의 상태를 빠르게 파악해야 하는 디버깅 상황이나 로깅 시 유용합니다.</p>
</li>
<li><p><code>toString()</code> 을 오버라이딩할 때는 민감 정보가 노출되지 않도록 주의해야 합니다.</p>
</li>
</ul>
<h3 id="요약">요약</h3>
<table>
<thead>
<tr>
<th>메서드명</th>
<th>기본 역할</th>
<th>오버라이딩 필요 여부</th>
</tr>
</thead>
<tbody><tr>
<td><code>equals()</code></td>
<td>객체 주소 비교</td>
<td>객체 내용 비교 시 필요</td>
</tr>
<tr>
<td><code>hashCode()</code></td>
<td>해시코드 반환</td>
<td><code>equals()</code>를 오버라이딩하면 필수</td>
</tr>
<tr>
<td><code>toString()</code></td>
<td>객체 정보 문자열 반환</td>
<td>출력 결과 개선 시 유용</td>
</tr>
</tbody></table>
<hr>
<h2 id="string-클래스--자바에서-문자열을-다루는-기본-도구">String 클래스 – 자바에서 문자열을 다루는 기본 도구</h2>
<p>자바에서 문자열 처리는 String 클래스를 통해 이루어집니다. String 클래스는 문자열을 저장하고 다양한 조작 메서드를 제공하며, Java 프로그래밍의 거의 모든 영역에서 필수적으로 사용되는 핵심 클래스입니다.</p>
<h3 id="클래스-정의-및-특징">클래스 정의 및 특징</h3>
<pre><code class="language-java">public final class String implements java.io.Serializable, Comparable&lt;String&gt; {
    private final char[] value;
    ...
}</code></pre>
<ul>
<li><p>String 클래스는 <code>final</code> 로 선언되어 있어 상속이 불가능합니다.</p>
</li>
<li><p>문자열 데이터는 내부적으로 <strong>문자 배열(char[])</strong>에 저장됩니다.</p>
</li>
<li><p>문자열 비교, 결합, 추출 등 문자열 처리를 위한 다양한 메서드를 제공합니다.</p>
</li>
</ul>
<h3 id="변경-불가능한immutable-클래스">변경 불가능한(Immutable) 클래스</h3>
<p>String 객체는 <strong>한 번 생성되면 그 내부 값을 변경할 수 없는 immutable 객체</strong>입니다.</p>
<pre><code class="language-java">String a = &quot;a&quot;;
String b = &quot;b&quot;;
a = a + b; // 새로운 String 인스턴스 생성</code></pre>
<p>위 예제에서 문자열을 결합하면 기존의 a 인스턴스를 수정하는 것이 아니라, <strong>새로운 String 인스턴스가 생성되어 그 참조값이 a에 재할당</strong>됩니다.</p>
<p>문자열 변경이 빈번하게 발생하는 상황(예: 반복문 내 문자열 누적)에서는 <code>StringBuffer</code> 나 <code>StringBuilder</code> 를 사용하는 것이 성능상 유리합니다.</p>
<h3 id="문자열-생성-방식--리터럴-vs-생성자">문자열 생성 방식 – 리터럴 vs 생성자</h3>
<table>
<thead>
<tr>
<th>방식</th>
<th>예시</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>문자열 리터럴</td>
<td><code>String str1 = &quot;abc&quot;;</code></td>
<td>JVM의 <strong>상수 풀(Constant Pool)</strong>을 사용하여 동일한 문자열을 재사용</td>
</tr>
<tr>
<td>생성자 사용</td>
<td><code>String str2 = new String(&quot;abc&quot;);</code></td>
<td>항상 <strong>새로운 인스턴스</strong>를 생성하며, 리터럴과 다른 주소값을 가짐</td>
</tr>
</tbody></table>
<pre><code class="language-java">class Ex {
    public static void main(String[] args) {
        String str1 = &quot;abc&quot;;
        String str2 = &quot;abc&quot;;
        // 재사용으로 인해 str1과 str2가 동일한 주소를 참조
        System.out.println(str1 == str2); // true
        System.out.println(str1.equals(str2)); // true

        String str3 = new String(&quot;abc&quot;);
        String str4 = new String(&quot;abc&quot;);
        System.out.println(str3 == str4); // false
        System.out.println(str3.equals(str4)); // true
    }
}
</code></pre>
<ul>
<li><p><code>==</code> : 참조(주소) 비교</p>
</li>
<li><p><code>.equals()</code> : 문자열 내용 비교</p>
</li>
</ul>
<p>따라서 <code>String str = new String(&quot;문자열&quot;);</code> 처럼 생성자를 사용하는 방식은 특별한 이유가 없다면 피하는 것이 좋습니다.</p>
<h3 id="문자열-리터럴과-상수-풀constant-pool">문자열 리터럴과 상수 풀(Constant Pool)</h3>
<p>자바 컴파일러는 <strong>모든 문자열 리터럴을 클래스 파일의 상수 풀(Constant Pool)</strong>에 저장합니다. 동일한 리터럴이 여러 번 등장해도 한 번만 메모리에 저장되고, 해당 값을 참조하도록 최적화됩니다.</p>
<pre><code class="language-java">class Ex {
    public static void main(String[] args) {
        String s1 = &quot;AAA&quot;;
        String s2 = &quot;AAA&quot;;
        System.out.println(s1 == s2); // true
    }
}</code></pre>
<ul>
<li><p>s1과 s2는 동일한 &quot;AAA&quot; 문자열을 참조합니다.</p>
</li>
<li><p>이와 같은 동작은 메모리 절약과 성능 향상에 도움을 줍니다.</p>
</li>
</ul>
<h3 id="stringjoin과-stringjoiner--문자열-결합의-새로운-방법">String.join()과 StringJoiner – 문자열 결합의 새로운 방법</h3>
<p>Java 8부터는 문자열 배열 또는 컬렉션을 구분자로 연결할 수 있는 기능이 도입되었습니다.</p>
<h4 id="✅-stringjoin">✅ String.join()</h4>
<pre><code class="language-java">String[] arr = { &quot;dog&quot;, &quot;cat&quot;, &quot;bear&quot; };
System.out.println(String.join(&quot;-&quot;, arr)); // dog-cat-bear</code></pre>
<ul>
<li><p>배열 또는 Iterable 타입 요소들을 하나의 문자열로 합칩니다.</p>
</li>
<li><p>간단하고 직관적인 API입니다.</p>
</li>
</ul>
<h4 id="✅-stringjoiner">✅ StringJoiner</h4>
<pre><code class="language-java">StringJoiner sj = new StringJoiner(&quot;/&quot;, &quot;[&quot;, &quot;]&quot;);
sj.add(&quot;dog&quot;).add(&quot;cat&quot;).add(&quot;bear&quot;);
System.out.println(sj.toString()); // [dog/cat/bear]</code></pre>
<ul>
<li><p>접두사, 구분자, 접미사를 지정하여 포맷된 문자열을 생성할 수 있습니다.</p>
</li>
<li><p>반복문이나 조건문과 조합하여 유연한 문자열 처리가 가능합니다.</p>
</li>
</ul>
<h3 id="요약-1">요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>요약 설명</th>
</tr>
</thead>
<tbody><tr>
<td>불변 객체</td>
<td>String은 한 번 생성되면 내부 값 변경 불가</td>
</tr>
<tr>
<td>문자열 생성 방식</td>
<td>리터럴은 재사용, 생성자는 항상 새 인스턴스 생성</td>
</tr>
<tr>
<td>상수 풀</td>
<td>같은 문자열 리터럴은 JVM에서 공유 저장</td>
</tr>
<tr>
<td><code>String.join()</code></td>
<td>간단한 문자열 배열 결합</td>
</tr>
<tr>
<td><code>StringJoiner</code></td>
<td>접두/접미 포함한 고급 문자열 조합 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="stringbuffer-클래스--변경-가능한-문자열을-위한-선택">StringBuffer 클래스 – 변경 가능한 문자열을 위한 선택</h2>
<p>앞서 살펴본 String 클래스는 <strong>불변(immutable)</strong> 객체로 설계되어 있어 한 번 생성된 문자열은 변경이 불가능합니다. 이러한 특성은 보안성과 안정성 측면에서는 이점을 가지지만, 문자열 변경이 빈번한 경우에는 성능 저하를 유발할 수 있습니다.</p>
<p>이러한 문제를 해결하기 위해 자바에서는 문자열의 수정이 가능한 StringBuffer 클래스를 제공합니다. StringBuffer는 내부에 편집 가능한 <strong>버퍼(buffer)</strong>를 갖고 있으며, 문자열의 삽입, 삭제, 수정 등이 모두 가능합니다.</p>
<h3 id="클래스-정의-및-구조">클래스 정의 및 구조</h3>
<pre><code class="language-java">public final class StringBuffer implements java.io.Serializable {
    private char[] value;
}</code></pre>
<ul>
<li><p><code>char[] value</code> 배열은 실제 문자열 데이터를 저장합니다.</p>
</li>
<li><p>StringBuffer 역시 <code>final</code> 클래스로 상속이 불가능합니다.</p>
</li>
<li><p>버퍼란 문자 데이터를 임시로 저장하고 조작할 수 있는 메모리 공간을 의미합니다.</p>
</li>
</ul>
<h3 id="생성자와-버퍼-초기화">생성자와 버퍼 초기화</h3>
<p>StringBuffer 인스턴스는 내부에 <strong>문자 배열(char[]) 기반의 버퍼</strong>를 생성합니다. 버퍼 크기를 개발자가 지정하지 않으면, <strong>기본 크기(16자)</strong>가 할당됩니다.</p>
<table>
<thead>
<tr>
<th>생성자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>new StringBuffer()</code></td>
<td>기본 16자의 버퍼 생성</td>
</tr>
<tr>
<td><code>new StringBuffer(int length)</code></td>
<td>지정된 길이의 버퍼 생성</td>
</tr>
<tr>
<td><code>new StringBuffer(String str)</code></td>
<td>초기 문자열과 16자의 여유 공간을 포함한 버퍼 생성</td>
</tr>
</tbody></table>
<pre><code class="language-java">StringBuffer sb1 = new StringBuffer(); // 16자 버퍼
StringBuffer sb2 = new StringBuffer(50); // 50자 버퍼
StringBuffer sb3 = new StringBuffer(&quot;hello&quot;); // &quot;hello&quot; + 16자 버퍼</code></pre>
<h4 id="⚠️-버퍼-크기-주의">⚠️ 버퍼 크기 주의</h4>
<p>문자열이 버퍼의 크기를 초과하게 되면 내부적으로 <strong>버퍼 확장 작업</strong>이 수행되며, 이는 다음과 같은 추가 연산을 포함합니다:</p>
<pre><code class="language-java">char newValue[] = new char[newCapacity];
System.arraycopy(value, 0, newValue, count); // count는 현재 문자열 길이
value = newValue;</code></pre>
<blockquote>
<p>이 연산은 성능에 영향을 줄 수 있으므로, 예상 문자열 크기를 고려하여 적절한 초기 버퍼 크기를 지정하는 것이 좋습니다.</p>
</blockquote>
<h3 id="문자열-비교-시-주의점">문자열 비교 시 주의점</h3>
<p>StringBuffer는 <code>equals()</code> 메서드를 오버라이딩하지 않았기 때문에, 인스턴스의 <strong>주소(참조)</strong>를 비교합니다.</p>
<pre><code class="language-java">StringBuffer sb1 = new StringBuffer(&quot;abc&quot;);
StringBuffer sb2 = new StringBuffer(&quot;abc&quot;);

System.out.println(sb1.equals(sb2)); // false
System.out.println(sb1 == sb2);      // false</code></pre>
<h4 id="올바른-비교-방법">올바른 비교 방법</h4>
<p>StringBuffer의 문자열 내용을 비교하고 싶다면, 반드시 <code>toString()</code> 메서드를 사용하여 String으로 변환한 후 비교해야 합니다.</p>
<pre><code class="language-java">String s1 = sb1.toString();
String s2 = sb2.toString();

System.out.println(s1.equals(s2)); // true</code></pre>
<blockquote>
<p>참고로, toString() 메서드는 오버라이딩되어 있어 저장된 문자열을 반환합니다.</p>
</blockquote>
<h3 id="stringbuffer-vs-stringbuilder">StringBuffer vs StringBuilder</h3>
<p>StringBuffer는 <strong>멀티쓰레드에 안전(thread safe)하도록 동기화</strong>되어 있다. 멀티쓰레드로 작성된 프로그램이 아닌 경우, StringBuffer의 동기화는 불필요하게 성능만 떨어뜨립니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>StringBuffer</th>
<th>StringBuilder</th>
</tr>
</thead>
<tbody><tr>
<td><strong>동기화 여부</strong></td>
<td>O (thread-safe)</td>
<td>X (단일 쓰레드 환경에서 권장)</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>상대적으로 느림</td>
<td>빠름</td>
</tr>
<tr>
<td><strong>사용 대상</strong></td>
<td>멀티쓰레드 환경</td>
<td>단일 쓰레드 환경</td>
</tr>
<tr>
<td><strong>API 호환성</strong></td>
<td>동일 (append, insert 등)</td>
<td>동일</td>
</tr>
</tbody></table>
<pre><code class="language-java">// StringBuilder 예시
StringBuilder sb = new StringBuilder();
sb.append(&quot;Hello&quot;);
sb.append(&quot; World&quot;);
System.out.println(sb.toString()); // Hello World</code></pre>
<blockquote>
<p>StringBuffer를 사용 중이던 코드를 단일 쓰레드 환경에서 최적화하고자 할 때, StringBuilder로의 전환은 매우 간단하며 효과적입니다.</p>
</blockquote>
<h3 id="요약-2">요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>불변성 여부</td>
<td><code>StringBuffer</code>는 가변(mutable) 객체</td>
</tr>
<tr>
<td>성능 팁</td>
<td>예상 문자열 길이만큼 버퍼 크기를 지정하여 생성하는 것이 바람직</td>
</tr>
<tr>
<td>문자열 비교</td>
<td><code>equals()</code>가 아니라 <code>toString().equals()</code> 사용</td>
</tr>
<tr>
<td>쓰레드 안전성</td>
<td><code>StringBuffer</code>는 thread-safe, <code>StringBuilder</code>는 thread-unsafe</td>
</tr>
<tr>
<td>사용 시점 구분</td>
<td>멀티쓰레드 환경 – <code>StringBuffer</code>, 단일 쓰레드 환경 – <code>StringBuilder</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="래퍼wrapper-클래스--기본형을-객체처럼-다루는-방법">래퍼(Wrapper) 클래스 – 기본형을 객체처럼 다루는 방법</h2>
<p>Java는 성능을 고려하여 <code>int</code> , <code>double</code> , <code>char</code> 등과 같은 <strong>8개의 기본형(primitive type)</strong> 데이터를 객체가 아닌 값 자체로 처리합니다. 이는 자바가 완전한 의미의 순수 객체지향 언어가 아니라는 평가를 받는 이유이기도 합니다.</p>
<p>하지만 프로그래밍을 하다 보면, 기본형 변수도 <strong>객체로 다뤄야 하는 상황</strong>이 종종 발생합니다. 대표적인 예로는 <strong>컬렉션 프레임워크, 제네릭, Object 타입의 파라미터</strong> 등이 있습니다.</p>
<p>이러한 상황에서 기본형을 객체로 감싸는 역할을 하는 것이 바로 <strong>래퍼 클래스(wrapper class)</strong>입니다.</p>
<h3 id="래퍼-클래스란">래퍼 클래스란?</h3>
<p>래퍼 클래스는 기본형 값을 내부에 저장하면서, 다양한 유틸리티 메서드를 제공하는 클래스입니다. 예를 들어, <code>int</code> 를 객체처럼 다루고 싶을 때는 <code>Integer</code> 클래스를 사용합니다.</p>
<pre><code class="language-java">public final class Integer extends Number implements Comparable&lt;Integer&gt; {
    private final int value;
    ...
}</code></pre>
<ul>
<li><p>래퍼 클래스는 <code>final</code> 로 정의되어 있으며 상속이 불가능합니다.</p>
</li>
<li><p>내부에 해당 기본형 값을 저장하고, 관련된 메서드들을 제공합니다.</p>
</li>
</ul>
<h3 id="기본형과-대응되는-래퍼-클래스-목록">기본형과 대응되는 래퍼 클래스 목록</h3>
<table>
<thead>
<tr>
<th>기본형</th>
<th>래퍼 클래스</th>
<th>주요 생성자</th>
<th>예시 코드</th>
</tr>
</thead>
<tbody><tr>
<td><code>boolean</code></td>
<td><code>Boolean</code></td>
<td><code>Boolean(boolean value)</code>, <code>Boolean(String s)</code></td>
<td><code>new Boolean(true)</code>, <code>new Boolean(&quot;true&quot;)</code></td>
</tr>
<tr>
<td><code>char</code></td>
<td><code>Character</code></td>
<td><code>Character(char value)</code></td>
<td><code>new Character(&#39;a&#39;)</code></td>
</tr>
<tr>
<td><code>byte</code></td>
<td><code>Byte</code></td>
<td><code>Byte(byte value)</code>, <code>Byte(String s)</code></td>
<td><code>new Byte((byte)10)</code>, <code>new Byte(&quot;10&quot;)</code></td>
</tr>
<tr>
<td><code>short</code></td>
<td><code>Short</code></td>
<td><code>Short(short value)</code>, <code>Short(String s)</code></td>
<td><code>new Short((short)10)</code>, <code>new Short(&quot;10&quot;)</code></td>
</tr>
<tr>
<td><code>int</code></td>
<td><code>Integer</code></td>
<td><code>Integer(int value)</code>, <code>Integer(String s)</code></td>
<td><code>new Integer(100)</code>, <code>new Integer(&quot;100&quot;)</code></td>
</tr>
<tr>
<td><code>long</code></td>
<td><code>Long</code></td>
<td><code>Long(long value)</code>, <code>Long(String s)</code></td>
<td><code>new Long(100L)</code>, <code>new Long(&quot;100&quot;)</code></td>
</tr>
<tr>
<td><code>float</code></td>
<td><code>Float</code></td>
<td><code>Float(float value)</code>, <code>Float(String s)</code></td>
<td><code>new Float(1.0f)</code>, <code>new Float(&quot;1.0&quot;)</code></td>
</tr>
<tr>
<td><code>double</code></td>
<td><code>Double</code></td>
<td><code>Double(double value)</code>, <code>Double(String s)</code></td>
<td><code>new Double(1.0)</code>, <code>new Double(&quot;1.0&quot;)</code></td>
</tr>
</tbody></table>
<blockquote>
<p>📌 주의: JDK 9부터는 대부분의 생성자가 Deprecated 되었으며, 객체 생성을 위해 valueOf() 또는 auto-boxing 방식을 사용하는 것이 권장됩니다.</p>
</blockquote>
<h3 id="boxing--unboxing">Boxing &amp; Unboxing</h3>
<h4 id="📦-boxing">📦 Boxing</h4>
<p>기본형 → 래퍼 클래스 객체로 변환</p>
<pre><code class="language-java">int n = 10;
Integer i = Integer.valueOf(n); // 권장
Integer i2 = n; // auto-boxing (자동 변환)</code></pre>
<h4 id="📤-unboxing">📤 Unboxing</h4>
<p>래퍼 클래스 객체 → 기본형으로 변환</p>
<pre><code class="language-java">Integer i = 20;
int n = i.intValue(); // 명시적 언박싱
int n2 = i; // auto-unboxing (자동 변환)</code></pre>
<h3 id="실무에서-래퍼-클래스가-필요한-이유">실무에서 래퍼 클래스가 필요한 이유</h3>
<table>
<thead>
<tr>
<th>사용 사례</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>컬렉션 사용 시</td>
<td><code>List&lt;int&gt;</code>는 불가능하지만, <code>List&lt;Integer&gt;</code>는 가능</td>
</tr>
<tr>
<td>제네릭 사용 시</td>
<td>기본형은 타입 파라미터로 사용할 수 없으므로 객체 필요</td>
</tr>
<tr>
<td>null 처리가 필요한 경우</td>
<td>기본형은 <code>null</code>을 가질 수 없지만, 래퍼 클래스는 가능</td>
</tr>
<tr>
<td>Object 파라미터와의 통합 처리</td>
<td>모든 래퍼 클래스는 <code>Object</code> 타입으로 전달 가능</td>
</tr>
</tbody></table>
<h3 id="주의할-점">주의할 점</h3>
<ul>
<li><p>래퍼 클래스는 <strong>기본형보다 메모리 사용량이 크고 연산 속도가 느립니다.</strong></p>
</li>
<li><p>빈번한 auto-boxing/unboxing은 성능 저하의 원인이 될 수 있습니다.</p>
</li>
<li><p><code>==</code> 비교 시 객체 주소를 비교하므로 <code>equals()</code> 를 사용해야 정확한 값 비교가 가능합니다.</p>
</li>
</ul>
<pre><code class="language-java">Integer a = 100;
Integer b = 100;
System.out.println(a == b);        // true ([-128~127] 캐시 범위)
System.out.println(a.equals(b));  // true

Integer c = 1000;
Integer d = 1000;
System.out.println(c == d);       // false (캐시 범위 밖)
System.out.println(c.equals(d));  // true</code></pre>
<h3 id="요약-3">요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>정의</td>
<td>기본형을 객체로 다루기 위한 클래스</td>
</tr>
<tr>
<td>총 개수</td>
<td>8개 (boolean, char, byte, short, int, long, float, double)</td>
</tr>
<tr>
<td>생성 방식</td>
<td><code>valueOf()</code> 또는 auto-boxing 권장</td>
</tr>
<tr>
<td>사용 용도</td>
<td>컬렉션, 제네릭, null 처리, Object 파라미터 등</td>
</tr>
<tr>
<td>주의 사항</td>
<td><code>==</code> 대신 <code>equals()</code>로 비교, auto-boxing 성능 유의</td>
</tr>
</tbody></table>
<hr>
<h2 id="number-클래스--숫자형-래퍼-클래스들의-공통-조상">Number 클래스 – 숫자형 래퍼 클래스들의 공통 조상</h2>
<p>자바의 래퍼 클래스 중에서도 <code>int</code> , <code>double</code> , <code>long</code> 등 숫자와 관련된 기본형을 객체로 다루기 위한 클래스들이 있습니다. 이러한 숫자형 래퍼 클래스들은 모두 <code>java.lang.Number</code> 클래스를 공통 조상으로 상속받습니다.</p>
<p>Number 클래스는 자바에서 <strong>숫자형 객체 계층의 기반 클래스</strong>로 정의되어 있으며, 객체에 저장된 값을 다양한 기본형 숫자 타입으로 변환할 수 있는 메서드들을 제공합니다.</p>
<h3 id="number-클래스-개요">Number 클래스 개요</h3>
<pre><code class="language-java">public abstract class Number implements java.io.Serializable {
    public abstract int intValue();
    public abstract long longValue();
    public abstract float floatValue();
    public abstract double doubleValue();

    public byte byteValue() {
        return (byte) intValue();
    }
    public short shortValue() {
        return (short) intValue();
    }
}</code></pre>
<ul>
<li><p><strong>추상 클래스</strong>이므로 직접 인스턴스를 생성할 수 없습니다.</p>
</li>
<li><p>핵심 기능은 <strong>다양한 숫자형으로의 변환 메서드</strong>를 제공하는 데 있습니다.</p>
</li>
<li><p><code>Integer</code> , <code>Long</code> , <code>Float</code> , <code>Double</code> , <code>Byte</code> , <code>Short</code> 등의 래퍼 클래스는 모두 Number 클래스를 상속받습니다.</p>
</li>
</ul>
<h3 id="상속-구조">상속 구조</h3>
<pre><code class="language-text">            Object
              ▲
           Number (추상 클래스)
         ┌────┬────┬────┬────┬────┬────┐
     Integer Long Float Double Byte Short
</code></pre>
<blockquote>
<p>이 구조 덕분에 다양한 숫자형 래퍼 객체를 <strong>다형성(polymorphism)</strong>을 활용해 하나의 인터페이스로 다룰 수 있습니다.</p>
</blockquote>
<p>예시:</p>
<pre><code class="language-java">public void printDoubleValue(Number number) {
    System.out.println(number.doubleValue());
}</code></pre>
<ul>
<li>위 메서드는 <code>Integer</code> , <code>Double</code> , <code>BigDecimal</code> 등 다양한 숫자 객체를 하나의 파라미터 타입으로 처리할 수 있습니다.</li>
</ul>
<h3 id="주요-메서드-설명">주요 메서드 설명</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>반환형</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>intValue()</code></td>
<td><code>int</code></td>
<td>객체의 값을 <code>int</code> 타입으로 변환하여 반환</td>
</tr>
<tr>
<td><code>longValue()</code></td>
<td><code>long</code></td>
<td>객체의 값을 <code>long</code> 타입으로 변환하여 반환</td>
</tr>
<tr>
<td><code>floatValue()</code></td>
<td><code>float</code></td>
<td>객체의 값을 <code>float</code> 타입으로 변환하여 반환</td>
</tr>
<tr>
<td><code>doubleValue()</code></td>
<td><code>double</code></td>
<td>객체의 값을 <code>double</code> 타입으로 변환하여 반환</td>
</tr>
<tr>
<td><code>byteValue()</code></td>
<td><code>byte</code></td>
<td><code>intValue()</code>를 바탕으로 변환된 byte 반환 (명시적 형변환 포함)</td>
</tr>
<tr>
<td><code>shortValue()</code></td>
<td><code>short</code></td>
<td><code>intValue()</code>를 바탕으로 변환된 short 반환 (명시적 형변환 포함)</td>
</tr>
</tbody></table>
<h4 id="✅-실무-팁-3">✅ 실무 팁</h4>
<ul>
<li><p><code>Number</code> 타입을 활용하면 다양한 숫자 래퍼 클래스를 하나의 메서드에서 유연하게 처리할 수 있습니다.</p>
</li>
<li><p>단, 명시적 캐스팅이 수행되는 <code>byteValue()</code> , <code>shortValue()</code> 는 오버플로우 위험이 있으므로 사용 시 주의가 필요합니다.</p>
</li>
</ul>
<h3 id="biginteger와-bigdecimal--확장-숫자-타입">BigInteger와 BigDecimal – 확장 숫자 타입</h3>
<p>Number 클래스의 자손에는 기본형 래퍼 클래스 외에도 아래와 같은 <strong>정밀 수치 계산용 클래스</strong>도 포함됩니다:</p>
<table>
<thead>
<tr>
<th>클래스</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>BigInteger</code></td>
<td><code>long</code>보다 더 큰 범위의 정수 처리 가능 (예: 암호화, 금융 연산 등)</td>
</tr>
<tr>
<td><code>BigDecimal</code></td>
<td><code>double</code>보다 더 정밀한 부동 소수점 계산 가능 (예: 통화 단위 계산)</td>
</tr>
</tbody></table>
<pre><code class="language-java">import java.math.BigInteger;
import java.math.BigDecimal;

BigInteger bigInt = new BigInteger(&quot;12345678901234567890&quot;);
BigDecimal bigDec = new BigDecimal(&quot;1234567890.12345678901234567890&quot;);

System.out.println(bigInt.multiply(BigInteger.TEN));
System.out.println(bigDec.add(new BigDecimal(&quot;0.00000000000000000001&quot;)));</code></pre>
<h4 id="✅-실무-팁-4">✅ 실무 팁</h4>
<ul>
<li><p>금융 및 과학 계산과 같이 <strong>정밀한 수치 연산</strong>이 필요한 경우, BigDecimal을 사용하는 것이 필수적입니다.</p>
</li>
<li><p>BigInteger, BigDecimal은 기본형처럼 연산자가 아닌 <strong>메서드</strong>를 통해 사칙연산을 수행합니다 (add, subtract, multiply, divide 등).</p>
</li>
</ul>
<h3 id="요약-4">요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>클래스 유형</td>
<td>추상 클래스 (<code>abstract</code>)</td>
</tr>
<tr>
<td>주요 자손 클래스</td>
<td><code>Integer</code>, <code>Long</code>, <code>Double</code>, <code>Float</code>, <code>Short</code>, <code>Byte</code></td>
</tr>
<tr>
<td>주요 기능</td>
<td>다양한 기본형 숫자 타입으로 변환하는 메서드 제공</td>
</tr>
<tr>
<td>확장 자손</td>
<td><code>BigInteger</code>, <code>BigDecimal</code></td>
</tr>
<tr>
<td>실무 적용 포인트</td>
<td>숫자 래퍼 객체를 다형적으로 처리 가능, 고정 소수점 계산에 BigDecimal 활용</td>
</tr>
</tbody></table>
<hr>
<h2 id="마무리하며">마무리하며</h2>
<p>자바 프로그래밍의 핵심이자 가장 기본이 되는 java.lang 패키지의 주요 클래스들을 체계적으로 살펴보았습니다.</p>
<ul>
<li><p>Object 클래스에서 출발해 모든 클래스의 공통 기반이 되는 메서드들을 이해하고,</p>
</li>
<li><p>String, StringBuffer, StringBuilder 클래스를 통해 문자열 처리의 원리와 성능 고려사항을 짚어보았으며,</p>
</li>
<li><p>Wrapper 클래스들과 Number 클래스를 통해 기본형을 객체로 다루는 방식과 정밀 수치 계산까지 확장할 수 있는 기반을 확인했습니다.</p>
</li>
</ul>
<p>java.lang 패키지의 클래스들은 import 문 없이도 사용 가능한 만큼, 자바 언어 설계자가 &quot;자바 개발자가 반드시 알아야 하는 클래스들&quot;로 판단한 집합입니다. 단순히 자주 쓰인다는 이유를 넘어서, 이들 클래스는 <strong>객체지향의 철학, 성능과 안정성의 균형</strong>, 그리고 <strong>자바라는 언어의 정체성</strong>을 고스란히 담고 있습니다.</p>
<p>자바 개발에 있어 java.lang 패키지에 대한 이해는 더 이상 기초가 아닌 <strong>핵심 역량</strong>입니다. 단순히 문법적인 사용법을 넘어, 클래스의 설계 의도와 내부 구조를 이해하고 응용하는 습관이 쌓이면 실무에서도 더 강력하고 신뢰할 수 있는 코드를 작성할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[예외 처리(Exception Handling)]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%ACException-Handling-%EC%95%8C%EC%95%84%EB%91%90%EB%A9%B4-%EB%93%A0%EB%93%A0%ED%95%9C-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%ACException-Handling-%EC%95%8C%EC%95%84%EB%91%90%EB%A9%B4-%EB%93%A0%EB%93%A0%ED%95%9C-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 08 Jun 2025 05:34:20 GMT</pubDate>
            <description><![CDATA[<p>코드를 작성하다 보면 <strong>예상치 못한 오류</strong>가 발생하는 경우가 자주 있습니다. 예를 들어, 배열의 범위를 초과하거나, 존재하지 않는 파일에 접근하려고 할 때 등등. 이런 오류를 <strong>예외(Exception)</strong> 라고 부르고, 자바에서는 예외를 안전하게 처리할 수 있는 <strong>예외 처리(Exception Handling)</strong> 시스템을 제공합니다.</p>
<p>예외 처리는 <strong>안정적인 프로그램</strong>을 위한 필수 개념입니다.</p>
<hr>
<h2 id="프로그램-오류의-종류와-런타임-에러">프로그램 오류의 종류와 런타임 에러</h2>
<p>코드를 작성할 때 흔히 마주치는 것이 <strong>프로그램 오류</strong>예요. 오류에는 여러 종류가 있는데, 대표적으로 아래처럼 나눌 수 있어요.</p>
<h4 id="컴파일-에러">컴파일 에러</h4>
<p>소스코드를 컴파일할 때, 컴파일러가 문법 오류나 자료형 체크 등 기본적인 검사를 수행하면서 발견되는 오류예요. 컴파일 에러를 해결하지 않으면 클래스 파일이 생성되지 않아서 프로그램 실행 자체가 불가능하죠.</p>
<h4 id="런타임-에러">런타임 에러</h4>
<p>컴파일을 성공적으로 마친 후에도, 프로그램이 실행되다가 예상치 못한 상황(예: 배열의 범위 초과, 0으로 나누기 등)이 발생해 프로그램이 비정상적으로 종료되는 오류를 말해요. 이건 컴파일러가 미리 검증할 수 없는 영역이라서, 실행 도중에만 드러나요.</p>
<h4 id="논리적-에러">논리적 에러</h4>
<p>프로그램이 정상적으로 실행되지만, 의도와 다른 결과가 나오는 오류예요. 예를 들어, 계산 로직이 잘못되거나 조건문 분기 처리가 잘못된 경우 등이 있죠.</p>
<hr>
<h3 id="런타임-에러를-피하기-위한-대비">런타임 에러를 피하기 위한 대비</h3>
<p>컴파일을 무사히 마쳤다고 안심할 수는 없어요. 프로그램 실행 중에는 생각지도 못한 예외 상황이 발생할 수 있기 때문이에요. 그래서 자바에서는 런타임 시 발생할 수 있는 오류를 <strong>‘에러(error)’</strong>와 <strong>‘예외(exception)’</strong>로 구분해서 다루고 있어요.</p>
<h4 id="에러error">에러(error)</h4>
<p>프로그램 코드로는 수습할 수 없는, 아주 심각한 오류예요. 주로 JVM 자체의 문제(OutOfMemoryError, StackOverflowError 등)로, <strong>코드 레벨에서 처리하지 않습니다.</strong></p>
<h4 id="예외exception">예외(exception)</h4>
<p>프로그램 코드로 충분히 수습할 수 있는 오류예요. 예를 들어 파일이 없거나, 네트워크 연결이 끊기는 상황 등을 예외로 다뤄서, <strong>프로그램이 비정상 종료되지 않도록</strong> 처리할 수 있어요.</p>
<blockquote>
</blockquote>
<p>“컴파일 에러 → 컴파일 단계에서 체크”
“런타임 에러 → 실행 중 발생 가능성 대비”</p>
<hr>
<h2 id="예외-클래스의-계층-구조">예외 클래스의 계층 구조</h2>
<p>자바에서는 프로그램 실행 중에 발생할 수 있는 오류들을 모두 <strong>클래스로 정의</strong>해두었어요.
이 예외 처리 관련 클래스들도 결국엔 <strong>Object 클래스의 자손</strong>이라는 점이 공통입니다.</p>
<p>자바의 예외 클래스들은 크게 아래처럼 <strong>2가지 그룹</strong>으로 나뉘어요.</p>
<hr>
<h3 id="exception-클래스">Exception 클래스</h3>
<ul>
<li><p>예외 처리의 <strong>최고 조상 클래스</strong>는 Exception 이에요.</p>
</li>
<li><p>보통 <strong>사용자 입력 오류나 외부적인 요인</strong>으로 인해 발생하는 예외들이 여기에 속해요.</p>
</li>
</ul>
<h4 id="예시">예시</h4>
<ul>
<li><p><code>FileNotFoundException</code> : 없는 파일을 입력한 경우</p>
</li>
<li><p><code>ClassNotFoundException</code> : 클래스를 찾을 수 없는 경우</p>
</li>
<li><p><code>DataFormatException</code> : 잘못된 형식의 데이터 입력 등</p>
</li>
</ul>
<blockquote>
<p>💡 여기서 ClassNotFoundException도 외부 요인으로 봐요. 예를 들어 사용자가 올바른 클래스 이름을 입력하지 않은 경우나, 클래스 경로를 잘못 설정한 경우를 말합니다.</p>
</blockquote>
<h3 id="runtimeexception-클래스">RuntimeException 클래스</h3>
<ul>
<li><p>RuntimeException 도 Exception 의 자손이에요.</p>
</li>
<li><p>주로 <strong>프로그래머의 실수</strong>로 인해 발생하는 예외들이 여기에 속합니다.</p>
</li>
</ul>
<h4 id="예시-1">예시</h4>
<ul>
<li><p><code>ArrayIndexOutOfBoundsException</code> : 배열의 범위를 벗어난 경우</p>
</li>
<li><p><code>NullPointerException</code> : 값이 null인 참조변수의 멤버를 호출</p>
</li>
<li><p><code>ClassCastException</code> : 클래스간 잘못된 형변환</p>
</li>
<li><p><code>ArithmeticException</code> : 0으로 나누는 실수 등</p>
</li>
</ul>
<hr>
<h3 id="계층-구조-한눈에-보기">계층 구조 한눈에 보기</h3>
<pre><code class="language-php">Object
 └── Throwable
      ├── Error
      └── Exception
           ├── RuntimeException
           └── IOException, SQLException, ...
</code></pre>
<hr>
<h2 id="예외-처리하기">예외 처리하기</h2>
<p>프로그래밍을 하다 보면, <strong>예외(exception)</strong> 는 언제든 발생할 수 있어요.
에러는 어쩔 수 없지만, 예외는 <strong>프로그래머가 미리 대비해서 처리</strong>할 수 있습니다.</p>
<h3 id="예외-처리란">예외 처리란?</h3>
<p><strong>예외 처리(exception handling)</strong> 는 프로그램 실행 중에 발생할 수 있는 예외를 미리 처리하는 코드를 작성하는 것을 말해요.</p>
<table>
<thead>
<tr>
<th>목적</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>비정상 종료 방지</td>
<td>프로그램이 갑자기 멈추지 않도록 해줍니다.</td>
</tr>
<tr>
<td>정상 상태 유지</td>
<td>사용자 경험을 해치지 않고, 다음 작업을 계속할 수 있어요.</td>
</tr>
</tbody></table>
<p>만약 예외를 처리하지 않으면, 프로그램은 비정상적으로 종료되며 <strong>JVM의 “예외처리기 (UncaughtExceptionHandler)”</strong> 가 예외 원인을 화면에 출력하게 됩니다.</p>
<hr>
<h3 id="try-catch-문-기본-구조">try-catch 문 기본 구조</h3>
<p>자바에서는 <code>try-catch</code> 문을 사용해 예외를 처리해요.
try 블럭 안에서 예외가 발생하지 않으면, catch 블럭은 실행되지 않아요.</p>
<pre><code class="language-java">class Main {
    public static void main(String[] args) {
        System.out.println(1);
        try {
            System.out.println(2);
            System.out.println(3);
        } catch (Exception e) {
            System.out.println(4); // 실행X
        }
        System.out.println(5);
    }
}</code></pre>
<p>예외가 발생하면, try 블럭을 바로 빠져나와서 <strong>해당 예외를 처리할 수 있는 catch 블럭</strong>으로 이동합니다.
catch 블럭 실행이 끝나면 전체 try-catch 문을 벗어나 <strong>다음 문장부터 실행</strong>이 이어져요.</p>
<p>만약 일치하는 catch 블럭을 찾지 못하면, 예외는 처리되지 않고 프로그램이 비정상적으로 종료됩니다.</p>
<pre><code class="language-java">class Main {
    public static void main(String[] args) {
        System.out.println(1);

        try {
            System.out.println(0/0); // ArithmeticException 발생
            System.out.println(2);   // 실행X
        } catch (ArithmeticException e) {
            System.out.println(3);
        }

        System.out.println(4);
    }
}</code></pre>
<hr>
<h3 id="예외-처리-순서">예외 처리 순서</h3>
<p>1️⃣ 예외가 발생하면, 해당 예외 클래스의 <strong>인스턴스가 만들어져요.</strong>
예를 들어, ArithmeticException이 발생하면 ArithmeticException 인스턴스가 생성됩니다.</p>
<p>2️⃣ catch 블럭을 <strong>위에서부터 차례대로</strong> 검사해요.
이때, instanceof 연산자를 통해 참조 변수 타입과 예외 인스턴스의 타입을 비교합니다.</p>
<p>3️⃣ 검사 결과가 <code>true</code>인 catch 블럭에서 예외를 처리합니다.
만약 일치하는 catch 블럭이 없으면 예외는 처리되지 못하고 프로그램이 종료됩니다.</p>
<hr>
<h3 id="exception-타입으로-안전하게-처리하기">Exception 타입으로 안전하게 처리하기</h3>
<p>모든 예외 클래스는 <code>Exception</code> 클래스의 자손이기 때문에, catch 블럭에서 Exception 타입의 참조 변수를 선언해두면 모든 예외를 처리할 수 있어요.</p>
<pre><code class="language-java">class Ex8_3 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);

        try {
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);
        } catch (Exception e) { // ArithmeticException도 처리 가능
            System.out.println(5);
        }

        System.out.println(6);
    }
}</code></pre>
<hr>
<h3 id="다중-catch-블럭-사용-예시">다중 catch 블럭 사용 예시</h3>
<p>여러 종류의 예외를 따로 처리해야 할 때, 다중 catch 블럭을 사용하면 좋아요.</p>
<pre><code class="language-java">class Ex8_4 {
    public static void main(String[] args) {
        System.out.println(1);            
        System.out.println(2);

        try {
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);
        } catch (ArithmeticException ae) {
            if (ae instanceof ArithmeticException) 
                System.out.println(&quot;true&quot;);

            System.out.println(&quot;ArithmeticException&quot;);
        } catch (Exception e){
            System.out.println(&quot;Exception&quot;);
        }

        System.out.println(6);
    }
}</code></pre>
<hr>
<h2 id="finally-블럭">finally 블럭</h2>
<p>예외 처리에서 try-catch문과 함께 자주 사용되는 게 바로 finally 블럭이에요.
finally는 <strong>예외 발생 여부와 상관없이</strong> 항상 실행되는 코드를 담는 블럭이에요.</p>
<h3 id="finally-블럭의-특징">finally 블럭의 특징</h3>
<ul>
<li><p>try나 catch 블럭을 빠져나갈 때 항상 실행돼요.</p>
</li>
<li><p>예외가 발생해도, 발생하지 않아도 무조건 실행됩니다.</p>
</li>
<li><p>자원 정리, 파일 닫기, 네트워크 연결 해제 등 반드시 실행해야 하는 코드를 적을 때 사용해요.</p>
</li>
</ul>
<h3 id="기본-구조">기본 구조</h3>
<pre><code class="language-java">try {
    // 예외가 발생할 수 있는 코드
} catch (Exception e) {
    // 예외 처리 코드
} finally {
    // 항상 실행되는 코드
}</code></pre>
<hr>
<h3 id="예제-코드">예제 코드</h3>
<pre><code class="language-java">class Main {
    public static void main(String[] args) {
        try {
            System.out.println(&quot;try 블럭 실행&quot;);
            // 예외 발생 가능 코드
            System.out.println(10/0); // ArithmeticException 발생
        } catch (ArithmeticException e) {
            System.out.println(&quot;예외 처리: &quot; + e);
        } finally {
            System.out.println(&quot;finally 블럭 실행&quot;);
        }
    }
}</code></pre>
<h4 id="출력-결과">출력 결과</h4>
<pre><code class="language-csharp">try 블럭 실행
예외 처리: java.lang.ArithmeticException: / by zero
finally 블럭 실행</code></pre>
<hr>
<h3 id="작은-팁">작은 팁</h3>
<ul>
<li><p>try 블럭에서 return을 만나더라도, finally 블럭은 <strong>무조건 실행</strong>됩니다.</p>
</li>
<li><p>예외 처리의 흐름을 이해할 때 finally를 항상 염두에 두면 좋아요!</p>
</li>
</ul>
<hr>
<h2 id="예외-정보-얻기-printstacktrace와-getmessage">예외 정보 얻기: PrintStackTrace()와 getMessage()</h2>
<p>자바에서 <strong>예외가 발생하면</strong>, 그 예외를 처리할 뿐만 아니라 <strong>무엇이 문제였는지</strong>를 알아내는 것도 중요해요.
이를 위해 예외 클래스의 인스턴스가 제공하는 <strong>유용한 메서드</strong> 두 가지가 있어요!</p>
<h3 id="printstacktrace">PrintStackTrace()</h3>
<p>예외가 발생했을 때, <strong>예외의 종류, 메시지, 호출 스택(Call Stack) 정보</strong>를 화면에 출력해줘요.</p>
<p>디버깅할 때 매우 유용해요!</p>
<h3 id="getmessage">getMessage()</h3>
<p>예외 인스턴스에 저장된 <strong>간단한 예외 메시지</strong>를 반환해요.</p>
<p>사용자에게 예외 상황을 간단히 알려줄 때 좋아요.</p>
<hr>
<h3 id="예제-코드-1">예제 코드</h3>
<pre><code class="language-java">class Ex8_5 {
    public static void main(String[] args) {
        System.out.println(1);            
        System.out.println(2);

        try {
            System.out.println(3);
            System.out.println(0/0); // ArithmeticException 발생
            System.out.println(4);   // 실행X
        } catch (ArithmeticException ae) {
            ae.printStackTrace(); // 예외 발생 위치까지 콜스택 정보 출력
            System.out.println(&quot;예외 메시지: &quot; + ae.getMessage());
        }

        System.out.println(6);
    }
}</code></pre>
<h4 id="출력-결과-1">출력 결과</h4>
<pre><code class="language-csharp">1
2
3
java.lang.ArithmeticException: / by zero
    at Ex8_5.main(Ex8_5.java:8)
예외 메시지: / by zero
6</code></pre>
<hr>
<h3 id="포인트-정리">포인트 정리</h3>
<ul>
<li><p><code>printStackTrace()</code> 는 예외 발생 위치까지의 모든 호출 경로를 알려줘서, 문제를 쉽게 추적할 수 있어요.</p>
</li>
<li><p><code>getMessage()</code> 는 짧은 예외 설명만 간단히 알려줍니다.</p>
</li>
</ul>
<blockquote>
<p>실무에서는 둘을 함께 활용해서, 예외의 상세 정보도 로그로 남기고, 사용자에게는 간단한 메시지를 보여주면 좋아요!</p>
</blockquote>
<hr>
<h2 id="예외-발생시키기-throw-키워드">예외 발생시키기: throw 키워드</h2>
<p>때로는 프로그래머가 <strong>직접 예외를 발생시켜야 할 상황</strong>이 생길 수 있어요.
이럴 때 자바는 <code>throw</code> 키워드를 통해 <strong>고의로 예외를 발생</strong>시킬 수 있게 해줍니다.</p>
<h3 id="기본-사용-방법">기본 사용 방법</h3>
<pre><code class="language-java">throw 예외_인스턴스;</code></pre>
<ul>
<li><p>throw 뒤에 <strong>Exception 객체</strong>를 넣어주면, 그 순간 예외가 발생해요.</p>
</li>
<li><p>주로 조건 검증 후 문제가 있을 때, <strong>직접 예외를 발생시켜 코드 흐름을 제어</strong>할 때 사용합니다.</p>
</li>
</ul>
<hr>
<h3 id="예제-코드-2">예제 코드</h3>
<pre><code class="language-java">class Ex8_6 {
    public static void main(String[] args) {
        try {
            Exception e = new Exception(&quot;고의로 발생시켰음.&quot;);
            throw e;
        } catch (Exception e) {
            System.out.println(&quot;에러 메시지 : &quot; + e.getMessage());
            e.printStackTrace();
        }

        System.out.println(&quot;프로그램이 정상 종료되었음.&quot;);
    }
}</code></pre>
<h4 id="출력-결과-2">출력 결과</h4>
<pre><code class="language-csharp">에러 메시지 : 고의로 발생시켰음.
java.lang.Exception: 고의로 발생시켰음.
    at Ex8_6.main(Ex8_6.java:4)
프로그램이 정상 종료되었음.</code></pre>
<hr>
<h2 id="checked-예외-vs-unchecked-예외">checked 예외 vs unchecked 예외</h2>
<p>자바에서는 예외를 크게 두 가지 종류로 나눌 수 있어요.</p>
<p>1️⃣ checked 예외
2️⃣ unchecked 예외</p>
<p>이 구분을 이해하면 컴파일 에러와 코드 작성 시의 예외 처리 방침을 더 쉽게 정리할 수 있답니다!</p>
<h3 id="checked-예외">checked 예외</h3>
<ul>
<li><p>Exception 클래스와 그 자손들을 말해요.</p>
</li>
<li><p>발생 가능성이 있는 코드에서 반드시 <strong>예외 처리를 강제</strong>해야 해요.</p>
</li>
<li><p>처리하지 않으면 <strong>컴파일 에러</strong>가 발생!</p>
</li>
</ul>
<pre><code class="language-java">class Ex8_7 {
    public static void main(String[] args) {
        throw new Exception(); // 컴파일 에러 발생
    }
}</code></pre>
<h4 id="출력-결과-3">출력 결과</h4>
<pre><code class="language-php">Exception in thread &quot;main&quot; java.lang.Error: Unresolved compilation problem: 
    Unhandled exception type Exception

    at Ex8_7.main(Ex8_7.java:3)</code></pre>
<h3 id="unchecked-예외">unchecked 예외</h3>
<ul>
<li><p>RuntimeException 클래스와 그 자손들을 말해요.</p>
</li>
<li><p>보통 프로그래머의 실수로 인해 발생하는 예외예요.</p>
</li>
<li><p><strong>예외 처리를 강제하지 않기 때문에</strong>, 컴파일은 정상적으로 됩니다.</p>
</li>
<li><p>만일 RuntimeException 클래스들에 속하는 예외가 발생할 가능성이 있는 모든 코드에도 예외 처리를 해야 한다면, 참조 변수와 배열이 사용되는 모든 곳에 예외 처리를 해주어야 할거에요</p>
</li>
</ul>
<pre><code class="language-java">class Ex8_8 {
    public static void main(String[] args) {
        throw new RuntimeException(); // 컴파일 OK!
    }
}</code></pre>
<h4 id="출력-결과-4">출력 결과</h4>
<pre><code class="language-php">Exception in thread &quot;main&quot; java.lang.RuntimeException
    at Ex8_8.main(Ex8_8.java:3)</code></pre>
<hr>
<h3 id="checked-vs-unchecked-예외-차이-정리">checked vs unchecked 예외 차이 정리</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>checked 예외</th>
<th>unchecked 예외</th>
</tr>
</thead>
<tbody><tr>
<td>대표 클래스</td>
<td><code>Exception</code>과 그 자손</td>
<td><code>RuntimeException</code>과 그 자손</td>
</tr>
<tr>
<td>컴파일 시점</td>
<td><strong>예외 처리 강제</strong> (컴파일 에러 발생)</td>
<td>예외 처리 강제 X (컴파일 OK)</td>
</tr>
<tr>
<td>예시</td>
<td><code>IOException</code>, <code>SQLException</code></td>
<td><code>NullPointerException</code>, <code>ArrayIndexOutOfBoundsException</code></td>
</tr>
<tr>
<td>주로 언제 발생?</td>
<td>외부적 오류, 예측 가능한 상황</td>
<td>프로그래머 실수, 잘못된 로직</td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
<ul>
<li>checked 예외: 외부적인 상황으로 발생 → 예외 처리 반드시 필요</li>
<li>unchecked 예외: 프로그래머 실수 → 예외 처리 강제 X, 필요 시 직접 처리</li>
</ul>
<hr>
<h2 id="메서드에-예외-선언하기-throws-키워드">메서드에 예외 선언하기: throws 키워드</h2>
<p>메서드에서 발생할 수 있는 예외를 <strong>호출한 쪽으로 넘겨주기 위해</strong> 사용하는 키워드가 바로 <code>throws</code> 입니다.
<code>throws</code> 는 메서드 선언부에 작성하며, 여러 예외가 있을 경우 쉼표(,)로 구분해 적어줍니다.</p>
<pre><code class="language-java">void method() throws Exception1, Exception2, ExceptionN {}</code></pre>
<hr>
<h3 id="예외-선언의-목적">예외 선언의 목적</h3>
<p>예외가 발생했을 때 <strong>자체적으로 처리할 수 없는 경우</strong>,
예외를 자신을 호출한 메서드에게 넘겨주기 위해 <code>throws</code> 를 사용합니다.</p>
<p>하지만 이렇게 예외를 넘긴다고 해서 <strong>예외가 처리된 건 아니고</strong>,
<strong>언젠가는 반드시 try-catch문으로 처리</strong>해야 한다는 점을 기억해 주세요</p>
<hr>
<h3 id="예외-전달-흐름-예제">예외 전달 흐름 예제</h3>
<p>예외가 선언되어 있으면 Exception과 같은 checked 예외를 try-catch문으로 처리하지 않아도 컴파일 에러가 발생하지 않습니다.</p>
<pre><code class="language-java">class Ex8_9 {
    public static void main(String[] args) throws Exception {
        method1();
      }

    static void method1() throws Exception {
        method2();
    }

    static void method2() throws Exception {
        throw new Exception();
    }
}</code></pre>
<h4 id="출력-결과-5">출력 결과</h4>
<pre><code class="language-php">Exception in thread &quot;main&quot; java.lang.Exception
    at Ex8_9.method2(Ex8_9.java:11)
    at Ex8_9.method1(Ex8_9.java:7)
    at Ex8_9.main(Ex8_9.java:3)</code></pre>
<h4 id="흐름-설명">흐름 설명</h4>
<p>1️⃣ <code>method2()</code> 에서 예외가 발생하지만, 처리하지 않고 선언만 해둠</p>
<p>2️⃣ <code>method1()</code> 도 <code>method2()</code> 의 예외를 다시 넘겨줌</p>
<p>3️⃣ <code>main()</code> 도 예외를 처리하지 않아, 결국 JVM의 예외 처리기로 전달 → 프로그램 비정상 종료</p>
<hr>
<h3 id="실전-예제-파일-이름-검증">실전 예제: 파일 이름 검증</h3>
<pre><code class="language-java">import java.io.*;

class Ex8_10 {
    public static void main(String[] args) {
        try {
            File f = createFile(args[0]);
            System.out.println(f.getName() + &quot;파일이 성공적으로 생성되었습니다.&quot;);
        } catch (Exception e) {
            System.out.println(e.getMessage() + &quot; 다시 입력해 주시기 바랍니다.&quot;);
        }
    }    

    static File createFile(String fileName) throws Exception {
        if (fileName == null || fileName.equals(&quot;&quot;)) {
            throw new Exception(&quot;파일이름이 유효하지 않습니다.&quot;);
        }

        File f = new File(fileName);
        f.createNewFile();

        return f;
    }
}</code></pre>
<h4 id="출력-결과-6">출력 결과</h4>
<pre><code class="language-bash">test파일이 성공적으로 생성되었습니다.
파일이름이 유효하지 않습니다. 다시 입력해 주시기 바랍니다.</code></pre>
<blockquote>
</blockquote>
<ul>
<li><code>throws</code> 를 사용하면 <strong>예외를 메서드 밖으로 전달</strong>할 수 있습니다.</li>
<li><strong>언제 처리할지 결정할 수 있는 유연성</strong>을 주지만, 결국에는 반드시 처리해야 함을 잊지 마세요.</li>
<li>외부에서 발생할 가능성이 높은 예외(예: 파일 처리, 네트워크 연결 등)는 이런 방식으로 <strong>예외를 넘기고 호출부에서 처리</strong>하는 경우가 많아요.</li>
</ul>
<hr>
<h2 id="사용자-정의-예외-만들기">사용자 정의 예외 만들기</h2>
<p>자바에서 기본적으로 제공되는 예외 클래스들도 충분히 유용하지만,
상황에 따라 <strong>예외를 직접 만들어서</strong> 더 명확하게 코드의 의미를 표현할 수도 있습니다.
이렇게 만든 예외를 <strong>사용자 정의 예외</strong> 라고 부릅니다.</p>
<hr>
<h3 id="기본-개념">기본 개념</h3>
<ul>
<li><p>기존 예외 클래스 (Exception, RuntimeException 등)를 <strong>상속</strong>해서 만듭니다.</p>
</li>
<li><p>예외 메시지나 추가 정보(에러 코드 등) 를 함께 담을 수 있어,상황에 딱 맞는 예외를 정의할 수 있습니다.</p>
</li>
<li><p>최근에는 <strong>checked 예외보다는 unchecked 예외(RunTimeException 상속)</strong> 로 작성하는 경우가 더 많습니다.</p>
</li>
</ul>
<hr>
<h3 id="사용자-정의-예외-예제">사용자 정의 예외 예제</h3>
<p>아래 예시는 Exception을 상속받아 <strong>에러 메시지 + 에러 코드</strong>를 함께 담는 예외 클래스입니다.</p>
<pre><code class="language-java">class MyException extends Exception {
    private final int ERR_CODE;

    MyException(String msg, int errCode) {
        super(msg); // Exception 클래스의 생성자 호출 (메시지 저장)
        ERR_CODE = errCode;
    }

    MyException(String msg) {
        this(msg, 100); // 기본 에러 코드 100으로 설정
    }

    public int getErrCode() {
        return ERR_CODE;
    }
}

class Main {
    public static void main(String[] args) {
        try {
            throw new MyException(&quot;사용자 정의 예외 발생!&quot;, 404);
        } catch (MyException e) {
            System.out.println(&quot;에러 메시지: &quot; + e.getMessage());
            System.out.println(&quot;에러 코드: &quot; + e.getErrCode());
        }
    }
}</code></pre>
<h4 id="출력-결과-7">출력 결과</h4>
<pre><code class="language-bash">에러 메시지: 사용자 정의 예외 발생!
에러 코드: 404</code></pre>
<hr>
<h2 id="연결된-예외-chained-exception">연결된 예외 (Chained Exception)</h2>
<p>프로그램을 작성하다 보면, <strong>하나의 예외가 다른 예외를 발생시키는 경우</strong>가 생기곤 합니다.
이렇게 서로 연결된 예외를 <strong>연결된 예외(Chained Exception)</strong> 라고 부릅니다.</p>
<hr>
<h3 id="기본-개념-1">기본 개념</h3>
<ul>
<li><p><strong>원인 예외</strong> : 새로운 예외가 발생하기 전에 발생했던 예외</p>
</li>
<li><p><strong>새로운 예외</strong> : 원인 예외를 감싸서 던져지는 예외</p>
</li>
</ul>
<p><code>Throwable</code> 클래스(예외의 최상위 클래스)가 제공하는 두 가지 메서드로 연결할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>initCause(Throwable cause)</code></td>
<td>원인 예외 등록하기</td>
</tr>
<tr>
<td><code>getCause()</code></td>
<td>등록된 원인 예외 가져오기</td>
</tr>
</tbody></table>
<hr>
<h3 id="왜-연결된-예외를-사용할까">왜 연결된 예외를 사용할까?</h3>
<p>1️⃣ <strong>다양한 예외를 하나로 묶어 처리</strong></p>
<ul>
<li>서로 상속 관계가 아닌 예외도 <strong>원인 예외로 연결하면</strong>, 하나의 큰 예외로 다룰 수 있어요.</li>
</ul>
<p>2️⃣ <strong>checked 예외를 unchecked 예외로 변환</strong></p>
<ul>
<li>예를 들어, Exception을 RuntimeException으로 감싸면 <strong>예외 처리 강제성</strong>을 해제할 수 있어요.</li>
</ul>
<hr>
<h3 id="예제-코드-3">예제 코드</h3>
<p>아래는 설치 중 발생할 수 있는 <strong>공간 부족 예외(SpaceException)</strong> 를 <strong>설치 예외(InstallException)</strong> 로 감싸서 던지는 예제입니다.</p>
<pre><code class="language-java">try {
    startInstall();
    copyFiles();
} catch (SpaceException e) {
    InstallException ie = new InstallException(&quot;설치 중 예외발생&quot;);
    ie.initCause(e); // 원인 예외 등록
    throw ie;
}</code></pre>
<p><strong>checked 예외를 unchecked 예외로 변환</strong>하는 예제입니다.</p>
<pre><code class="language-java">throw new RuntimeException(new MemoryException(&quot;메모리가 부족합니다.&quot;));</code></pre>
<hr>
<h2 id="마무리하며">마무리하며</h2>
<p>이번 글에서는 자바의 예외 처리의 기본 개념부터, try-catch-finally 구조, throw/throws 키워드, 사용자 정의 예외, 그리고 연결된 예외까지 정리했습니다.</p>
<p>예외 처리는 단순히 “에러를 막기 위한 기술”을 넘어서, 프로그램의 안정성과 유지보수성을 높이는 중요한 부분입니다.
어떤 예외를 어떻게 처리할지, 어떤 예외를 호출자에게 넘길지를 고민하는 습관은 개발자로서의 역량을 한 단계 더 업그레이드시켜줄 거예요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[추상 클래스와 인터페이스]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Fri, 06 Jun 2025 10:16:25 GMT</pubDate>
            <description><![CDATA[<p>자바에서 객체 지향 프로그래밍을 제대로 활용하기 위해 꼭 이해하고 넘어가야 할 두 가지 중요한 개념이 있습니다. 바로 추상 클래스(Abstract Class) 와 인터페이스(Interface) 입니다.</p>
<hr>
<h2 id="추상-클래스란">추상 클래스란?</h2>
<p>추상 클래스는 <strong>미완성 설계도</strong>입니다.
여러 클래스에 공통적으로 사용되는 로직을 정의하되, 일부는 구현하지 않고 자손 클래스에서 반드시 구현하도록 강제할 수 있습니다.</p>
<blockquote>
<p>즉, 클래스 간의 공통 로직은 미리 작성하고,
구체적인 동작은 자식 클래스에서 작성하게 하는 &#39;틀(template)&#39;입니다.</p>
</blockquote>
<h3 id="추상-클래스의-특징-요약">추상 클래스의 특징 요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>정의 방식</td>
<td><code>abstract class 클래스명</code></td>
</tr>
<tr>
<td>인스턴스 생성</td>
<td>❌ 직접 생성 불가능</td>
</tr>
<tr>
<td>상속 여부</td>
<td>✅ 반드시 상속을 통해 사용</td>
</tr>
<tr>
<td>추상 메서드</td>
<td>하나 이상 포함 가능</td>
</tr>
<tr>
<td>목적</td>
<td>공통 기능 제공 + 강제 구현 유도</td>
</tr>
<tr>
<td>키워드</td>
<td><code>abstract</code> 사용</td>
</tr>
</tbody></table>
<hr>
<h3 id="추상-메서드란">추상 메서드란?</h3>
<p>추상 클래스의 핵심은 바로 추상 메서드입니다.</p>
<pre><code class="language-java">abstract class Animal {
    abstract void sound(); // 추상 메서드
}</code></pre>
<ul>
<li><p>메서드 선언부만 존재하고 <strong>구현부(body)</strong>는 없습니다.</p>
</li>
<li><p>상속받는 자식 클래스는 반드시 이 메서드를 오버라이딩해서 구현해야 합니다.</p>
</li>
<li><p>오버라이딩하지 않으면, 자식 클래스도 abstract로 선언해야 합니다.</p>
</li>
</ul>
<hr>
<h3 id="예제-유닛-이동-기능-구현">예제: 유닛 이동 기능 구현</h3>
<p>추상 클래스를 사용해서 여러 종류의 유닛에게 공통적인 <code>move()</code> 동작을 강제하는 예제입니다.</p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        Unit[] group = { new Marine(), new Tank(), new Dropship() };

        for (int i = 0; i &lt; group.length; i++)
            group[i].move(100, 200); // 모두 move 동작이 다르게 정의됨!
    }
}</code></pre>
<p>추상 클래스와 자식 클래스들</p>
<pre><code class="language-java">abstract class Unit {
    int x, y;
    abstract void move(int x, int y); // 자식 클래스에서 구현해야 함
    void stop() {
        System.out.println(&quot;Unit stopped&quot;);
    }
}

class Marine extends Unit {
    void move(int x, int y) {
        System.out.println(&quot;Marine[x=&quot; + x + &quot;,y=&quot; + y + &quot;]&quot;);
    }
    void stimPack() {}
}

class Tank extends Unit {
    void move(int x, int y) {
        System.out.println(&quot;Tank[x=&quot; + x + &quot;,y=&quot; + y + &quot;]&quot;);
    }
    void changeMode() {}
}

class Dropship extends Unit {
    void move(int x, int y) {
        System.out.println(&quot;Dropship[x=&quot; + x + &quot;,y=&quot; + y + &quot;]&quot;);
    }
    void load()   {}
    void unload() {}
}</code></pre>
<hr>
<h3 id="실무에서의-사용-포인트">실무에서의 사용 포인트</h3>
<ul>
<li><p>공통된 행동은 <strong>추상 클래스에 미리 구현</strong>해두고, 각 클래스별로 달라야 하는 행동은 <strong>추상 메서드로 선언</strong>해두면 좋습니다.</p>
</li>
<li><p>프레임워크나 템플릿 메서드 패턴에서 많이 사용됩니다.</p>
</li>
</ul>
<hr>
<h3 id="정리-추상-클래스는-언제-사용하면-좋을까">정리: 추상 클래스는 언제 사용하면 좋을까?</h3>
<table>
<thead>
<tr>
<th>사용 목적</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>여러 클래스가 공통적으로 사용하는 필드/메서드 정의</td>
<td>모든 동물이 <code>eat()</code>, <code>sleep()</code>을 가지고 있음</td>
</tr>
<tr>
<td>공통 로직은 부모에, 구체 로직은 자식에게 맡기고 싶을 때</td>
<td><code>move()</code>는 공통이지만, 각 유닛의 움직임 방식은 다름</td>
</tr>
<tr>
<td>상속을 강제하고 싶은 경우</td>
<td>모든 유닛은 반드시 <code>move()</code>를 구현해야 함</td>
</tr>
</tbody></table>
<hr>
<h2 id="인터페이스란">인터페이스란?</h2>
<blockquote>
<p>자바에서 <strong>인터페이스(interface)</strong>는 클래스보다 더 높은 수준의 추상화를 제공하는 일종의 &quot;기본 설계도&quot;입니다.</p>
</blockquote>
<p>자바에서 <code>interface</code> 키워드로 선언되는 일종의 추상 클래스입니다.
그 자체로는 완성된 기능이 아니며, <strong>다른 클래스가 해당 인터페이스를 구현</strong>함으로써 완성됩니다.
일반적으로 <strong>표준화된 명세, 다형성을 통한 유연한 코드 설계, 유지보수성 향상</strong> 등을 목적으로 사용됩니다.</p>
<pre><code class="language-java">interface 인터페이스이름 {}

class 클래스이름 implements 인터페이스이름 {
    // 추상 메서드 구현 필수
}</code></pre>
<hr>
<h3 id="인터페이스의-제약-사항">인터페이스의 제약 사항</h3>
<p>자바 인터페이스는 다음과 같은 특징과 제한을 가집니다:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>구성 요소</td>
<td>추상 메서드, 상수만 가능 (JDK 1.8부터는 default/static 메서드도 허용)</td>
</tr>
<tr>
<td>메서드</td>
<td>모든 메서드는 <code>public abstract</code> (생략 가능)</td>
</tr>
<tr>
<td>변수</td>
<td>모든 변수는 <code>public static final</code> (생략 가능)</td>
</tr>
<tr>
<td>인스턴스 생성</td>
<td>불가능</td>
</tr>
<tr>
<td>제어자</td>
<td>대부분 생략 가능하며 컴파일러가 자동으로 추가</td>
</tr>
</tbody></table>
<hr>
<h3 id="인터페이스의-상속">인터페이스의 상속</h3>
<p>인터페이스는 오직 <strong>다른 인터페이스만 상속 가능</strong>하며, <strong>다중 상속</strong>이 허용됩니다.
Object 클래스와 같은 최고 조상이 없습니다.</p>
<pre><code class="language-java">interface Movable {
    void move(int x, int y);
}

interface Attackable {
    void attack(Unit u);
}

interface Fightable extends Movable, Attackable {}</code></pre>
<hr>
<h3 id="인터페이스의-구현">인터페이스의 구현</h3>
<p>클래스가 인터페이스를 구현하려면 <code>implements</code> 키워드를 사용합니다. 
클래스는 확장한다는 의미의 키워드 <code>extends</code> 를 사용하지만 인터페이스는 구현한다는 의미의 키워드 <code>implements</code> 를 사용합니다.
모든 추상 메서드를 반드시 구현해야 합니다.</p>
<pre><code class="language-java">class Fighter implements Fightable {
    public void move(int x, int y) { /* 구현 */ }
    public void attack(Unit u) { /* 구현 */ }
}</code></pre>
<p>일부만 구현할 경우 추상 클래스로 선언해야 합니다.</p>
<pre><code class="language-java">abstract class Fighter implements Fightable {
    public void move(int x, int y) { /* 구현 */ }
}</code></pre>
<p>클래스 상속(extends)과 인터페이스 구현(implements)은 동시에 가능합니다.</p>
<pre><code class="language-java">class Fighter extends Unit implements Fightable {
    public void move(int x, int y) { /* 구현 */ }
    public void attack(Unit u) { /* 구현 */ }
}
</code></pre>
<hr>
<h3 id="인터페이스를-활용한-다형성">인터페이스를 활용한 다형성</h3>
<p>인터페이스 타입의 참조 변수로 구현체를 참조할 수 있어 유연한 코드 구현이 가능합니다.</p>
<pre><code class="language-java">Fightable f = new Fighter(); // 다형성</code></pre>
<p>인터페이스는 <strong>매개변수 타입</strong>으로도 사용 가능합니다. 
메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 합니다.</p>
<pre><code class="language-java">class Fighter extends Unit implements Fightable {
    public void move(int x, int y) { }
    public void attack(Fightable f) { }
}

Fighter f = new Fighter();
f.attack(new Fighter());</code></pre>
<p><strong>리턴 타입으로 사용</strong>하면 다양한 구현체를 리턴할 수 있습니다.
리턴 타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미합니다.</p>
<pre><code class="language-java">Fightable createFighter() {
    return new Fighter();
}</code></pre>
<hr>
<h2 id="jdk-18-이후-인터페이스의-변화">JDK 1.8 이후 인터페이스의 변화</h2>
<blockquote>
<p>인터페이스에는 추상 메서드만 선언할 수 있는데, JDK1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었습니다.</p>
</blockquote>
<h3 id="static-메서드">static 메서드</h3>
<p>static 메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 별 문제없이 인터페이스에 추가할 수 있습니다.</p>
<pre><code class="language-java">interface MyInterface {
    static void staticMethod() {
        System.out.println(&quot;static method&quot;);
    }
}</code></pre>
<h3 id="default-메서드">default 메서드</h3>
<p>조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 합니다.</p>
<p>디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 됩니다.</p>
<p>디폴트 메서드는 앞에 키워드 <code>default</code> 를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통 <code>{}</code>이 있어야 합니다. 디폴트 메서드 역시 접근 제어자가 <code>public</code> 이며, 생략 가능합니다.</p>
<pre><code class="language-java">interface MyInterface {
    default void method1() {
        System.out.println(&quot;method1() in Myinterface&quot;);
    }

    default void method2() {
        System.out.println(&quot;method2() in Myinterface&quot;);
    }
}</code></pre>
<p>인터페이스에 디폴트 메서드를 추가하면 조상 클래스에 새로운 메서드를 추가한 것과 동일해 지는 것입니다.</p>
<h3 id="디폴트-메서드-충돌-규칙">디폴트 메서드 충돌 규칙</h3>
<ul>
<li><p><strong>여러 인터페이스의 디폴트 메서드 간의 충돌</strong> : 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.</p>
</li>
<li><p><strong>디폴트 메서드와 조상 클래스의 메서드 간의 충돌</strong> : 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.</p>
</li>
</ul>
<pre><code class="language-java">class Main {
    public static void main(String[] args) {
        Child3 c = new Child3();
        c.method1(); // method1() in Child3
        c.method2(); // method2() in Parent3
        MyInterface.staticMethod(); // staticMethod() in Myinterface
        MyInterface2.staticMethod(); // staticMethod() in Myinterface2
    }
}

class Child3 extends Parent3 implements MyInterface, MyInterface2 {
    public void method1() {
        System.out.println(&quot;method1() in Child3&quot;);
    }
}

class Parent3 {
    public void method2() {
        System.out.println(&quot;method2() in Parent3&quot;);
    }
}

interface MyInterface {
    default void method1() {
        System.out.println(&quot;method1() in Myinterface&quot;);
    }

    default void method2() {
        System.out.println(&quot;method2() in Myinterface&quot;);
    }

    static void staticMethod() {
        System.out.println(&quot;staticMethod() in Myinterface&quot;);
    }
}

interface MyInterface2 {
    default void method1() {
        System.out.println(&quot;method1() in Myinterface2&quot;);
    }

    static void staticMethod() {
        System.out.println(&quot;staticMethod() in Myinterface2&quot;);
    }
}
</code></pre>
<hr>
<h2 id="인터페이스-vs-추상-클래스-비교">인터페이스 vs 추상 클래스 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>인터페이스 (<code>interface</code>)</th>
<th>추상 클래스 (<code>abstract class</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td><strong>기능 명세</strong> (무엇을 할 수 있는가) 정의</td>
<td><strong>공통 로직</strong>을 상속을 통해 재사용하기 위해 사용</td>
</tr>
<tr>
<td>인스턴스 생성</td>
<td>불가능</td>
<td>불가능</td>
</tr>
<tr>
<td>키워드</td>
<td><code>interface</code></td>
<td><code>abstract class</code></td>
</tr>
<tr>
<td>상속/구현</td>
<td><code>implements</code> 키워드로 구현</td>
<td><code>extends</code> 키워드로 상속</td>
</tr>
<tr>
<td>메서드 구성</td>
<td><strong>모든 메서드는 기본적으로 <code>abstract</code></strong> (JDK 1.8 이후 <code>default</code>, <code>static</code> 허용)</td>
<td><strong>추상 메서드 + 일반 메서드 모두 가능</strong></td>
</tr>
<tr>
<td>변수 구성</td>
<td><strong>상수만 가능</strong> (<code>public static final</code>, 생략 가능)</td>
<td><strong>멤버 변수 선언 가능</strong>, 접근제어자 사용 가능</td>
</tr>
<tr>
<td>다중 상속</td>
<td><strong>다중 구현 가능</strong></td>
<td><strong>다중 상속 불가</strong></td>
</tr>
<tr>
<td>사용 용도</td>
<td>서로 관련 없는 클래스들 간의 <strong>공통된 동작 정의</strong></td>
<td><strong>비슷한 객체들의 공통 기능을 묶을 때</strong> 사용</td>
</tr>
<tr>
<td>결합도</td>
<td>느슨한 결합 (low coupling)</td>
<td>클래스 간 강한 결합 가능</td>
</tr>
<tr>
<td>예시</td>
<td><code>Comparable</code>, <code>Runnable</code></td>
<td><code>HttpServlet</code>, <code>AbstractList</code> 등</td>
</tr>
</tbody></table>
<blockquote>
<p>✅ 정리:
인터페이스는 “무엇을 할 수 있는가”에 중점
추상 클래스는 “어떻게 동작할 것인가”에 공통 구현 포함</p>
</blockquote>
<hr>
<h2 id="마무리-요약-추상-클래스">마무리 요약: 추상 클래스</h2>
<ul>
<li><p>추상 클래스는 직접 인스턴스를 만들 수 없고, <strong>자식 클래스에서 상속하여 구현해야 함.</strong></p>
</li>
<li><p>추상 메서드는 <strong>선언만 하고 구현하지 않으며</strong>, 자식 클래스에서 반드시 구현해야 함.</p>
</li>
<li><p>자바에서는 <strong>공통 로직을 효율적으로 관리하고 다형성을 유도</strong>할 때 추상 클래스가 자주 쓰임.</p>
</li>
</ul>
<h2 id="마무리-요약-인터페이스">마무리 요약: 인터페이스</h2>
<ul>
<li><p>인터페이스는 인스턴스를 만들 수 없고, 클래스에서 <code>implements</code> 키워드로 구현해야 함.</p>
</li>
<li><p>인터페이스의 모든 메서드는 <strong>선언만 존재</strong>하며, 구현 클래스에서 반드시 구현해야 함.
(단, JDK 1.8 이후부터 default, static 메서드는 구현 가능)</p>
</li>
<li><p>자바에서는 <strong>기능 명세를 정의하고, 다형성과 느슨한 결합을 유도</strong>할 때 인터페이스가 자주 사용됨.</p>
</li>
<li><p>여러 인터페이스를 동시에 구현 가능하여, <strong>다중 상속의 효과</strong>를 낼 수 있음.</p>
</li>
</ul>
<blockquote>
<p>💡 인터페이스는 “무엇을 할 수 있는가”를 명확히 정의하고, 그 구현은 클래스에 맡기는 유연한 구조를 만든다.</p>
</blockquote>
<hr>
<h2 id="마무리하며">마무리하며</h2>
<p>자바에서 추상 클래스와 인터페이스는 &quot;설계&quot;라는 측면에서 매우 중요한 개념입니다.</p>
<p>추상 클래스는 공통된 기능을 묶고 재사용하기 위한 기반이 되고,</p>
<p>인터페이스는 클래스 간의 규약을 정의하고 느슨한 결합을 유도합니다.</p>
<p>현업에서는 이 두 개념을 혼용하거나 상황에 따라 적절히 선택해서 사용합니다.
예를 들어, 공통 로직이 있다면 추상 클래스를,
다양한 구현체가 동일한 기능을 제공해야 한다면 인터페이스를 선택하는 식이죠.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바의 패키지(package): 구조, 선언, classpath]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%9E%90%EB%B0%94%EC%9D%98-%ED%8C%A8%ED%82%A4%EC%A7%80package-%EA%B5%AC%EC%A1%B0-%EC%84%A0%EC%96%B8-classpath</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%9E%90%EB%B0%94%EC%9D%98-%ED%8C%A8%ED%82%A4%EC%A7%80package-%EA%B5%AC%EC%A1%B0-%EC%84%A0%EC%96%B8-classpath</guid>
            <pubDate>Tue, 03 Jun 2025 14:23:16 GMT</pubDate>
            <description><![CDATA[<p>자바를 학습하다 보면 <code>package</code> 라는 키워드를 자주 만나게 됩니다. 처음에는 단순한 폴더 개념처럼 보이지만, 실제로는 클래스의 관리, 충돌 방지, 배포, 보안 등 다양한 역할을 수행하는 중요한 개념입니다.</p>
<p>이번 글에서는 자바의 패키지에 대해 자세히 알아보고, 실제 코드 예제와 함께 classpath 설정, static import 문까지 깔끔하게 정리해보겠습니다.</p>
<hr>
<h2 id="✅-패키지란">✅ 패키지란?</h2>
<p><strong>패키지(Package)</strong>는 쉽게 말해 클래스들의 논리적 묶음입니다. 물리적으로는 디렉터리 구조이며, 논리적으로는 클래스 네임스페이스 역할을 합니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>정의</td>
<td>관련된 클래스나 인터페이스를 묶는 단위</td>
</tr>
<tr>
<td>물리적 구조</td>
<td>하나의 디렉터리</td>
</tr>
<tr>
<td>클래스 전체 이름</td>
<td><code>패키지명.클래스명</code> (ex. <code>java.lang.String</code>)</td>
</tr>
</tbody></table>
<blockquote>
<p>예시: java.lang.String 클래스는 실제로 java 디렉터리 안의 lang 디렉터리에 위치한 String.class 파일입니다.</p>
</blockquote>
<h3 id="왜-필요한가요">왜 필요한가요?</h3>
<ul>
<li><p><strong>클래스 충돌 방지</strong>: 같은 이름의 클래스라도 서로 다른 패키지에 있으면 사용할 수 있습니다.</p>
</li>
<li><p><strong>관리 용이성</strong>: 관련 있는 클래스를 모아 관리하기 쉽습니다.</p>
</li>
<li><p><strong>접근 제어</strong>: 패키지 접근 제어자를 통해 클래스의 접근 범위를 제한할 수 있습니다.</p>
</li>
</ul>
<hr>
<h2 id="📄-패키지-선언-방법">📄 패키지 선언 방법</h2>
<p>패키지를 선언할 때는 .java 소스 파일의 맨 위에 <code>package</code> 문을 작성해야 합니다.</p>
<pre><code class="language-java">
package com.codechobo.book;

public class PackageTest {
    public static void main(String[] args) {
        System.out.println(&quot;Hello World!&quot;);
    }
}</code></pre>
<h3 id="선언-규칙">선언 규칙</h3>
<ul>
<li><p>파일 최상단(주석/공백 제외)에 선언해야 함</p>
</li>
<li><p>한 파일에 한 번만 선언 가능</p>
</li>
<li><p>일반적으로 소문자로 구성 (클래스명과 구분 용이)</p>
</li>
</ul>
<hr>
<h2 id="❓-이름-없는-패키지란">❓ 이름 없는 패키지란?</h2>
<p>패키지를 선언하지 않은 클래스는 자동으로 <strong>이름 없는 패키지(default package)</strong>에 포함됩니다.</p>
<pre><code class="language-java">public class Hello {
    public static void main(String[] args) {
        System.out.println(&quot;Hello from default package!&quot;);
    }
}</code></pre>
<blockquote>
<p>⚠ 주의: 이름 없는 패키지는 대규모 프로젝트나 외부 라이브러리와 연동 시 충돌 가능성이 높기 때문에, 실무에서는 거의 사용하지 않습니다.</p>
</blockquote>
<hr>
<h2 id="🛣-classpath란">🛣 classpath란?</h2>
<p>classpath는 자바가 클래스를 찾는 경로입니다. 컴파일하거나 실행할 때 JVM이 .class 또는 .jar 파일을 어디서 찾을지를 지정합니다.</p>
<h3 id="예시-classpath-설정-linuxmacos-기준">예시: classpath 설정 (Linux/macOS 기준)</h3>
<pre><code class="language-bash">export CLASSPATH=.:lib/mylib.jar</code></pre>
<table>
<thead>
<tr>
<th>설정 값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>.</code></td>
<td>현재 디렉터리 포함</td>
</tr>
<tr>
<td><code>lib/mylib.jar</code></td>
<td>외부 라이브러리 경로 지정</td>
</tr>
</tbody></table>
<h3 id="클래스-로딩-순서">클래스 로딩 순서</h3>
<table>
<thead>
<tr>
<th>단계</th>
<th>클래스 로더</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>부트스트랩</td>
<td>JDK 기본 클래스 로딩 (<code>rt.jar</code>, 모듈 시스템 대체)</td>
</tr>
<tr>
<td>2</td>
<td>확장 클래스 로더</td>
<td><code>lib/ext</code> 디렉터리 등</td>
</tr>
<tr>
<td>3</td>
<td>애플리케이션 클래스 로더</td>
<td><strong>사용자 classpath 기반 로딩</strong> ✅</td>
</tr>
</tbody></table>
<h3 id="자주-발생하는-classpath-오류">자주 발생하는 classpath 오류</h3>
<table>
<thead>
<tr>
<th>오류명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ClassNotFoundException</code></td>
<td>클래스 경로에 <code>.class</code> 파일이 없음</td>
</tr>
<tr>
<td><code>NoClassDefFoundError</code></td>
<td>컴파일 시 존재했지만 실행 시 로딩 실패</td>
</tr>
</tbody></table>
<blockquote>
<p>🛠 실무 팁: Maven, Gradle 같은 빌드 도구를 사용하면 classpath 문제를 자동으로 해결할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="🧙♂️-static-import-문">🧙‍♂️ static import 문</h2>
<p><code>import static</code> 구문을 사용하면 <strong>static 멤버(메서드, 변수)</strong>를 클래스명 없이 사용할 수 있습니다.</p>
<pre><code class="language-java">import static java.lang.System.out;
import static java.lang.Math.*;

class Main {
    public static void main(String[] args) {
        out.println(random());        // Math.random()
        out.println(&quot;PI: &quot; + PI);     // Math.PI
    }
}</code></pre>
<table>
<thead>
<tr>
<th>일반 방식</th>
<th>static import 사용 시</th>
</tr>
</thead>
<tbody><tr>
<td><code>System.out.println(...)</code></td>
<td><code>out.println(...)</code></td>
</tr>
<tr>
<td><code>Math.random()</code></td>
<td><code>random()</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="🧾-마무리-요약">🧾 마무리 요약</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>핵심 요약</th>
</tr>
</thead>
<tbody><tr>
<td>패키지란?</td>
<td>클래스의 논리적/물리적 묶음</td>
</tr>
<tr>
<td>선언 방법</td>
<td><code>.java</code> 파일 최상단에 <code>package</code> 작성</td>
</tr>
<tr>
<td>이름 없는 패키지</td>
<td>패키지 미지정 시 포함, 실무에서는 비추천</td>
</tr>
<tr>
<td>classpath</td>
<td>자바 클래스의 탐색 경로 설정</td>
</tr>
<tr>
<td>static import</td>
<td>static 멤버를 클래스명 없이 사용 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="💡-실무-팁-정리">💡 실무 팁 정리</h2>
<ul>
<li><p>모든 클래스를 <strong>명확한 패키지</strong>로 분리하세요. (com.회사명.프로젝트.모듈)</p>
</li>
<li><p>classpath 관련 문제는 <strong>빌드 도구</strong>(Maven/Gradle)로 해결하세요.</p>
</li>
<li><p>static import는 <strong>유틸성 상수</strong>나 <strong>메서드</strong>에 한해 사용하면 유용합니다.</p>
</li>
</ul>
<hr>
<p>자바의 패키지는 단순한 디렉터리 구조 이상의 개념입니다. 구조를 잘 잡아두면 협업, 유지보수, 배포 모든 측면에서 강력한 장점이 생깁니다. 익숙해질수록 자바의 세계가 더 잘 보이게 될 거예요! 💪</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[참조 변수의 형변환]]></title>
            <link>https://velog.io/@dev_gyeongmin/%EC%B0%B8%EC%A1%B0-%EB%B3%80%EC%88%98%EC%9D%98-%ED%98%95%EB%B3%80%ED%99%98-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_gyeongmin/%EC%B0%B8%EC%A1%B0-%EB%B3%80%EC%88%98%EC%9D%98-%ED%98%95%EB%B3%80%ED%99%98-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 03 Jun 2025 13:38:18 GMT</pubDate>
            <description><![CDATA[<p>Java를 공부하다 보면 반드시 마주치는 개념 중 하나가 <strong>&quot;형변환(casting)&quot;</strong>입니다. 기본형(primitive type)에서도 형변환은 자주 사용되지만, 이번 글에서는 참조형(reference type)의 형변환에 초점을 맞춰 정리해 보겠습니다.</p>
<hr>
<h2 id="🔁-참조형의-형변환이란">🔁 참조형의 형변환이란?</h2>
<p>기본형처럼 참조형도 형변환이 가능합니다. 하지만 참조형의 형변환은 오직 상속 관계가 있는 클래스 사이에서만 허용됩니다. 즉,</p>
<p>자손 타입 → 조상 타입: ✅ 가능 (자동 형변환, upcasting)</p>
<p>조상 타입 → 자손 타입: ✅ 가능 (명시적 형변환, downcasting)</p>
<p>상속 관계가 없는 타입 간의 형변환: ❌ 불가능</p>
<hr>
<h2 id="🔄-upcasting--downcasting-예제">🔄 Upcasting &amp; Downcasting 예제</h2>
<p>다음 코드를 보면서 이해해 봅시다:</p>
<pre><code class="language-java">class Main {
    public static void main(String args[]) {
        Car car = null;
        FireEngine fe = new FireEngine();  // 자손 클래스 객체 생성
        FireEngine fe2 = null;

        fe.water();      // 자손 클래스의 메서드 호출
        car = fe;        // 🔼 Upcasting (자동 형변환)

        fe2 = (FireEngine) car; // 🔽 Downcasting (명시적 형변환)
        fe2.water();     // 다시 자손 메서드 호출 가능
    }
}

class Car {
    String color;
    int door;

    void drive() {
        System.out.println(&quot;drive, Brrrr~&quot;);
    }

    void stop() {
        System.out.println(&quot;stop!!!&quot;);
    }
}

class FireEngine extends Car {
    void water() {
        System.out.println(&quot;water!!!&quot;);
    }
}</code></pre>
<h3 id="🔍-실행-순서-요약">🔍 실행 순서 요약</h3>
<ol>
<li><p>fe는 FireEngine 인스턴스를 가리킴.</p>
</li>
<li><p>car = fe; → FireEngine 인스턴스를 Car 타입 참조 변수로 참조.</p>
</li>
<li><p>car는 FireEngine의 인스턴스를 참조하고 있지만 Car 타입이므로 water() 메서드는 호출할 수 없음.</p>
</li>
<li><p>다시 형변환하여 fe2 = (FireEngine) car; → water() 호출 가능.</p>
</li>
</ol>
<hr>
<h2 id="🎯-중요한-특징들">🎯 중요한 특징들</h2>
<h3 id="✅-형변환-시-인스턴스-자체는-변하지-않는다">✅ 형변환 시 인스턴스 자체는 변하지 않는다</h3>
<p>참조형의 형변환은 참조 변수의 타입만 바꾸는 것입니다. 실제 인스턴스(객체)는 변하지 않고 그대로 존재합니다.</p>
<h3 id="✅-모든-클래스는-object로-형변환-가능">✅ 모든 클래스는 Object로 형변환 가능</h3>
<p>Java에서 모든 클래스는 Object를 상속받기 때문에, 모든 참조 변수는 Object 타입으로 형변환이 가능합니다.</p>
<pre><code class="language-java">FireEngine fe = new FireEngine();
Object obj = fe; // 가능</code></pre>
<h3 id="✅-형변환-생략-가능-여부">✅ 형변환 생략 가능 여부</h3>
<table>
<thead>
<tr>
<th>형변환 방향</th>
<th>생략 가능 여부</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>자손 → 조상 (Upcasting)</td>
<td>✅ 생략 가능</td>
<td><code>Car car = new FireEngine();</code></td>
</tr>
<tr>
<td>조상 → 자손 (Downcasting)</td>
<td>❌ 생략 불가</td>
<td><code>FireEngine fe = (FireEngine) car;</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="🧐-instanceof-연산자">🧐 instanceof 연산자</h2>
<p><code>instanceof</code> 는 형변환 전에 객체의 실제 타입을 확인할 수 있게 해줍니다. Downcasting 하기 전에는 반드시 instanceof로 확인하는 습관을 들이는 것이 좋습니다.</p>
<pre><code class="language-java">if (car instanceof FireEngine) {
    FireEngine fe = (FireEngine) car;
    fe.water();
}</code></pre>
<ul>
<li><code>instanceof</code> 는 실제 인스턴스가 해당 타입이거나 그 자손인지를 확인합니다.</li>
</ul>
<hr>
<h2 id="📌-마무리-요약">📌 마무리 요약</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>형변환 대상</td>
<td>상속 관계가 있는 클래스 간에만 가능</td>
</tr>
<tr>
<td>Upcasting</td>
<td>자손 → 조상, 생략 가능</td>
</tr>
<tr>
<td>Downcasting</td>
<td>조상 → 자손, 생략 불가</td>
</tr>
<tr>
<td>실제 객체는 변하는가?</td>
<td>❌ 변하지 않음</td>
</tr>
<tr>
<td>형변환 전 타입 확인 방법</td>
<td><code>instanceof</code> 사용</td>
</tr>
<tr>
<td>모든 참조형의 조상</td>
<td><code>Object</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="🚀-마무리하며">🚀 마무리하며</h2>
<p>참조형의 형변환은 다형성(polymorphism) 개념과 함께 Java 객체지향 프로그래밍의 중요한 기초입니다. 복잡한 상속 구조에서 객체를 다룰 때 형변환과 instanceof의 활용은 코드의 안정성과 유연성을 크게 높여줍니다.</p>
<p>👉 꼭 기억하세요:</p>
<blockquote>
<p>참조 변수의 타입과 인스턴스의 타입은 일치하지 않을 수 있다.</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>