<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>cv_.log</title>
        <link>https://velog.io/</link>
        <description>ㅇ0ㅇ</description>
        <lastBuildDate>Fri, 17 Apr 2026 00:02:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. cv_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/cv_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[마크20]]></title>
            <link>https://velog.io/@cv_/%EB%A7%88%ED%81%AC20</link>
            <guid>https://velog.io/@cv_/%EB%A7%88%ED%81%AC20</guid>
            <pubDate>Fri, 17 Apr 2026 00:02:04 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Redis만으로 서버 대기열을 구현하는 여러 방법들]]></title>
            <link>https://velog.io/@cv_/Redis%EB%A7%8C%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B2%84-%EB%8C%80%EA%B8%B0%EC%97%B4%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EC%97%AC%EB%9F%AC-%EB%B0%A9%EB%B2%95%EB%93%A4</link>
            <guid>https://velog.io/@cv_/Redis%EB%A7%8C%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B2%84-%EB%8C%80%EA%B8%B0%EC%97%B4%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EC%97%AC%EB%9F%AC-%EB%B0%A9%EB%B2%95%EB%93%A4</guid>
            <pubDate>Sat, 21 Sep 2024 06:13:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>티켓팅, 한정판 및 선착순 구매 등 갯수에 대한 데이터 정합성이 중요할 때가 있다.
하지만 많은 유저가 몰리는 순간 모든 요청을 받으면 DB, 서버가 버티지 못할 위험이 있다.
많은 개발자들이 MQ를 생각하지만 작업 시간과 카프카와 같은 인프라가 없을 수도 있다.
또한 MQ를 도입한다는 것이 실무에서는 꽤나 부담스러운 경우가 많다.
때문에 Spring 기반의 서버와 레디스(Lettuce)만으로 대기열을 구현하는 4가지 정도의 방법이 있다.</p>
</blockquote>
<ol>
<li>스케줄러<ul>
<li>서버에서 주기적으로 대기열에서 활성열로 옮겨준다.</li>
</ul>
</li>
<li>Pub/Sub<ul>
<li>서버에서 이벤트를 직접 발행한다.</li>
<li>별도의 이벤트 리스너가 대기열에서 활성열로 옮겨준다.</li>
</ul>
</li>
<li>Keyspace Notification<ul>
<li>레디스에서 활성열 데이터가 삭제되면 이벤트가 발행된다.</li>
<li>별도의 이벤트 리스너가 대기열에서 활성열로 옮겨준다.</li>
</ul>
</li>
<li>Stream<ul>
<li>아래의 대기열 진입 구조를 따르지 않으며 MQ방식과 비슷하다.</li>
</ul>
</li>
</ol>
<h1 id="대기열-진입-구조">대기열 진입 구조</h1>
<p>대기열을 위해 <code>Sorted Set</code>을 사용한다.
Score에 진입 시각을 넣으면 자연스럽게 FIFO 구조가 된다.
<code>ZRANK</code> 명령어로 순번을 아주 빠르게 조회할 수 있다.</p>
<p>이후 활성열 관리 방법에 따라 위 1, 2, 3번의 방식으로 나뉘어진다.</p>
<h2 id="dto">DTO</h2>
<pre><code class="language-kotlin">sealed class QueueResult {
    data class Waiting(
        val uid: String,
        val rank: Long,
        val token: String
    ) : QueueResult()

    data class Active(
        val uid: String,
        val token: String
    ) : QueueResult()

    object NotFound : QueueResult()
}</code></pre>
<h2 id="대기열-진입-로직">대기열 진입 로직</h2>
<blockquote>
<p>상품별로 대기열을 분리한다. 
그래야 인기 없는 상품의 토큰으로 인기 상품 결제를 시도하는 편법을 막을 수 있다.
이 예시에서는 유저별로 JWT를 발급하고 해당 토큰으로 유저가 유저의 상태를 조회할 수 있다.</p>
</blockquote>
<pre><code class="language-kotlin">// 대기열 Sorted Set Key/Member
private const val WAIT_KEY = &quot;wait:id:%d&quot;
private const val WAIT_MEMBER = &quot;user:%s:token:%s&quot;

// 활성열 Sorted Set Key
private const val ACTIVE_KEY = &quot;active:id:%d&quot;

// 대기열이 존재하는 상품 ID 목록
private const val ACTIVE_PRODUCTS_KEY = &quot;queue:products&quot; 

// 최대 활성열 갯수
private const val ACTIVE_MAX = 10

// 활성 상태 유효 시간 (10분)
private const val ACCESS_TTL_MILLIS = 10 * 60 * 1000L

// redis -&gt; org.springframework.data.redis.core.StringRedisTemplate
// jwt -&gt; JWT 커스텀 컴포넌트

fun createWaitToken(pid: Long, uid: String): QueueResult.Waiting {
    val token = jwt.create(&quot;wait&quot;, mapOf(&quot;pid&quot; to pid, &quot;uid&quot; to uid))
    val now = System.currentTimeMillis().toDouble()
    val key = WAIT_KEY.format(pid)
    val member = WAIT_MEMBER.format(uid, token)

    redis.opsForSet().add(ACTIVE_PRODUCTS_KEY, pid.toString())
    redis.opsForZSet().add(key, member, now)

    val rank = redis.opsForZSet().rank(key, member)?.plus(1) ?: 1L
    return QueueResult.Waiting(uid, rank, token)
}</code></pre>
<p>저장 구조:</p>
<ul>
<li>키: <code>wait:id:{pid}</code></li>
<li>멤버: <code>user:{uid}:token:{token}</code></li>
<li>스코어: 진입 시각 (밀리초 타임스탬프)</li>
</ul>
<h3 id="순번-조회">순번 조회</h3>
<pre><code class="language-kotlin">fun getWaitQueueRank(pid: Long, uid: String, token: String): Long? {
    val key = WAIT_KEY.format(pid)
    val member = WAIT_MEMBER.format(uid, token)
    return redis.opsForZSet().rank(key, member)?.plus(1) // 0-based → 1-based
}</code></pre>
<hr>
<h1 id="스케줄러">스케줄러</h1>
<blockquote>
<p>가장 간단하고 빠르게 구현할 수 있다.
일시적으로 빠르게 대기열이 필요하다면 가장 많이 쓰이는 방법이지 않을까</p>
<p>스케줄러가 주기적으로 활성열을 확인한다.
여유 갯수만큼 대기열에서 꺼내 이동시킨다.
활성열도 Sorted Set으로 관리하며, Score에 만료 타임스탬프를 저장한다.</p>
</blockquote>
<h2 id="활성열-이동">활성열 이동</h2>
<pre><code class="language-kotlin">@Scheduled(fixedDelay = 5000)
fun promote() {
    val setops = redis.opsForSet()
    val zsetops = redis.opsForZSet()
    val ids = setops.members(ACTIVE_PRODUCTS_KEY) ?: return

    ids.forEach { pid -&gt; 
        val waitKey = WAIT_KEY.format(pid)
        val activeKey = ACTIVE_KEY.format(pid)
        val now = System.currentTimeMillis()

        // 활성열의 만료된 멤버 제거
        zsetops.removeRangeByScore(activeKey, 0.0, now.toDouble())

        // 활성열의 여유 갯수 계산
        val size = zsetops.size(activeKey) ?: 0L
        val spares = (ACTIVE_MAX - size).coerceAtLeast(0)
        if (spares == 0L) return@forEach

        // 대기열의 상위 멤버 추출
        val candidates = zsetops.range(waitKey, 0, spares - 1L)
        if (candidates.isNullOrEmpty()) {
            set.remove(ACTIVE_PRODUCTS_KEY, pid.toString())
            return@forEach
        }

        // 활성열로 이동
        val expireAt = (now + ACCESS_TTL_MILLIS).toDouble()
        candidates.forEach { member -&gt;
            zset.add(activeKey, member, expireAt)
            zset.remove(waitKey, member)
    }</code></pre>
<p>매 주기마다 <code>ZREMRANGEBYSCORE</code>로 만료된 멤버를 일괄 제거한다.
이후 빈 슬롯만큼 대기열 상위 멤버를 활성열로 옮긴다.
유저가 다음 과정(ex. 결제)없이 이탈해도 Score(만료시각)가 지나면 다음 주기에 자동으로 정리된다.</p>
<h3 id="lua-스크립트">Lua 스크립트</h3>
<p>위 코드는 읽기(ZCARD)와 쓰기(ZADD)가 분리되어 있다.
<strong>멀티 인스턴스 환경</strong>에서 두 인스턴스가 동시에 실행하면:</p>
<pre><code>Instance A: ZCARD → 8  (2개 여유)
Instance B: ZCARD → 8  (2개 여유)
Instance A: ZADD 멤버1, 멤버2
Instance B: ZADD 멤버3, 멤버4  → ACTIVE_MAX 초과</code></pre><p>분산락으로 막을 수 있지만, 락 획득 실패 시 주기를 통째로 건너뛰게 된다.
락을 가지고 서버가 다운되면 TTL 만료까지 활성열 이동 로직이 멈춘다.
근본적인 해결은 <strong>읽기-판단-쓰기를 하나의 원자적 연산으로 묶는 것</strong>이다.</p>
<p>Redis는 싱글 스레드로 명령을 처리하고, Lua 스크립트는 중단 없이 실행된다.</p>
<pre><code class="language-lua">-- promote.lua
-- KEYS[1] = activeKey
-- KEYS[2] = waitKey

-- ARGV[1] = now
-- ARGV[2] = expireAt
-- ARGV[3] = ACTIVE_MAX

redis.call(&#39;ZREMRANGEBYSCORE&#39;, KEYS[1], 0, ARGV[1])

local size = redis.call(&#39;ZCARD&#39;, KEYS[1])
local spares = math.max(tonumber(ARGV[3]) - size, 0)
if spares == 0 then return 0 end

local candidates = redis.call(&#39;ZRANGE&#39;, KEYS[2], 0, spares - 1)
if #candidates == 0 then return -1 end

local zaddArgs = {}
local zremArgs = {}
for _, member in ipairs(candidates) do
    table.insert(zaddArgs, ARGV[2])
    table.insert(zaddArgs, member)
    table.insert(zremArgs, member)
end

redis.call(&#39;ZADD&#39;, KEYS[1], unpack(zaddArgs))
redis.call(&#39;ZREM&#39;, KEYS[2], unpack(zremArgs))

return #candidates</code></pre>
<pre><code class="language-kotlin">private val script = RedisScript.of(ClassPathResource(&quot;promote.lua&quot;), Long::class.java)

@Scheduled(fixedDelay = 5000)
fun promote() {
    val ids = redis.opsForSet().members(ACTIVE_PRODUCTS_KEY) ?: return
    ids.forEach { pid -&gt;
        val keys = listOf(ACTIVE_KEY.format(pid), WAIT_KEY.format(pid))
        val now = System.currentTimeMillis()
        val expireAt = now + ACCESS_TTL_MILLIS

        val result = redis.execute(script, keys,
            now.toString(), 
            expireAt.toString(), 
            ACTIVE_MAX.toString()
        )

        if (result == -1L)
            redis.opsForSet().remove(ACTIVE_PRODUCTS_KEY, pid)
    }
}</code></pre>
<p>이후 결제 등 다음 단계 완료 시 pid, uid와 token으로 활성열의 데이터를 지운다.</p>
<p>하지만 활성열의 여유가 있어도 다음 주기까지 N초를 기다리게 된다.
많은 사람들이 이 지연을 굉장히 싫어한다.
때문이 여유가 있다면 아래 방법을 생각해볼 수 있다.</p>
<hr>
<h1 id="pubsub--keyspace-notification">Pub/Sub &amp; Keyspace Notification</h1>
<p>두 방식 모두 스케줄러처럼 주기를 기다리지 않는 이벤트 기반이다.
활성열에 여유가 생기면 즉시 트리거된다.
이 둘의 차이는 <strong>누가 이벤트를 발행하는가</strong>, 발행 주체의 차이이다.</p>
<ul>
<li><code>Pub/Sub</code>: 서버가 결제 완료 시점에 직접 <code>PUBLISH</code> 한다.</li>
<li><code>Keyspace Notification</code>: 키 만료, 삭제 시점에 레디스에서 자동으로 발행된다.</li>
</ul>
<p><code>Keyspace Notification</code> 또한 기본적으로 <code>Pub/Sub</code> 으로 동작한다.
때문에 발행 시점에 구독자가 없다면 이벤트는 유실된다.
단독으로 쓰면 위험할 수 있으니 <strong>보완용 스케줄러</strong>도 필요하다.</p>
<h2 id="기본-활성열-이동-로직">기본 활성열 이동 로직</h2>
<p>활성열 하나가 만료될 때마다 1명씩 대기열에서 이동한다.
카운터 확인 → 대기열 추출 → 슬롯 생성 Lua로 원자적 처리한다.</p>
<p>멀티 인스턴스에서 모든 구독자가 메시지를 수신한다. 
하지만 Lua의 원자성으로 하나의 인스턴스만 promote가 성공한다.
나머지 인스턴스들은 활성열에 여유 슬롯이 없어 return 한다. </p>
<pre><code class="language-lua">-- promote-single.lua

-- KEYS[1] = waitKey
-- KEYS[2] = activeCountKey

-- ARGV[1] = slotKeyPrefix
-- ARGV[2] = ACTIVE_MAX
-- ARGV[3] = TTL(초)

local count = tonumber(redis.call(&#39;GET&#39;, KEYS[2]) or &#39;0&#39;)
if count &gt;= tonumber(ARGV[2]) then return 0 end

local top = redis.call(&#39;ZRANGE&#39;, KEYS[1], 0, 0)
if #top == 0 then return -1 end

local member = top[1]
local uid = string.match(member, &#39;user:(.-):token:&#39;)

redis.call(&#39;SETEX&#39;, ARGV[1] .. uid, ARGV[3], member)
redis.call(&#39;ZREM&#39;, KEYS[1], member)
redis.call(&#39;INCR&#39;, KEYS[2])
return 1</code></pre>
<pre><code class="language-kotlin">// ... 기존 상수들
private const val ACTIVE_COUNT_KEY = &quot;active:count:%d&quot;
private const val ACCESS_TTL_SECONDS = ACCESS_TTL_MILLIS / 1000L

private val script = RedisScript.of(ClassPathResource(&quot;promote-single.lua&quot;), Long::class.java)

fun promote(pid: Long) {
    val keys = listOf(WAIT_KEY.format(pid), ACTIVE_COUNT_KEY.format(pid))
    val result = redis.execute(script, keys,
        &quot;active:slot:$pid:&quot;, 
        ACTIVE_MAX.toString(), 
        ACCESS_TTL_SECONDS.toString()
    )

    if (result == -1L)
        redis.opsForSet().remove(ACTIVE_PRODUCTS_KEY, pid.toString())
}</code></pre>
<h2 id="pubsub">Pub/Sub</h2>
<p>활성열의 구조는 스케줄러 방식과 동일하다. (<code>Sorted Set</code>의 <code>Score</code>에 만료시각 저장)
달라지는 것은 활성열 이동 로직의 트리거뿐이다.</p>
<p>스케줄러로 실행시키는 것 대신, 유저가 결제한 순간 해당 멤버를 삭제하고 바로 이벤트를 발행한다.</p>
<h3 id="컴포넌트">컴포넌트</h3>
<pre><code class="language-kotlin">@Component
class SlotAvailableListener(
    private val queueManager: WaitingQueueManager, // 대기열 관리 컴포넌트
) : MessageListener {
    override fun onMessage(message: Message, pattern: ByteArray?) {
        val pid = String(message.body).toLongOrNull() ?: return
        queueManager.promote(pid) // 대기열 -&gt; 활성열 이동
    }
}</code></pre>
<h3 id="설정">설정</h3>
<pre><code class="language-kotlin">@Configuration
class PubSubConfig(
    private val connectionFactory: RedisConnectionFactory,
    private val listener: SlotAvailableListener,
) {
    @Bean
    fun pubSubListenerContainer(): RedisMessageListenerContainer {
        val container = RedisMessageListenerContainer()

        container.setConnectionFactory(connectionFactory)
        container.addMessageListener(listener, PatternTopic(&quot;queue:slot-available:*&quot;))

        return container
    }
}</code></pre>
<h3 id="활성열-이동-1">활성열 이동</h3>
<p>활성열이 스케줄러와 동일한 Sorted Set이므로, 스케줄러에서 사용한 <code>promote.lua</code>를 그대로 사용한다.
차이점은 전체 상품을 순회하지 않고, 이벤트가 발생한 <strong>해당 상품</strong>에 대해서만 실행한다.</p>
<pre><code class="language-kotlin">private val script = RedisScript.of(ClassPathResource(&quot;promote.lua&quot;), Long::class.java)

fun promote(pid: Long) {
    val keys = listOf(ACTIVE_KEY.format(pid), WAIT_KEY.format(pid))
    val now = System.currentTimeMillis()
    val expireAt = now + ACCESS_TTL_MILLIS

    val result = redis.execute(script, keys,
        now.toString(), expireAt.toString(), ACTIVE_MAX.toString()
    )

    if (result == -1L)
        redis.opsForSet().remove(ACTIVE_PRODUCTS_KEY, pid.toString())
}</code></pre>
<p>멀티 인스턴스에서 모든 구독자가 메시지를 수신한다.
하지만 <code>promote.lua</code>가 원자적으로 실행되므로 첫 번째 인스턴스만 promote에 성공하고,
나머지는 여유 슬롯이 없어 0을 반환하며 즉시 종료된다.
(<code>if spares == 0 then return 0 end</code> 부분)</p>
<h3 id="보완용-스케줄러">보완용 스케줄러</h3>
<p>명시적인 결제 완료만 감지할 뿐, 결제 없이 이탈할 경우 활성열에 남는다.
때문에 스케줄러가 필요하다.</p>
<pre><code class="language-kotlin">@Scheduled(fixedDelay = 30_000) // 30초
fun promoteAll() {
    val ids = redis.opsForSet().members(ACTIVE_PRODUCTS_KEY) ?: return
    ids.forEach { pid -&gt; promote(pid.toLong()) }
}</code></pre>
<p><code>promote.lua</code>가 먼저 <code>ZREMRANGEBYSCORE</code>로 만료된 멤버를 정리하므로,
<code>promote(pid)</code>를 호출하는 것만으로 정리와 충원이 동시에 이루어진다.
Pub/Sub이 정상 동작하는 동안에는 여유 슬롯이 없어 Lua에서 즉시 종료되므로 부하가 거의 없다.</p>
<h2 id="keyspace-notification">Keyspace Notification</h2>
<p>활성 슬롯이 <strong>만료되는 순간</strong> 레디스가 자동으로 이벤트를 발행한다.
이를 구독해서 promote를 트리거하는 방식이다.</p>
<p>Pub/Sub과 달리 서버가 명시적으로 이벤트를 발행할 필요가 없다.
유저가 결제를 완료하든, 결제 없이 이탈하든, <strong>슬롯이 만료되면 자동으로</strong> 다음 유저가 진입한다.</p>
<h3 id="활성열-구조-변경">활성열 구조 변경</h3>
<p><code>Sorted Set</code>의 <code>Score</code> 기반 만료는 키 만료 이벤트를 발생시키지 않는다.
<code>Keyspace Notification</code>을 사용하려면 활성열 구조를 <strong>실제 TTL이 있는 개별 키</strong>로 변경해야 한다.</p>
<ul>
<li>스케줄러 &amp; Pub/Sub:<ul>
<li>active:id:{pid}  (Sorted Set, score = 만료시각)</li>
</ul>
</li>
<li>Keyspace Notification:<ul>
<li>active:slot:{pid}:{uid} (개별 키, TTL = 10분)</li>
<li>active:count:{pid} (활성 인원 카운터)</li>
</ul>
</li>
</ul>
<p>Sorted Set에서는 <code>ZCARD</code>로 O(1)에 활성 인원을 조회할 수 있었다.
하지만 개별 키 방식에서는 키를 일일이 셀 수 없으므로 별도 카운터 <code>active:count:{pid}</code>를 사용한다.</p>
<pre><code class="language-kotlin">// 기존 상수에 추가
private const val ACTIVE_COUNT_KEY = &quot;active:count:%d&quot;
private const val ACCESS_TTL_SECONDS = ACCESS_TTL_MILLIS / 1000L</code></pre>
<h3 id="설정-1">설정</h3>
<p><code>Keyspace Notification</code>은 기본적으로 비활성화되어 있다. 
때문에 추가적인 설정이 필요하다.</p>
<pre><code class="language-yaml"># application.yml
spring:
  data:
    redis:
      notify-keyspace-events: Ex   # E = Keyevent, x = expired</code></pre>
<pre><code class="language-kotlin">@Configuration
class KeyspaceNotificationConfig(
    private val connectionFactory: RedisConnectionFactory,
    private val listener: ActiveSlotExpiredListener,
) {
    @Bean
    fun redisMessageListenerContainer(): RedisMessageListenerContainer {
        val container = RedisMessageListenerContainer()

        container.setConnectionFactory(connectionFactory)
        container.addMessageListener(listener, PatternTopic(&quot;__keyevent@0__:expired&quot;))

        return container
    }
}</code></pre>
<h3 id="리스너">리스너</h3>
<pre><code class="language-kotlin">@Component
class ActiveSlotExpiredListener(
    private val redis: StringRedisTemplate,
    private val queueManager: WaitingQueueManager,
) : MessageListener {

    override fun onMessage(message: Message, pattern: ByteArray?) {
        val key = String(message.body) // &quot;active:slot:42:userA&quot;
        if (!key.startsWith(&quot;active:slot:&quot;)) return

        val pid = key.removePrefix(&quot;active:slot:&quot;)
            .substringBefore(&quot;:&quot;).toLongOrNull() ?: return

        redis.opsForValue().decrement(ACTIVE_COUNT_KEY.format(pid))
        queueManager.promote(pid)
    }
}</code></pre>
<p>활성열 슬롯 하나가 만료될 때마다 이벤트가 발생한다.
카운터를 1 감소시키고 대기열에서 1명을 promote한다.</p>
<h3 id="활성열-이동-2">활성열 이동</h3>
<p>카운터 확인 → 대기열 추출 → 슬롯 생성을 Lua로 원자적 처리한다.</p>
<pre><code class="language-lua">-- promote-single.lua
-- KEYS[1] = waitKey
-- KEYS[2] = activeCountKey

-- ARGV[1] = slotKeyPrefix
-- ARGV[2] = ACTIVE_MAX
-- ARGV[3] = TTL(초)

local count = tonumber(redis.call(&#39;GET&#39;, KEYS[2]) or &#39;0&#39;)
if count &gt;= tonumber(ARGV[2]) then return 0 end

local top = redis.call(&#39;ZRANGE&#39;, KEYS[1], 0, 0)
if #top == 0 then return -1 end

local member = top[1]
local uid = string.match(member, &#39;user:(.-):token:&#39;)

redis.call(&#39;SETEX&#39;, ARGV[1] .. uid, ARGV[3], member)
redis.call(&#39;ZREM&#39;, KEYS[1], member)
redis.call(&#39;INCR&#39;, KEYS[2])
return 1</code></pre>
<p>Pub/Sub의 <code>promote.lua</code>와는 구조가 완전히 다르다.
Pub/Sub은 활성열이 <code>Sorted Set</code>이라 <code>ZREMRANGEBYSCORE</code>로 만료 멤버를 정리하고 여러 명을 한 번에 이동시킨다.
여기서는 활성열이 개별 키(TTL)이므로 만료 정리가 필요 없고, 1명씩 이동시킨다.</p>
<pre><code class="language-kotlin">private val script = RedisScript.of(
    ClassPathResource(&quot;promote-single.lua&quot;), Long::class.java
)

fun promote(pid: Long): Long {
    val keys = listOf(WAIT_KEY.format(pid), ACTIVE_COUNT_KEY.format(pid))
    val result = redis.execute(script, keys,
        &quot;active:slot:$pid:&quot;,
        ACTIVE_MAX.toString(),
        ACCESS_TTL_SECONDS.toString()
    ) ?: 0L

    if (result == -1L)
        redis.opsForSet().remove(ACTIVE_PRODUCTS_KEY, pid.toString())

    return result
}</code></pre>
<p>멀티 인스턴스에서 모든 구독자가 만료 이벤트를 수신한다.
하지만 Lua의 원자성 덕분에 하나의 인스턴스만 promote에 성공하고,
나머지는 카운터가 이미 <code>ACTIVE_MAX</code>에 도달해 0을 반환한다.</p>
<h3 id="멀티-인스턴스-환경-주의사항">멀티 인스턴스 환경 주의사항</h3>
<blockquote>
<p>키 하나가 만료되면 <strong>모든 인스턴스가 동일한 이벤트를 수신</strong>한다.
리스너에서 각자 <code>DECR</code>을 실행하면 카운터가 N만큼 감소하지만,
이후 <code>promote-single.lua</code>의 <code>INCR</code>은 Lua 원자성으로 1개 인스턴스만 실행된다.</p>
<p>예시 (3개 인스턴스, 슬롯 1개 만료):</p>
<pre><code>Before: counter = 10, 실제 활성 슬롯 = 10

3개 인스턴스 각각 DECR → counter = 7
인스턴스 A: Lua 성공, INCR → counter = 8, 슬롯 1개 생성
인스턴스 B: Lua 성공, INCR → counter = 9, 슬롯 1개 생성
인스턴스 C: Lua 성공, INCR → counter = 10, 슬롯 1개 생성

After: counter = 10, 실제 활성 슬롯 = 12 → ACTIVE_MAX 초과</code></pre><p>완전한 정합성이 필요하다면 <code>DECR</code>과 promote를 하나의 Lua 스크립트로 합치고,
만료된 키를 식별하는 NX 락으로 중복 처리를 방지해야 한다.
단일 인스턴스 환경에서는 이 문제가 발생하지 않는다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[내가 받아봤던 개발자 면접질문과 기본 지식 정리]]></title>
            <link>https://velog.io/@cv_/%EB%82%B4%EA%B0%80-%EB%B0%9B%EC%95%84%EB%B4%A4%EB%8D%98-%EB%A9%B4%EC%A0%91%EC%A7%88%EB%AC%B8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@cv_/%EB%82%B4%EA%B0%80-%EB%B0%9B%EC%95%84%EB%B4%A4%EB%8D%98-%EB%A9%B4%EC%A0%91%EC%A7%88%EB%AC%B8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 07 Jun 2023 02:20:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>경력이 있어서 그런지 기술 관련 내용을 엄청 안물어봄
그래도 지나가는 말이나 언급되었던 내용과 관련내용을 정리해봄
주로 커뮤니케이션과 퇴사 후 구직단계 사이의 공백을 주로 물어봄</p>
</blockquote>
<h1 id="db">DB</h1>
<h2 id="데이터-무결성">데이터 무결성</h2>
<p>데이터의 정확성, 일관성, 유효성을 유지하는 것
<strong>정확성</strong>은 데이터의 누락, 중복이 없다는 것
<strong>일관성</strong>은 원인과 결과가 같은, 같은 Input이 같은 Output을 보장되어 변하지 않는 것
<strong>유효성</strong>은 의미가 없는 데이터나 유효한 범위에 속한 데이터를 의미</p>
<h3 id="개체-무결성entity-integrity">개체 무결성(Entity integrity)</h3>
<p>기본키(Primary Key) 제약, 각 ROW는 기본키를 가져야 한다.</p>
<p>테이블은 기본키를 지정하고 그에 따른 무결성 원칙을 지켜야 하는 조건
<em>테이블의 ROW가 유일하게 식별되고, 중복 데이터를 방지</em></p>
<p>기본키는 Unique하며 Null이 들어갈 수 없는 조건</p>
<p><em>아래 키 무결성(Key integrity)을 포함하는 개념</em></p>
<h3 id="키-무결성key-integrity">키 무결성(Key integrity)</h3>
<p>레코드에 데이터는 하나의 식별할 수 있는 Key의 존재 조건
기본키를 구성하는 컬럼의 값은 다른 행에서 중복되면 안되는 제약</p>
<h3 id="참조-무결성referential-integrity">참조 무결성(Referential integrity)</h3>
<p>외래키(Foreign Key) 제약, 외래키는 Null이거나 참조하는 테이블의 PK를 데이터로 가지며 관계를 맺는다.</p>
<p><em>외래키로 테이블 사이의 관계가 일관되어야 하며 참조하는 테이블의 무결성을 지켜야 한다.</em>
(참조하는 테이블에 존재하지 않는 PK는 가질 수 없다는 뜻)</p>
<h3 id="도메인-무결성domain-integrity">도메인 무결성(Domain integrity)</h3>
<p>필드의 무결성이라고도 할 수 있다.
데이터가 NULL이 아닐 때, 정해진 포멧을 지켜야 하는 제약
<em>(숫자 컬럼에는 숫자 데이터, 문자 컬럼에는 문자 데이터 ... )</em></p>
<h3 id="null-무결성null-integrity">Null 무결성(Null integrity)</h3>
<p>데이터 컬럼에 Null을 허용 여부의 조건
Null이 허용되지 않는다면, 필수적으로 해당 컬럼에 데이터를 가져야한다.</p>
<h3 id="고유-무결성unique-integrity">고유 무결성(Unique integrity)</h3>
<p>레코드에 존재하는 데이터가 유일해야 하는 조건
모든 ROW는 유일해야한다. (즉, 중복이 있으면 안된다.)</p>
<h3 id="관계-무결성relationship-integrity">관계 무결성(Relationship integrity)</h3>
<p>테이블의 어느 한 레코드의 삽입 가능 여부 또는 한 테이블과 다른 테이블의 레코드들 사이의 관계에 대한 적절성 여부를 지정한 조건</p>
<p>1:1, 1:N 등의 관계에서 주체가 되는 테이블과 그 반대의 테이블로 관계가 성립되어야 하는 제약</p>
<blockquote>
<p>이외에도 동시성 제어 무결성, 보안 무결성 등이 있다.</p>
</blockquote>
<h2 id="정규화normalization">정규화(Normalization)</h2>
<p>테이블은 데이터 무결성을 따르는 구조로 일관성과 정확성을 보장하고, 중복을 최소화해서 성능과 효율성을 고려하여 설계하는 과정</p>
<h3 id="1정규화-1nf">1정규화 (1NF)</h3>
<p>테이블의 각 컬럼은 원자값(Atomic value)을 가져야 한다.
또한 <strong>중복된 데이터를 포함하지 않아야 한다.</strong>
때문에 테이블은 하나 이상의 열로 구성된다.</p>
<p><em>원자값이란 이름 처럼 더 이상 나눠질 수 없는 값</em>
<em>예시로 이름을 더 이상 나눌 수 없지만 주소는 나라, 우편번호 등으로 나눠짐</em></p>
<h3 id="2정규화-2nf">2정규화 (2NF)</h3>
<p>테이블의 모든 컬럼은 기본키에 완전함수종속(Fully functional dependent)되어야 한다. 이를 위해 부분 종속성(Partial dependency)을 제거하고 기본 키를 기준으로 테이블을 분해한다.</p>
<p>쉽게 말해서 
<em>완전함수종속은 키가 바뀌면 해당 키의 존재하는 다른 컬럼의 데이터도 변경</em>
<em>부분종속성은 키가 바뀌어도 다른 컬럼이 바뀌지 않는 데이터</em></p>
<p><strong>2NF는 이 부분종속성을 제거하기 위해 테이블을 분리하는 정규화 과정</strong></p>
<h3 id="3정규화-3nf">3정규화 (3NF)</h3>
<p>테이블의 모든 열이 기본키에 대해 이행적 함수 종속(Transitive functional dependency)을 갖지 않아야 합니다.
이를 위해 <strong>이행 종속성(Transitive dependency)을 제거하고 테이블을 분리</strong>한다.</p>
<p><em>이행적함수종속은 위 완전함수종속이 연속된 형태</em>
<em>예를 들어 A가 B에 종속하고, B가 C에 종속한다면, A는 C에 대해 이행적으로 종속된 것.</em></p>
<p><em>이행종속성은 위 예시에서 A가 C에 대해서 이행적을 종속되지 않는 것</em></p>
<h3 id="정규화-예시">정규화 예시</h3>
<p>학생과 성적테이블이 존재한다.</p>
<p>학생테이블</p>
<table>
<tr>
  <td style="text-align : center"> 컬럼 </td>
  <td style="text-align : center"> 타입 </td>
</tr> 
  <tr>
    <td> SEQ </td>
    <td> INTEGER (PK) </td>
  </tr>
  <tr>
    <td> 학생명 </td>
    <td> String </td>
  </tr>
</table>

<p>성적테이블</p>
<table>
  <tr>
    <td style="text-align : center"> 컬럼 </td>
    <td style="text-align : center"> 타입 </td>
  </tr>
<tr>
  <td> 성적SEQ </td>
  <td> INTEGER (PK) </td>
</tr>
<tr>
  <td> 학생SEQ </td>
  <td> INTEGER (FK) </td>
</tr>
<tr>
  <td> 국어성적 </td>
  <td> INTEGER </td>
</tr>
<tr>
  <td> 수학성적 </td>
  <td> INTEGER </td>
</tr>
<tr>
  <td> 영어성적 </td>
  <td> INTEGER </td>
</tr>
</table>

<p>위 테이블을 정규화한다면, 어떻게 할 것인가.</p>
<p>과목테이블을 추가한다.</p>
<table>
  <tr>
    <td style="text-align : center"> 컬럼 </td>
    <td style="text-align : center"> 타입 </td>
  </tr>
  <tr>
    <td> 과목코드 </td>
    <td> INTEGER (PK) </td>
  </tr>
  <tr>
    <td> 과목명 </td>
    <td> String </td>
  </tr>
</table>

<p>그리고 성적테이블을 아래와 같이 수정한다.</p>
<table>
  <tr>
    <td style="text-align : center"> 컬럼 </td>
    <td style="text-align : center"> 타입 </td>
  </tr>
  <tr>
    <td> 성적SEQ </td>
    <td> INTEGER (PK) </td>
  </tr>
  <tr>
    <td> 학생SEQ </td>
    <td> INTEGER (FK) </td>
  </tr>
  <tr>
    <td> 과목코드 </td>
    <td> INTEGER (FK) </td>
  </tr>
  <tr>
    <td> 성적 </td>
    <td> INTEGER </td>
  </tr>
</table>

<p>위와 같이 수정하면 과목이 추가되어도 JPA, SQL 문이 수정될 일이 적다. </p>
<pre><code class="language-sql">-- 응시과목이 2개 이상인 학생의 각 성적 통계 예시

SELECT Student.학생명      AS 이름
     , Grade.과목명
       , AVG(Grade.성적) AS 평균
  FROM 학생 Student
 INNER JOIN 성적 Grade
    ON Student.SEQ = Grade.학생SEQ
 INNER JOIN 과목 Subject
    ON Grade.과목코드 = Subject.과목코드
-- WHERE 필요없음
 GROUP BY Student.학생명
          , Subject.과목코드
HAVING COUNT(Student.SEQ) &gt; 1</code></pre>
<p>이 설명을 어설프게 했다.
이래서 말을 잘해야한다.</p>
<h2 id="index-동작-방식">INDEX 동작 방식</h2>
<blockquote>
<p>B-TREE 인덱싱 
데이터를 정렬된 상태로 유지하며 효율적인 검색, 삽입 및 삭제 작업을 수행할 수 있도록 지원한다. 
주로 범위 검색(range search)에 뛰어난 성능을 발휘한다.
대용량의 데이터를 효율적으로 처리할 수 있다. 
또한 데이터의 변경 작업에 대해서도 효율적으로 처리할 수 있어 실시간으로 데이터를 업데이트하는 환경에 적합하다.
균형 트리 형태, 다단계 인덱싱, 분할과 병합, 다중 키 등을 지원한다.</p>
</blockquote>
<h3 id="mysql">MySQL</h3>
<h4 id="해시-인덱스-innodb-스토리지-엔진-한정">해시 인덱스 (InnoDB 스토리지 엔진 한정)</h4>
<p>MySQL은 해시 인덱스를 메모리 기반 테이블에 대한 인덱싱에 사용할 수 있다.
해시 인덱스는 동등 비교에 기반한 빠른 검색을 제공하며 같은 해시값이 나오는 충돌 가능성을 대비해서 체이닝 기법을 사용한다.</p>
<p>그러므로 MySQL에서의 쿼리의 성능을 최적화 하기 위해서 BETWEEN, <code>&gt;</code> , <code>&lt;</code>
등의 연산자는 지양해야 한다.
<code>=</code> 연산자로 레코드의 범위를 줄이는 것이 성능개선에 효율적일 것이다.</p>
<h3 id="mssql">MSSQL</h3>
<h4 id="columnstore-인덱스-2012버전부터-지원">ColumnStore 인덱스 (2012버전부터 지원)</h4>
<p>컬럼 단위로 데이터를 저장하여 압축 및 저장/조회 쿼리 성능을 향상시킨다.
(수정/삭제에는 거의 영향이 없다. 오히려 더 느려질 수 있다.)</p>
<p>대량의 데이터를 조회하는 쿼리를 여러 번 반복할 경우, 
MSSQL은 이전에 수행한 쿼리 결과를 컬럼 단위로 캐시하고 컬럼스토어 인덱스를 활용하여 다음 쿼리 수행 시 점진적으로 빨라진다.</p>
<p>실제 과거 프로젝트에서 정렬 + 연산/집계함수가 들어간 대규모의 조회쿼리 작성 시,
HAVING, WHERE 등으로 범위를 최대한 줄이며 최적화 하고,
다른테이블과의 참조가 없는 컬럼에 따로 인덱스를 지정해 줘서 ColumnStore 인덱스 사용을 유도했다.
이로 인해 연산/집계함수 결과 컬럼이 캐시되도록 유도되어 수 차례 조회쿼리 수행 시 점차 빨라지게 개선한 경험이 있다.</p>
<h3 id="oracle">ORACLE</h3>
<h4 id="비트맵-인덱스">비트맵 인덱스</h4>
<p>대량의 데이터에 대한 비트맵 기반의 효율적인 질의를 지원한다.</p>
<p>값의 존재 여부를 비트 벡터(Bit Vector)로 표현하여 인덱싱하는 방식
등호(Equality) 비교에 효과적이며, AND, OR, NOT과 같은 비트 연산을 활용하여 빠른 집합 연산이 가능하다.</p>
<h4 id="함수-기반-인덱스">함수 기반 인덱스</h4>
<p>컬럼 값의 특정 함수를 적용하여 인덱싱하는 방식</p>
<p>여러 문자열 변환 함수, 날짜 변환 등의 조회쿼리 결과를 인덱스로 저장하여 검색 성능을 향상시킨다. 잦은 업데이트가 이루어 지는 로직에서는 성능에 악영향을 미칠 수 있다.</p>
<p>실제로 여러 SELECT 절에 여러 함수로 조회되는 쿼리를 티베로로 Migration 할때 WHERE 문으로 최대한 범위를 좁혀 개선한 경험이 있다.</p>
<p><em>옵티마이저의 인덱스 결정에 영향을 주는 힌트 주석 예시)</em></p>
<pre><code class="language-sql">SELECT /*+ INDEX(table_name index_name) */ column1, column2
  FROM table_name
 WHERE column3 = &#39;value&#39;;</code></pre>
<h4 id="기타-dual-의-존재">기타 (DUAL 의 존재)</h4>
<p>DUAL은 시스템 내장 테이블로서 사용되는 특별한 테이블이다.
실제 데이터를 저장하지 않고, 단일 행 하나를 가진다.
SELECT 문과 같은 쿼리에서 임시로 사용된다.
스칼라 서브쿼리에서 조인을 회피하거나 메모리 접근을 최소화할 때 사용될 수 있다.</p>
<h2 id="db-필수-개념">DB 필수 개념</h2>
<h3 id="트랜잭션">트랜잭션</h3>
<p>데이터가 일관적으로 처리되기 위한 하나의 논리적 작업단위</p>
<ul>
<li>원자성 : 하나의 트랜잭션의 데이터 처리는 모두 수행되거나 아무것도 수행되지 않아야 한다.</li>
<li>일관성 : 트랜잭션 이전과 이후에 DB는 미리 정의된 일관성 규칙이 적용되어야 한다.</li>
<li>격리성 : 다른 트랜잭션으로부터 격리되어야 한다. 동시에 실행 중인 여러 트랜잭션은 서로 간섭 없이 독립적으로 수행되어야 한다.</li>
<li>지속성 : 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 저장되어 데이터의 지속성이 보장되어야 한다.</li>
</ul>
<h3 id="isolation-레벨-격리수준">ISOLATION 레벨 (격리수준)</h3>
<p>트랜잭션들이 동시에 실행될 때 어떻게 상호작용하는지를 제어하는 설정</p>
<h4 id="read-uncommitted-미커밋된-읽기">READ UNCOMMITTED (미커밋된 읽기)</h4>
<p><strong>가장 낮은 격리수준</strong>
트랜잭션이 수행 중인 다른 트랜잭션에서 변경 중인 데이터를 읽을 수 있다. 
이로 인해 Dirty Read(더티 리드)라는 현상이 발생할 수 있다. </p>
<p><em>Dirty Read는 하나의 트랜잭션이 다른 트랜잭션에서 아직 커밋되지 않은 데이터를 읽는 현상을 의미한다.</em></p>
<h4 id="read-committed-커밋된-읽기">READ COMMITTED (커밋된 읽기)</h4>
<p><strong>각 트랜잭션이 커밋된 데이터만 읽을 수 있도록 설정한다.</strong>
Dirty Read는 발생하지 않는다.</p>
<p>하지만 Non-Repeatable Read(반복 불가능한 읽기)라는 현상이 발생할 수 있다.</p>
<p><em>Non-Repeatable Read는 하나의 트랜잭션이 같은 쿼리를 여러 번 실행할 때, 각 실행마다 다른 결과를 반환하는 현상을 의미한다.</em></p>
<h4 id="repeatable-read-반복-가능한-읽기">REPEATABLE READ (반복 가능한 읽기)</h4>
<p><strong>한 트랜잭션이 읽은 데이터를 다른 트랜잭션에서 변경하지 못하도록 설정한다.</strong></p>
<p>따라서 Non-Repeatable Read는 발생하지 않는다.</p>
<p>하지만 Phantom Read(유령 읽기)라는 현상이 발생할 수 있다. </p>
<p><em>Phantom Read는 하나의 트랜잭션이 동일한 쿼리를 여러 번 실행할 때, 
각 실행마다 결과 집합에 새로운 레코드가 추가되거나 삭제되는 현상을 의미한다.</em></p>
<h4 id="serializable-직렬화">SERIALIZABLE (직렬화):</h4>
<p><strong>가장 높은 격리 수준</strong>
모든 트랜잭션을 직렬화하여 동시에 실행되는 것처럼 처리한다. 
따라서 Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생하지 않습니다. </p>
<p><strong>하지만 동시성을 제한하고 처리 속도를 늦출 수 있다.</strong></p>
<blockquote>
<p><strong>팬텀리드가 일어나는 경우</strong></p>
</blockquote>
<ol>
<li>트랜잭션 A가 WHERE 절을 포함하는 범위 쿼리를 실행한다.</li>
<li>트랜잭션 B가 트랜잭션 A가 실행 중인 동안 데이터를 삽입, 갱신 또는 삭제한다.</li>
<li>트랜잭션 A가 동일한 쿼리를 다시 실행하면 트랜잭션 B에 의해 변경된 새로운 데이터가 표시되어 일관성을 잃는다.</li>
</ol>
<blockquote>
<p><strong>과거 프로젝트에서 팬텀리드를 해결한 방법</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(1) AS RESULT
  FROM sys.dm_tran_locks
 WHERE resource_type = &#39;OBJECT&#39;
   AND resource_database_id = DB_ID(&#39;데이터베이스이름&#39;)
   AND resource_description = &#39;스키마이름.테이블명&#39;</code></pre>
<blockquote>
<p>결과값이 1 이상이라면 특정 프로시저의 실행하지 않고
<code>일괄{?}업무 결과가 반영중 입니다.</code> 라는 예외처리로 메세지를 띄웠다.</p>
</blockquote>
<h3 id="옵티마이저-optimizer">옵티마이저 (Optimizer)</h3>
<p>DB에서 쿼리의 실행 계획을 생성하고 최적화하는 역할을 담당한다.
옵티마이저는 쿼리를 실행하기 전에 데이터베이스의 스키마, 인덱스, 통계 정보 등을 분석하여 최적의 실행 계획을 결정한다.
MySQL, MSSQL, Oracle은 각각 자체적인 옵티마이저를 가지고 있다.</p>
<h1 id="spring--java">Spring &amp; Java</h1>
<h2 id="interface--abstract-차이">Interface &amp; Abstract 차이</h2>
<p>둘다 추상화를 통해 다형성을 구현하는 데 사용되는 개념이지만
가장 큰 차이점은 사용의도이다.
추상 클래스는 <strong>&#39;~이다.&#39;</strong> , 인터페이스는 <strong>&#39;~할 수 있다&#39;</strong></p>
<h3 id="abstract-class">Abstract Class</h3>
<blockquote>
<p> 클래스 간의 상속 관계를 표현하고 공통된 코드를 재사용</p>
</blockquote>
<p>일반적인 클래스와 마찬가지로 필드, 생성자, 메소드를 포함할 수 있다.
상속계층구조로 표현된다.
자식클래스는 하나의 부모클래스만 상속 받을 수 있다.</p>
<p><strong>공통된 특성을 추상화</strong>하여 _<strong>코드 재사용성</strong>_을 높인다.
부모클래스에 선언된 추상메소드를 구현해야 한다.</p>
<h3 id="interface">Interface</h3>
<blockquote>
<p>클래스 간의 관계를 느슨하게 형성하고 다형성을 구현하기 위해 사용</p>
</blockquote>
<p>필드를 가질 수 없고, 추상 메서드와 디폴트 메서드, 정적 메서드를 선언할 수 있다. 
모든 메서드는 기본적으로 추상 메서드이며, 구현클래스에서 반드시 구현해야한다.</p>
<p>다른 클래스들의 공통된 기능을 정의하여 관계를 형성한다.
이로써 <em><strong>다형성을 구현한다.</strong></em></p>
<p>하나의 클래스가 여러개의 클래스를 구현할 수 있다.
때문에 다른 인터페이스들의 기능을 조합할 수 있다.</p>
<h2 id="spring-aop-설명">Spring AOP 설명</h2>
<p><strong>Aspect Oriented Programming</strong>의 약자로 관점 지향 프로그래밍이라고 불린다. </p>
<p>관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 
여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다. </p>
<h3 id="취지">취지</h3>
<p>소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 
<strong>흩어진 관심사 (Crosscutting Concerns)</strong> 라고 부른다. </p>
<p>흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.</p>
<h3 id="사용되는-용어와-설명">사용되는 용어와 설명</h3>
<ul>
<li>Aspect 
위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.</li>
<li>Target
Aspect를 적용하는 곳 (클래스, 메서드 ... )</li>
<li>Advice
실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 <strong>구현체</strong></li>
<li>JointPoint
Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능</li>
<li>PointCut 
JointPoint의 상세한 스펙을 정의한 것. &#39;A란 메서드의 진입 시점에 호출할 것&#39;과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음</li>
</ul>
<h2 id="dto-dao-vo-차이점">DTO, DAO, VO 차이점</h2>
<h3 id="dto">DTO</h3>
<p>DTO는 계층간(Controller, View, Business Layer) 데이터 교환을 위한 자바 빈즈(Java Beans)를 의미한다.
DTO는 로직을 가지지 않는 데이터 객체이고 getter/setter메소드만 가진 클래스를 의미한다.
DTO(Data Transfer Object)는 데이터 전송(이동) 객체라는 의미를 가지고 있다.
DTO는 주로 비동기 처리를 할 때 사용한다.</p>
<h3 id="dao">DAO</h3>
<p>DAO는 DB의 data에 접근하기 위한 객체로 직접 DB에 접근하여 데이터를 삽입, 삭제, 조회 등 조작할 수 있는 기능을 수행한다.</p>
<p>보통 DB 커넥션 라이브러리 등이 커넥션풀까지 제공되고 있기 때문에 DAO를 별도로 만드는 경우는 드물다.</p>
<h3 id="vo">VO</h3>
<p>VO는 Read-Only속성을 값 오브젝트
VO의 핵심 역할은 equals()와 hashcode() 를 오버라이딩 하는 것이다.
VO 내부에 선언된 속성(필드)의 모든 값들이 VO 객체마다 값이 같아야, 똑같은 객체라고 판별한다.</p>
<h3 id="stream-의-foreach-와-map-의-다른점">Stream 의 forEach 와 map 의 다른점?</h3>
<p>둘다 스트림 객체의 내부 값을 변경할 수 있다는 공통점이 있다.</p>
<p>그런데 이 둘의 차이점은?</p>
<p>map은 스트림의 값 자체를 변경하기 위한 함수이다.
forEach는 값을 꺼내서 그 값으로 작업을 하는 것이다.</p>
<p>forEach는 일반적인 for-loop 보다 오버헤드가 많이 발생하며 처리순서를 보장하지 않는다.
스트림 내부의 값 각각이 stream() 메서드로 생성되고 한번의 반복이 끝나면 버려지기 때문이다.</p>
<p>때문에 forEach는 지양해야 한다.</p>
<h2 id="jpa">JPA</h2>
<h3 id="setter를-지양하는-이유">Setter를 지양하는 이유</h3>
<p>@Setter는 사용 의도/목적이 분명치 않다. (Update인지 Create인지)</p>
<p>모든 영역에서 접근할 수 있어서 무분별한 변경으로 객체의 일관성을 보장하기 어렵다. 
(때문에 빌더패턴을 권장)</p>
<p>Setter을 무작정 사용한다면 해당 엔티티의 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어 유지보수의 복잡성을 증가시킬 수 있디.</p>
<h3 id="dirty-checking">Dirty Checking</h3>
<h1 id="cs개발론">CS/개발론</h1>
<h2 id="hash-란">Hash 란?</h2>
<p>Hash는 해시 함수(hash function)를 통해 생성되는 값이다.
이 값은 해시값(hash value) 또는 해시코드(hash code)라고도 불린다.</p>
<p>해시 함수는 입력값에 대해서 고유한 해시값을 생성하는 특징이 있다.
같은 입력에 대해서는 항상 같은 해시 값을 반환한다, 약간의 변화에도 완전히 다른 해시 값이 생성된다.
때문에 해시 함수는 데이터의 무결성 검증, 암호화, 데이터베이스 검색 및 저장, 압축 등 다양한 분야에서 활용된다.</p>
<p><em>프로그래밍 언어에서</em>
해쉬는 보통 Key-Value로 이루어진 해시테이블(hash table)을 구현하는 데 사용된다.
키에 해쉬값을 대입한뒤 배열의 인덱스로 바로 사용하여 특정 위치에 값을 저장하거나 검색한다.
이 인덱스로 인해 Key에 바로 접근할 수 있으며 빠른 탐색/수정/삭제가 가능하게 한다.
대표적으로 자바의 HashMap, HashSet 등의 자료구조가 있다.</p>
<p>충돌이 나면 체이닝 알고리즘을 사용하여 같은 공간에 2개의 값을 저장하고, 그 중 하나의 값을 찾아야 된다면 같은 공간에 접근해 해당 값을 찾는 식으로 동작한다.</p>
<h2 id="oop-solid-원칙">OOP SOLID 원칙</h2>
<p>좋은 설계를 위해 지향하는 원칙들이며  코드는 유연하고, 확장할 수 있고, 유지보수가 용이하고, 재사용할 수 있어야한다.
(클린코드 책의 저자인 로버트 C. 마틴이 세운 원칙)</p>
<h3 id="단일-책임-원칙-srp">단일 책임 원칙 SRP</h3>
<p><strong>Single Responsibility Principle</strong>
단일의 클래스나 모듈은 하나의 책임만을 가져야 한다.
클래스가 변경되는 이유는 단 하나뿐이어야 한다.
이를 통해 클래스의 응집도를 높이고 변화에 대한 영향을 최소화한다.</p>
<h3 id="개방-폐쇄-원칙-ocp">개방-폐쇄 원칙 OCP</h3>
<p><strong>Open/Closed Principle</strong>
클래스, 모듈, 함수 등은 <strong>확장</strong>에는 OPEN, 변경에는 CLOSED.
새로운 기능이 추가되거나 변경이 필요할 때 기존 코드를 수정하지 않고 확장을 통해 변경을 처리할 수 있어야 한다.</p>
<h3 id="리스코프-치환-원칙-lsp">리스코프 치환 원칙 LSP</h3>
<p><strong>Liskov’s Substitution Principle</strong>
부모 객체는 자식 객체로 치환되도 기능의 일관성을 유지해야 한다.
서브 클래스는 슈퍼 클래스로 사용될 때 어떠한 기능도 손상시키거나 무효화해서는 안 됩니다.</p>
<h3 id="인터페이스-분리-원칙-isp">인터페이스 분리 원칙 ISP</h3>
<p><strong>Interface Segregation Principle</strong>
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
한 클래스가 필요로 하는 인터페이스만 구현하도록 분리해서
클라이언트가 불필요한 의존성을 가지지 않도록 해야한다.</p>
<h3 id="의존성-역전-법칙-dip">의존성 역전 법칙 DIP</h3>
<p><strong>Dependency Inversion Principle</strong>
추상화된 것은 구체적인 것(구현된 것)에 의존해서는 안된다.
부모는 자식에게 의존하면 안된다. 양쪽 모두 추상화된 것에 의존해야 한다. 
이를 통해 모듈 간의 결합도를 낮추고 유연성을 확보해야 한다.</p>
<h2 id="함수형-프로그래밍">함수형 프로그래밍</h2>
<h2 id="반응형-프로그래밍">반응형 프로그래밍</h2>
<p><strong>비동기적인 데이터 흐름과 변경에 대응하는 프로그래밍 패러다임</strong></p>
<h3 id="옵저버블observable과-옵서버observer">옵저버블(Observable)과 옵서버(Observer)</h3>
<p><strong>옵저버블</strong>은 데이터 스트림을 나타내는 객체이며 <strong>데이터의 생산자</strong>
<strong>옵서버</strong>는 옵저버블로부터 데이터를 받아 처리하는 객체로, <strong>데이터의 소비자</strong>
옵저버는 옵저버블의 변화에 대응하여 알림을 받고, 필요한 작업을 수행한다.</p>
<p>비동기 작업을 처리하기 위해 콜백(callback) 함수, 프로미스(Promise), 비동기 제너레이터(Async/Await) 등을 사용하여 비동기 코드를 효율적으로 작성하고 데이터 흐름을 관리한다.</p>
<p>자바에서는 퓨처(Future)객체, 콜백(Callback)타입, 리액티브 스트림(Reactive Streams) 등으로 구현할 수 있다.</p>
<p>WebFlux라는 모듈을 사용한다면 Publisher-Subscriber 패턴을 사용하여 데이터를 비동기적으로 처리한다.</p>
<h2 id="http-메서드">HTTP 메서드</h2>
<p><a href="https://velog.io/@cv_/HTTP-Method-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">https://velog.io/@cv_/HTTP-Method-알아보기</a></p>
<h2 id="가비지컬렉션gc">가비지컬렉션(GC)</h2>
<h2 id="scope">SCOPE</h2>
<p>변수 또는 식별자(identifier)가 유효한 범위를 의미한다.
작은 스코프부터 큰 스코프로 범위를 탐색한다.</p>
<h2 id="스크립트함수에대한것">스크립트함수에대한것</h2>
<p>JS 같은 스크립트 언어에서 코드를 모듈화하고 재사용하기 위해 사용되는 기능
스크립트 언어는 주로 프로그래밍 언어보다는 간단한 작업이나 스크립트를 실행하기 위해 사용
사실 이걸 왜 물어보는지 싶었다.</p>
<h2 id="세션--쿠키">세션 &amp; 쿠키</h2>
<p><strong>사용자 식별과 상태 관리를 위한 메커니즘</strong></p>
<ul>
<li>세션은 서버 측에, 쿠키는 클라이언트 측에서 상태 정보를 관리</li>
<li>세션은 서버에 데이터를 저장하고 클라이언트에 전달,
쿠키는 클라이언트에 데이터를 저장하고 서버로 전송</li>
<li>세션은 서버 측에 데이터를 저장하기 때문에 보안이 더 우수
쿠키는 클라이언트 측에 저장되므로 상대적으로 보안에 취약</li>
<li>세션은 더 많은 데이터를 저장할 수 있지만, 쿠키는 크기가 제한적입니다.</li>
<li>세션은 브라우저를 종료하거나 세션이 만료되면 종료되지만
쿠키는 설정된 만료일까지 유지될 수 있습니다.</li>
</ul>
<h2 id="rest-api">REST API</h2>
<h2 id="기타">기타</h2>
<p>파이썬으로 핑퐁게임을 구현할 때,
Stackoverflow (1000번의 재귀 제한)을 뚫고
pingpong(30000) 을 구현하는 방법에 대한 의견을 서술</p>
<h1 id="devops">DevOps</h1>
<p>도커 사용경험
학습내용 : <a href="https://velog.io/@cv_/Docker-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">https://velog.io/@cv_/Docker-간단하게-알아보기</a></p>
<p>쿠버네티스 사용경험</p>
<p>AWS로 파이프라인 구축 경험</p>
<blockquote>
<p>AWS 파이프라인이란?
애플리케이션 개발 및 배포 과정을 자동화하기 위한 서비스 (CI/CD 자동화)</p>
<h3 id="1-소스-스테이지source-stage">1. 소스 스테이지(Source Stage)</h3>
<p>소스 스테이지는 코드를 저장하고 있는 소스 코드 형상 관리 도구(Git, AWS CodeCommit 등)와 연결되어 코드 변경 사항을 감지합니다. 
변경 사항이 발생하면 자동으로 빌드 단계로 이동합니다.</p>
<h3 id="2-빌드-스테이지build-stage">2. 빌드 스테이지(Build Stage):</h3>
<p>빌드 스테이지에서는 소스 코드를 컴파일하고 테스트하며, 필요한 빌드 아티팩트를 생성합니다. 
이 단계에서는 품질 테스트, 정적 코드 분석, 문서 생성 등의 작업을 수행할 수 있습니다.</p>
<h3 id="3-배포-스테이지deploy-stage">3. 배포 스테이지(Deploy Stage):</h3>
<p>배포 스테이지에서는 빌드된 아티팩트를 프로덕션 환경으로 배포합니다. 
이 단계에서는 AWS 서비스를 활용하여 애플리케이션을 구성하고 프로비저닝합니다. 
예를 들어, AWS Elastic Beanstalk, AWS Lambda, Amazon EC2 등을 사용하여 애플리케이션을 배포할 수 있습니다.</p>
<h3 id="4-테스트-스테이지test-stage">4. 테스트 스테이지(Test Stage):</h3>
<p>테스트 스테이지에서는 배포된 애플리케이션을 테스트합니다. 
이 단계에서는 자동화된 테스트, 성능 테스트, 사용자 인터페이스(UI) 테스트 등을 수행하여 애플리케이션의 품질을 검증합니다.</p>
<h3 id="5-프로덕션-스테이지production-stage">5. 프로덕션 스테이지(Production Stage):</h3>
<p>프로덕션 스테이지는 배포 및 테스트를 마친 애플리케이션을 실제 운영 환경에 배포하는 단계입니다. 
이 단계에서는 사용자가 실제로 애플리케이션을 사용하고, 모니터링 및 로깅을 통해 애플리케이션의 성능과 안정성을 지속적으로 관찰합니다.</p>
</blockquote>
<p>기타 AWS 기능들 정리필요</p>
<h2 id="jenkins-오픈소스">Jenkins (오픈소스)</h2>
<p>젠킨스(Jenkins)는 지속적 통합(Continuous Integration, CI) 및 지속적 배포(Continuous Deployment, CD)를 지원하는 자동화 도구입니다. 소프트웨어 개발 프로세스에서 젠킨스는 소스 코드의 통합, 빌드, 테스트, 배포 등을 자동화하여 개발자들이 더 빠르고 신뢰할 수 있는 방식으로 소프트웨어를 개발하고 배포할 수 있도록 돕습니다.</p>
<p>젠킨스는 다음과 같은 기능을 제공합니다:</p>
<h3 id="1-지속적-통합ci">1. 지속적 통합(CI)</h3>
<p>젠킨스는 코드 저장소(Git, SVN 등)와 연동하여 소스 코드의 변경 사항을 감지하고 자동으로 빌드 프로세스를 실행합니다. 이를 통해 여러 개발자가 작업한 코드를 통합하고 충돌이나 빌드 오류를 사전에 감지할 수 있습니다.</p>
<h3 id="2-자동화된-빌드">2. 자동화된 빌드</h3>
<p>젠킨스는 소스 코드를 가져와 컴파일, 패키징, 테스트 등의 빌드 작업을 자동화합니다. 이를 통해 일관된 빌드 프로세스를 유지하고 개발자들이 더 빠르게 소프트웨어를 빌드할 수 있습니다.</p>
<h3 id="3-자동화된-테스트">3. 자동화된 테스트</h3>
<p>젠킨스는 다양한 유형의 테스트를 자동화하여 코드 변경에 따른 잠재적인 문제를 탐지합니다. 단위 테스트, 통합 테스트, 성능 테스트 등을 자동으로 실행하여 소프트웨어 품질을 향상시킵니다.</p>
<h3 id="4-지속적-배포cd">4. 지속적 배포(CD)</h3>
<p>젠킨스는 빌드된 소프트웨어를 자동으로 배포하고 필요한 환경에 적용하는 작업을 자동화합니다. 배포 파이프라인을 구축하여 스테이징 환경, 프로덕션 환경 등으로의 배포 과정을 자동화할 수 있습니다.</p>
<h4 id="알림-및-보고">알림 및 보고</h4>
<p>젠킨스는 빌드 및 배포 상태에 대한 알림을 제공하며, 다양한 보고서를 생성하여 개발자들이 소프트웨어 개발 프로세스의 상태를 파악할 수 있습니다.</p>
<p>젠킨스는 확장 가능한 플러그인 아키텍처를 가지고 있어 다양한 개발 도구, 테스트 도구, 배포 도구와 통합할 수 있습니다. 이를 통해 개발자들은 자신들이 선호하는 도구를 사용하여 개발 및 배포 프로세스를 자동화할 수 있습니다.</p>
<p>젠킨스는 오픈 소스 프로젝트로 시작되었으며, 현재는 많은 개발자와 기업에서 널리 사용되고 있습니다.</p>
<h2 id="기본적인-리눅스-명령어들">기본적인 리눅스 명령어들</h2>
<p>그냥 아는대로 아래 내용 대답</p>
<ul>
<li>GREP</li>
<li>touch</li>
<li>cat </li>
<li>find</li>
<li>head 등</li>
</ul>
<h1 id="domain">Domain</h1>
<h2 id="dns-서버-관련-질문">DNS 서버 관련 질문</h2>
<p>DNS Server : 도메인 이름과 IP 주소 간의 매핑을 관리하는 서버</p>
<p>BIND(Berkeley Internet Name Domain)와 같은 DNS 서버 소프트웨어가 필요하다.</p>
<h3 id="dns-존-설정">DNS 존 설정</h3>
<p>DNS 존은 도메인 이름과 IP 주소 간의 매핑 정보를 포함하는 공간.
DNS 서버 소프트웨어의 설정 파일을 편집하여 DNS 존을 생성하고 도메인 이름과 해당하는 IP 주소를 매핑한다.</p>
<h3 id="dns-레코드-설정">DNS 레코드 설정</h3>
<p>DNS 존 내에서 각 도메인 이름에 대한 레코드를 설정.
이 레코드는 도메인 이름과 IP 주소 매핑을 정의하는 
A 레코드, MX 레코드, CNAME 레코드 등으로 구성된다.</p>
<p>이후에 서버를 구성하고 실행시킨다.</p>
<h2 id="cdn--cache--media">CDN / Cache / Media</h2>
<h2 id="cdn-content-delivery-network">CDN (Content Delivery Network)</h2>
<p>전 세계에 분산된 서버 네트워크
CDN은 웹/앱 등의 콘텐츠를 사용자에게 가까운 위치에서 제공하여 로딩 속도를 향상시키고 전송 대역폭을 줄인다.
다시 말해, CDN은 가장 가까운 서버에서 해당 콘텐츠를 전송하여 빠른 속도와 효율적인 네트워크 전송을 제공한다.
CDN은 정적 파일(이미지, CSS, JavaScript 등)과 동적 컨텐츠(동영상, 게임, 소셜 미디어 등)를 제공하는 데 사용된다.</p>
<h2 id="실시간-전송-애플리케이션">실시간 전송 애플리케이션</h2>
<h3 id="websocket-모듈">WebSocket 모듈</h3>
<h3 id="메세지-큐">메세지 큐</h3>
<p>RabbitMQ, Apache Kafka 등이 있습니다. 자바와 스프링 부트에서는 메시지 큐 시스템을 통해 실시간 데이터를 처리하고 전송할 수 있습니다.</p>
<h3 id="대용량-데이터-시스템-아키텍처에서는">대용량 데이터 시스템 아키텍처에서는?</h3>
<h2 id="msa">MSA</h2>
<h1 id="communication--문제해결능력">Communication &amp; 문제해결능력</h1>
<p>의사소통이 힘들때 어떻게 해결했나</p>
<p>어떻게 어떤 기능을 구현했나 (이전프로젝트 업무관련)</p>
<p>자신의 장단점 (성격 등)</p>
<p>지원동기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA로 테이블 설계, 및 주의점]]></title>
            <link>https://velog.io/@cv_/JPA%EB%A1%9C-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cv_/JPA%EB%A1%9C-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 02 Jun 2023 10:42:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>MyBatis 등으로 RDBMS에 직접 쿼리 작성이 익숙한 인간이라서 JPA 엔티티가 조금만 복잡해져도 ORM과 RDBMS의 패러다임 불일치 때문에 원하는대로 테이블을 정규화하기 까다로웠다.</p>
</blockquote>
<p>때문에 성능이나 N+1 등의 문제와는 별개로 JPQL로 직접 쿼리를 작성했었다.</p>
<blockquote>
</blockquote>
<p>조금은 원할하게 원하는대로 (+객체지향적으로) 테이블을 설계하고 JPA로 구현된 로직을 좀 더 쉽고 구체화 할 수 있도록 정리할 필요를 느꼈다.</p>
<h1 id="테이블-설계">테이블 설계</h1>
<p>엔티티를 구성해보기 위해, DB에 직접 여러 연관관계가 있는 DB테이블을 먼저 설계해본다.
<img src="https://velog.velcdn.com/images/cv_/post/323f6206-d8f6-4f0e-8827-2cdf948d39be/image.png" alt=""></p>
<p>위 그림으로 멤버기준 연관관계를 맺는다.</p>
<ul>
<li>Address 테이블 (OneToOne)
멤버 한명 당 하나의 주소를 갖는다.</li>
<li>Team 테이블 (OneToMany/ManyToOne)
멤버 한명 당 하나의 팀을 가지지만, 팀은 여러 멤버를 갖는다.</li>
<li>Skill 테이블 (ManyToMany)
멤버는 여러개의 스킬을 가질 수 있고, 스킬 또한 여러 멤버를 가질 수 있다.</li>
</ul>
<p><em>번외 : Vacation 테이블은 복합키로 구성한다.</em></p>
<h1 id="onetoone">OneToOne</h1>
<p>1:1 연관관계이다. 연관관계를 맺는 키가 있는 곳이 주인이 되기 때문에
이 경우에 Member 엔티티에 선언해준다.</p>
<p>JoinColumn을 통해서 조인될 컬럼을 선언해줄 수 있다.
name은 선언되는 Member의 조인될 컬럼이며
referencedColumnName은 Address 엔티티의 조인될 컬럼이다.
(쉽게 SQL에서 name은 FROM절의 컬럼, referencedColumnName은 JOIN ON절의 컬럼)</p>
<pre><code class="language-java">@Entity
@Getter
@Table(name = &quot;Member&quot;)
public class Member {
    @Id
    int uid;

    String name;
    char gender;
    String hp_no;
    String email;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = &quot;uid&quot;, referencedColumnName = &quot;uid&quot;)
    private Address address;
}</code></pre>
<h2 id="cascadetype">CascadeType</h2>
<p>부모, 자식 간의 영속성을 설정하는 옵션 중 하나인 <code>CascadeType.ALL</code>을 선언해준 이유는 Address와 Member가 1:1 관계를 맺고 있고 데이터 무결성을 유지하기 위해 사용되었다.</p>
<p>멤버는 하나의 주소를 가지고 있고 그 반대의 경우도 마찬가지이기 때문에 멤버 엔티티의 저장, 수정, 삭제 시 자식 엔티티(또는 자식 테이블)도 저장, 수정, 삭제가 일어나게 만든다.</p>
<p>ALL 외에도 세부적으로 다른 CascadeType 옵션들이 있다.</p>
<ul>
<li>CascadeType.<strong>PERSIST</strong>
부모 엔티티가 영속상태가 되면 자식 엔티티도 영속 상태가 되도록 한다.
ALL과 다른점은 부모 엔티티가 저장될 때 자식 엔티티를 함께 저장하지만,
삭제될 때는 자식 엔티티는 삭제되지 않는다.
즉 생성될 때(<strong>Transient</strong> 상태)만 영속성을 전이한다.</li>
<li>CascadeType.<strong>MERGE</strong>
병합(또는 수정)이 부모 엔티티에서 일어나게 된다면 자식 엔티티에서도 변경이 일어난다.
위 예시에서 멤버의 <em>uid</em> 값이 변경되면 Address의 <em>uid</em> 또한 같은 값으로 변경된다.</li>
<li>CascadeType.<strong>REMOVE</strong>
마찬가지로 부모 엔티티가 삭제 시, 자식 엔티티도 삭제된다.
위 예시에서 멤버의 uid가 10인 값을 삭제한다면,
자식 엔티티인 Address에서 또한 uid가 10인 엔티티가 삭제된다.</li>
</ul>
<hr>
<h2 id="실행쿼리-단방향">실행쿼리 (단방향)</h2>
<ul>
<li>단건조회쿼리<pre><code class="language-sql">SELECT member0_.uid    as uid1_1_,
     member0_.email  as email2_1_,
     member0_.gender as gender3_1_,
     member0_.hp_no  as hp_no4_1_,
     member0_.name   as name5_1_ 
FROM Member member0_
WHERE member0_.uid = ?</code></pre>
</li>
</ul>
<hr>
<p>SELECT address0_.uid      as uid1_0_0_,
       address0_.city    as city2_0_0_,
       address0_.country as country3_0_0_,
       address0_.detail  as detail4_0_0_ 
  FROM Address address0_ 
 WHERE address0_.uid = ?</p>
<pre><code>
- 전체조회쿼리 (FindAll)
```sql
SELECT member0_.uid      as uid1_1_0_,
       address1_.uid     as uid1_0_1_,
       member0_.email    as email2_1_0_,
       member0_.gender   as gender3_1_0_,
       member0_.hp_no    as hp_no4_1_0_,
       member0_.name     as name5_1_0_,
       address1_.city    as city2_0_1_,
       address1_.country as country3_0_1_,
       address1_.detail  as detail4_0_1_ 
  FROM Member member0_ 
  LEFT 
 OUTER
  JOIN Address address1_ 
    ON member0_.uid = address1_.uid</code></pre><h3 id="단건조회는-왜-두개의-쿼리가-실행되는가">단건조회는 왜 두개의 쿼리가 실행되는가</h3>
<p>단건조회라도 하나의 쿼리로 불러오는게 효율적이라고 생각했다.
하지만 연관 엔티티 수(N) 만큼 쿼리를 실행시키는게 더 효율적인 경우가 있어 
JPA는 N개의 쿼리를 더 실행한다.</p>
<ol>
<li>필드가 많은 경우
모든 연관관계를 포함한 하나의 쿼리는 DB에 부하를 줄 수 있다. 
만약 필드가 많고 연관된 엔티티들이 복잡한 경우, 데이터베이스의 부담이 커질 수 있다.  </li>
</ol>
<ol start="2">
<li>캐시의 효율을 위해
JPA는 따로 설정해주지 않아도 영속성 컨텍스트(Persistence Context)를 통해 엔티티를 관리하고 캐시 기능을 제공한다. 
이 기능으로 자주 사용되는 데이터가 저장되어 성능을 향상시킬 수 있는데 
하나의 쿼리로 JOIN을 통해 모든 연관관계의 정보를 불러온다면 
캐시 메모리의 효율성이 낮아지는 원인이 될 수 있다.</li>
</ol>
<p>물론 연관관계만큼 N개의 쿼리를 더 실행하는 것이 항상 효율적인건 아니다. (ex. N+1)
기타 성능문제로 한번의 쿼리만 실행시키고 싶다면 JPQL이나 EntityManager의 createNativeQuery 메소드 등을 사용해야 한다.</p>
<h3 id="invaliddefinitionexception">InvalidDefinitionException</h3>
<pre><code>에러 내용)

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
No serializer found for 
class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor 
and no properties discovered to create BeanSerializer 

(to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

(through reference chain: com.study.Entity.Team[&quot;manager&quot;]-&gt;
com.study.Entity.Member$HibernateProxy$VjcA2Ldi[&quot;hibernateLazyInitializer&quot;])</code></pre><p>일단 Team의 manager는 Member와 OneToOne 연관관계이다.
멤버아이디를 입력받아 매니저도 설정할 수 있다고 가정했을때, 저런 예외를 마주칠 수 있다.</p>
<p>일단 이 예외가 일어나는 이유는 입력데이터가 팀의 이름과, 매니저의 아이디라고 할 때,
<code>getReferenceById</code>로 Member객체를 가져온다면, 이 객체는 프록시 객체이다.</p>
<p>이 때, JSON 객체로 직렬화할때, 
프록시 객체(Entity.Member$HibernateProxy$VjcA2Ldi)이기 때문에 직렬화를 할 수 없기 때문이다.</p>
<p>그래서 엔티티 자체를 반환하지 않고 DTO 클래스로 변환해서 반환한다면 저런 예외는 일어나지 않는다.</p>
<p>또는 위 에러의 설명처럼 disable SerializationFeature.FAIL_ON_EMPTY_BEANS 처리를 해주거나 <code>getReferenceById</code>이 아니라 <code>findById</code> 등의 메소드를 사용한다면 이 경우에서 저런 예외는 일어나지 않는다.</p>
<h2 id="양방향-연관관계">양방향 연관관계</h2>
<p>위 예시는 Member에서 Address를 단방향으로 참조하고 있었다. 
즉, 주소를 조회하면 그 주소와 1:1 관계인 멤버는 확인할 수 없다는 뜻이다.</p>
<p>그런데 Address를 조회해도 Member에 대한 정보도 조회되게 하고 동시에 그 반대도 가능하게 하고싶다면?</p>
<p><strong>무한 루프에 빠지게 된다.</strong>
(Member의 Address필드 참조 -&gt; Address에서 Member의 참조 -&gt; 반복...)</p>
<p>가장 쉬운 방법은 엔티티에 <code>@JsonIdentityInfo</code> 를 붙여서 객체 식별자를 통해 중복을 방지하는 방법이 있다. 
때문에 @id 정보가 추가된 것을 확인 할 수 있다.
(클라이언트에게 전달할 때는 DTO로 감싸서 보이지 않게 해주자)</p>
<pre><code class="language-java">예시)

@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    int uid;

    // ... 생략

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = &quot;uid&quot;, referencedColumnName = &quot;uid&quot;)
    private Address address;
}

... 

@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
public class Address {

    @Id
    int uid;

    // ... 생략

    @OneToOne(mappedBy = &quot;address&quot;)
    @JoinColumn(name = &quot;uid&quot;, referencedColumnName = &quot;uid&quot;)
    private Member member;
}</code></pre>
<p>무한루프를 방지하지만, Address를 전체조회한다면 N+1 문제가 생긴다.
JPQL의 JOIN FETCH를 사용해서 해결하자</p>
<pre><code class="language-java">@Query(&quot;SELECT a FROM Address a JOIN FETCH a.member&quot;)
List&lt;Address&gt; findAll();</code></pre>
<h2 id="공유-pk-사용">공유 PK 사용</h2>
<p>Member와 Address 각각 uid 라는 공유 PK를 사용할 때 저장 시, 상황에 따라 같은 PK값으로 들어가지 않을 수 도 있다. </p>
<p>실제로 위 코드에서 Member와 Address를 동시에 저장했을때 Member 테이블에는 uid가 1, Address에는 0가 들어가서 데이터 무결성이 깨질 수 있다.
(데이터베이스에서 생성된 시퀸스가 각각의 엔티티마다 +1이 되기 때문이며 Address에는 @GeneratedValue가 선언되지 않았기 때문)</p>
<p>만약 멤버가 저장될 때, 주소가 필수이면서, 테이블이 따로 나뉘어져 저장되게 하고싶다면 Member에서 생성된 같은 값의 PK가 Address에도 들어가게 설정해줘야 한다.</p>
<p>부모의 <strong>PrimaryKeyJoinColumn</strong>은 OneToOne관계에서 PK로 조인되게 하는 옵션이며,
<strong>MapsId</strong>는 PK를 FK로 사용하는 옵션이다.
때문에 @MapsId를 사용할 경우에는 @JoinColumn과 달리 외래키를 생성하는 것이 아니라, 같은 값을 공유하는 기본키를 사용하므로 외래키 제약조건이 생성되지 않을 수 있다.</p>
<p>실제로 데이터베이스에 저장될 때는 Address 엔티티에서 @Id로 선언한 uid가 아니라 JPA 구현체에서 자동으로 생성하는 외래 키 이름 규칙에 따라서 member_uid로 저장된다.
저장 시 양방향 연관관계를 가지고 있음으로 Member의 필드인 Address에도 Member를 설정해줘야 한다.</p>
<h2 id="결과">결과</h2>
<pre><code class="language-java">@Entity
@Getter
@Table(name = &quot;Member&quot;)
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    int uid;

    String name;
    char gender;
    String hp_no;
    String email;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @PrimaryKeyJoinColumn
    private Address address;
}

@Entity
@Getter
@Table(name = &quot;Address&quot;)
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
public class Address {
    @Id
    int uid;

    String country;
    String city;
    String detail;

    @OneToOne(mappedBy = &quot;address&quot;)
    @MapsId
    private Member member;

    public void setMember(Member member) {
        this.member = member;
    }
}

// service
public Member saveMember(Member member){
    member.getAddress().setMember(member); 
    return memberRepo.save(member);
}</code></pre>
<ul>
<li>멤버 조회 결과<pre><code class="language-js">{
&quot;uid&quot;: 1,
&quot;name&quot;: &quot;Karina&quot;,
&quot;gender&quot;: &quot;F&quot;,
&quot;hp_no&quot;: &quot;010-1111-2222&quot;,
&quot;email&quot;: &quot;karina@aespa.com&quot;,
&quot;address&quot;: {
  &quot;uid&quot;: 1,
  &quot;country&quot;: &quot;Korea&quot;,
  &quot;city&quot;: &quot;Seoul&quot;,
  &quot;detail&quot;: &quot;Yongsan-gu&quot;,
  &quot;member&quot;: 1
}
}</code></pre>
</li>
<li>멤버 조회시 실행되는 쿼리<pre><code class="language-sql">SELECT member0_.uid         as uid1_1_0_,
     address1_.member_uid as member_u4_0_1_,
     member0_.email         as email2_1_0_,
     member0_.gender         as gender3_1_0_,
     member0_.hp_no         as hp_no4_1_0_,
     member0_.name         as name5_1_0_,
     member0_.team_id     as team_id6_1_0_,
     address1_.city         as city1_0_1_,
     address1_.country     as country2_0_1_,
     address1_.detail     as detail3_0_1_ 
FROM Member member0_ 
INNER 
JOIN Address address1_ 
  ON member0_.uid = address1_.member_uid</code></pre>
</li>
<li>주소 조회 결과<pre><code class="language-js">{
&quot;uid&quot;: 1,
&quot;country&quot;: &quot;Korea&quot;,
&quot;city&quot;: &quot;Seoul&quot;,
&quot;detail&quot;: &quot;Yongsan-gu&quot;,
&quot;member&quot;: {
  &quot;uid&quot;: 1,
  &quot;name&quot;: &quot;Karina&quot;,
  &quot;gender&quot;: &quot;F&quot;,
  &quot;hp_no&quot;: &quot;010-1111-2222&quot;,
  &quot;email&quot;: &quot;karina@aespa.com&quot;,
  &quot;address&quot;: 1
    }
}</code></pre>
</li>
<li>주소 조회시 실행되는 쿼리<pre><code class="language-sql">SELECT address0_.member_uid as member_u4_0_0_,
     member1_.uid         as uid1_1_1_,
     address0_.city         as city1_0_0_,
     address0_.country     as country2_0_0_,
     address0_.detail     as detail3_0_0_,
     member1_.email         as email2_1_1_,
     member1_.gender         as gender3_1_1_,
     member1_.hp_no         as hp_no4_1_1_,
     member1_.name         as name5_1_1_,
FROM Address address0_ 
INNER
JOIN Member member1_ 
  ON address0_.member_uid = member1_.uid</code></pre>
</li>
</ul>
<p><code>&quot;member&quot;: 1</code>, <code>&quot;address&quot;: 1</code> 각각의 의미는 연관관계에 있는 엔티티의 PK이다.</p>
<blockquote>
<p>공유PK 를 사용하지 않는 Team의 manager_id와 Member의 uid는 
Team -&gt; Member 단방향으로 설정되어도 무방함으로 </p>
</blockquote>
<pre><code class="language-java">@Entity
public class Team {
    //... 생략    
    @OneToOne
    @JoinColumn(name = &quot;manager_id&quot;)
    Member manager;
}</code></pre>
<p>팀 엔티티에만 설정하도록 하자</p>
<h1 id="onetomany--manytoone">OneToMany / ManyToOne</h1>
<p>1:N 관계에는 한 팀은 여러멤버를 가지고 한 멤버는 한 팀을 가지는 멤버-팀의 관계가 대표적이다.
이 경우 Team에서 Member에 대해서 OneToMany, Member에서 Team에 대해서 ManyToOne관계가 된다.</p>
<pre><code class="language-java">@Entity
@Getter
public class Team {
    @Id
    @Column(name = &quot;team_id&quot;)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    int id;

    // ... 생략

    @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.EAGER)
    @PrimaryKeyJoinColumn
    List&lt;Member&gt; members;
}


@Entity
@Getter
@Table(name = &quot;Member&quot;)
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    int uid;

    // ... 생략
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @PrimaryKeyJoinColumn
    Address address;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;
}</code></pre>
<p>위 처럼 OneToMany / ManyToOne 양방향 관계이기 때문에 서로 함께 설정해야 한다.</p>
<h2 id="주의점">주의점</h2>
<h3 id="의도하지-않은-자식-엔티티의-삭제">의도하지 않은 자식 엔티티의 삭제</h3>
<p>위와 같은 경우엔, 팀이 없는 멤버가 존재할 수 있음으로 팀 삭제 시, 멤버가 삭제가 되면 안된다.
CascadeType을 ALL, 또는 REMOVE 등으로 설정 하면 의도치 않은 Member 엔티티의 삭제가 일어날 수 있으므로 영속성 전이 옵션에 대해서 알고 넘어가야 한다.</p>
<h3 id="외래키-값의-관리-중간-테이블-생성-방지">외래키 값의 관리 (중간 테이블 생성 방지)</h3>
<p>ManyToOne 관계에서 외래키를 관리하는 주체는 Many 쪽이다. <a href="https://wonin.tistory.com/476">참고</a></p>
<p><strong>ManyToOne</strong> 쪽에서 외래 키를 설정하고, 연관 엔티티를 저장하기 위해 
<strong>OneToMany</strong> 쪽에서 mapped By로 주인 엔티티를 명시해줘서 외래키를 제대로 설정해줘야 한다.
외래키 설정이 제대로 설정하지 않았다면, 의도치 않은 중간테이블이 생길 수 있다.</p>
<p>위 예시의 경우 Member테이블에서 @JoinColumn(name = &quot;team_id&quot;) 으로
Team테이블의 team_id 컬럼을 참조하는 외래키를 만들 수 있다.</p>
<h3 id="n1-문제">N+1 문제</h3>
<p>위 예시의 경우
Team을 조회할 때 연관된 Member 엔티티들도 EAGER 로딩 설정에 의해 함께 조회된다.
이때 각각의 Member 엔티티의 team 필드를 접근할 때, LAZY 로딩 설정에 의해 Member의 수(N) 번의 Team 조회 쿼리가 추가로 실행될 수 있다.
즉 1번의 Team 조회, Member의 수(N) 가 따로 쿼리를 실행시켜 조회된다.</p>
<p>이 경우를 FETCH JOIN 을 통해 해결했다고 가정한다면</p>
<pre><code class="language-java">ex)

@Query(&quot;SELECT t FROM Team t JOIN FETCH t.members WHERE t.id = :id&quot;)
Team findByIdWithMembers(@Param(&quot;id&quot;) int id);</code></pre>
<p>이젠 EAGER로 설정된 Address 필드때문에 N+1 문제가 발생할 수 있다.
(멤버수 만큼의 address필드 조회(N) + FETCH JOIN으로 한번의 쿼리)
address필드를 LAZY로 바꿔도 마찬가지였다.</p>
<p>또한 계속 findByIdWithMembers 메소드로 조회한다면 N+1 문제가 발생한다.
JOIN FETCH 로 참조된 멤버 엔티티(t.members)의 각 필드의 로딩 전략으로 
주소를 조회하는 쿼리를 실행시키기 때문이다.</p>
<p>로딩전략을 수정하여 위 FETCH JOIN을 사용하지 않으면서
<code>List&lt;Member&gt; members</code> 필드를 <strong>Lazy</strong>로 변경한다면 N+1 문제를 수정할 수 있어보인다. 하지만 members 필드를 사용해야만 한다면 Address 엔티티에 대한 N+1 문제가 해결되지 않는다.</p>
<p>이 가정들을 토대로 문제를 정리해보면</p>
<ol>
<li>멤버들의 주소 필드는 사용되지 않는다.</li>
<li>하나의 팀을 조회 시 팀에 속한 멤버들의 일부 정보(이름)을 반환하기 위해 불러와야한다.</li>
</ol>
<p>1번 문제를 해결하기 위해 가장 쉽게 생각할 수 있는 방법은 FETCH JOIN으로 
Address 엔티티까지 같이 불러오는 방법이 떠오른다.
N+1 문제는 해결할 수 있지만 상황에 따라 사용되지 않는 테이블이 조인된다면 부담이 될 수 있다.</p>
<pre><code class="language-java">@Query(&quot;SELECT t &quot; +
       &quot;  FROM Team t &quot; +
       &quot;  JOIN FETCH t.members m &quot; +
       &quot;  LEFT JOIN FETCH m.address a &quot; +
       &quot; WHERE t.id = :id&quot;)
Team findByIdWithMembers(@Param(&quot;id&quot;) int id);</code></pre>
<p>또한 Member 필드를 사용하면(2번 문제) OneToOne 관계인 address를 
<strong>LAZY로 변경해도 N+1문제를 해결할 수 없다.</strong></p>
<p>이 문제는 근본적으로 해결할 수가 없다.
지연 로딩이 동작하는 방식은 LAZY 전략으로 설정되어있는 엔티티를 프록시 객체로 가져온다. 
하지만 프록시 객체는 Null을 담을 수 없기 때문에 EAGER 방식으로 동작한다. <a href="https://1-7171771.tistory.com/143">참고</a></p>
<p>그럼 위 JPQL을 최적화를 해야될 것 같다.
성능적인 측면에서 Address 테이블을 조인으로 조회하기 부담스럽고, 아래 DTO로 반환해야 한다고 가정했을때,</p>
<pre><code class="language-java">// DTO
public class TeamResponse {
    private String name;
    private MemberResponse manager;
    private List&lt;String&gt; memberNames;
}</code></pre>
<p>아래와 같이 수정하여 모든 멤버들의 주소 정보를 조회하는 N+1문제를 해결했다.</p>
<pre><code class="language-java">// service
@Transactional
public TeamResponse getTeam(int id) {

    // members에 대한 로딩 전략을 LAZY로 변경
    Team team = repo.findById(id)
        .orElseThrow(TeamNotFoundException::new);

    MemberResponse manager = new MemberResponse();
    manager.setName(team.getManager().getName());
    // ... 생략

    TeamResponse res = new TeamResponse();
    res.setName(team.getName());
    res.setManager(manager);
    res.setMemberNames(repo.findNamesByTeamId(id)); // 이름만 불러옴

    return res;
}

// findNamesByTeamId
@Query(&quot;SELECT m.name &quot; +
       &quot;  FROM Member m &quot; +
       &quot; WHERE m.team.id = :id &quot;)
List&lt;String&gt; findNamesByTeamId(@Param(&quot;id&quot;) int id);</code></pre>
<p>N+1 문제가 해결되고 실행되는 쿼리는 아래와 같다.</p>
<pre><code class="language-sql">// Team 조회
 SELECT team0_.team_id         as team_id1_2_0_,
        team0_.manager_id     as manager_3_2_0_,
        team0_.name         as name2_2_0_,
        member1_.uid         as uid1_1_1_,
        member1_.email         as email2_1_1_,
        member1_.gender     as gender3_1_1_,
        member1_.hp_no         as hp_no4_1_1_,
        member1_.name         as name5_1_1_,
        member1_.team_id     as team_id6_1_1_ 
   FROM Team team0_ 
   LEFT 
  OUTER
   JOIN Member member1_ 
     ON team0_.manager_id = member1_.uid 
  WHERE team0_.team_id = ? 

 // Team의 manager(OneToOne)의 address 필드 조회
 SELECT address0_.member_uid as member_u4_0_0_,
        address0_.city          as city1_0_0_,
        address0_.country      as country2_0_0_,
        address0_.detail      as detail3_0_0_,
        member1_.uid          as uid1_1_1_,
        member1_.email          as email2_1_1_,
        member1_.gender      as gender3_1_1_,
        member1_.hp_no          as hp_no4_1_1_,
        member1_.name          as name5_1_1_,
        member1_.team_id      as team_id6_1_1_ 
   FROM Address address0_ 
  INNER
   JOIN Member member1_ 
        ON address0_.member_uid = member1_.uid 
  WHERE address0_.member_uid = ?

// 필요한 이름 목록만 조회
 SELECT member0_.name as col_0_0_ 
   FROM Member member0_ 
  WHERE member0_.team_id = ?</code></pre>
<p>Team, Member (manager 필드) JPA의 영속성 컨텍스트에 저장될 수 있다.
때문에 필요한 경우 캐시에서 가져와서 사용할 수도 있어 성능상의 이점도 있다.</p>
<p>(또는 DTO로 직접 프로젝션해서 하는 방법도 있을 수 있겠다. <a href="https://thalals.tistory.com/359">참고</a>)</p>
<h1 id="manytomany">ManyToMany</h1>
<p>멤버들 스킬과 스킬은 ManyToMany 관계라고 할 수 있다.
그럼 ManyToMany로 연결하는게 편리해 보이지만 ManyToMany는 권장되지 않는다.</p>
<h2 id="manytomany가-권장되지-않는-이유"><em>ManyToMany가 권장되지 않는 이유</em></h2>
<p>RDBMS의 특성 상 중간테이블이 생성될 수 밖에 없다.
중간테이블이 생성되면 JPA를 활용하면서 여러 곤란한 단점들이 드러난다.</p>
<h3 id="자동으로-생성되는-중간-테이블의-제약">자동으로 생성되는 중간 테이블의 제약</h3>
<p>자동으로 생성되는 중간 테이블은 두 엔티티(테이블)간의 외래키만 가지고 있다.
만약 위 예시 테이블 구조에서 Skill 테이블에 다른 쿨타임, 레벨 등의 컬럼 등을
<strong>중간 테이블에 추가하기 어렵거나 불가능</strong>하다.</p>
<p>또한, 엔티티가 아닌 오직 <strong>DB에만 존재하는 중간 테이블</strong>이기 때문에,
중간 테이블에 특정 조건에 따라 로우를 추가/수정/삭제하려는 경우에는 
<strong>별도의 로직을 구현</strong>해야 하기 때문에 확장성이 떨어진다고 할 수 있다.</p>
<h3 id="실행되는-쿼리의-복잡성">실행되는 쿼리의 복잡성</h3>
<p>중간 테이블을 거치기 때문에 엔티티의 조회 쿼리가 복잡해진다.
만약 비교적 많은 데이터를 조회하거나 심지어 특정 멤버의 스킬들이나,
특정 스킬을 가진 멤버를 조회할때도 비교적 조인되는 테이블이 많아지고
곧 성능의 영향을 미친다.</p>
<p>또한 성능 최적화 등의 이유로 작성한 관련 JPQL 모두 수정해야할 여지가 있기에 
유지보수 측면에서도 단점을 보인다.</p>
<h3 id="데이터의-일관성정합성-문제">데이터의 일관성/정합성 문제</h3>
<p>중간 테이블이 자동으로 생성되는 쿼리는 아래와 같다.</p>
<pre><code class="language-sql">CREATE TABLE Member_Skill (
    members_uid Integer NOT NULL,
    Skill_code  Integer NOT NULL) engine = InnoDB

ALTER TABLE Member_Skill
    ADD CONSTRAINT FKe2tlol3lhtiyer8ypa4u6sw2q 
    FOREIGN KEY (members_uid) REFERENCES Member (uid)

ALTER TABLE Member_Skill
    ADD CONSTRAINT FKdl4pohd1cgckcn89nn7ev7qaa 
    FOREIGN KEY (Skill_code) REFERENCES Skill (code)</code></pre>
<p>이 경우 여러 문제가 발생할 여지가 생긴다.</p>
<h4 id="중복-데이터-문제">중복 데이터 문제</h4>
<p>복합키(PK)까지 자동으로 생성하지 않기 때문에 <strong>중복된</strong>
(같은 members_uid, Skill_code 컬럼) 데이터가 들어갈 수 있다.</p>
<h4 id="데이터-불일치">데이터 불일치</h4>
<p>Member와 Skill테이블에는 자동으로 생성된 Member_Skill 테이블에 대해
<strong>어떤 제약조건도 생성되지 않는다.</strong> (중간 테이블 Member_Skill에만 각 컬럼에 외래키가 생성될 뿐)</p>
<p>Member와 Skill테이블에 컬럼이 수정/삭제 된다면 
중간 테이블까지 수정되지 않음으로 <strong>데이터의 정합성 문제</strong>가 생길 수 있다.</p>
<p>이 경우 중간 테이블의 정합성을 맞춰주는 로직이 요구되며 
<strong>확장성이 떨어지는 원인</strong>이 된다.</p>
<hr>
<p>그래도 굳이 위 Member - Skill - Skill_Code 의 관계를 ManyToMany로 표현하고 싶다면 아래와 같이 설정할 수 있다. (위 테이블 설계 예시와 같다.)</p>
<pre><code class="language-java">public class Member {
    // ... 생략 

    @ManyToMany
    @JoinTable(name = &quot;Skill&quot;, // 중간테이블 이름
        joinColumns        = @JoinColumn(name = &quot;uid&quot;),
        inverseJoinColumns = @JoinColumn(name = &quot;skill_code&quot;))
    private List&lt;Skill&gt; skills;
}


public class Skill {
    @Id
    private int code;

    private String name;

    @ManyToMany(mappedBy = &quot;skills&quot;)
    private List&lt;Member&gt; members;
}</code></pre>
<p>그래도 위 중복 데이터 문제를 방지하기 위해 <code>uniqueConstraints</code> 옵션을 사용할 수 있다.</p>
<pre><code class="language-java">@ManyToMany
@JoinTable(name = &quot;Skill&quot;,
    joinColumns        = @JoinColumn(name = &quot;uid&quot;),
    inverseJoinColumns = @JoinColumn(name = &quot;skill_code&quot;),
    uniqueConstraints  = @UniqueConstraint(columnNames = {&quot;uid&quot;, &quot;skill_code&quot;}))
private List&lt;Skill&gt; skills;</code></pre>
<p><em>uniqueConstraints</em> 로 uid, skill_code 컬럼에 UNIQUE제약조건을 추가하여 중복 데이터 문제를 방지한다.</p>
<p>중복 데이터가 생기는 로직 실행 시 <code>ConstraintViolationException</code> 예외가 발생한다.</p>
<blockquote>
</blockquote>
<p>비슷하게 <code>(cascade = CascadeType.ALL)</code> 추가해서 데이터 불일치 현상을 방지할수 있어 보인다. 
하지만 중간 테이블은 DB에만 존재한다.<strong><em>(엔티티로써 관리되지 않는다는 뜻)</em></strong></p>
<blockquote>
</blockquote>
<p>때문에 데이터 일관성을 보장할 수 없기에 사실은 별도의 로직이 필요하다.</p>
<h2 id="다른-관계로-설계변경">다른 관계로 설계변경</h2>
<p>Member - Skill(중간테이블) - Skill_Code 구조처럼 ManyToMany를 고집하기 보단
<strong>중간 테이블을 하나의 엔티티</strong>로써 활용할 수도 있겠다.</p>
<p>그렇다면 <strong><code>Member(One) - (Many)Skill(Many) - (One)Skill_Code</code></strong> 구조로 OneToMany - ManyToOne 관계로 설계하는 방법이 성능/확장성/유지보수 측면에서 더 이점이 있다고 할 수 있다.
<del><em>(그래서 ManyToMany는 권장되지 않나보다...)</em></del></p>
<hr>
<p>또는 위 테이블 구조 예시의 Member - Vacation 관계처럼 
복합키를 이용한 관계로 수정하는 방법도 고려할 수 있다.</p>
<p>휴가와 멤버의 연관관계를 생각해보면 </p>
<ul>
<li>특정 휴가 기간에 여러 멤버가 갈 수 있다.</li>
<li>한 멤버도 여러번 휴가를 갈 수 있다.</li>
</ul>
<p>이 관계는 ManyToMany로도 구현이 가능할 것 같지만, 복합키를 이용한 구조로 설계하여 확정성과 추가적인 컬럼도 선언할 수 있다.</p>
<h2 id="복합키-생성">복합키 생성</h2>
<h3 id="jpa에서-복합키를-생성하는-방법">JPA에서 복합키를 생성하는 방법</h3>
<h4 id="1-idclass를-사용하는-방법">1. @IdClass를 사용하는 방법</h4>
<pre><code class="language-java">// Serializable를 구현하는 PK 클래스 구현

@Data
@NoArgsConstructor
public class VacationKey implements Serializable {
    private Integer vacationId;
    private Integer uid;
}</code></pre>
<pre><code class="language-java">// Entity 구현

@Entity
@IdClass(VacationKey.class)
public class Vacation {

    @Id
    @Column(name = &quot;vacation_id&quot;)
    private Integer vacationId;

    @Id
    private Integer uid;

    // ... 생략
}</code></pre>
<h4 id="2-embeddable를-사용하는-방법">2. @Embeddable를 사용하는 방법</h4>
<pre><code class="language-java">@Data
@NoArgsConstructor
@Embeddable
public class VacationKey implements Serializable {
    private int vacationId;
    private int uid;
}</code></pre>
<pre><code class="language-java">@Entity
public class Vacation {

    @EmbeddedId
    private VacationKey vacationPK;

    // ... 생략
}</code></pre>
<h4 id="3-두가지-방법의-차이는">3. 두가지 방법의 차이는?</h4>
<p><strong>공통점</strong></p>
<ul>
<li>복합키 클래스를 Serializable 인터페이스로 구현해야 한다.</li>
<li>equals, hashCode 메소드가 Override 되어야 한다.</li>
<li>기본 생성자를 구현해야 한다.</li>
</ul>
<p><strong>@IdClass</strong></p>
<ul>
<li>비교적 RDBMS에 더 친화적이다.</li>
<li>엔티티 클래스에 <code>@IdClass(클래스명.class)</code> 어노테이션을 지정해줘야 한다.</li>
<li>엔티티 클래스의 PK로 사용될 필드들이 동일하게 선언 되어야한다.</li>
</ul>
<p><strong>@EmbeddedId</strong></p>
<ul>
<li>비교적 OOP에 더 친화적이다.
(때문에 KEY 자체가 재사용될 경우 더 적절하다)</li>
<li>복합키 클래스에 <code>@Embeddable</code> 어노테이션을 지정해줘야 한다.</li>
<li>엔티티 클래스는 복합키 클래스 자체를 필드로 가지며
<code>@EmbeddedId</code> 어노테이션을 지정해줘야 한다.</li>
</ul>
<h3 id="복합키-클래스는-왜-serializable를-구현해야-하는가">복합키 클래스는 왜 Serializable를 구현해야 하는가</h3>
<blockquote>
<p>인터페이스인 Serializable은 객체의 상태를 바이트 스트림으로 변환(직렬화),
바이트 스트림을 객체의 상태로 복원(역직렬화)하는 메커니즘을 제공한다.</p>
</blockquote>
<p>직렬화 참고 : <a href="https://velog.io/@cv_/%EC%9E%90%EB%B0%94%EC%9D%98-%EC%A7%81%EB%A0%AC%ED%99%94">https://velog.io/@cv_/자바의-직렬화</a></p>
<p>영속성 컨텍스트는 엔티티의 ID를 사용해서 엔티티를 관리한다.
다시 말해, <strong>엔티티의 상태를 추적하고 영속화하기 위해 엔티티의 식별자(ID)를 사용</strong>한다.
때문에 JPA가 사용하는 객체는 직렬화가 가능해야 하며 단일 식별자일 경우 기본적으로 직렬화할 수 있는 타입(Primitive Type 또는 자바에서 제공하는 타입)이 들어가기 때문에 굳이 Serializable를 구현할 필요는 없다.</p>
<p>하지만 복합키는 말 그대로 여러개의 식별자 필드를 가지고 있다는 뜻이고 JPA가 관리를 위해 식별하기 위해서는 복합키 클래스 자체가 직렬화 되어 메모리에 올라갈 수 있어야되기 때문이다.</p>
<blockquote>
<p>참고 목록</p>
</blockquote>
<p><a href="https://stackoverflow.com/questions/9271835/why-composite-id-class-must-implement-serializable">https://stackoverflow.com/questions/9271835/why-composite-id-class-must-implement-serializable</a></p>
<blockquote>
</blockquote>
<p><a href="https://stackoverflow.com/questions/3585034/how-to-map-a-composite-key-with-jpa-and-hibernate#answer-3588400">https://stackoverflow.com/questions/3585034/how-to-map-a-composite-key-with-jpa-and-hibernate#answer-3588400</a></p>
<blockquote>
</blockquote>
<p><a href="https://www.inflearn.com/questions/16570/%EB%B2%84%EA%B7%B8-%EB%AC%B8%EC%9D%98%EB%93%9C%EB%A0%A4%EB%B4%85%EB%8B%88%EB%8B%A4">https://www.inflearn.com/questions/16570/버그-문의드려봅니다</a></p>
<h4 id="복합키-클래스의-equals-hashcode-메소드">복합키 클래스의 equals, hashCode 메소드</h4>
<p>앞서 말했듯 JPA의 영속성 컨텍스트는 엔티티의 ID(데이터베이스의 PK)를 사용해서 엔티티를 관리한다.
당연히 ID를 사용해서 동등성을 비교하고, 데이터베이스 연산(CRUD)에 필요하기 때문일 것이다.</p>
<p><strong>equals</strong> 메서드는 두 객체가 동일한 값을 가지는지 비교한다.
복합키일 경우 모든 PK를 비교해야 하는데 구현하지 않는다면 동등성 비교 결과가 다르게 나올 수 있다.</p>
<p><strong>hashCode</strong> 메서드는 객체의 해시 코드를 반환한다. 
이 메서드를 구현하지 않으면 복합키를 사용하는 엔티티에서 해시 기반의 자료 구조(HashMap이나 HashSet 등)에서 값이 다르게 도출된다면 문제가 발생할 가능성이 있다.</p>
<p>결국 구현하지 않으면 값이 같더라도 동등성 비교 결과가 다르다고 나올 수 있는 큰 문제의 발생 가능성 때문에 필수로 구현해야 한다고 설명한다.
(단일 키일 경우 Integer, Long 등의 일반 타입은 내부에 이미 내부에 구현이 되어있다.)</p>
<p><em>테스트</em></p>
<pre><code class="language-java">@Test
@DisplayName(&quot;실제로는 필드값이 모두 동일할 경우 엔티티들을 동등한 것으로 인식해야 한다.&quot;)
void CompareCompositeIdAndEntity() {
    VacationKey key1 = new VacationKey(1, 1);
    VacationKey key2 = new VacationKey(1, 1);

    Member testmember = memberRepo.findAll().get(0);

    Vacation vacation1 = new Vacation();
    vacation1.setVacationId(1);
    vacation1.setMember(testmember); 
    // 기타 필드 생략..

    Vacation vacation2 = new Vacation();
    vacation2.setVacationId(1);
    vacation2.setMember(testmember); 
    // 기타 필드 생략..

    assertThat(key1.equals(key2)).isTrue();
    assertThat(vacation1.equals(vacation2)).isTrue();

    assertEquals(key1.hashCode(), key2.hashCode());
    assertEquals(vacation1.hashCode(), vacation2.hashCode());
}</code></pre>
<h4 id="왜-꼭-명시적으로-override-해야될까">왜 꼭 명시적으로 Override 해야될까</h4>
<p>기본적으로 equals, hashCode 메서드는 참조 동등성(Reference Equality)를 기반으로 동작한다. 
즉, 객체의 참조 주소가 동일할 때만 동일한 객체로 간주한다.</p>
<pre><code class="language-java">// 참조 동등성 간단 비교 예시)
Vacation vacation1 = new Vacation();
vacation1.setVacationId(1);
vacation1.setUid(100);

Vacation vacation2 = new Vacation();
vacation2.setVacationId(1);
vacation2.setUid(100);

System.out.println(vacation1.equals(vacation2)); // 결과 : false
System.out.println(vacation1.hashCode()); // 결과 : 1728445186
System.out.println(vacation2.hashCode()); // 결과 : 237410024

// 같은 값을 내포하고 있어도 참조 주소가 다르다.</code></pre>
<p>하지만 실제로는 동일한 값을 가진 복합키 객체들을 동등한 것으로 인식해야 한다.</p>
<h4 id="어떻게-override-해야하는가">어떻게 Override 해야하는가</h4>
<ol>
<li>자기 자신인지 비교한다.</li>
<li>proxy 객체를 고려하여 실제 클래스 타입을 비교한다.
(Lazy FetchType 또는 getReferenceById 메소드로 인한 proxy 객체)</li>
<li>실제 PK 값을 비교한다.</li>
</ol>
<p>다음은 인텔리제이의 JPA-Buddy 플러그인이 자동으로 Override 해준 코드이다.</p>
<p><strong>@IdClass 방식</strong></p>
<pre><code class="language-java">@Getter 
@Setter
public class VacationKey implements Serializable {

    private int vacationId;
    private int uid;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
        VacationKey that = (VacationKey) o;
        return Objects.equals(vacationId, that.vacationId)
            &amp;&amp; Objects.equals(getUid(), that.getUid());
    }

    @Override
    public int hashCode() {
        return Objects.hash(vacationId, uid);
    }
}</code></pre>
<pre><code class="language-java">@Entity
@IdClass(VacationKey.class)
public class Vacation {

    @Id
    @Column(name = &quot;vacation_id&quot;)
    private int vacationId;

    @Id
    private int uid;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
        Vacation vacation = (Vacation) o;
        return Objects.equals(getVacationId(), vacation.getVacationId())
            &amp;&amp; Objects.equals(getUid(), vacation.getUid());
    }

    @Override
    public int hashCode() {
        return Objects.hash(vacationId, uid);
    }</code></pre>
<p><strong>@Embeddable 방식</strong></p>
<pre><code class="language-java">@Data
@Embeddable
public class VacationKey implements Serializable {

    @Column(name = &quot;vacation_id&quot;)
    private int vacationId;

    private int uid;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
        VacationKey that = (VacationKey) o;
        return Objects.equals(vacationId, that.vacationId)
            &amp;&amp; Objects.equals(getUid(), that.getUid());
    }

    @Override
    public int hashCode() {
        return Objects.hash(vacationId, uid);
    }
}</code></pre>
<pre><code class="language-java">@Entity
public class Vacation {

    @EmbeddedId
    private @Getter @Setter VacationKey key;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
        Vacation vacation = (Vacation) o;
        return getKey() != null &amp;&amp; Objects.equals(getKey(), vacation.getKey());
    }

    @Override
    public int hashCode() {
        return Objects.hash(key);
    }
}</code></pre>
<p><strong>테이블 생성 쿼리</strong></p>
<pre><code class="language-sql">Hibernate:
CREATE TABLE Vacation (
    uid         INTEGER not null,
    vacation_id INTEGER not null,
    PRIMARY KEY (uid, vacation_id)
) engine=InnoDB</code></pre>
<blockquote>
<p>엔티티에 직접 equals, hashcode 메소드를 Overrride 할 때
주의점과 문제점에 대해서는 아래 포스트를 참고</p>
</blockquote>
<p><a href="https://jaeseo0519.tistory.com/m/153">https://jaeseo0519.tistory.com/m/153</a></p>
<blockquote>
</blockquote>
<p><a href="https://velog.io/@nmrhtn7898/JPA-Entity%EC%97%90%EC%84%9C-equals-hashcode-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%A0%90#%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%A0%90">https://velog.io/@nmrhtn7898/JPA-Entity에서-equals-hashcode-사용시-발생할-수-있는-문제점#발생할-수-있는-문제점</a></p>
<h3 id="복합키를-사용하면-generatevalue를-사용할-수-없다">복합키를 사용하면 @GenerateValue를 사용할 수 없다.</h3>
<p><code>@SequenceGenerator</code>도 사용할 수 없다.
<code>@GenerateValue</code>는 단일키를 위한 어노테이션이다.
어떤 방법을 다 써봐도 저장 시 null값(primitive type일 경우 0)이 들어가서 <strong>데이터 무결성이 깨지거나</strong> <code>Column &#39;vacation_id&#39; cannot be null</code> 에러가 난다.</p>
<p><strong>그러므로 vacation_id의 값을 구하는 로직을 직접 구현해야한다.</strong></p>
<p><em>@IdClass 구현예시)</em></p>
<pre><code class="language-java">@Entity
@Getter
@Setter
@NoArgsConstructor
@IdClass(VacationKey.class)
public class Vacation {

    @Id
    @Column(name = &quot;vacation_id&quot;)
    private Integer vacationId;

    @Id
    @ManyToOne
    @JoinColumn(name = &quot;uid&quot;)
    private Member member;

    // ... 생략

    public Vacation(Integer vacationId, Member member){
        this.vacationId = vacationId;
        this.member = member;
    }

    // ... 생략

}</code></pre>
<pre><code class="language-java">@Transactional
public Vacation save(int id) throws MemberNotFoundException {
    Member requestedMember = memberRepo.findById(id)
        .orElseThrow(MemberNotFoundException::new);

    Vacation vacation = new Vacation(getVacationId(requestedMember), requestedMember);
    // .. 생략

    return repo.save(vacation);
}

// Member로 조회 후 최대값 + 1을 삽입해준다.
private int getVacationId(Member requestedMember) {
    List&lt;Vacation&gt; result = repo.findByMember(requestedMember);

    return result.stream()
        .map(Vacation::getVacationId)
        .max(Integer::compareTo)
        .orElse(0) + 1;
}</code></pre>
<p><code>@EmbeddedId</code> 방식도 마찬가지이다.</p>
<p><em>@EmbeddedId 구현예시)</em></p>
<pre><code class="language-java">@Entity
@Getter
@Setter
@ToString
@RequiredArgsConstructor
public class Vacation {

    @EmbeddedId
    private VacationKey key;

    @ManyToOne
    @JoinColumn(name = &quot;uid&quot;, insertable = false, updatable = false)
    private Member member; // VacationKey의 uid와 mapping

    // ... 생략

    public Vacation(EmbeddedVacationKey key) {
        this.key = key;
    }

    public int getVacationId() {
        return this.key.getVacationId();
    }

    // ... 생략
}</code></pre>
<pre><code class="language-java">public VacationResponse save(int id) throws MemberNotFoundException {
    Member requestedMember = memberRepo.findById(id)
                .orElseThrow(MemberNotFoundException::new);

    VacationKey key = new VacationKey(getVacationId(requestedMember), id);

    Vacation vacation = new Vacation(key);
    vacation.setMember(requestedMember);

    repo.save(vacation);

    return toResponse(requestedMember, vacation);
}</code></pre>
<p><em>(RDBMS는 보통 단일키 일때만 AUTO_INCRESMENT 속성을 부여할 수 있는 것과 비슷한 이유로 추정된다.)</em></p>
<h3 id="다른-엔티티를-참조할-때-복합키-클래스의-직렬화-문제">다른 엔티티를 참조할 때, 복합키 클래스의 직렬화 문제</h3>
<p>복합키 중 다른 엔티티를 참조하는 PK가 있을 때, </p>
<pre><code class="language-java">// 복합키 클래스 예시)
public class VacationKey implements Serializable {
    private Integer vacationId;
    private Member member;
}</code></pre>
<p>이 경우 Member 객체도 직렬화 대상이 되기 때문에 성능에 악영향을 끼칠 수 있다.
(<em>객체 그래프 탐색 시 복잡성 증가, 직렬화 데이터 자체의 크기 증가</em>)
또한 객체 지향 관점에서 복합키 클래스가 Member 엔티티를 직접 참조함으로써 <strong>단일 책임 원칙</strong>을 위반하는 것이 아닌가 의심이 들었다.</p>
<p><a href="https://stackoverflow.com/questions/65979298/jpa-composite-primary-key-with-reference-chain">이 글</a>을 보면 Compilation 클래스의 ID인 <code>private User userId;</code> 필드와 CompilationID 복합키 클래스의 <code>private int userId;</code> 필드가 일치 하지 않는 것을 볼 수 있다. (그럼에도 잘 동작한다.)</p>
<p>응용해보면</p>
<pre><code class="language-java">@Getter
@Setter
public class VacationKey implements Serializable {
    private int vacationId;
    private int member;

    // ... 생략
}</code></pre>
<pre><code class="language-java">@Entity
@IdClass(VacationKey.class)
public class Vacation {

    @Id
    @Column(name = &quot;vacation_id&quot;)
    private Integer vacationId;

    @Id
    @ManyToOne
    @JoinColumn(name = &quot;uid&quot;)
    private Member member;

    // ... 생략
}</code></pre>
<pre><code class="language-java">@Transactional
public VacationResponse testsave(int id) throws MemberNotFoundException {
    Member requestedMember = memberRepo.findById(id)
                .orElseThrow(MemberNotFoundException::new);

    Vacation vacation = new Vacation();
    vacation.setMember(requestedMember);
    vacation.setVacationId(getVacationId(requestedMember));

    repo.save(vacation);

    return toResponse(requestedMember, vacation); // DTO 변환 로직
}</code></pre>
<p>이런 방식으로 구현하는 방법도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수형 프로그래밍]]></title>
            <link>https://velog.io/@cv_/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</link>
            <guid>https://velog.io/@cv_/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</guid>
            <pubDate>Sat, 22 Apr 2023 06:40:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>자바8의 변경사항 및 새로운 기능을 정리하다가 함수형 프로그래밍에 대해 간단하게 짚고 넘어갈 필요성을 느꼈다.</p>
<p><a href="https://velog.io/@cv_/Java8-%EB%B6%80%ED%84%B0-%EC%82%AC%EC%9A%A9%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%AC%B8%EB%B2%95%EA%B3%BC-%EA%B8%B0%EB%8A%A5">https://velog.io/@cv_/Java8-부터-사용가능한-문법과-기능</a></p>
</blockquote>
<p><strong>함수형</strong>에서 함수는 말 그대로 수학에서 사용되는 함수처럼 입력값을 받고 출력값을 반환하는 함수이다.
함수형 프로그래밍에서는 이 함수를 중심으로 함수를 조합하고 변환하여 결과를 도출해내는 프로그래밍 스타일이라고 할 수 있다.</p>
<p>함수를 객체로 취급해야하며 변수나 인자로 함수를 전달하고 함수를 반환하는 고차함수로도 사용될 수 있어야 한다.
<em>(그래서 자바8에서 Supplier, Function 인터페이스가 추가되었나보다.)</em></p>
<hr>
<h1 id="기본-개념">기본 개념</h1>
<h2 id="불변성immutability">불변성(Immutability)</h2>
<p>불변성이란 말 그대로 변하지 않는다는 뜻인데, 변수에 값이 한번 할당되면 변수의 값이 변하지 않는다는 것을 의미한다.
이 변수(데이터)로 새로운 데이터를 만드는 함수의 입력값으로 사용되어 데이터의 안정성을 높여야 한다.</p>
<h2 id="순수-함수pure-function">순수 함수(Pure Function)</h2>
<p>오직 입력되는 불변성을 가지는 데이터에만 의존하는 함수를 뜻한다.
외부 상태에 의존하지 않고, 같은 입력값에 같은 출력값을 보장할 수 있는 함수라고 할 수 있다.
때문에 병렬처리, 멀티쓰레드로 처리되기에 매우 적합하며 재사용성 또한 높아지기에 순수 함수 구현을 지향해야 한다.</p>
<h2 id="일급-함수first-class-function">일급 함수(First-class Function)</h2>
<p>함수를 값으로 다룰 수 있는 개념이다.
즉, 함수를 변수에 할당할 수 있고, 함수의 인자로 전달하거나, 함수 자체를 반환값으로 사용할 수 있다. 
다시 말해 함수를 다른 데이터 타입과 똑같이 다룰 수 있다는 것이다.</p>
<h2 id="고차-함수higher-order-function">고차 함수(Higher-order Function)</h2>
<p>함수 자체를 인자로 받거나 반환하는 함수이다.
때문에 함수를 다양하게 조합하고 활용하거나 추상화하여 사용하는데 유리하며 곧 특정 데이터를 유연하게 처리할 수 있다.</p>
<h3 id="일급-함수와-고차-함수의-차이">일급 함수와 고차 함수의 차이</h3>
<blockquote>
<p>딱보면 거기서 거기인거 같아 햇갈리고 혼용되는거 같다.
또한 어떠한 경우에는 일급 함수이면서 고차 함수일 수도 있어서 더욱 햇갈리는거 같다.
<del><em>일급 함수는 함수를 반환값으로, 고차 함수는 함수를 반환하는..(???)</em></del></p>
<p>내가 찾아보고 생각하는 바로는 관점의 차이가 아닐까 싶다.
<del><em>(속시원하게 차이점을 설명해주는 내용을 찾을 수 없음;;)</em></del></p>
<p><em>일급 함수는</em> <strong>함수 자체를 값으로 다룬다는 것</strong>에 초점을 맞춰 설명하고 있다.
그래서 함수를 인자값으로, 변수값으로, 반환값으로 활용한다면 일급 함수라고 할 수 있다.</p>
<p>반면 <em>고차 함수는</em> <strong>함수 자체를 인자로 받거나 반환한다는 것</strong>에 초점을 두고 설명한다.</p>
<p>그럼 함수 자체를 값으로 다루면서 함수를 반환하면? <strong>일급 함수이면서 고차 함수이다.</strong></p>
<p>그럼에도 약간의 차이를 보이는데 위에서 설명한대로
함수를 값으로 다루기만 한다면 일급 함수, 함수를 반환하기만 한다면 고차 함수이다.</p>
</blockquote>
<p><em>일급 함수인 예시)</em></p>
<pre><code class="language-java">void firstClassExample() { 
    // 함수 자체를 값으로 다루기에 변수에 대입한다.
    Function&lt;Integer, Integer&gt; plusOne = v -&gt; v + 1;
    Function&lt;Integer, Integer&gt; plusTwo = v -&gt; v + 2;

    System.out.println(plusOne.apply(10) + plusTwo.apply(10)); // (10+1) + (10+2) = 23
}</code></pre>
<p><em>고차 함수인 예시)</em></p>
<pre><code class="language-java">// 같은 입력값에 같은 출력값을 보장할 수 있는 순수 함수(Pure Function)이기도 하다.
Function&lt;Integer, Integer&gt; plusFive(int number) {
    return v -&gt; v + 5; // 인자(v)에 5를 더해주는 &#39;함수&#39;를 반환한다.
}</code></pre>
<p><em>일급 함수이면서, 고차 함수인 예시)</em></p>
<pre><code class="language-java">// 입력값 Integer, 반환값은 Function
Function&lt;Integer, Function&lt;Integer, Integer&gt;&gt; plus5AndReturnTimesFunc = number -&gt; {
    // 위 고차함수 예시인 plusFive 메소드를 값처럼 다룸
    Function&lt;Integer, Integer&gt; plus5Func = plusFive(number);

    // 입력값을 plus5Func의 결과와 곱하는 함수 반환
    return v -&gt; v * plus5Func.apply(number);
};

// 7 + 5의 결과값인 12를 곱해주는 함수 반환
Function&lt;Integer, Integer&gt; timesFunc = plus5AndReturnTimesFunc.apply(7);
System.out.println(timesFunc.apply(12)); // 10을 곱해 120을 출력</code></pre>
<p>마지막 예시에서 <code>plusFive</code> 메소드를 <em>plus5Func</em> <strong>변수에 할당함으로서 일급 함수의 특징을 가지며</strong>, 입력값에 <code>plusFive</code>의 결과인 12를 곱해주는 <strong>함수를 반환함으로서 고차 함수의 특징</strong>을 가진다.
즉 일급 함수이면서, 동시에 고차 함수라고 할 수 있다.</p>
<blockquote>
<p>정리하자면,
일급 함수는 함수 자체를 값처럼 다룬다는 것으로, <strong>함수를 동적으로 활용</strong>할 수 있게 해주는 함수라고 하면 될 것 같다.</p>
</blockquote>
<p>고차 함수는 함수 자체를 인자로 받거나 반환한다는 것으로, <strong>함수를 다양한 형태로 조합하거나 추상화</strong>하는 함수라고 정리할 수 있지 않을까...</p>
<h2 id="람다-함수lambda-function">람다 함수(Lambda Function)</h2>
<p>익명 함수(Anonymous Function)라고도 불린다.
위 고차 함수와 같이 사용하여 효율적으로, 간결하게 표현하는데 자주 사용된다.
예를 들어 자바에서 Stream API의 중간연산 메소드들을 활용할때 가독성 높은 코드를 작성할 수 있다.</p>
<p>람다 함수는 <strong>Closure</strong> 라는 개념을 가지고 있는데 해당 람다 함수 외부의 값을 활용할 수 있는 개념이다.
이 개념때문에 유용한 함수를 작성할 수 있으니 아래 서술할 <strong>부작용에 유의해야한다.</strong></p>
<h3 id="클로저closure란">클로저(Closure)란?</h3>
<p>람다 함수에서 함수 외부의 값을 참조하고, 나중에 람다 함수에서 그 외부의 값을 그대로 사용한다는 람다 함수의 특성 중 하나이다.</p>
<p>위 <em>일급 함수이면서, 고차 함수인 예시</em> 중에 <code>return v -&gt; v * plus5Func.apply(number);</code> 부분에서 외부에서 선언된 number(값 : 7)는 클로저라고 할 수 있다.</p>
<h2 id="부작용side-effect-이란">부작용(Side-effect) 이란?</h2>
<p>함수가 실행되면 함수 외부의 값이나 상태를 변경하거나 함수 외부, 또는 다른 함수의 내부와의  상호작용을 의미한다.</p>
<p>예를 들어, 위의 <em>&#39;일급 함수이면서, 고차 함수인 예시&#39;에서</em> <code>number</code>의 값인 12가 변경된다면 부작용이라고 볼 수 있다.</p>
<p><code>number</code>가 12인 것은 <code>plus5AndReturnTimesFunc.apply(7);</code> 에서 예측할 수 있지만 외부의 영향 등으로 값이 변경된다면 예측하기 어려워 진다.
이는 곧 예상치 못한 동작을 유발하거나 테스트, 디버깅을 어렵게 만들 수 있다.
또한 함수 외부와 상호작용 한다는 것은 병렬처리에도 여러움이 있을 가능성이 크다.</p>
<p>때문에 함수형 프로그래밍에서는 함수가 독립적이고 예측 가능하게 동작하도록 구현하는 것을 지향해야한다.</p>
<hr>
<h1 id="장점">장점</h1>
<p>위에서 서술한 기본 개념을 생각해보면 함수형 프로그래밍의 특징과 지향하는 점들은 부작용과 버그 방지, 테스트 등에서 많은 장점을 가진다.</p>
<p>순수 함수의 개념에서 가지는 특성 중 하나인 독립적인 동작과, 고차함수와 일급함수의 특성을 활용하여 로직의 테스트 및 디버깅이 쉬워져 함수단위에서 높은 테스트 커버리지를 가질 수 있다.
이 외에도 몇가지 장점이 있다.</p>
<h2 id="병렬성분산성">병렬성/분산성</h2>
<p>데이터가 불변성을 가진다는건 공유자원으로 사용되기 쉽다.
즉, <a href="https://velog.io/@cv_/%EC%9E%90%EB%B0%94%EC%9D%98Thread%EB%9E%80">멀티쓰레드</a> 환경에서 공유자원에 대해 Race Condition 을 피할 수 있다.</p>
<p>함수형 프로그래밍에서는 데이터의 불변성을 유지하기 위해 상태를 변경하는 대신 <strong>데이터를 복사하여 새로운 데이터를 생성하는 방식</strong>을 지향해야 한다.</p>
<p>이를 통해 여러 개의 스레드나 프로세스에서 동시에 작업을 수행하더라도 원본 데이터가 변경되지 않고 각각의 작업이 독립적으로 처리되어 데이터의 <strong>일관성을 유지함과 동시에 병렬성과 분산성을 높일 수 있다.</strong></p>
<h2 id="모듈화재사용성">모듈화/재사용성</h2>
<p>위 고차 함수와 일급 함수의 설명처럼 함수형 프로그래밍은 작은 함수들을 조합하여 더 큰 함수나 모듈을 만들어내는 방식으로 코드를 작성한다.
각각 특정 기능을 하는 작은 함수들로 더 큰 함수로 구성하게 되면 코드의 구조와 로직을 쉽게 파악되며 유지보수에도 유리하다.
이렇게 <strong>작게 함수를 나누고 조합하는 것을 컴포지션(Compostion)</strong>이라고 한다.</p>
<p>또한 오직 입력에만 의존하는 순수 함수 개념으로 함수들을 독립적으로 재사용 가능하기에, 해당 함수를 다양한 로직에서 재사용이 가능한데, 이를 통해 코드의 중복을 피하고 높은 생산성을 기대할 수 있다.</p>
<hr>
<blockquote>
<p><strong>함수 합성 (Compostion) 이란?</strong></p>
</blockquote>
<p>위에서 언급한 작게 함수를 나누고 조합하는 것을 컴포지션이라고 한다.
크게 Pipeline과 Chain 방식이 있다.</p>
<blockquote>
</blockquote>
<p>기본적으로 함수들을 연속적으로 호출하여 입력값을 순차적으로 처리하는 방식이다. </p>
<blockquote>
</blockquote>
<p>A ➡️ B ➡️ C 의 순서로 함수가 실행된다고 하면,
A함수의 출력은 B의 입력, B의 출력은 C의 입력되어 처리되는 방식이다. <em>ex.) C(B(A(x)))</em></p>
<blockquote>
</blockquote>
<p>함수형 프로그래밍이에서 이런 함수 합성을 통해 구현된 함수를 합성 함수(Compose Function)라고도 한다.</p>
<blockquote>
<blockquote>
</blockquote>
<p>자바에서 연속해서 메소드를 호출하는 <strong>메소드 체이닝</strong>이 대표적인 예라고 할 수 있다.
자바에서 <a href="https://velog.io/@cv_/Java8-%EB%B6%80%ED%84%B0-%EC%82%AC%EC%9A%A9%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%AC%B8%EB%B2%95%EA%B3%BC-%EA%B8%B0%EB%8A%A5">Stream API</a> 등에서 메소드 체이닝을 구현하는 것을 함수형 프로그래밍 관점에서 <strong>Pipeline</strong> 방식이라고 볼 수 있다.</p>
<blockquote>
</blockquote>
<p>Pipeline 말고도 <strong>Curry</strong> 라고 불리는 방식도 있다.
Curry(또는 Currying)은 각각의 함수에서 하나의 인자를 가지는 함수를 생성하면서, 함수의 인자를 하나씩 하나씩 적용하는 것이다. 
하지만 자바에서는 타입을 추론하지 못하기에 직접적으로 이 방식을 지원하지않는다.
<em>(자바는 8부터 함수형 프로그래밍을 지원해도 태생이 객체지향이라서??)</em>
자바에서의 Currying 방식 참고 : <a href="http://egloos.zum.com/ryukato/v/1160506">http://egloos.zum.com/ryukato/v/1160506</a></p>
</blockquote>
<h1 id="실제-활용-예시">실제 활용 예시</h1>
<p><em>(함수형 프로그래밍을 지원하기 시작한)</em> 자바8부터 지원하는 Stream API를 사용하여 <strong>데이터 조작</strong>하는 방법 및 <strong>병렬 및 동시성 처리</strong> 참고 : <a href="https://velog.io/@cv_/Java8-%EB%B6%80%ED%84%B0-%EC%82%AC%EC%9A%A9%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%AC%B8%EB%B2%95%EA%B3%BC-%EA%B8%B0%EB%8A%A5#stream-api">https://velog.io/@cv_/Java8-부터-사용가능한-문법과-기능#stream-api</a></p>
<p>또는 <code>CompletableFuture</code> 객체를 활용하여 비동기적인 작업을 조합하고 연결하거나 결과를 합성, 에러 처리 등의 함수형 프로그래밍을 지원한다.
참고 : <a href="https://anythingis.tistory.com/119#CompletableFuture-1">https://anythingis.tistory.com/119#CompletableFuture-1</a></p>
<h1 id="함수형-프로그래밍-주의점">함수형 프로그래밍 주의점</h1>
<h2 id="병렬-및-동시성-처리의-주의점">병렬 및 동시성 처리의 주의점</h2>
<p>순수 함수 개념과 불변성을 강조하는 만큼 특정 데이터와 타입에 대해 알아놔야할 필요가 있다.
예시로, 자바에서는 Immutable Collections, 열거형 클래스, Atomic 타입 등이 있다.
<em>(때문에 함수형 프로그래밍은 객체지향 프로그래밍과는 다른 새로운 개념과 용어를 익혀야 하는 불편함이 있는것 같다)</em>
이를 통해 멀티쓰레드 환경에서 안전하게 병렬 처리가 되도록 구현해야 한다.</p>
<h2 id="라이브러리-및-지원-부족">라이브러리 및 지원 부족</h2>
<p>명령형 프로그래밍이 보다 오랜 역사와 넓은 생태계를 가지고 있기 때문에, 함수형 프로그래밍에 비해 라이브러리와 생태계가 상대적으로 제한적이며 직접 구현해야되는 경우가 비교적 많을 수 있다.
<del><em>(Reactor(WebFlux), <a href="https://medium.com/@johnmcclean/circular-programming-in-java-with-cyclops-e72ff672a708">Cyclops</a>, Vavr 등이 있긴 한거 같은데??)</em></del></p>
<p>위 Curry 방식을 직접적으로 지원하지 않는것부터 특정 프로젝트를 위한 불변성과 순수 함수의 개념을 포함한 라이브러리나 프레임워크가 부족할 수 있다고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API의 규칙]]></title>
            <link>https://velog.io/@cv_/REST-API%EC%9D%98-%EA%B7%9C%EC%B9%99</link>
            <guid>https://velog.io/@cv_/REST-API%EC%9D%98-%EA%B7%9C%EC%B9%99</guid>
            <pubDate>Fri, 14 Apr 2023 07:11:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>REST API란? <a href="https://velog.io/@cv_/REST-API-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%A0%95%EB%A6%AC">참고</a></p>
</blockquote>
<h1 id="rest-규칙">REST 규칙</h1>
<h2 id="무상태성-stateless">무상태성 (Stateless)</h2>
<p><a href="https://velog.io/@cv_/Stateful%EA%B3%BC-Stateless-%EC%B0%A8%EC%9D%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">Stateless vs Stateful과 비교</a></p>
<p>서버는 클라이언트의 상태를 관리하지 않는다.
그러므로 요청이 들어오면 그에 맞는 응답을 보내는 역할만을 수행하는, 즉 모든 요청은 독립적으로 처리된다.
때문에 클라이언트의 이전 요청과 상태에 대한 정보는 API 서버에서 저장되지 않으니 클라이언트는 필요한 모든 정보를 요청에 포함시킬 수 밖에 없다.</p>
<p>이런 무상태성(Stateless)의 예시로 단방향으로 데이터를 전송하는 UDP 프로토콜 등이 있다.</p>
<h2 id="client-server-구조">Client-Server 구조</h2>
<p>클라이언트와 서버는 반드시 독립적으로 분리되어 있어야하며 서로 변경/발전함에 있어서 영향을 줄 수 없어야한다.
클라이언트는 UI/UX를 담당하며 서버는 클라이언트 요청에 따라 데이터 CRUD 등의 처리와 비즈니스 로직을 담당해야한다.</p>
<h2 id="캐시-사용-cacheable">캐시 사용 (Cacheable)</h2>
<p>요청을 통해 보내는 자료들은 저장되어 캐시가 가능하도록 구축되어야 한다.
이를 통해서 저장된 자료들을 주고 받을 때나 동일한 요청에 대한 반복적인 서버 요청에 대해 
속도를 향상시키며 서버의 부하를 줄일 수 있다.
<em>(서버에서 응답 시 HTTP 헤더의 Cache-Control 등으로 캐시 제어 정보 값을 반환한다.)</em></p>
<h2 id="일관된-인터페이스-uniform-interface">일관된 인터페이스 (Uniform Interface)</h2>
<p>서버의 데이터에 접근하는 방식을 통일해서 데이터를 쉽게 식별 가능하게 해야한다.
HTTP 메소드를 사용하여 리소스나 데이터에 대한 작업을 요청하고, URI를 통해 접근해야 한다.
정확히는 URL만 보고도 어느 데이터를 어떤 상태로 전송해야 하는지 구별할 수 있어야 한다.</p>
<p>ex) <a href="https://velog.io/@cv_/REST-API-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%A0%95%EB%A6%AC">https://velog.io/@cv_/REST-API-간단하게-정리</a>
위 URI에서 @cv_ 사용자의 <em>REST-API-간단하게-정리</em> 라는 포스트를 가리키는 것을 알 수 있다.</p>
<h2 id="계층화-시스템-layered-system">계층화 시스템 (Layered System)</h2>
<p>서버는 여러 계층으로 구성하여 각각의 계층이 다른 역할을 가지고 독립적으로 확장/개발이 가능해야 한다.
이를 통해 시스템의 유연성과 확장성을 확보해야 한다.
이렇게 계층화된 시스템으로 이후에 필요에 따라 새로운 계층을 추가 또는 기존 계층을 수정하거나 제거하는데 있어서 용이해야한다.
이를 위해 상위 계층은 하위 계층에게 추상화 인터페이스를 제공하고, 하위 계층은 상위 계층으로부터 제공된 인터페이스를 사용한다.</p>
<h1 id="restful-api-서버의-url-규칙">RESTful API 서버의 URL 규칙</h1>
<ul>
<li><strong>소문자를 사용</strong>
<em>ex)</em> <code>.../Users</code> 대신 <code>.../users</code> 로 사용</li>
</ul>
<ul>
<li><strong>명사를 사용</strong>
<em>ex)</em> <code>.../users/create</code> 대신 <code>.../users</code> 에 POST 메소드를 사용 </li>
</ul>
<ul>
<li><strong>복수형을 사용</strong>
<em>ex)</em> <code>.../user</code> 대신 <code>.../users</code> 로 사용</li>
</ul>
<ul>
<li><strong>구분자는 <code>-</code>(하이픈)을 사용</strong>
<em>ex)</em> <code>.../userProfiles</code> 대신 <code>.../user-profiles</code> 로 사용</li>
</ul>
<ul>
<li><strong>URL의 마지막엔 <code>/</code>(슬래쉬)를 사용하지 않는다.</strong>
<em>ex)</em> <code>.../users/</code> 대신 <code>.../users</code> 로 사용</li>
</ul>
<ul>
<li><strong>파일 확장자는 포함하지 않는다.</strong>
<em>ex)</em> <code>.../users/profiles/image.png</code> 대신 <code>.../users/profiles/image</code> 로 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API 간단하게 정리]]></title>
            <link>https://velog.io/@cv_/REST-API-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@cv_/REST-API-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 12 Apr 2023 05:22:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><em>REST (Representational State Transfer)</em></p>
<p>어떤 자원에 대해 CRUD를 수행하기 위해 URI(Resource)로 자원을 특정하고 HTTP Method로 행위를 요청하며 상태를 결과로서 전달하는 방식의 웹 아키텍쳐이다.
이때 자원은 특정한 형태(Representation of Resource)로 표현되며 형태는 JSON, XML, RSS 등이 있으며 상태는 201, 404와 같은 HTTP status code가 있다.</p>
<p>웹의 장점을 최대한 활용하며 HTTP 프로토콜을 그대로 활용하는 클라이언트와 서버 사이의 통신 방식 중 하나이다.</p>
</blockquote>
<p><em>REST API</em> 는 위 REST의 설계를 따르는 인터페이스이다.
HTTP프로토콜을 이용해 URI로 서버에 접근하여 HTTP Method 로 URI에 해당하는 자원을 CRUD 등의 요청을 받아 처리하는 형태이다.</p>
<p>내가 생각할 때, 
URI라는 자원에 대해서 GET, DELETE 등의 HTTP Method로 행위를 유추할 할 수 있게 만들어져야한다.
이렇게 되면 자연스럽게 <code>https://api.??.com/board/1</code> 1번 게시글이라는 자원만 나타내고 이 게시글에 대해서 GET, DELETE, PATCH 등의 HTTP Method 로만 행위를 표현하고 HTTP status code 로 응답을 받는 것이 REST API 라고 정리하고 싶다.
<em>(새로운 게시글을 등록하는 경우 POST 메소드로 JSON, XML등의 형태로 전달된다.)</em></p>
<p>REST의 규칙을 기반으로 설계된 API 서버는 RESTful 하다 라고 한다.</p>
<p>REST API와 RESTful API는 거의 같은것 같지만 굳이 나눠보자면
REST API의 경우 &#39;REST로 구현된다면 이렇게 되어야만 한다&#39; 라는 느낌의 개념이 강한것 같고
RESTful API의 경우 저런 REST의 규칙을 지향하기 때문에 RESTful 하다고 하는것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring에서 자주 사용되는 어노테이션들]]></title>
            <link>https://velog.io/@cv_/Spring%EC%97%90%EC%84%9C-%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EB%93%A4</link>
            <guid>https://velog.io/@cv_/Spring%EC%97%90%EC%84%9C-%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EB%93%A4</guid>
            <pubDate>Tue, 11 Apr 2023 09:48:08 GMT</pubDate>
            <description><![CDATA[<ul>
<li>@Configuration
스프링의 설정클래스를 정의하는데 사용한다.</li>
</ul>
<ul>
<li>@Bean
설정클래스 내부에서 빈객체를 명시할 때 사용된다.</li>
</ul>
<ul>
<li>@Component
빈으로 등록될 클래스를 정의할 때 가장 일반적으로 많이 사용되는 어노테이션
컴포넌트에 해당하는 클래스를 정의할 때 사용된다.</li>
</ul>
<ul>
<li>@Autowired
DI를 사용하기 위한 어노테이션으로, 의존성을 자동으로 주입해준다.</li>
</ul>
<ul>
<li>@Controller
MVC 패턴에서 클라이언트의 요청을 받고 응답을 반환하는 컨트롤러 클래스를 정의할 때 사용된다.<ul>
<li>@RequestMapping
웹 요청의 URL과 처리할 메소드를 연결하는 데 사용된다.</li>
<li>@RequestParam
웹 요청의 파라미터 값을 메소드의 파라미터에 바인딩할 때 사용된다.</li>
<li>@PathVariable
웹 요청의 URL에서 값을 추출하기 위해 사용된다.</li>
</ul>
</li>
</ul>
<ul>
<li>@Service
비즈니스 로직을 처리하는 서비스 클래스를 정의할 때 사용된다.</li>
</ul>
<ul>
<li>@Repository
스프링에서 데이터 접근 계층(Data Access Layer)에 해당하고 데이터베이스와의 데이터 처리를 담당하는 레포지토리(Repository) 클래스를 정의할 때 사용된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP Method 알아보기]]></title>
            <link>https://velog.io/@cv_/HTTP-Method-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@cv_/HTTP-Method-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 11 Apr 2023 08:47:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>HTTP</strong>(<em>Hypertext Transfer Protocol</em>)란?</p>
<p>HTML 문서와 같은 리소스들을 가져올 수 있도록 해주는 프로토콜이다.
클라이언트-서버 프로토콜이기도 하며 웹에서 이루어지는 모든 데이터 교환의 기초이다.
참고 : <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Overview">https://developer.mozilla.org/ko/docs/Web/HTTP/Overview</a></p>
</blockquote>
<p>HTTP Method 는 클라이언트에서 서버로 데이터나 리소스를 전송할 때, 서버의 처리방식도 같이 요청하는 방법이다.</p>
<ul>
<li>GET 
데이터를 조회한다.</li>
</ul>
<ul>
<li>POST
새로운 데이터를 생성한다.</li>
</ul>
<ul>
<li>PUT
기존 데이터를 변경하고, 기존의 데이터가 없으면 생성한다.</li>
</ul>
<ul>
<li>PATCH 
데이터의 일부분을 변경한다.</li>
</ul>
<ul>
<li>DELETE 
데이터를 삭제한다.</li>
</ul>
<ul>
<li>HEAD 
GET과 동일하지만 본문(body)을 제외하고, 상태값과 헤더값만을 반환한다.
때문에 GET 요청보다 속도가 빠르다.</li>
</ul>
<ul>
<li>CONNECT
HTTPS 같은 암호화된 연결을 설정한다.
클라이언트와 서버간의 직접적으로 연결함으로 터널링을 생성한다고도 한다.
정확히는 프록시 서버에 터널링을 설정하여 서버와 클라이언트를 연결한다.</li>
</ul>
<ul>
<li>OPTIONS
대상 리소스에 대한 사용가능한 메서드를 조회한다.
CORS정책 확인용 예비요청(preflight)으로서 자주 사용된다.</li>
</ul>
<ul>
<li>TRACE
클라이언트의 요청과 서버의 응답을 확인하여 디버깅/테스트 용도로 사용된다.
서버는 요청과 응답의 내용을 전부 포함하는 메세지를 응답으로 반환하기에 운영단계에서는 기본적으로 사용을 제한한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java8 부터 사용가능한 문법과 기능]]></title>
            <link>https://velog.io/@cv_/Java8-%EB%B6%80%ED%84%B0-%EC%82%AC%EC%9A%A9%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%AC%B8%EB%B2%95%EA%B3%BC-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@cv_/Java8-%EB%B6%80%ED%84%B0-%EC%82%AC%EC%9A%A9%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%AC%B8%EB%B2%95%EA%B3%BC-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Sat, 08 Apr 2023 06:59:37 GMT</pubDate>
            <description><![CDATA[<p>함수형 프로그래밍이 유행하면서 그에 맞춰 함수형 프로그래밍을 지원하기 위해 2014년 자바8에선 이전과 다르게 많은 기능들이 변경되거나 개선되었다.
이로 인해 가독성과 생산성이 비약적으로 상승하였으며 많은 호평을 받았었다.</p>
<blockquote>
<p><em><strong>함수형 프로그래밍(Functional Programming)이란?</strong></em></p>
<p>하나의 프로그래밍 패러다임으로 정의되는 일련의 코딩 접근 방식이며, 
자료처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임을 의미한다.</p>
<p>데이터나 자료구조의 상태를 변경하는 대신, 함수의 조합을 통해 원하는 결과를 만들어내는 방식으로 동작한다.</p>
<p>특징으로는 순수 함수(Pure Function), 불변성(Immutability), 고차 함수(Higher-Order Function) 등의 개념으로 안정성, 확장성, 테스트 용이성을 높인다.</p>
<p>참고 : <a href="https://jongminfire.dev/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%B4%EB%9E%80">https://jongminfire.dev/함수형-프로그래밍이란</a></p>
</blockquote>
<p>추가/변경된 내용 몇가지를 정리해본다.</p>
<h1 id="람다식-lambda-expressions">람다식 (Lambda expressions)</h1>
<p>가끔 코드를 간단하게, 깔끔하게 작성하기 위해 람다식을 활용했었는데 자바8부터 지원되는 기능이다.
가장 쉽게 쓰이는 경우는 아마도 어떤 배열/컬렉션 요소들을 정렬할 때 많이 쓰일것이다.</p>
<p><em>예시 1)</em></p>
<pre><code class="language-java">// 람다식을 쓰지 않고 내림차순 정렬
String[] arr = new String[]{&quot;1&quot;, &quot;3&quot;, &quot;2&quot;, &quot;4&quot;, &quot;6&quot;, &quot;5&quot;, &quot;7&quot;};

// 익명 클래스를 사용하여 Comparator의 인터페이스에서 compare메소드를 구현
Arrays.sort(arr, new Comparator&lt;String&gt;() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

System.out.println(Arrays.toString(arr)); // [7, 6, 5, 4, 3, 2, 1]
</code></pre>
<p><em>예시 2)</em></p>
<pre><code class="language-java">// 람다식으로 내림차순 정렬
String[] arr = new String[]{&quot;1&quot;, &quot;3&quot;, &quot;2&quot;, &quot;4&quot;, &quot;6&quot;, &quot;5&quot;, &quot;7&quot;};

// 람다식 사용하여 Comparator의 인터페이스에서 compare메소드를 구현
Arrays.sort(arr, (a, b) -&gt; b.compareTo(a));

System.out.println(Arrays.toString(arr)); // [7, 6, 5, 4, 3, 2, 1]</code></pre>
<p><em>예시 2)</em> 가 <em>예시 1)</em> 보다 람다식으로 더 간단하게 작성되었다!</p>
<p><em>예시2) 에서 람다식은 어떻게 동작할까?</em></p>
<p>위에서 사용된 <code>Arrays.sort</code> 메소드는 다음과 같은 시그니처를 가진다.
<code>public static &lt;T&gt; void sort(T[] a, Comparator&lt;? super T&gt; c) {...}</code>
2번째 인자로 받는 <code>Comparator</code>는 인터페이스이며 <code>int compare(T o1, T o2);</code> 를 추상메서드로 명시하고 있다.</p>
<p>람다식을 사용했을때 <code>Comparator</code> 인터페이스에서 <code>(String a, String b)</code>를 인자로 받을 수 메서드 시그니처가 <code>int compare(T o1, T o2)</code> 메서드가 <strong>유일</strong>하다.</p>
<p>다시말해 <strong>람다식을 사용할 때 <code>Comparator</code> 인터페이스에서 <code>compare</code>메서드를 구현한다는 것은, 람다식에서 사용되는 문맥(context)을 통해 추론(inference)할 수 있는 것이다.</strong></p>
<p>때문에 같은 메서드 시그니처를 가지는 다른 추상메소드가 있다면, 
<strong>람다식은 사용할 수 없다.</strong></p>
<h1 id="stream-api">Stream API</h1>
<p>Stream 객체를 활용할 수 있는 것도 자바8 부터인데, 위 람다식과 마찬가지로 함수형 프로그래밍을 지원하기 위해 등장했다. 이 스트림은 파일관련 객체에 사용되는 _I/O Stream_과는 다른 개념으로 사용된다.</p>
<p>또한 함수형 프로그래밍을 지원하기 위해 등장한 만큼 관련된 여러 특징을 가지고 있다.</p>
<h2 id="원본-데이터를-변경하지-않는다">원본 데이터를 변경하지 않는다.</h2>
<p>함수형 프로그래밍에서 중요한 개념 중 하나인 불변성(Immutability)을 구현하기 위해 원본의 데이터를 변경하지 않는다.</p>
<pre><code class="language-java">List&lt;Integer&gt; list1 = new ArrayList&lt;&gt;(List.of(1, 3, 2, 4, 6, 5, 7));
List&lt;Integer&gt; list2 = list1.stream().sorted().collect(Collectors.toList());

System.out.println(list1);
System.out.println(list2);</code></pre>
<pre><code>// 실행결과)

[1, 3, 2, 4, 6, 5, 7]
[1, 2, 3, 4, 5, 6, 7]</code></pre><p>위 예시에서는 <code>list1</code>을 <code>Stream</code>으로 정렬시키더라도 <code>list1</code>은 원래의 순서를 보장하고 있다.</p>
<p>기존의 정렬방식으로 컬렉션 객체로 정렬을 하게되면 (<code>Collections.sort(list1);</code> 로 정렬하게 되면)
원본 데이터인 <code>list1</code>이 정렬되며 원본 데이터의 원래 순서를 잃어버리게 된다.</p>
<h2 id="휘발성-객체이다">휘발성 객체이다.</h2>
<p>위 예시에서 <code>list1.stream().sorted().collect(Collectors.toList());</code> 부분은 <code>list1</code>의 데이터를 모두 읽고나면 사라진다.</p>
<p>다시 예시로 확인해본다면</p>
<pre><code class="language-java">List&lt;Integer&gt; list1 = new ArrayList&lt;&gt;(List.of(1, 3, 2, 4, 6, 5, 7));

Stream&lt;Integer&gt; stream1 = list1.stream();
stream1.forEach(num -&gt; System.out.println(num)); // 실행가능!
stream1.forEach(num -&gt; System.out.println(num)); // 실행불가!</code></pre>
<pre><code>// 실행결과)
1
3
2
4
6
5
7
Exception in thread &quot;main&quot; java.lang.IllegalStateException: 
stream has already been operated upon or closed
...</code></pre><p>위 예시처럼 <code>Stream</code>이 한번 처리하면 새로운 스트림이 새로 생성되기에 기존의 스트림은 다시 사용할 수 없다.</p>
<blockquote>
<p><em><strong>왜 불편하게 휘발성 객체인가?</strong></em></p>
</blockquote>
<p>위에서 언급한 불변성(Immutability)과 다시 관련이 있는데
결론부터 말하면 <strong>스트림이 데이터 소스를 변경하지 않고 새로운 스트림을 반환하기 때문</strong>이다. 
때문에 새로운 객체가 계속 생성되며 메모리 사용량이 늘어날 수도 있고 스트림을 여러번 사용해야되는 상황이라면 <code>Colletions</code> 를 사용하는 것보다 불편할 수 있다.</p>
<blockquote>
</blockquote>
<p>하지만 스트림이 처리되고 사라짐으로서 위 <code>list1</code>은 불변성이 보장되며 <code>list1</code>이 병렬처리 되기 훨씬 쉽기때문이다.
함수형 프로그래밍에서는 데이터의 불변성을 보장하기 위해 원본 데이터를 복사해서 처리하는 방식을 지향하는데 자바에서는 스트림을 활용함으로써 이 불변성을 보장한다.</p>
<blockquote>
<p><em><del>(각각의 스트림도 그냥 사라짐으로서 다른 처리결과나 나올 경우를 아예 없애버리는 듯...)</del></em></p>
</blockquote>
<p>예를 들어 멀티쓰레드 환경에서 위 예시의 list1을 개별적으로 스트림으로 처리한다고 가정했을때, 여러 쓰레드들은 list1에 대한 Race Condition을 피할 수 있다.</p>
<blockquote>
</blockquote>
<p><a href="https://velog.io/@cv_/%EC%9E%90%EB%B0%94%EC%9D%98Thread%EB%9E%80">(멀티쓰레드 환경에서 Race Condition에 대한 내용 참고)</a></p>
<h2 id="반복-작업을-쉽게-처리할-수-있다">반복 작업을 쉽게 처리할 수 있다.</h2>
<p>스트림은 내부적으로 많은 기능들이 구현된 메소드들을 가지고 있다. 
때문에 for문 등을 활용한 반복문을 활용해야할 경우를 크게 줄여준다.
크게 여러번 처리할 수 있는 <strong>중간연산(intermediate operations)</strong>, 한번만 처리할 수 있는 <strong>최종연산(terminal operations)</strong> 으로 메소드들을 나눌 수 있으며 
<strong>중간연산은 항상 스트림객체를 반환하며 최종연산이 호출되지 않으면 실행되지 않는다.</strong></p>
<p>자주쓰이는 메소드들은 다음과 같다.</p>
<h3 id="중간-연산intermediate-operations">중간 연산(intermediate operations)</h3>
<ul>
<li>filter 
주어진 조건에 맞는 데이터만 걸러냄</li>
<li>map
주어진 조건으로 각각의 데이터들을 처리함</li>
<li>flatMap 
위 map과 동일하지만 평면화까지 진행
예를 들어 스트림이 다른 스트림들을 가지는 구조라면,
내부의 모든 스트림을 하나의 스트림으로 평면화</li>
<li>distinct
중복되는 요소를 제거</li>
<li>sorted
스트림의 요소들을 오름차순으로 정렬</li>
<li>peek
각각의 요소들을 중간에 소비하고 반환
주로 디버깅이나 로그를 남길때 사용</li>
<li>limit(long maxSize)
스트림의 첫 maxSize개의 요소로 제한</li>
<li>skip(long n)
첫 N개의 요소를 제외하고 반환</li>
</ul>
<h3 id="최종-연산terminal-operations">최종 연산(terminal operations)</h3>
<ul>
<li>forEach
모든 요소를 돌면서 특정 동작을 수행</li>
<li>count
스트림의 요소 수를 반환</li>
<li>collect
모든 요소를 <code>Collection</code> 객체로 수집</li>
<li>reduce
주어진 연산을 사용하여 스트림의 요소를 하나로 줄임</li>
<li>anyMatch
주어진 조건의 요소가 하나라도 있는지 확인 (boolean 반환)</li>
<li>allMatch
모든 요소가 주어진 조건의 부합하는지 확인</li>
<li>noneMatch
allMatch와 반대로 모든 요소가 주어진 조건의 부합하지 않는지 확인
(모든 요소가 주어진 조건에 false라면 true를 반환)</li>
<li>findAny
스트림의 임의의 요소를 반환</li>
<li>findFirst
스트림의 첫 번째 요소를 반환</li>
</ul>
<blockquote>
<p>findAny, findFirst 메소드 참고 
<a href="https://codechacha.com/ko/java8-stream-difference-findany-findfirst/">https://codechacha.com/ko/java8-stream-difference-findany-findfirst/</a></p>
</blockquote>
<h1 id="parallel-stream">Parallel Stream</h1>
<p>위 스트림을 말 그대로 병렬적으로 처리할 수 있는 스트림이다.
내부적으로 자바의 내장 쓰레드 풀인 <code>ForkJoinPool</code>을 사용하며 병렬적으로 처리되기 때문에 스트림과 다르게 <strong>순서가 보장되지 않는다.</strong></p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

System.out.println(&quot;일반스트림 peek&quot;);
numbers.stream()
    .peek(v -&gt; System.out.print(v + &quot; &quot;))
    .collect(Collectors.toList());

System.out.println(&quot;\n&quot;); // 개행

System.out.println(&quot;병렬스트림 peek&quot;);
numbers.parallelStream()
    .peek(v -&gt; System.out.print(v + &quot; &quot;))
    .collect(Collectors.toList());</code></pre>
<pre><code>// 실행결과)

일반스트림 peek
1 2 3 4 5 6 7 8 9 10 

병렬스트림 peek
7 2 1 5 3 10 8 9 6 4 </code></pre><h2 id="왜-사용하는가">왜 사용하는가</h2>
<p>병렬처리가 가능하다는 것은 작업 속도가 빨라질 수 있다는 것을 의미한다.
때문에 대용량 데이터를 처리하는 시간을 줄이기 위해 사용한다.</p>
<pre><code class="language-java">// 1부터 10억까지 처리시간 테스트
LongStream numbers1 = LongStream.rangeClosed(1, 1000000000);
LongStream numbers2 = LongStream.rangeClosed(1, 1000000000).parallel(); // 병렬 스트림

long start = 0;

// 일반스트림으로 테스트
start = System.currentTimeMillis();
numbers1.map(v -&gt; v + 1) // 각 요소에 +1
        .sum();
System.out.println(&quot;일반스트림 처리 시간: &quot; + (System.currentTimeMillis() - start) + &quot;ms&quot;);

// 병렬스트림으로 테스트
start = System.currentTimeMillis();
numbers2.map(v -&gt; v + 1) // 각 요소에 +1
        .sum();
System.out.println(&quot;병렬스트림 처리 시간: &quot; + (System.currentTimeMillis() - start) + &quot;ms&quot;);</code></pre>
<pre><code>// 실행결과)

일반스트림 처리 시간: 2286ms
병렬스트림 처리 시간: 173ms</code></pre><p>훨씬 빠르다!</p>
<h2 id="항상-빠른것은-아니다">항상 빠른것은 아니다</h2>
<p><code>Parallel Stream</code>이 병렬적으로 처리된다는 것은 내부적으로 여러개의 쓰레드를 사용한다는 의미인데 <strong>경우에 따라 더 느릴수도 있다.</strong></p>
<h3 id="작업의-개수가-적거나-매우-빠를-때">작업의 개수가 적거나 매우 빠를 때</h3>
<p>이 경우 오히려 병렬스트림이 더 오래 걸릴 수 있다.
위 예시에서 10억이 아닌 10으로 바꾸기만해도 병렬스트림이 더 오래 걸리게된다.</p>
<pre><code class="language-java">// 1부터 10억까지 처리시간 테스트
LongStream numbers1 = LongStream.rangeClosed(1, 10);
LongStream numbers2 = LongStream.rangeClosed(1, 10).parallel(); // 병렬 스트림
// ... 생략 </code></pre>
<pre><code>// 실행결과)

일반스트림 처리 시간: 1ms
병렬스트림 처리 시간: 9ms</code></pre><p>실제로 비율만 따졌을때 9배나 더 느려졌다...</p>
<h3 id="peek-메소드-사용">peek 메소드 사용</h3>
<p>peek 메소드는 중간연산 메소드이며 각 요소를 참조하여 작업하는데 스트림 요소를 수정하지 않고 각 요소를 조사할 때 유용하다. </p>
<p>하지만 병렬 스트림에서는 다른 연산보다 더 많은 오버헤드를 발생시킬 수 있다.
각 요소를 참조하여 작업할 때, 휘발성을 가지는 스트림의 특성상 새로운 peek 메소드를 다시 호출하기에 여러 쓰레드가 순차적으로 처리할 때 보다 메모리를 더 사용하게 되고 <strong>스트림이 커지면 커질수록 추가적인 오버헤드가 발생할 수 있다.</strong></p>
<pre><code class="language-java">// ... 생략 
numbers1.map(v -&gt; v + 1) // 각 요소에 +1
                .peek(v -&gt; Math.random()) // 의미없는 기능 추가
                .sum();
// ... 생략 
numbers1.map(v -&gt; v + 1) // 각 요소에 +1
                .peek(v -&gt; Math.random()) // 의미없는 기능 추가
                .sum();
// ... 생략 
</code></pre>
<pre><code>// 실행결과)

일반스트림 처리 시간: 2924ms
병렬스트림 처리 시간: 37918ms</code></pre><blockquote>
<p>또한 Thread-Safe한 객체를 공유자원으로 사용한다면 공유 자원에 Lock이 걸려 병목현상을 일으켜 병렬스트림에서 생성된 쓰레드들이 병목되면서 순차적으로 처리할때 보다 더 느려질 수 있다.</p>
</blockquote>
<h1 id="optional">Optional</h1>
<p>자바8 이전에 null값은 많은 문제를 일으킬 수 있었다. <code>NullPointerException</code> 예외처리를 위해 잦은 null체크 로직이 반복되면서 가독성을 해쳤고, 복잡한 코드일수록 심각해져서 유지보수에 어려움이 따랐다.</p>
<h2 id="활용">활용</h2>
<p><em>Optional객체 생성</em></p>
<pre><code class="language-java">Optional&lt;String&gt; optional = Optional.empty();</code></pre>
<p>Optional에 값이 없이 생성만 할 경우이다.</p>
<p>내부에 static final EMPTY 객체를 가지고 있기에 값을 가지고 있지 않은 Optional객체가 아무리 많아도 하나의 EMPTY 객체를 공유함으로서 메모리를 절약한다.</p>
<hr>
<p><em>Null이 아닐경우가 보장될 때</em></p>
<pre><code class="language-java">Optional&lt;String&gt; optional = Optional.of(&quot;value&quot;);</code></pre>
<hr>
<p><em>Null일 수 있을 때</em></p>
<pre><code class="language-java">Optional&lt;String&gt; optional = Optional.ofNullable(test.getValue());</code></pre>
<hr>
<p>Optional로 값을 감쌀 수 있게 되면서 람다식과 메소드 참조를 함께 사용하며 가독성을 높이고 코드도 더 간결하게 작성할 수 있게 되었다. </p>
<p><em>예시코드)</em></p>
<pre><code class="language-java">class User {
    private Address address;
    // .. 생략
    public Address getAddress() {
        return address;
    }
}

class Address {
    private String postcode;
    // .. 생략
    public String getPostCode() {
        return postcode;
    }
}</code></pre>
<pre><code class="language-java">// Optional을 사용하기 전에
public String findPostCode() {
    User user = getUser(); // 유저객체 반환

    if (user != null) {
        Address address = user.getAddress();

        if (address != null) {
            String postcode = address.getPostCode();

            if (postcode != null) {
                return postcode;
            }
        }
    }

    return &quot;우편번호 없음&quot;;
}</code></pre>
<pre><code class="language-java">// Optional, 람다식을 사용했을때
public String findPostCode() {
    Optional&lt;User&gt; user = Optional.ofNullable(getUser());

    return user
        .map(user -&gt; user.getAddress())
        .map(address -&gt; address.getPostCode())
        .orElse(&quot;우편번호 없음&quot;);
}</code></pre>
<pre><code class="language-java">// 메소드 참조까지 사용했을때
public String findPostCode() {
    Optional&lt;User&gt; user = Optional.ofNullable(getUser());

    return user
        .map(User::getAddress)
        .map(Address::getPostCode)
        .orElse(&quot;우편번호 없음&quot;);
}        </code></pre>
<p>Optional을 사용하기 전의 지저분한 코드를 
Optional,람다식,메소드 참조로 간결하고 깔끔하게 작성할 수 있다.</p>
<h2 id="orelse--orelseget">orElse / orElseGet</h2>
<p>Optional에서 값이 Null일 경우 반환 값을 지정해주는 두개의 메소드는 값을 불러오는 과정에서 약간의 차이가 있다.</p>
<p><em>실제 구현 부분 코드)</em></p>
<pre><code class="language-java">public T orElse(T other) {
    return value != null ? value : other;
}

public T orElseGet(Supplier&lt;? extends T&gt; supplier) {
    return value != null ? value : supplier.get();
}</code></pre>
<p><code>orElse</code>는 차이는 <strong>특정 값이나 특정 메소드가 실행되고 반환된 값을 가지고 있다</strong>가 비교 후 반환한다.</p>
<p><code>orElseGet</code>는 차이는 <strong>특정 메소드자체를 가지고 있다가 비교 후 Null일 경우 해당 메소드를 실행</strong>해서 반환한다.</p>
<blockquote>
<p><strong>Supplier</strong>란?</p>
<p>자바8부터 추가된 함수형 인터페이스이다. 
위 예시처럼 메소드를 변수화하여 파라미터를 넘길 수 있다. 
주로 해당 메소드에 매개변수가 없을 경우에 사용되며 매개변수가 존재하는 메소드라면 Function인터페이스가 주로 사용된다.</p>
</blockquote>
<h2 id="주의점">주의점</h2>
<p>Optional은 Null을 대체하기 위해(Null-safe) 지원되는 클래스이다.</p>
<p>따라서, 무분별한 Optional 사용은 가독성을 오히려 해칠 수 있다.
예를 들어, Optional 객체 자체도 null로 선언될 수 있어서 Optional의 null 체크도 같이 수행되어야 한다고 하면, 불필요한 확인 과정이 추가될 수 있다.</p>
<p>어떤 값을 감싸는 Wrapper 클래스 이기에 성능에 악영향이 끼칠 가능성도 있다.
Optional 객체는 객체 내부에 실제 값을 포함하고 있기 때문에 메모리를 더 많이 사용하며 대용량 데이터 처리 시 문제가 발생할 경우가 생길 수 있다. </p>
<h3 id="직렬화">직렬화</h3>
<p>자바8에서는 Optional은 직렬화를 지원하지 않는다.
자바9부터 지원한다고 하지만 직렬화는 많은 단점이 있어 지양해야 한다.</p>
<p>Optional 객체는 값이 없는 경우로 존재할 수 있는 클래스이다.
그런데 <strong>Optional 객체가 가지는 값이 직렬화</strong>가 되며 <strong>실제 값이 없더라도 Optional 객체가 직렬화된다면</strong> 직렬화된 객체의 크기를 불필요하게 늘릴 수 있다. 
때문에 이 경우 Optional 객체를 클래스의 필드값으로 사용하는것보다 실제값을 사용하는것이 더 효율적이다.</p>
<p>또한 역직렬화시 Optional의 특성 상 <strong>직렬화 이전과 같은 값을 가진다는 보장이 없다.</strong>
<em>만약 &quot;VELOG&quot; 라는 문자열 값을 가지고 있는 Optional 객체의 값을 직렬화한다고 가정했을 때,</em> 
Optional 객체 내부의 &quot;<em>VELOG</em>&quot;라는 문자열이 직렬화 되겠지만, <code>Optional.empty()</code>일 경우에는 당연히 null값이 직렬화된다.</p>
<p>이때 역직렬화를 한다고 해서 <code>Optional.empty()</code>, 또는 &quot;<em>VELOG</em>&quot; 라는 값을 가질 수 있는 <strong>Optional 객체가 아닌 null값으로 역직렬화</strong>가 될 수 있다.
<em>(다시말해 직렬화 이전과 다르게 역직렬화로 객체를 가져오게 된다면 empty() 가 아닌 null로 인해 <code>NullPointerException</code>, <code>NoSuchElementException</code>이 발생할 수 있다.)</em></p>
<p>Optional 객체를 직렬화할 때 객체 자체를 직렬화하는 것이 아니라, Optional 객체가 가지는 값의 정보를 직렬화하게 되므로 역직렬화할 때 같은 방식으로 역직렬화가 된다는 보장이 없을 수 밖에 없다.</p>
<h3 id="반환타입으로-사용되어야-한다">반환타입으로 사용되어야 한다.</h3>
<p>애초에 Optional은 반환 타입으로서 <code>NullPointerException</code> 등의 에러가 발생할 수 있는 경우에 <strong>결과가 없음</strong>을 명확히 하여 <strong>반복되는 null 확인과정을 방지</strong>하고 Stream API와 결합되어 유연하고 간결한 메소드 체이닝과 함수형 프로그래밍을 지원하기 위해 설계되었다.</p>
<p>때문에 코드의 가독성과 성능을 해치지 않게 적절하게 사용되어야 한다.</p>
<blockquote>
<p>출처
<a href="https://mangkyu.tistory.com/70">https://mangkyu.tistory.com/70</a>
<a href="https://mangkyu.tistory.com/203">https://mangkyu.tistory.com/203</a>
<a href="https://stackoverflow.com/questions/74213739/jackson-deserializing-json-into-an-object-having-an-optional-field">https://stackoverflow.com/questions/74213739/jackson-deserializing-json-into-an-object-having-an-optional-field</a></p>
</blockquote>
<h1 id="interface의-default-static-메소드">Interface의 default, static 메소드</h1>
<p>자바8 이전에는 오직 추상메소드만 선언할 수 있었다.
인터페이스는 호환성과 유연성을 높이고, 코드의 재사용성과 확장성을 높이는데 목적이 있다.
하지만 상황에 따라 인터페이스에 모든 추상메서드를 구현해야 했으므로 중복된 코드와 간결해지기 힘들었다. </p>
<p>때문에 <em>default, static</em> 메소드가 추가되었다.</p>
<p>이 두 메소드는 인터페이스에 기능을 직접 구현함으로써 해당 인터페이스를 구현할 때 반복되는 코드의 양을 줄일 수 있어 더 유연한 개발이 가능해졌다.</p>
<h2 id="default-메소드">default 메소드</h2>
<p>인터페이스에 <code>default</code> 붙여 선언하며 정의된 메소드에 기본기능을 구현할 수 있다.
구현체에서 <strong>오버라이드가 가능</strong>하며 구현체에 의해 사용가능하다.</p>
<p>자주쓰이는 경우에는 <code>Iterable</code> 인터페이스의 <code>forEach(Consumer&lt;? super T&gt; action)</code>과 같은 default 메소드가 있다.</p>
<p><em>java.lang.Iterable 예시)</em></p>
<pre><code class="language-java">default void forEach(Consumer&lt;? super T&gt; action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}</code></pre>
<h2 id="static-메소드">static 메소드</h2>
<p>인터페이스에 <code>static</code> 붙여 선언하며 정의된 메소드에 특정 기능을 구현해 놓을 수 있다.
구현체에서 <strong>오버라이드가 불가능</strong>하며 직접 인터페이스에서 호출해서 사용해야한다.</p>
<p>자주쓰이는 경우에는 <code>LocalDate</code> 인터페이스의 <code>now()</code>과 같은 static 메소드가 있다.</p>
<p><em>java.time.LocalDate 예시)</em></p>
<pre><code class="language-java">public static LocalDate now() {
    return now(Clock.systemDefaultZone());
}</code></pre>
<blockquote>
<p>또한 인터페이스의 필드에 상수를 선언할 수 있게 되었다.</p>
<p>상수필드에 <code>public static final</code> 의 키워드가 생략되었다면 컴파일러가 자동으로 붙여준다.
자바9 부터는 <code>private</code> 이나 <code>private static</code> 키워드도 사용하여 인터페이스 내부 사용 용도로 사용하는 상수도 선언할 수 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Controller - Service - Repository]]></title>
            <link>https://velog.io/@cv_/Controller-Service-Repository</link>
            <guid>https://velog.io/@cv_/Controller-Service-Repository</guid>
            <pubDate>Fri, 07 Apr 2023 07:58:25 GMT</pubDate>
            <description><![CDATA[<h1 id="controller">Controller</h1>
<p>클라이언트의 요청을 직접 받는 역할</p>
<p>요청을 받을 때, 쿼리스트링이나 DTO 객체를 활용해 구체적인 요청을 받는다.
요청을 받기만 할 뿐, 직접 도메인에 관한 문제를 해결하거나 데이터를 가공하는 로직(비즈니스로직)을 가지고 있지 않다.
클라이언트와의 상호작용이 가장 큰 목적이며 이후 Service, Repository 등에서 처리/조회된 결과, 도메인 객체 등을 반환한다.</p>
<h1 id="service">Service</h1>
<p>실제 도메인 객체의 비즈니스 로직을 처리하는 역할</p>
<p>Controller에서 전달받은 데이터를 기반으로 도메인 객체를 다룬다.
도메인을 생성하거나 실제 도메인이 가져야되는 규칙/제한 등을 처리하는 핵심 로직을 담고 있어야한다. </p>
<h1 id="repository">Repository</h1>
<p>도메인에 대한 정보를 저장/조회하는 역할
내/외부의 데이터베이스와 상호작용하며 도메인 데이터의 지속성, 영속성을 유지하기 위해 필요하다. 
Service에서 비즈니스 로직을 처리하기 위해 데이터를 제공해야한다.</p>
<blockquote>
<p><em>비즈니스 로직?</em>
서비스를 제공하는데 있어서 필요한 요구사항/규칙/제한 등
유지보수성, 유연성, 확장성을 넓히는 코드 컨벤션 등도 포함된다고 생각함
예를 들어 회원가입 시 로그인 아이디 중복 등을 체크하여 회원가입에 제한을 두어야하며
추후 소셜 로그인 등을 지원하기 위해 확장성있는 코드로 구현되어야 할듯
도메인 모델/컨텍스트바운더리의 모든 흐름이 비즈니스 로직이라고 할 수 있을듯</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Entity ,DTO ,VO 각각의 의미들]]></title>
            <link>https://velog.io/@cv_/Entity-DTO-VO-%EA%B0%81%EA%B0%81%EC%9D%98-%EC%9D%98%EB%AF%B8%EB%93%A4</link>
            <guid>https://velog.io/@cv_/Entity-DTO-VO-%EA%B0%81%EA%B0%81%EC%9D%98-%EC%9D%98%EB%AF%B8%EB%93%A4</guid>
            <pubDate>Wed, 05 Apr 2023 11:31:58 GMT</pubDate>
            <description><![CDATA[<h1 id="entity">Entity</h1>
<p>데이터베이스와 직접 연결되는 클래스이다.
그러므로 도메인모델의 실체화라고도 할 수 있다. 왜? 해당 도메인의 실제 데이터를 담고 있으니까
어떤 로직을 담고있는 경우가 많다. (setName 등으로...)</p>
<p>보통 테이블에서 하나의 ROW를 객체로 표현한다.</p>
<h1 id="dto-data-transfer-object">DTO (Data Transfer Object)</h1>
<p>이름 그대로 오직 계층간의 데이터를 주고 받는 역할을 하는 클래스
딱 이역할만을 하기 때문에 다른 로직등을 포함하고 있지 않음
주로 클라이언트와 서비스/컨트롤러에서 데이터를 주고받는데 사용된다.</p>
<h1 id="vo-value-object">VO (Value Object)</h1>
<p>값(Value)만 가지는 객체이다
기본적으로 값을 변경할 수 없으며 그냥 가지고 있다.
보통 속성 값의 집합으로 사용되며 DDD에서 중요한 개념이다.</p>
<p>나는 과거에 프로젝트를 진행할때 어떤 도서의 ISBN값이나 지은이, 제목 등의 불변값 등을 VO 객체로 활용했었다.</p>
<p>Entity 객체의 복잡성을 줄이는데 많이 사용된다.</p>
<blockquote>
<p>요약
Entity: 데이터베이스와 매핑되는 객체, 로직을 포함하는 경우가 많음
DTO: 계층 간 데이터 교환을 위한 객체, 다른 기능은 없음
VO: 값을 갖는 객체, 불변성을 가지는 객체</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDD vs SQL 중심 설계 차이]]></title>
            <link>https://velog.io/@cv_/DDD-vs-SQL-%EC%A4%91%EC%8B%AC-%EC%84%A4%EA%B3%84-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@cv_/DDD-vs-SQL-%EC%A4%91%EC%8B%AC-%EC%84%A4%EA%B3%84-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 05 Apr 2023 11:09:00 GMT</pubDate>
            <description><![CDATA[<p>DDD(<em>Domain-Driven Design</em>)와 SQL-DD (<em>Structured Query Language-Driven Design</em>)는 소프트웨어를 개발하는 여러 방법론중 일부이다.</p>
<p>이름으로 유추할 수 있듯 두 방식은 설계할 때부터 접근방법이 다르다.</p>
<h1 id="ddd-domain-driven-design">DDD (Domain-Driven Design)</h1>
<blockquote>
<p>도메인이란 특정 분할된 업무 분야나 문제 영역을 뜻하며 비슷한 업무들의 집합으로 분류될 수 있다. 특정 임의의 개념으로 나누거나 개념들 사이의 관계 또한 포함하고 있는 개념이다.</p>
<p>예를 들어, 어떤 교육을 진행한다고 가정할 때 선생과 학생이라는 개념이 포함된다.
학생은 강의를 듣고, 수업을 듣고, 과제를 제출한다.
선생은 강의를 개설하고, 수업을 진행하고, 과제를 채점한다.</p>
<p>이 경우 도메인은 강의, 수업, 성적을 도메인으로 볼 수 있으며 모두 선생과 학생이라는 도메인의 관계도 포함하고 있다.</p>
</blockquote>
<p>위의 설명처럼 <em>DDD</em> 는 좀 더 객체지향에 가깝다고 생각해볼 수 있어서 자주 쓰이는 방법론중 하나이다. </p>
<p>(<em>이런 개념까지 나온 이유는 아마도 구현하려는 서비스의 개념자체가 복잡할 경우, 최대한 구현하려는 사람이 편하기위해, 실수를 줄이기 위해, 유지보수가 편하기 위해 고안된 방법이라고 생각한다.</em>)</p>
<p>이름에서 나타나듯 도메인을 중심으로 설계하고 디자인하여 도메인들끼리의 조합을 통해 시너지를 내며 최종 결과의 복잡성을 줄이고 유연성을 높여 유지보수를 용이하게 할 수도 있다.</p>
<p>이를 가능하게 하기 위해서 핵심은 크게 3가지로 나눌 수 있다.</p>
<ol>
<li>핵심 도메인을 나누고 해당 도메인에 집중</li>
<li>각각의 도메인을 정교하게 구축하고 세분화</li>
<li>해당 도메인의 문제를 해결할 수 있는 전문가와 적극적인 협업</li>
</ol>
<p>이를 가능하게 하기 위해 DDD는 전략적(<em>Strategic</em>)설계와 전술적(<em>Tactical</em>)설계로 나뉜다. </p>
<ul>
<li>전략적 설계(Strategic Design)</li>
<li><em>모델과 바운디드 컨텍스트를 나누는 설계*</em>이며 협업을 위한 유비쿼터스 언어의 정리도 포함된다.
개념을 코드로 구현하기 전에 모델들 사이의 상호작용을 고려하여 인터페이스 등을 정의하거나 <strong>전체적인 흐름을 파악하고 경계를 나누며 여러 문제를 명확히 이해할 수 있도록 하는 과정</strong>이다.</li>
<li>전술적 설계(Tactical Design)
세부 아키텍처, 코드의 구현과 관련된 부분이며 전략적 설계(Strategic Design)에서 나뉜 <strong>경계와 모델 등을 유지보수성과 확장성을 고려하여 코드를 구성하는 등의 작업과정</strong>이다.
데이터 흐름 등을 고려한 세부적인 설계를 수행하고, 구현 과정에서 발생할 수 있는 다양한 문제들을 해결하고 <strong>코드의 유연성과 유지보수성을 보장하는 과정</strong>이기도 하다.</li>
</ul>
<p>이 방식으로 설계하기 위해, 또는 설명하기 위해서 여러가지 개념들이 포함된다.</p>
<h2 id="모델">모델</h2>
<p>모델이란 도메인에 대한 이해와 복잡성을 해결하기 위해 해당 도메인의 규칙, 개념, 프로세스 등을 포함하는 추상적 개념이다.
모델은 다른 모델과의 경계가 확실해야하며 소프트웨어 개발자와 해당 도메인의 전문가가 같이 구축해 나가는 개념이다.</p>
<p>예를 들어 위 예시에서 학생과 선생으로 클래스로 나눌 수 있다.
학생은 이름, 학번, 수강과목 등의 속성을 가지고 선생은 수업을 개설할 수 있으며 때에 따라 휴강할 수 있는 동작도 정의할 수 있다.</p>
<p>즉, 객체지향적인 관점에서 소프트웨어를 개발하기 위해 현실 세계의 개념을 객체로 추상화된 개념으로 정의할 수 있다.</p>
<h2 id="바운디드-컨텍스트-bounded-context">바운디드 컨텍스트 (Bounded Context)</h2>
<p>위 모델을 하나의 경계로 묶는 개념이다. 때문에 하나의 논리적인 모델을 갖는 것이 일반적이다.
바운디드 컨텍스트는 보통 용어를 기준으로 구분하며 실제로 다른 패키지에서 개발되는 코드라면 다른 바운디드 컨텍스트를 갖는다고 할 수 있다.</p>
<p>바운디드 컨텍스트에서 개발할 때 주의할 점은 모델이 섞이지 않도록 하는 것이다. 바운디드 컨텍스트는 서로 다른 패키지에서 개발하고 서로의 영역이 명확해야 한다.</p>
<h2 id="유비쿼터스-언어-ubuquitous-languege">유비쿼터스 언어 (Ubuquitous Languege)</h2>
<p>개발자와 도메인 전문가와의 정확하고 효율적인 협업을 위해 공통된 용어를 정의하거나 개념을 정리하는 것을 뜻한다. 예를 들어 위 예시에서 선생이 아니라 강사라고 지칭한다면, 도메인 전문가는 시간제 강사로 오해할 수 있는 여지가 충분하기에 정확이 문제를 해결하기 위한 언어라고 할 수 있다.</p>
<p>용어뿐만이 아니라 ERD, 다이어그램, 유스케이스 또한 포함될 수 있다.</p>
<h2 id="도메인-모델-패턴">도메인 모델 패턴</h2>
<blockquote>
<p>출처 : <a href="https://joont92.github.io/ddd/%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8/">https://joont92.github.io/ddd/도메인-모델/</a></p>
</blockquote>
<p>어떤 서비스의 수행 패턴을 말하는데 <strong>객체지향적으로 위에서 정의된 모델과 바운디드 컨텍스트 등의 규칙/개념 등을 구현하거나 처리하는 패턴</strong>이다. </p>
<p>일반적으로 크게 4가지의 계층으로 나뉜다.</p>
<ul>
<li>Presentation(표현)
사용자의 요청을 처리하고 사용자에게 정보를 보여준다</li>
<li>Application(응용)
사용자가 요청한 기능을 실행한다
업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다</li>
<li>Domain(도메인)
시스템이 제공할 도메인의 규칙을 구현한다</li>
<li>Intrastructure(인프라 구조)
외부 시스템과의 연동을 처리한다(DB, Messaging 등)</li>
</ul>
<h1 id="sql-dd-structured-query-language-driven-design">SQL-DD (Structured Query Language-Driven Design)</h1>
<p>말그대로 데이터베이스의 설계에서 시작되는 소프트웨어 개발 방법론 중 하나이다.</p>
<p>데이터베이스에 대한 전문적인 지식이 있다면 <strong>도메인 모델과 관계형 데이터베이스 간의 일관성을 유지하여 도메인 모델링과 관계형 데이터베이스 설계 간의 간극을 줄이는 것</strong>이 가능하며 이것이 곧 목적이기도 하다.</p>
<h2 id="역정규화-denormalization">역정규화 (Denormalization)</h2>
<p>SQL-DD에서 중요한 개념인 역정규화는 도메인 모델에 따라 데이터베이스 스키마를 수정되는것을 의미한다. 이를 통해 도메인 모델과 데이터베이스 간의 일관성을 유지한다.</p>
<p>데이터베이스의 스키마에서 도메인이 가지고 있는 개념도 포함되어 있기 때문에 개발속도를 높이는데도 이점이 있으며 SQL 쿼리문을 간단하게 수정될 수 있어 가독성을 높일 수 있다.</p>
<h2 id="생산성과-이식성">생산성과 이식성</h2>
<p>직관적인 SQL 구문을 사용하여 테이블 구조를 정의하고 데이터베이스를 구축할 수 있기에 빠른 개발이 가능해서 생산성이 높아진다.
또한 데이터베이스를 사용한다는 것은 추상적인 개념 위주 대신 직관적인 데이터를 위주로 개발할 수 있기 때문에 데이터베이스 유지보수에 이점이 있다.
업무 로직 변경 등으로 인한 데이터베이스 변경이 필요할 때도 비교적 빠른 수정이 가능하다.</p>
<p>또한 SQL 쿼리문은 대부분의 DBMS에서 지원하기 때문에 이식성이 뛰어나다.
때문에 다른 데이터베이스 구조나 특성, 쿼리 구문을 일일히 수정하지 않고 마이그레이션을 할 수 있다.</p>
<h2 id="왜-ddd보다-안쓰이는가">왜 DDD보다 안쓰이는가?</h2>
<p>요즘 소프트웨어 개발의 패러다임은 당연히 객체지향이다.
근데 SQL-DD는 데이터베이스와의 너무 높은 결합도를 가지고 있기에 객체지향의 장점인 확장성과 유연성에 방해가 될 수 있다.</p>
<h3 id="성능이슈">성능이슈</h3>
<p>역정규화로 성능을 높이기 때문에 중복된 코드의 양이 늘어나거나 데이터 무결성을 저해할 수 있다. 때문에 데이터 일관성, 코드 컨벤션의 유지가 어려워지는 등의 유지보수의 문제와 더불어 그에 따라 성능에 악영향을 피할 수 없는 경우가 발생할 수 있다.</p>
<h3 id="유지보수">유지보수</h3>
<p>SQL-DD는 데이터 모델링과 구현에 집중된 방법론이기에 서비스가 동적인 변화가 잦다면 대응하기 어려울 수 있다.</p>
<p>예를 들면, 위 도메인 예시에서 학생, 선생 이외에 강사, 교환학생 등의 도메인이 추가된다면, 새로운 데이터베이스 스키마가 필요하고 그로 인해 관련된 모든 쿼리문의 수정이 불가피할 경우가 생긴다.</p>
<h1 id="ddd-vs-sql-dd">DDD vs SQL-DD</h1>
<p>그래서 SQL-DD가 무조껀 안좋을까? 상황에 따라 다르다고 생각한다.</p>
<p>DDD는 엄청 복잡한 서비스라고 할지라도 도메인별로 모델 등을 분류함으로써 개발자 입장에서 해당 도메인에만 집중할 수 있으며 유지보수가 쉬워진다.</p>
<p>SQL-DD는 규모가 작은 프로젝트라면 더 빠르고 데이터베이스의 구조로 전체적인 흐름을 DDD보다 더 빠르고 쉽게 파악이 가능할 수 있다. 
데이터베이스의 구조를 명확하게 정의하고 데이터의 일관성을 유지하는 방법이기에 버그나 예기치 못한 상황을 방지하기 더 편할 것이다.</p>
<p>DDD는 도메인에 대한 이해가 확실해야 할 뿐더러 SQL-DD보다 조금 더 복잡한 방법론이기에 복잡성을 해결하기엔 효율적이나 설계 단계에서 오랜 시간이 걸릴 수 있다.
때문에 많은 비용도 감수해야할 상황이 생길 수 있으며 작은 규모의 프로젝트에서는 오히려 더 복잡해지는 상황이 생길 수 있다.</p>
<p>하지만 SQL-DD는 데이터 중심 설계를 통해 이미 개발 단계에서 바로 적극적인 데이터 활용으로 빠른 생산성을 가지기에 작은 규모에서 더 유리할 것이다.</p>
<p><em>즉, 작은 규모의 사이즈에선 SQL-DD가, 큰 규모와 높은 복잡성을 가진다면 DDD가 효율적이라고 생각한다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[IoC와 DI란]]></title>
            <link>https://velog.io/@cv_/IoC%EC%99%80-DI%EB%9E%80</link>
            <guid>https://velog.io/@cv_/IoC%EC%99%80-DI%EB%9E%80</guid>
            <pubDate>Tue, 04 Apr 2023 04:17:32 GMT</pubDate>
            <description><![CDATA[<h1 id="ioc와-di란">IoC와 DI란</h1>
<p>IoC는 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 
외부에서 결정되는 디자인패턴을 말하는데 이 객체에 대한 제어권을 스프링 컨테이너가 가지고 있기때문에 제어의 역전이라고 불린다.</p>
<blockquote>
<p>대표적 예시 중 하나로 스프링부트에서 @SpringBootApplication 어노테이션이 보통 프로젝트 메인 클래스에 정의되어 있다.
이 어노테이션이 스프링 컨테이너를 생성하고 컴포넌트 스캔을 수행하여 빈 객체를 찾아 의존성을 자동으로 주입한다. </p>
</blockquote>
<p>주로 @Component, @Service, @Repository, @Controller 등의 어노테이션이 붙은 클래스들이 빈으로 등록될 대상이 되며 
@ComponentScan 어노테이션으로 basePackages, basePackageClasses, includeFilters, excludeFilters 등의 옵션으로 Spring Container의 빈 객체 관리에 대해 세밀하게 지정할 수 있다.</p>
<p>개발자가 직접 객체 간의 의존성을 생성하고 관리하면 추후 코드의 가독성과 유지보수성을 떨어뜨리는 문제 등이 발생할 수 있어 고안된 방법이며 코드의 가독성과 유지보수성을 높일 수 있다.</p>
<p>IoC의 구현 방식 중 하나인 DI(Dependency Injection)은 IoC를 구현하는 방법 중 하나이다. 
*<em>IoC가 제어의 역전 개념을 통틀어 말한다면 DI는 이 개념을 구현하기 위해 의존관계를 외부에서 주입(Injection)하는 방식이다. *</em></p>
<hr>
<h2 id="어떻게-di는-객체끼리-결합도를-낮추는가">어떻게 DI는 객체끼리 결합도를 낮추는가</h2>
<p>일단 결합도가 낮다는 의미는 객체끼리의 영향을 끼치는 정도로 말할 수 있다.</p>
<p><a href="https://velog.io/@cv_/%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Servlet-Container-Spring-Container">이전글</a>과 비슷한 예시로</p>
<pre><code class="language-java">public class A {
    private B b = new B();
}</code></pre>
<p>이 코드는 A가 B를 직접 생성하기에 객체끼리 결합도가 높다고 볼 수 있다.
<em>B의 생성자에서 파라미터가 추가된다면?</em> , A의 코드도 수정되어야 할 것이다.</p>
<pre><code class="language-java">// 예시2)

public class A {
    private B b;

    public A (B b) {
        this.b = b;
    }

    // 또는
    public void setB(B b) {
        this.b = b;
    }
}</code></pre>
<p>A에 생성자나 Setter로 외부에서 생성된 B를 받는다면?
A는 수정될 필요가 없다.</p>
<p>이런 방법은 결합도를 낮춰 재사용성을 높여 유연하게 만들 뿐만 아니라
코드도 간결해지고 모의객체를 활용해 테스트도 불편함 없이 수행할 수 있다.</p>
<h2 id="왜-di만-쓰는가">왜 DI만 쓰는가</h2>
<p>IoC를 구현하는 방법에는 여러가지가 있었는데 DI 방식보다 비효율적이라서 사장된듯 하다.</p>
<h3 id="service-locator-pattern">Service Locator Pattern</h3>
<p>객체를 생성하기 위해 InitialContext 클래스로 객체를 확인하고 생성하는 방식
객체끼리의 결합도는 낮출수는 있으나 테스트에 어려움이 있꼬 코드가 길어진다.</p>
<blockquote>
<p>InitialContext 클래스는 <code>javax.naming</code> 패키지에 위치해 있으며
패키지와 클래스 이름으로 프로젝트 내에서 해당 클래스를 찾아볼 수 있는 클래스이다.</p>
</blockquote>
<h3 id="service-provider-framework-pattern">Service Provider Framework Pattern</h3>
<p>Provider (제공자) 클래스를 따로 구현하여 이 제공자 클래스로 객체를 요청하고 제공자 클래스가 객체를 반환해주는 방식이다.</p>
<p>이 경우 객체를 제공해주는 클래스 자체가 인터페이스와 구현객체들에게 의존하고 있어서 결합도를 높이는 방식으로 동작할 수 밖에 없으며 소스코드 또한 길어지게 된다.</p>
<p>간단한 프로그램에선 적용시키기 어렵고 적용시키더라도 불필요한 과정이 추가되며
북잡한 프로그램에선 성능에 악역향을 끼칠 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유튜브 웹 기능들]]></title>
            <link>https://velog.io/@cv_/%EC%9C%A0%ED%8A%9C%EB%B8%8C-%EC%9B%B9-%EA%B8%B0%EB%8A%A5%EB%93%A4</link>
            <guid>https://velog.io/@cv_/%EC%9C%A0%ED%8A%9C%EB%B8%8C-%EC%9B%B9-%EA%B8%B0%EB%8A%A5%EB%93%A4</guid>
            <pubDate>Mon, 03 Apr 2023 05:22:00 GMT</pubDate>
            <description><![CDATA[<h2 id="계정">계정</h2>
<ul>
<li>로그인 및 회원가입 (구글)</li>
<li>한 계정에서 다중 채널 개설 및 계정전환</li>
<li>채널 유료 멤버쉽 생성 기능, 구독 기능</li>
<li>유튜브 프리미엄 결재 기능 (요금제 구체화 및 다른 계정과 공유)</li>
<li>워터마크, 배너, 채널사진 설정</li>
<li>채널 방문용 핸들 설정</li>
<li>저작권 신청 및 설정</li>
<li>기타 연락처 설정</li>
<li>동영상 수익 및 광고 신청</li>
<li>영상제작용 오디오 보관함 관리</li>
<li>시청 시 맞춤 광고 설정</li>
<li>고객센터운영</li>
</ul>
<h2 id="동영상">동영상</h2>
<ul>
<li>동영상 재생, 업로드, 삭제, 비공개 등</li>
<li>동영상 업로드시 설명 가능</li>
<li>영상화질 설정 및 음량 설정</li>
<li>내 동영상 목록 공개범위 지정 (재생목록, 구독정보)</li>
<li>동영상 즐겨찾기, 시청로그, 좋아요/싫어요 및 조회수 누적</li>
<li>동영상 추천, 관련 동영상 목록</li>
<li>동영상 클립 편집 및 업로드</li>
<li>비동기로 동영상 플레이리스트 적용/수정</li>
<li>동영상 자막 제작 및 플레이어에서 자막 설정</li>
<li>음성스크립트 자동 생성 및 조회, 해당 내용으로 이동</li>
<li>다음 영상 자동재생, PIP, 에어플레이 및 크롬캐스트</li>
</ul>
<h3 id="실시간-스트리밍">실시간 스트리밍</h3>
<ul>
<li>채팅 기능</li>
<li>채팅 시 광고/링크/전화번호/도배 필터</li>
<li>채팅 권한 제한</li>
<li>슈퍼챗 도네이션 (+결재)</li>
<li>도네이션 내용 읽어주기 (TTS)</li>
<li>실시간 스트리밍 일시정지/뒤로 돌리기</li>
</ul>
<h2 id="댓글">댓글</h2>
<ul>
<li>댓글 작성/수정/삭제</li>
<li>동영상 별 댓글, 대댓글 및 사용자 태그</li>
<li>댓글 신고기능</li>
<li>댓글에 영상 시간 링크 가능, 클릭 시 해당 동영상의 시간대로 이동</li>
<li>댓글, 대댓글 별 좋아요 및 중복방지</li>
<li>댓글, 동영상 업로드 알림 비동기 처리, 댓글 조회 시 비동기 로드</li>
<li>댓글 정렬 (최신순/인기순)</li>
<li>영상 별 댓글 제한/보류/사용안함 설정</li>
</ul>
<h2 id="검색">검색</h2>
<ul>
<li>영상 제목 검색 (영상 별 키워드태그 및 스크립트 포함) &amp; 검색어 자동완성</li>
<li>마우스커서 오버 시 짧게 구간 재생</li>
<li>검색결과 필터링 (화질 별, 길이 별, 날짜 별 etc..) &amp; 제한모드</li>
<li>함께 검색한 항목 노출 (통계)</li>
<li>쇼츠, 일반동영상 나눠서 결과 표시</li>
<li>해당 검색어의 관련 채널 노출</li>
<li>영상 신고, 신고 이유 구체화</li>
<li>안보는 채널 등록</li>
</ul>
<h2 id="통계">통계</h2>
<ul>
<li>시청로그/댓글활동 등 전체 및 일부 삭제</li>
<li>채널 별 조회수/시청시간/시청자층/시간대 분석</li>
<li>영상별 수익 및 실시간 구독/총조회수 조회</li>
<li>카테고리 분류 및 인기 급상승 영상 표시</li>
</ul>
<h2 id="기타">기타</h2>
<ul>
<li>유튜브(구글)계정 타사와 연결 - 영상 시청 이벤트 등</li>
<li>언어/지역 설정</li>
<li>라이트/다크모드 설정</li>
<li>단축키 설정 </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] @Override 꼭 적어야할까]]></title>
            <link>https://velog.io/@cv_/Java-Override-%EA%BC%AD-%EC%A0%81%EC%96%B4%EC%95%BC%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@cv_/Java-Override-%EA%BC%AD-%EC%A0%81%EC%96%B4%EC%95%BC%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Wed, 29 Mar 2023 09:09:52 GMT</pubDate>
            <description><![CDATA[<p>Override는 메소드를 재정의하는 행위를 뜻한다.
보통 상위클래스를 상속받고 상위클래스의 메소드를 재정의할때 사용한다.</p>
<p>이 경우 <code>@Override</code> 어노테이션을 생략이 가능하다.
근데 저 짧은 어노테이션 하나로 <em>상위클래스에 같은 메소드를 시그니쳐를 가지고 있다!</em>
라는 것을 알려주는데 굳이 생략할 필요가 있을까..?</p>
<p>생략이 가능하기에 어느날 상위클래스에서 해당 메소드가 사라져도 에러가 안날 것이다..
곧 자식클래스만의 메소드로 남게 될 것이며 이로 인해 프로그램에서 뜻하지 않은 버그가 생겨날 여지가 있다고 생각하기에 <code>@Override</code> 는 왠만하면 생략하지 않는 것이 좋을 것같다.</p>
<p>비슷하게 메소드 <em>Overload</em> 도 있다.
같은 메소드이름으로 파라미터를 다르게 설정한다면 <em>Overload</em> 할 수 있다.</p>
<pre><code class="language-java">class Parent {
    public void hello(String name) {
        System.out.println(&quot;안녕하세요, 저는 &quot; + name + &quot;입니다.&quot;);
    }
}

class Child extends Parent {
    // @Override 사용불가
    public void hello() {
        System.out.println(&quot;안녕!&quot;);
    }
}</code></pre>
<p>부모클래스와 자식클래스의 <code>hello</code> 메소드는 파라미터가 다르기에 사실 상 다른 기능을 하는 메소드이다.
이 경우 <em>Overload</em> 이기에 <code>@Override</code> 어노테이션을 <strong>사용할 수 없다.</strong></p>
<p>하지만 메소드 시그니쳐를 동일하게 바꾼다면 <code>@Override</code>를 사용할 수 있다.
<em>(생략도 가능하다.)</em></p>
<pre><code class="language-java">class Parent {
    public void hello(String name) {
        System.out.println(&quot;안녕하세요, 저는 &quot; + name + &quot;입니다.&quot;);
    }
}

class Child extends Parent {
    @Override // 생략가능
    public void hello(String name) {
        System.out.println(&quot;안녕! &quot; + name + &quot;!&quot;);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 인터페이스가 가지고 있는 객체지향적 특징]]></title>
            <link>https://velog.io/@cv_/Java-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EA%B0%80-%EA%B0%80%EC%A7%80%EA%B3%A0-%EC%9E%88%EB%8A%94-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%A0%81-%ED%8A%B9%EC%A7%95</link>
            <guid>https://velog.io/@cv_/Java-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EA%B0%80-%EA%B0%80%EC%A7%80%EA%B3%A0-%EC%9E%88%EB%8A%94-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%A0%81-%ED%8A%B9%EC%A7%95</guid>
            <pubDate>Tue, 28 Mar 2023 08:13:24 GMT</pubDate>
            <description><![CDATA[<p><code>Interface</code> 는 보통 설계도의 성격을 띄기에 <strong>추상화</strong>의 특징이 드러나지만
또 다른 <code>Interface</code>를 상속할 수 있기에 경우에 따라서 <strong>상속성</strong>의 특징도 나타날 수 있다.</p>
<p>또 생각해보면 정의된 메소드들은 구현 클래스에 따라서 모두 다른동작을 하거나
인터페이스의 타입으로 구현클래스를 다룰 수 있어서 <strong>다형성</strong>의 특징이 나타난다고도 말할 수 있을것같다. 
<code>ex) Map map = new HashMap();</code></p>
<p>근데 자바에서 <code>Interface</code>는 static한 메소드도 쓸 수 있는거 보면 
해당 메소드의 지역변수 한에서는 <strong>캡슐화</strong>의 특징도 어느정도 나타난다고 볼 수 있지 않을까?</p>
<p>결국 아무리 <code>Interface</code>라고 해도 어떻게 사용하냐에 따라 모든 객체지향적 특징이 드러날 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체지향의 4가지 특징]]></title>
            <link>https://velog.io/@cv_/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-4%EA%B0%80%EC%A7%80-%ED%8A%B9%EC%A7%95</link>
            <guid>https://velog.io/@cv_/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-4%EA%B0%80%EC%A7%80-%ED%8A%B9%EC%A7%95</guid>
            <pubDate>Tue, 28 Mar 2023 07:49:35 GMT</pubDate>
            <description><![CDATA[<h1 id="캡슐화-encapsulation">캡슐화 (Encapsulation)</h1>
<p>내가 생각하는 캡슐화는 하나의 어떤 로직, 데이터를 클래스 또는 객체로 묶어서 데이터의 손상을 방지하거나 정해진 규칙이나 로직에 따라서만 사용 가능하게 만드는 방식이다.
예를 들어 VO(Variable Object) 등의 패턴을 사용할 때 사용된다.
접근제어자의 이해가 필요하다.</p>
<pre><code class="language-java">// 코드 예시)
import java.time.LocalDate;

public class Main {
    public static void main(String[] args) {
        Student student = new Student(&quot;김송아&quot;, 20);

        System.out.println(&quot;학생성명 : &quot; + student.getName());
        System.out.println(&quot;출생년도 : &quot; + student.getBornYear());
    }
}
</code></pre>
<pre><code class="language-java">class Student {
    // 접근제어자로 외부로부터 데이터를 숨김
    private String name;
    private int age;

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

    public String getName(){
        // 공백을 포함하는 규칙을 포함
        return String.join(&quot; &quot;, name.split(&quot;&quot;));
    }

    public int getBornYear() {
        // 출생년도를 구하는 로직을 포함
        return LocalDate.now().getYear() - age;
    }
}
</code></pre>
<h1 id="추상화-abstraction">추상화 (Abstraction)</h1>
<p>공통적인 요소를 선별하고 정의하여 필수요소누락, 추가 등의 부분에서 실수를 방지한다.
전체적인 틀을 모델링하는 설계도와 비슷한 개념이다.</p>
<pre><code class="language-java">public interface IAnimal {

    // 구현해야할 메소드들
    void setName(String name);
    String getName();

    void setGender(String gender);

    void setSound(String sound);
    void sounds();
}</code></pre>
<pre><code class="language-java">/*
IAnimal 인터페이스를 구현한다면
위 5개의 정의된 메소드를 모두 구현해야한다.
*/

public class lamb implements IAnimal {
    String name;
    String gender;
    String sound;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public void setSound(String sound) {
        this.sound = sound;
    }

    @Override
    public void sounds() {
        System.out.println(this.sound);
    }
}</code></pre>
<h1 id="다형성-polymorphism">다형성 (Polymorphism)</h1>
<p>객체는 캐스팅해서 여러 타입의 형태를 가질 수 있다. ex) 한국은 국가이다.
하지만 모든 국가가 한국이 아니듯이 객체 간의 관계를 제대로 파악, 설계하는 것이 중요하다.</p>
<p>제네릭 와일드카드 구현할 때 이 개념을 모르면 캐스팅해서 파라미터를 넘길 때 얼탈 수도 있다. <a href="https://velog.io/@cv_/%EC%9E%90%EB%B0%94%EC%9D%98-%EC%A0%9C%EB%84%A4%EB%A6%ADGeneric-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">참고</a></p>
<pre><code class="language-java">public class Country {
    public String hello() {
        return &quot;shake hands&quot;;
    }
}

class Korea extends Country {
    @Override
    public String hello() {
        return &quot;안녕하세요~&quot;;
    }
}

class Japan extends Country {
    @Override
    public String hello() {
        return &quot;곤니찌와~&quot;;
    }
}</code></pre>
<p>위와 같은 관계가 있다고 할 때 </p>
<pre><code class="language-java">public class Main {
    public static void sayHello(Country country){
        System.out.println(country.hello());
    }

    public static void main(String[] args) {
        sayHello(new Country());
        sayHello(new Korea());
        sayHello(new Japan());
    }
}</code></pre>
<pre><code>출력결과)

shake hands
안녕하세요~
곤니찌와~</code></pre><p><code>sayHello</code>의 <code>Country</code> 파라미터는 <code>Korea</code>, <code>Japan</code> 등의 상속받는 다양한 타입으로 변환될 수 있다.</p>
<p>다형성을 잘 알고있으면 <code>sayHello</code> 메소드처럼 재사용성이 높아지게 코드를 작성할 수 있다.</p>
<h1 id="상속성-inheritance">상속성 (Inheritance)</h1>
<p>부모의 특징을 유전받는다고 생각하면 편하다.
<code>extends</code> 키워드로 어떤 클래스를 상속받은 클래스라면,
상속하는(부모) 클래스의 메소드, 필드 등도 상속받는(자식)클래스에서도 사용이 가능하다.</p>
<p>부모와 자식 관계로 생각하면 편한 이유는 부모가 서로 눈 색깔이 다르다고 하면, 오드아이가 아니라면 자식은 둘중 하나의 색깔을 유전받기 때문에 <code>extends</code> 키워드를 사용한다면 두개 이상의 클래스를 상속 받을 수 없다는 개념과 이어지기 때문이다. (implements는 가능)</p>
<pre><code class="language-java">// 부모클래스
public class Mom {
    String eyes = &quot;blue&quot;;

    public String hobby() {
        return &quot;gamble&quot;;
    }
}</code></pre>
<pre><code class="language-java">// 자식클래스
public class Son extends Mom {
    public String getEyesColor() {
        return eyes;
    }
}</code></pre>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        Son son = new Son();
        System.out.println(son.getEyesColor()); // blue출력
        System.out.println(son.hobby()); // gamble출력
    }
}</code></pre>
<p><code>Son</code>클래스에서 <code>eyes</code>나 <code>hobby()</code> 메소드를 구현하지 않아도 사용할 수 있어서 중복된 코드를 방지할 수 있고 나중에 수정할 때도 <code>Mom</code> 클래스만 코드만 수정할 수 있어서 유지보수성 또한 높아진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 간단하게 알아보기]]></title>
            <link>https://velog.io/@cv_/Docker-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@cv_/Docker-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 27 Mar 2023 10:49:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Docker의 탄생</strong></p>
<p>기존의 VMWare, Virtual Box 등의 가상화 기술은 많은 컴퓨터 자원을 필요로 하고 시스템 운영과 소프트웨어 개발의 지식을 모두 필요로 했기에 관리적인 측면에서 부담이 컸다.</p>
<p>이러한 부담을 줄이기 위해 도커(Docker)가 개발되고 2013년 3월 26일에 출시되었으며 
2008년에 개발된 리눅스 컨테이너(LXC) 기반으로 애플리케이션의 개발, 배포, 실행을 더 쉽고 간편하게 할 수 있도록 개발된 오픈소스 플랫폼이다.</p>
<p><em>도커 첫 시연 영상</em>
!youtube[wW9CAH9nSLs]</p>
<p><em>도커 소개영상</em>
!youtube[Q5POuMHxW-0]</p>
<p>도커의 발표 이후, 
당시 다양한 이유로 서버 환경과 개발 환경이 바뀌는 부담을 획기적으로 줄일 수 있어서 각광받기 시작했으며 본격적으로 컨테이너 기술의 확산에 기폭제 역할을 했다.</p>
</blockquote>
<h1 id="동작방식">동작방식</h1>
<p>리눅스 컨테이너(LXC) 기술을 기반으로 하며, 애플리케이션을 개발, 배포, 실행할 때 격리된 환경을 제공하여 도커가 실행되는 기반 시스템과의 간섭을 최소화된 상태로 동작한다.</p>
<blockquote>
<p>리눅스 컨테이너를 기술을 기반으로 하기 때문에 초기에는 리눅스에서만 사용이 가능했으나
맥에서는 이전에 VirtualBox 또는 <a href="https://github.com/machyve/xhyve">Xhyve</a> 가상화 기술을 사용하여 리눅스에서 실행되었으며 현재는 내장된 <a href="https://developer.apple.com/documentation/hypervisor">Hypervisor</a>를 사용하고, 윈도우에서는 <a href="https://learn.microsoft.com/ko-kr/virtualization/hyper-v-on-windows/about/">Hyper-V</a>를 사용하여 도커를 사용할 수 있다.</p>
</blockquote>
<p>도커로 가상화 기술을 구현하기 위해 크게 3가지로 나눌 수 있다.</p>
<h2 id="dockerfile">Dockerfile</h2>
<p>도커파일은 이미지가 빌드되기 위한 스크립트파일이다.
이미지를 빌드할 때 필요한 설정과 명령어등의 생성과정을 담고 있다.
기본적인 구조는 아래와 같다.</p>
<ul>
<li>FROM
빌드할 이미지의 베이스 이미지를 지정</li>
<li>RUN
새로운 레이어에서 명령어를 실행</li>
<li>CMD
컨테이너가 시작될 때 실행될 기본 명령어를 설정</li>
<li>COPY
호스트 파일을 컨테이너 내부로 복사</li>
<li>ADD
호스트 파일을 컨테이너 내부로 복사하며, 압축 파일의 경우 자동으로 압축을 푼다.</li>
<li>WORKDIR
작업 디렉토리를 설정</li>
<li>EXPOSE
컨테이너가 사용하는 포트를 설정</li>
</ul>
<h2 id="docker-image">Docker Image</h2>
<p><code>dockerfile</code>에 의해 빌드된 이미지는 컨테이너를 생성하기 위해 사용되며, 이미지는 해당 프로그램이 실행되기 위한 파일, 코드, 라이브러리 등을 포함한다.</p>
<p>이미지는 <a href="https://hyeo-noo.tistory.com/340">레이어(Layer)라는 개념</a>을 사용하여 구성되며, 같은 계층의 레이어는 이미지들 사이에서 공유되기 때문에 이미지의 용량이 커지는 것을 방지하고 이미지의 빌드, 배포 시간을 단축시킬 수 있다.</p>
<p>예를 들어,
<img src="https://velog.velcdn.com/images/cv_/post/e68cb55f-df57-4a2f-84aa-4b4303419baf/image.png" alt="">
위 그림처럼 같은 계층의 레이어 정보를 <code>nginx</code>, <code>webapp</code> 이미지가 저장하고 있는 것이 아닌 <code>ubuntu</code> 이미지의 A,B,C 레이어들을 상속받아 사용하고 있는것이다.</p>
<p>때문에 용량을 절약할 수 있을 뿐만 아니라, 레이어를 공유하는 이미지를 더 빠르게 공유할 수 있고, 더 쉽게 이미지의 정보를 업데이트 할 수 있다.</p>
<blockquote>
<p>출처 : <a href="https://www.ahnlab.com/kr/site/securityinfo/secunews/secuNewsView.do?seq=30533">https://www.ahnlab.com/kr/site/securityinfo/secunews/secuNewsView.do?seq=30533</a></p>
</blockquote>
<h2 id="컨테이너">컨테이너</h2>
<p>독립된 실행환경을 제공하는 도커의 핵심적인 기능이다.
위에서 설명한 이미지를 로드해서 독립된 환경에서 간섭없이 실행시켜준다.
가상 머신(VM)과는 달리 <strong>커널을 공유</strong>하기 때문에 비교적 가볍고 빠르게 실행될 수 있다.</p>
<h3 id="가상머신과의-차이점">가상머신과의 차이점</h3>
<p><img src="https://velog.velcdn.com/images%2Fgeunwoobaek%2Fpost%2F3fbe6059-4e28-44c7-9f18-bbf357e02d34%2Fimage.png" alt=""></p>
<p>기존의 VM은 보통 하나의 가상화를 위해 위 그림처럼 <strong>게스트 OS</strong>를 각각 가상화 대상에 설치해줘야 한다. 
각각의 게스트 OS에 사용될 자원을 미리 정해놓고 동작하기 때문에 효율적으로 동작되기 힘들고 곧 <a href="https://en.wikipedia.org/wiki/Scalability">확장성(Scalability)</a>의 문제로 이어진다. </p>
<h4 id="namespace-cgroupscontrol-group">Namespace, cgroups(Control Group)</h4>
<p>도커의 가상화 기술은 위에서 말한대로 리눅스 컨테이너 기반으로 개빌되었다.</p>
<ul>
<li><p><strong>Namespace</strong>
프로세스들을 격리시키는 리눅스의 가상화 기술이다. 
이 기술을 사용하여 실제로 도커의 컨테이너가 동작한다.</p>
</li>
<li><p><strong>cgroups</strong>
프로세스들의 자원 사용량을 제한, 제어하는 리눅스의 기술이다.
도커에서는 가상머신과는 다르게 처음부터 사용될 자원을 할당하지 않고 컨테이너에서 사용될 메모리, 네트워크 대역폭 등의 자원을 동적으로 설정하기 위해서 사용된다.</p>
</li>
</ul>
<blockquote>
<p>출처
<a href="https://velog.io/@geunwoobaek/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%B0%8F-%EB%8F%84%EC%BB%A4-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC">https://velog.io/@geunwoobaek/컨테이너-및-도커-개념정리</a>
<a href="https://imjeongwoo.tistory.com/106">https://imjeongwoo.tistory.com/106</a></p>
</blockquote>
<h1 id="사용-예시">사용 예시</h1>
<h2 id="어플리케이션-예시">어플리케이션 예시</h2>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        String osName = System.getProperty(&quot;os.name&quot;);

        while (true) {
            System.out.println(&quot;Hello, Docker!&quot;);
            System.out.println(&quot;현재 운영체제 : &quot; + osName + &quot;\n&quot;);
            Thread.sleep(1000);
        }
    }
}</code></pre>
<pre><code>실행결과

Hello, Docker!
현재 운영체제 : Mac OS X

Hello, Docker!
현재 운영체제 : Mac OS X</code></pre><p>간단하게 자바로 현재 운영체제의 이름을 반복적으로 출력하는 코드를 작성 후 
<code>dockertest.jar</code> 파일로 만들었다.</p>
<h2 id="dockerfile-작성-예시">Dockerfile 작성 예시</h2>
<pre><code># 자바11 이미지 기반 
FROM openjdk:11 

# 작업 경로 설정
WORKDIR /app

# 이미지 안에서 복사될 실행파일과 경로 설정
COPY dockertest.jar /app

# java -jar dockertest.jar 로 실행하라고 명령어 설정
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;dockertest.jar&quot;]</code></pre><p>위와 같이 Dockerfile을 작성하고 해당 디렉토리에서 <code>docker build -t docker_test .</code> 명령어를 실행해서 이미지를 빌드한다.
<em>(<code>-t docker_test</code> 부분을 이미지의 이름을 지정하며 <code>.</code> Dockerfile의 위치를 가르킨다.)</em></p>
<p>베이스 이미지로 사용하는 jdk이미지가 없다면 도커허브(<em>docker.io/Library/</em>)에서 다운로드 받아오기 때문에, 조금 오래 걸릴 수 있으며 jdk를 포함한 해당 이미지는 약 <em>650MB</em> 의 용량을 가졌다.</p>
<p>빌드 완료 후 <code>docker images</code> 명령어로 이미지 목록을 확인할 수 있다.</p>
<pre><code>ex)

REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
docker_test   latest    dfea1eb2a2f8   2 minutes ago   645MB</code></pre><h2 id="실행-및-확인">실행 및 확인</h2>
<p>기본적으로 <code>docker run docker_test</code> 명령어를 통해서 해당 이미지를 실행시킬 수 있다.
이 경우 자동으로 컨테이너가 생성되고 자동으로 컨테이너의 이름도 지정해주고 바로 출력값을 확인할 수 있다.</p>
<p>이름을 지정해주고 싶다면 <code>--name</code> 명령어를, 백그라운드로 실행시키고 싶다면 <code>-d</code> 명령어를 추가할 수 있다.
출력 결과를 확인하기 위해 <code>docker logs 컨테이너이름</code> 명령어를 사용할 수 있다.</p>
<pre><code>실행 예시)

~/dockertest$ docker run --name test-container -d docker_test
9b69006b570c9de25c4a8d17489927fb313aef5ea4eeb5542daaaa0b3532a30f

~/dockertest$ docker logs test-container
Hello, Docker!
현재 운영체제 : Linux

Hello, Docker!
현재 운영체제 : Linux

Hello, Docker!
현재 운영체제 : Linux

~/dockertest$ docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS              PORTS     NAMES
9b69006b570c   docker_test   &quot;java -jar dockertes…&quot;   About a minute ago   Up About a minute             test-container</code></pre><p><code>docker logs test-container</code> 명령어에 <code>-f</code>를 추가하여 도커 첫 시연 영상과 비슷하게 실시간으로 출력결과를 확인할 수 있다.</p>
<p>또한 도커는 리눅스 기반으로 동작하기에 <code>System.getProperty(&quot;os.name&quot;);</code> 의 값이 <code>Mac OS X</code>에서 <code>Linux</code>로 변경된것도 확인할 수 있다.</p>
<h1 id="도커를-잘-활용하는-방법">도커를 잘 활용하는 방법</h1>
<h2 id="docker-hub-httpshubdockercom">Docker Hub (<a href="https://hub.docker.com">https://hub.docker.com</a>)</h2>
<p>Docker Hub는 Github처럼 도커 이미지를 저장하고 공유하는 데 사용되는 공식 레지스트리이다. Docker Hub에 이미지를 저장하고 관리하면 다른 사람들과 공유할 수 있다.</p>
<p>터미널에서 <code>docker search &#39;이미지명&#39;</code> 명령어로 간단하게 검색하고 
<code>docker pull &#39;이미지명&#39;</code>로 간단하게 받아올 수 있다.</p>
<h2 id="멀티스테이지-빌드">멀티스테이지 빌드</h2>
<p>이미지를 더 효율적으로 빌드하기 위해 고안된 방법 중 하나이다.
여러 스테이지로 이미지를 빌드하면 최종 이미지의 용량을 줄일 수 있으며 곧 더 빠른 배포로 이어지게 된다.
예를들어 빌드과정에서 필요한 이미지는 빌드과정에서만 사용하고 컨테이너로 실행될 때는 제외시킬 수 있다.</p>
<p>아래는 멀티스테이지로 빌드하는 예시이다.</p>
<pre><code># 빌드환경
FROM gradle:7.3.3-jdk11 AS build
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY src ./src
RUN gradle build --no-daemon

# 런타임환경
FROM openjdk:11-slim
WORKDIR /app
COPY --from=build /app/build/libs/dockertest-0.0.1-SNAPSHOT.jar ./dockertest.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;dockertest.jar&quot;]</code></pre><p>위 예시는 <em>빌드환경에서</em> Gradle을 사용하여 빌드된 <code>dockertest-0.0.1-SNAPSHOT.jar</code> 파일을 실행환경을 <code>openjdk:11-slim</code> 베이스이미지만을 사용하여 실행시키는 간단한 예시이다. </p>
<p>각각의 환경을 <strong>스테이지</strong>라고 부른다.
이 스테이지들은 독립적으로 실행되며 이전 단계의 스테이지에서 파일을 복사해올 수도 있다.
<em>(위 예시의 <code>/app/build/libs/dockertest-0.0.1-SNAPSHOT.jar</code> 파일)</em></p>
<p>이미지를 빌드하면 Gradle 이미지를 포함하고 있지 않기에 더 가벼워진다.</p>
<p><a href="https://kimjingo.tistory.com/63">참고</a></p>
<h3 id="여러-빌드환경을-사용할때">여러 빌드환경을 사용할때?</h3>
<p><em>또한 하나의 프로그램을 서로 다른 환경에서 지원하기 위해 사용되기도 한다.</em></p>
<p>이를 위해 인수(argument)를 사용할 수 있다.
예시로 위 Dockerfile을 빌드환경에서 Maven 또는 Gradle을 사용할지 인수로 판단할 수 있다.</p>
<pre><code>수정된 Dockerfile 예시)

ARG BUILD_TOOL

# 빌드환경
FROM gradle:7.3.3-jdk11 AS build
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY src ./src
# BUILD_TOOL 인수에 따른 다른 명령어 실행
RUN if [ &quot;$BUILD_TOOL&quot; = &quot;gradle&quot; ]; then \
        ./gradlew build --no-daemon; \
    else \
        apt-get update &amp;&amp; apt-get install -y maven &amp;&amp; mvn package ; \
    fi

# 런타임환경
FROM openjdk:11-slim
WORKDIR /app
COPY --from=build /app/build/libs/dockertest-0.0.1-SNAPSHOT.jar ./dockertest.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;dockertest.jar&quot;]</code></pre><p>위 수정된 예시는 <code>BUILD_TOOL</code> 인수를 받아 빌드도구를 선택한다.
<code>gradle</code>로 인수를 설정했으면 <code>./gradlew build --no-daemon</code> 명령어를,
아니면 <code>apt-get update &amp;&amp; apt-get install -y maven &amp;&amp; mvn package</code> 명령어로 maven을 설치하고 빌드한다.</p>
<p>이미지로 빌드할때 인수를 넘기는 방법은 다음과 같다.</p>
<pre><code>docker build --build-arg BUILD_TOOL=gradle -t docker_test .</code></pre><h2 id="volume-사용">Volume 사용</h2>
<p>도커에서의 Volume이란, 컨테이너와 호스트 운영체제가 파일을 공유하는 공간이다.
이 볼륨을 잘 활용하면 <strong>데이터의 지속성과 유연성을 보장할 수 있다.</strong></p>
<p>예를 들어 Mysql을 볼륨없이 사용하고 있을 때, 컨테이너를 중지하거나 삭제하면 테이블과 컬림의 정보가 모두 날아갈 것이다.</p>
<p>하지만 볼륨을 사용하여 컨테이너의 파일 시스템과 호스트의 파일 시스템을 공유하게 되면, 어떤 이유로 컨테이너가 중지되었어도 데이터를 유지할 수 있다.</p>
<pre><code>볼륨 사용 예시)

// 볼륨 생성
~$ docker volume create test-mysql-vol
test-mysql-vol

// 생성한 볼륨과 mysql의 컨테이너의 디렉토리 공유한 상태로 실행
~$ docker run -d --name mysql-container -v test-mysql-vol:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=1234 mysql

// 실행이 성공했다면 컨테이너의 ID값이 출력됨!
f3a38df6ff17b5641decc1ef27c03ad5749a4145663a1863bf427d0d9ae7f2e5</code></pre><h3 id="공유된-도커-볼륨을-직접-접근하려면">공유된 도커 볼륨을 직접 접근하려면?</h3>
<p><code>inspect</code> 명령어로 <code>test-mysql-vol</code>의 경로를 확인해보면 </p>
<pre><code>~$ docker inspect volume test-mysql-vol
[
    {
        &quot;CreatedAt&quot;: &quot;2023-03-30T04:07:40Z&quot;,
        &quot;Driver&quot;: &quot;local&quot;,
        &quot;Labels&quot;: {},
        &quot;Mountpoint&quot;: &quot;/var/lib/docker/volumes/test-mysql-vol/_data&quot;,
        &quot;Name&quot;: &quot;test-mysql-vol&quot;,
        &quot;Options&quot;: {},
        &quot;Scope&quot;: &quot;local&quot;
    }
]</code></pre><p>Mountpoint 정보에서 해당 경로를 확인할 수 있는데 아마 <strong>해당 경로는 눈을 씻고 찾아봐도 없을 것이다.</strong></p>
<p>이유는 위에서 서술한대로 맥 OS환경에서는 <a href="https://github.com/machyve/xhyve">Xhyve</a>, 또는 <a href="https://developer.apple.com/documentation/hypervisor">Hypervisor</a>를 사용하여 리눅스 운영체제 환경에서 도커가 실행되기 때문에 맥에 내장된 가상화 환경에 존재하는 디렉토리이다.
즉, <code>docker volume create</code>로 생성된 볼륨은 VM환경에서 존재한다.</p>
<p>이 내장된 가상환경에 엑세스하기 위해서는 복잡할 뿐더러 <strong>권장되지 않는 방법</strong>이기에 리눅스 환경이 아닐 경우 보통 docker volume create 명령어를 사용하는 대신 직접 경로를 입력해주는게 여러모로 편하다.</p>
<pre><code>직접 디렉토리를 설정하는 예시)

~/$ docker run -d --name mysql-container -v $(pwd)/test-vol:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=your_password mysql
5cd3d1fe614fd2a2561b2efb4be7d60d065c9614e970607373f674bc5de7069f</code></pre><p>명령어에 <code>-v $(pwd)/test-vol:/var/lib/mysql</code> 부분은
volume을 현재경로에서(pwd) <code>test-vol</code> 디렉토리와 컨테이너의 <code>/var/lib/mysql</code>와 공유하겠다는 의미이다.
<em>(<code>:</code>으로 구분하며 현재경로란 해당 이미지를 빌드하는 Dockerfile의 위치이다.)</em></p>
<p><img src="https://velog.velcdn.com/images/cv_/post/8fcd4670-9aba-461d-8b24-98b07f48ecd4/image.png" alt="">
위 명령어로 컨테이너를 실행하면 <code>test-vol</code> 폴더와 mysql의 파일들이 생성된걸 확인할 수 있다.</p>
<p>이 방식으로 컨테이너가 날아가도 데이터의 지속성을 보장할 수 있다.</p>
<h2 id="docker-swarm--kubernates">Docker Swarm &amp; Kubernates</h2>
<p>도커 컴포즈와 쿠버네티스는 모두 컨테이너 오케스트레이션 도구이다.</p>
<blockquote>
<p>컨테이너 오케스트레이션이란? <a href="https://www.hpe.com/kr/ko/what-is/container-orchestration.html">참고</a></p>
<p>개별적으로 컨테이너화 되는 MSA 아키텍쳐로 설계된 서비스나 그의 준하는 설계의 서비스의 경우 다수의 컨테이너를 정리하는 프로그램, 프로세스 등을 뜻한다.
대규모 서비스의 경우 단일 호스트 운영체제로만 컨테이너를 실행시키는 경우는 드물뿐더러 각기 다른 호스트 위에서 상호작용하는 컨테이너들의 관리가 어려워질 수 있기에 사용되는 도구이다.</p>
<p>기본적으로 컨테이너들의 스케일링, 모니터링, 스케쥴링 등의 기능을 지원한다.</p>
</blockquote>
<h3 id="docker-swarm">Docker Swarm</h3>
<p>도커 1.12 버전이라면 (2016년) 기본적으로 내장되어있는 컨테이너 오케스트레이션이며 쉽게 사용할 수 있다. 때문에 가장 진입장벽이 낮다.</p>
<p><a href="https://seongjin.me/docker-swarm-introduction-nodes/">참고</a></p>
<h3 id="kubernates">Kubernates</h3>
<p>K8s라는 약어를 사용하며 Docker Swarm보다 확장된 기능을 지원하며 대규모 서비스의 컨테이너 오케스트레이션에서 거의 표준으로 사용되는 구글에서 개발한 오픈소스 도구이다.</p>
<p><a href="https://kubernetes.io/ko/docs/concepts/">참고</a></p>
<h2 id="docker의-자주-사용되는-명령어-정리">Docker의 자주 사용되는 명령어 정리</h2>
<p><em>맨 앞 <code>docker</code> 명령어는 생략됨</em></p>
<p><code>-d, --detach</code>, <code>-p, --publish</code> 와 같은 옵션은
각각의 명령어에 <code>-h</code> 또는 <code>--help</code> 옵션으로 <strong>사용가능한 옵션을 조회</strong>할 수 있다.</p>
<h3 id="이미지-관련">이미지 관련</h3>
<ul>
<li>build : 이미지를 빌드</li>
<li>pull : 이미지를 Docker Hub 등에서 다운로드</li>
<li>push : 이미지를 Docker Hub 등에 업로드</li>
<li>images : 이미지 목록 조회</li>
<li>history : 이미지 생성 과정을 보여줌</li>
<li>save : 이미지를 아카이브로 저장</li>
<li>tag : 이미지에 태그를 추가 또는 변경</li>
<li>inspect : 이미지에 대한 정보를 상세하게 출력</li>
<li>import : 파일에서 이미지를 생성</li>
<li>rmi : 도커 이미지 삭제</li>
</ul>
<h3 id="컨테이너-관련">컨테이너 관련</h3>
<ul>
<li>run : 컨테이너를 생성하고 실행</li>
<li>start : 컨테이너를 실행</li>
<li>stop : 컨테이너를 정지</li>
<li>pause : 컨테이너 일시 정지</li>
<li>unpause : 일시 정지된 컨테이너 재개</li>
<li>rm : 컨테이너를 삭제</li>
<li>ps : 컨테이너 목록을 조회</li>
<li>exec : 도커 컨테이너 내부에서 명령어를 실행</li>
<li>logs : 도커 컨테이너의 로그를 출력</li>
<li>cp : 도커 컨테이너와 로컬 파일 시스템 사이에서 파일을 복사</li>
<li>top : 컨테이너에서 실행 중인 프로세스 목록 표시</li>
<li>stats : 컨테이너의 CPU, 메모리, 네트워크 등의 성능을 실시간 모니터링</li>
<li>attach : 실행 중인 컨테이너에 접속</li>
<li>wait : 컨테이너가 종료될 때까지 대기</li>
<li>diff : 컨테이너 내부에서 변경된 파일 목록 출력</li>
<li>rename : 컨테이너 이름 변경</li>
<li>export : 컨테이너 파일 시스템 내용을 tar 압축으로 출력</li>
</ul>
<h3 id="네트워크-관련-명령어">네트워크 관련 명령어</h3>
<ul>
<li>network ls : 네트워크 목록을 조회</li>
<li>network create : 네트워크 생성</li>
<li>network connect : 네트워크와 컨테이너를 연결</li>
<li>network disconnect : 네트워크와 컨테이너 연결해제</li>
</ul>
<h3 id="볼륨-관련">볼륨 관련</h3>
<ul>
<li>volume ls : 볼륨 목록 조회</li>
<li>volume create : 볼륨 생성</li>
<li>volume rm : 볼륨 삭제</li>
<li>volume inspect : 볼륨 상세 정보 조회</li>
</ul>
<h3 id="시스템-관련">시스템 관련</h3>
<ul>
<li>version: 클라이언트와 서버의 도커 버전 확인</li>
<li>info: 도커 서버의 정보 확인</li>
<li>events : 도커에서 발생하는 이벤트를 실시간으로 조회</li>
<li>system df: 도커에서 사용 중인 자원들의 사용량 확인</li>
<li>system events: 도커에서 발생하는 이벤트들을 실시간 조회</li>
<li>inspect : 도커 자원의 상세 정보를 조회</li>
<li>system prune: 사용하지 않는 자원들 정리</li>
<li>login: 도커 레지스트리(Docker Hub)에 로그인</li>
<li>logout: 도커 레지스트리(Docker Hub)에서 로그아웃</li>
<li>search: 도커 허브에서 이미지를 검색</li>
<li>update : 컨테이너나 서비스의 설정 업데이트</li>
<li>wait : 컨테이너가 종료될 때까지 대기</li>
</ul>
<h3 id="서비스-관련">서비스 관련</h3>
<ul>
<li>service ls : 서비스 목록 조회</li>
<li>service create : 서비스 생성</li>
<li>service scale : 서비스의 레플리카 수 조정</li>
<li>service update : 서비스 설정 업데이트</li>
<li>service rm : 서비스 삭제</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] IO와 NIO]]></title>
            <link>https://velog.io/@cv_/Java-IO%EC%99%80-NIO</link>
            <guid>https://velog.io/@cv_/Java-IO%EC%99%80-NIO</guid>
            <pubDate>Thu, 23 Mar 2023 18:04:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>IO(Input/Output)란 그 의미에서 알 수 있듯 파일 시스템, 네트워크, 기타 장치 등 다양한 소스와 대상으로 데이터를 읽고 쓰는 기능을 제공하는 자바 API이다.</p>
<p>java.io 패키지에서 Byte 기반이라면 InputStream과 OutputStream을, 문자 기반이라면 Reader와 Writer를 시스템의 파일들을 읽고 쓰는데 자주 사용된다.</p>
<p>이후 비동기식 입출력을 지원하는 NIO(New Input/Output)가 자바4에서 등장하게 되고
기존의 IO에서 업그레이드된 기능을 지원하게 된다.
이후에도 java.nio 패키지가 계속 업데이트 되며 자바7부터 NIO2라고 불리지만 기존 NIO와 따로 구분짓지는 않는듯 하다.</p>
</blockquote>
<h1 id="기존의-io">기존의 IO</h1>
<p>거의 초창기 부터 지원했던 API이며 NIO가 나온 이후 Classic IO 라고도 불린다.
크게 Byte 입출력 기반의 Stream과 문자 입출력 기반의 Stream으로 나뉘어진다. <a href="https://javaconceptoftheday.com/byte-stream-vs-character-stream-in-java/">참고</a>
기본적으로 <strong>Stream</strong>을 기반으로 하고 있다.
NIO가 나온 지금도 자주 사용되는 클래스들이며 <code>BufferedInputStream</code> 등의 보조 스트림을 활용하여 데이터 처리 속도를 높일 수도 있다.</p>
<blockquote>
<p>Stream이란? <a href="https://kurukurucoding.tistory.com/48">참고</a></p>
<p>사전적으로 시냇물, 흐르다 라는 의미를 가진다.
일상 생활에서의 인터넷 방송할 때 Streaming한다는 의미와 같다.
이 글에서 의미는 운영체제에 의해 생성되는 데이터의 이동되는 흐름, 데이터가 이동되는 통로의 개념을 뜻하며 선입선출(FIFO)의 흐름을 가진다.</p>
</blockquote>
<h2 id="byte-입출력-기반의-stream">Byte 입출력 기반의 Stream</h2>
<p>주로 이진 데이터(이미지, 음악 파일, 동영상 파일 등)를 읽고 쓰는데 사용된다.
파일의 경로와 File객체를 활용해 파일을 불러와 <code>FileInputStream</code>을 활용해서
파일을 다룬다. (또는 생성되는 파일의 경로를 설정한다.)
외부자원을 사용할 때 발생할 수 있는 메모리 누수 문제를 방지하기 위해 <code>close()</code> 메소드를 사용해줘야 한다.</p>
<pre><code class="language-java">// 간단한 예시
public boolean saveAsLogFile() {
    try {
            FileInputStream inputStream = new FileInputStream(&quot;./testInput.txt&quot;);
            FileOutputStream outputStream = new FileOutputStream(&quot;./testOutput.txt&quot;);

            // 보조 스트림 
            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);

            // 입력 스트림에서 데이터를 읽어 출력 스트림으로 데이터를 쓰기
            int data;
            while ((data = bufferedInputStream.read()) != -1) {
                bufferedOutputStream.write(line);
            }

            // 메모리 누수 방지를 위한 스트림 닫기
            bufferedInputStream.close();
            bufferedOutputStream.close();

            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
}        </code></pre>
<p>위 코드는 단순히 testInput.txt 파일의 내용을 testOutput.txt에 옮겨 적는 코드이다.
출력 경로에 파일을 생성하고 이미 존재한다면 덮어씌운다.</p>
<p>txt파일이 아닌 동영상이나 오디오 파일이라면<code>AudioInputStream</code>, <code>AudioOutputStream</code>을 사용하고 좀 더 복잡한 기능이 요구되지만 위 코드와 전체적인 흐름은 다르지 않다.</p>
<h2 id="문자-입출력-기반의-stream">문자 입출력 기반의 Stream</h2>
<p>문자 기반이라면 Reader와 Writer를 사용한다.</p>
<pre><code class="language-java">// 예시

public boolean saveAsLogFile(LogResult result) {
    try {
            File file = new File(OUTPUT_FILE_PATH.toString());

            // 파일 입력
            FileWriter fileWriter = new FileWriter(file);

            // 보조스트림
            BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

            // 파일에 로그결과 문자열 입력
            bufferedWriter.write(result.getMostCalledApikeyString());
            bufferedWriter.write(result.getMostRequestedServicesString());
            bufferedWriter.write(result.getBrowserUsageRateString());
            // ... 생략

            // 메모리 누수 방지를 위한 스트림 닫기
            bufferedWriter.close();
            fileWriter.close();

            return true;
        } catch (IOException e) {
            return false;
        }
}</code></pre>
<p>위 예시에서는 문자 기반 출력 스트림인 <code>FileWriter</code>와 <code>BufferedWriter</code> 보조 스트림을 활용해서
<code>OUTPUT_FILE_PATH</code>파일에 문자열을 입력하는 토이프로젝트 코드이다.</p>
<p>보조스트림의 경우 필수는 아니지만 보조 스트림 없이 <code>fileWriter</code>만을 사용하며 작성한다면
매번 입출력 작업을 처리해야 되서 로그 결과의 데이터가 많아지면 느려질 수 있다.</p>
<p><code>BufferedWriter</code>를 사용함으로써 내부의 데이터를 버퍼에 저장하고 있다가 flush(), close() 메소드 또는 버퍼가 가득찼을때만 입출력 작업을 처리하기에 효율적이다.</p>
<h1 id="nio">NIO</h1>
<p>위 Stream기반의 기존 I/O API는 동기식이라는 단점이 있다.
<a href="https://velog.io/@cv_/%EC%9E%90%EB%B0%94%EC%9D%98Thread%EB%9E%80">멀티쓰레드</a>환경으로 구성하더라도 I/O API 자체가 비동기 처리를 지원하지 않기 때문에 입출력 작업을 완료할 때까지 쓰레드들 사이의 대기시간이 길어져 효율적으로 쓰레드를 활용하기 힘들다.</p>
<p>NIO를 사용하면 대기시간을 최소한으로 줄이고 비동기 처리를 지원하기에 훨씬 효율적인 멀티쓰레딩을 구현할 수 있다.</p>
<p>이 외에도 다른 차이점이 존재한다.</p>
<h2 id="기존의-io와-다른점">기존의 IO와 다른점</h2>
<h3 id="stream-기반에서-buffer-기반으로">Stream 기반에서 Buffer 기반으로</h3>
<p>Buffer의 의미는 데이터를 저장하거나 읽고 쓰는 임시적인 데이터 저장소 또는 컨테이너이다. <code>ByteBuffer</code>, <code>IntBuffer</code>, <code>FloatBuffer</code> 등이 있으며 추상클래스 Buffer를 상속한다.</p>
<blockquote>
<p>버퍼 사용법 참고 <a href="https://jamssoft.tistory.com/221">https://jamssoft.tistory.com/221</a></p>
</blockquote>
<p>Stream 기반의 IO는 기본적으로 한번 읽은 데이터를 다시 읽거나 건너뛸 수 없다.
또한 출력스트림이 만약 1바이트의 데이터를 쓰면 입력스트림이 1바이트를 읽기는 방식으로 동작하기에 속도가 느릴 수 있다.
때문에 위 예시처럼 버퍼를 만들어 캐싱해주는 기능을 따로 구현해주어야 한다.</p>
<p>하지만 Buffer 기반의 NIO라면 이미 처리된 buffer로부터 데이터를 읽기에 따로 구현할 필요가 없는 유연성을 제공한다.</p>
<h3 id="non-blocking">Non-Blocking</h3>
<p>기존의 IO와 가장 큰 차이점이다. 멀티쓰레드 환경이 아니더라도 입출력 시 <strong>Channel</strong>을 통해 다른 작업을 처리할 수 있으며 여러개의 IO작업을 동시에 처리할 수 있다.</p>
<p>이런 비동기식의 기능을 위해 NIO는 2가지의 핵심요소를 담고있다. </p>
<h4 id="channel">Channel</h4>
<p>Channel은 입출력 작업을 수행하는 객체이며 대상이 되는 데이터를 직접 Buffer에 읽고 쓰는 역할을 한다. 직접 Buffer와 상호작용하는 객체이기 때문에 Buffer의 상태(position, limit, capacity)를 변경한다.</p>
<p>또한 Channel은 <code>FileChannel</code>,<code>AsynchronousFileChannel</code>(NIO2), <code>SocketChannel</code>, <code>DatagramChannel</code> 등 다양한 타입이 존재하며, 
입출력 방식에 따라 사용하는 채널 타입이 달라진다.</p>
<h4 id="selector-네트워크-io의-경우">Selector (네트워크 I/O의 경우)</h4>
<p>NIO를 활용할 때, 비동기식으로 여러개의 입출력 작업을 처리하기 위해
새로운 쓰레드를 생성하는 대신 Selector를 활용한다.</p>
<p>Selector는 위 Channel들을 활용해 실질적으로 Non-Blocking 입출력 작업을 가능하게 하는 객체이다.
다시 말해 Selector는 여러 개의 채널을 모니터링하면서, 해당 채널에서 입출력 처리를 담당한다.
이 과정에서 보통 <code>SelectionKey</code> 객체와 함께 사용되며 Selector가 이벤트를 감지하면 이벤트의 종류를 판별할 수 있다.
이는 곧 여러개의 쓰레드를 활용하지 않고 하나의 Selector로 IO작업을 동시에 처리하는데 있어서 멀티쓰레드 환경에서의 동시 IO처리보다 효율성이 높다.</p>
<blockquote>
<p><code>SelectionKey</code>의 주요 메소드</p>
<ol>
<li><p>isAcceptable() : 현재 채널에서 클라이언트의 연결 요청의 처리 가능 여부를 확인한다.</p>
</li>
<li><p>isWritable() : 현재 채널에서 쓸 수 있는 공간의 여부를 확인한다.</p>
</li>
<li><p>isReadable() : 현재 채널에서 읽을 수 있는 데이터의 존재 여부를 확인한다.</p>
</li>
<li><p>isConnectable() : 현재 채널이 클라이언트와 연결 여부를 확인한다.</p>
</li>
</ol>
</blockquote>
<h3 id="nio를-활용한-비동기-처리예시-reactor-패턴">NIO를 활용한 비동기 처리예시 (Reactor 패턴)</h3>
<pre><code class="language-java">public class SocketChannelTest {

    public static void main(String[] args) throws IOException {
        // channel 생성
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false); // 비동기 설정

        // selector 생성
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println(&quot;비동기 테스트 시작&quot;);

        while (true) {
            int readyChannels = selector.select();
            if (readyChannels == 0) continue; // 클라이언트를 기다리기 위해 무한루프

            Set&lt;SelectionKey&gt; selectedKeys = selector.selectedKeys();
            Iterator&lt;SelectionKey&gt; keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) { 
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);

                    System.out.println(&quot;접속주소 : &quot; + socketChannel.getRemoteAddress());
                } 
                else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();

                    // 버퍼생성
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = socketChannel.read(buffer);

                    if (bytesRead == -1) {
                        socketChannel.close();

                        System.out.println(socketChannel.getRemoteAddress() + &quot;접속종료\n&quot;);
                    } else if (bytesRead &gt; 0) {
                        buffer.flip();
                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes);

                        System.out.println(socketChannel.getRemoteAddress() + &quot;의 비동기 수신 메세지 :  &quot; + new String(bytes));
                    }
                }

                keyIterator.remove();
            }
        }
    }
}</code></pre>
<h4 id="실행-예시">실행 예시</h4>
<pre><code>// telnet을 사용해 접속
telnet localhost 8080

Trying ::1...
Connected to localhost.

test1
test2
... 생략</code></pre><pre><code>비동기 테스트 시작
접속주소 : /0:0:0:0:0:0:0:1:55495

/0:0:0:0:0:0:0:1:55495의 비동기 수신 메세지 :  test1
/0:0:0:0:0:0:0:1:55495의 비동기 수신 메세지 :  test2
... 생략</code></pre><h3 id="asynchronousfilechannel을-이용한-파일입출력-예시">AsynchronousFileChannel을 이용한 파일입/출력 예시</h3>
<pre><code class="language-java">public class asyncFileChannelTest {

    static long beforeTime = System.currentTimeMillis();

    public static void main(String[] args) throws IOException, InterruptedException {
        Path path1 = Paths.get(&quot;./programmers/input1.txt&quot;);
        readAsync(path1);

        Path path2 = Paths.get(&quot;./programmers/input2.txt&quot;);
        readAsync(path2);

        System.out.println(&quot;비동기식으로 동작한다면 이 문자열이 먼저 출력됨!&quot;);

        // 비동기 파일읽기 전 프로그램 종료 방지
        Thread.sleep(2000);
    }

    private static void readAsync(Path filePath) throws IOException {
        // 채널 생성
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(filePath);

        ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
        long position = 0;

        channel.read(buffer, position, null, new CompletionHandler&lt;Integer, Void&gt;() {
            @Override
            public void completed(Integer result, Void attachment) {
                buffer.flip();

                byte[] data = new byte[buffer.limit()];
                buffer.get(data);

                // 1초의 작업시간이 걸린다고 가정한다.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println(new String(data));
                buffer.clear();

                System.out.println(&quot;처리시간 : &quot; + (System.currentTimeMillis() - beforeTime));
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });
    }
}
</code></pre>
<h4 id="실행-예시-1">실행 예시</h4>
<pre><code>비동기식으로 동작한다면 이 문자열이 먼저 출력됨!
테스트 로그 문자열 002
테스트 로그 문자열 001
처리시간 : 1011
처리시간 : 1011</code></pre><p>비동기 방식으로 동시에 처리되기 때문에 처리되는 시간차이가 (거의)같으며 
readAsync메소드에서 블로킹되지 않기에 
&quot;비동기식으로 동작한다면 이 문자열이 먼저 출력됨!&quot;
문장이 먼저 출력되었다.</p>
<h1 id="왜-io가-여전히-자주-쓰일까">왜 IO가 여전히 자주 쓰일까</h1>
<h2 id="호환성과-단순함">호환성과 단순함</h2>
<p>비동기로 처리할 필요가 없는 상황 이외에도 기존의 IO API는 여전히 자주 쓰인다고 한다.
NIO는 기존보다 비교적 복잡한 편이기에 작은 서비스를 구현한다고 하면 IO가 더 적합할 수도 있다.
또한 JDK1.0부터 사용되어온 IO API 이기에, 호환성을 고려한다면 기존 IO를 사용해야 할 것이며 이외에도 다른 이유가 있다.</p>
<h2 id="특정상황에서의-성능">특정상황에서의 성능</h2>
<p>특정상황에서 Buffer를 사용하는 NIO보다 기존 IO에서 성능의 이점을 보일때가 있다.</p>
<p>예를 들어 작은 단위의 데이터를 반복적으로 입/출력을 하는 상황이라면 Buffer를 사용하는 NIO에서는 너무 잦은 Buffer의 flip과 clear 메소드가 반복, 또는 생성될 수 있기에 성능에 악영향을 미칠 수 있다.</p>
<p>또 다른 예시로 로그파일처럼 Line by Line 단위의 처리가 필요할 경우,
NIO에서는 라인을 나누는 로직이 포함이 되어야 하기 때문에 기존 IO에서 지원하는 <code>bufferedReader</code>를 사용하는 방법이 더 빠르게 처리될 수 있다. 
<a href="https://taes-k.github.io/2021/01/06/java-nio/">테스트 결과 참고1</a>
<a href="https://funnelgarden.com/java_read_file/#Files-lines">테스트 결과 참고2</a></p>
]]></description>
        </item>
    </channel>
</rss>