<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>nuh__d develog</title>
        <link>https://velog.io/</link>
        <description>董訓은 영어로 mentor</description>
        <lastBuildDate>Thu, 07 Aug 2025 14:33:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>nuh__d develog</title>
            <url>https://velog.velcdn.com/images/nuh__d/profile/ef1ac953-e112-4c83-ae93-d660910b34b7/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. nuh__d develog. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/nuh__d" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[ai는 똑똑한걸까? 멍청한걸까?]]></title>
            <link>https://velog.io/@nuh__d/ai%EB%8A%94-%EB%98%91%EB%98%91%ED%95%9C%EA%B1%B8%EA%B9%8C-%EB%A9%8D%EC%B2%AD%ED%95%9C%EA%B1%B8%EA%B9%8C</link>
            <guid>https://velog.io/@nuh__d/ai%EB%8A%94-%EB%98%91%EB%98%91%ED%95%9C%EA%B1%B8%EA%B9%8C-%EB%A9%8D%EC%B2%AD%ED%95%9C%EA%B1%B8%EA%B9%8C</guid>
            <pubDate>Thu, 07 Aug 2025 14:33:52 GMT</pubDate>
            <description><![CDATA[<p>open-ai의 모델들을 사용해서 git commit message을 자동으로 생성해주는 도구를 만들게 되었습니다. ai-commiter라는 도구인데요, 소스코드와 사용방법은 <a href="https://github.com/hunknownn/ai-commiter">여기</a>에서 확인할 수 있습니다. 물론 이 툴을 설치한다고해서 바로 사용할 수는 없고, 개인적으로 open-ai의 api-key을 발급받아 env등에 넣어주면 사용가능합니다</p>
<p>ai-commiter에 대해 간략히 얘기하면 <code>ai-파일의 변경사항들을 전달하여 모델이 분석 후 promt template대로 git commit message을 반환해주는 프로세스를 거치고 있습니다</code></p>
<p>오늘은 이 도구를 만들면서(v0.1.5로 버전 업 하면서) 경험한, 이상하지만 이해가가는 그런 얘기를 하려고 합니다. 사실 지금 v0.1.5 개발하다가 너무 웃겨서 블로그에 남기고 싶어졌습니다...하하</p>
<blockquote>
<p>v0.1.5로 업데이트되면서 cli에서 사용시 ai-commit 이 아닌 <code>grpt</code>로 변경되었습니다. 아래 사진은 v0.1.4환경임을 알려드립니다.
<code>ai-commit --help -&gt; grpt --help</code></p>
</blockquote>
<hr>
<h2 id="ai는-똑똑한걸까-멍청한걸까">ai는 똑똑한걸까? 멍청한걸까?</h2>
<p>우선 현재 v0.1.4버전으로 ai-commiter에게 다음과 같은 변경사항을 준 뒤, 커밋 메시지를 생성하게 해보았습니다.
<img src="https://velog.velcdn.com/images/nuh__d/post/d359c45a-6fa8-4af0-819e-dd1b08631224/image.png" alt="">
<img src="https://velog.velcdn.com/images/nuh__d/post/b8fe5e4d-1c71-4be6-9ff4-d72e1b7dd6f0/image.png" alt=""></p>
<p>변경사항은 잘 분석하여 적절한 commit message을 남겨주었습니다.</p>
<p>이제 다음과 같은 변경사항을 주게되면, 이상한 일이 벌어집니다.
<code>파일의 변경사항을 읽고, 변경사항대로 promt가 입력된 듯이 동작한다는 것입니다.</code></p>
<p><img src="https://velog.velcdn.com/images/nuh__d/post/2e85060e-68b0-41df-8c5c-b19aa0326404/image.png" alt="">
v0.1.5 배포에 앞서, custom promt의 예제 템플릿을 README.md 에 추가해주었는데요. 바로 이 변경사항을 읽고, 이 promt을 받아 동작한 듯이 commit message을 생성했습니다.
<img src="https://velog.velcdn.com/images/nuh__d/post/903005d3-33c1-4f71-96a0-77fbd001ad0e/image.png" alt="">
위 변경사항을 잘 보시면 example output 처럼 commit message body에 &#39;-&#39; 으로 시작하는 것을 볼 수 있습니다.</p>
<p>그리고 이제 배포를 위해 마지막 readme.md을 정리하고 다시 ai-commit (v0.1.4)을 사용해보니 다음과 같이 원래대로 template이 적용된 것을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/nuh__d/post/4f55fc4a-8eca-4956-906e-31d32732de73/image.png" alt=""></p>
<h2 id="ai는-생각보다-멍청했다">ai는 생각보다 멍청했다</h2>
<p>제가 만든 도구는, 파일의 변경사항들을 전달하여 모델이 분석 후 promt template대로 git commit message을 반환해주는 프로세스를 거치고 있습니다. 여기서 변경사항에 promt template이 포함되니, 모델이 착각하여 동작하게 되는 것 같습니다. 혹시나, open-ai의 모델들을 사용해서 code-review 또는 저처럼 commit message 자동 생성기를 만드시는 분들은 이러한 점을 유의하셔서 사용하시면 좋을 것 같습니다.</p>
<hr>
<p>소스코드 <a href="https://github.com/hunknownn/ai-commiter">https://github.com/hunknownn/ai-commiter</a>
Pypi 배포 버전 <a href="https://pypi.org/project/ai-commiter/">https://pypi.org/project/ai-commiter/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dynamic Factory Pattern 을 이용한 리팩토링]]></title>
            <link>https://velog.io/@nuh__d/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@nuh__d/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Sun, 09 Jun 2024 07:40:32 GMT</pubDate>
            <description><![CDATA[<p>내가 마주한 문제를 해결 하기 위해 고려한 디자인 패턴은 총 3가지로 <code>Dynamic Factory Pattern</code>, <code>Strategy Pattern</code>, <code>Facade Pattern</code> 이 있었다.
각각의 패턴에 대해서는 이미 너무 많은 정보가 있기 때문에 과감히 생략한다. 이제부터 얘기할 문제상황에 대해 읽기전에 위 3가지 패턴의 어떤 특징으로 인해 어떤 상황속에서 유용하게 쓰일지 한 번 고민해보면 좋을 것 같다.</p>
<hr>
<p>어떤 도메인으로 예시를 들까하다가, 카페 알바생 분이 주스를 만드는 모습을 보고 <code>Cafe</code> 도메인으로 구성해보려한다.</p>
<p>먼저 실제 코드에서는 <code>Dynamic Factory Pattern</code> + <code>Facade Pattern</code>의 조합으로 리팩토링을 진행했지만, 실제 도메인 (문제점)을 Cafe도메인으로 완벽히 설명할 수 없는 점으로 인해 이 글 에서는 Dynamic Factory Pattern만을 적용해보겠다.</p>
<h2 id="cafe-domain">Cafe Domain</h2>
<p>[1] 카페에 가면 키오스크를 통해 음료를 주문하고 주문번호를 받는다. [1-1] 이 때 1개 또는 N개의 음료를 선택 후 주문한다.
[2] 주문이 접수되면 직원은 주문을 처리하기 시작한다. [2-1] 직원은 각 음료에 맞는 제조법에 따라 제조한다.[3] 주문번호를 통해 주문정보를 확인한다. [4] 제조가 완료되면 각 음료는 음료에 맞는 컵에 담겨 손님에게 전달될 것이다.</p>
<p>위 도메인을 웹서비스로 풀어서 얘기해보자.</p>
<p>[1] 음료를 주문하는 POST API가 된다. /api/beverages/order
[1-1] 각자의 선택(취향?) 이겠지만, 나는 2개의 api로 분리한다. /order, /bulk-order
[2] api를 서버가 받은 후 처리하는 service 로직이 된다. fun executeOrder() {}
[2-1] 각 음료를 만드는 servcice 로직이다. fun makeCoffee(), fun makeJuice()
[3] 해당 주문의 정보를 확인하는 GET API이다. /api/beverages/order/{orderId}
[4] controller단의 응답구조가 된다. BeverageReponse()</p>
<p>코드로도 확인해보자</p>
<pre><code class="language-kotlin">
@RestController()
@RequestMapping(&quot;/api/cafe&quot;)
class CafeController(
    private val cafeService: CafeService,
) {
    @PostMapping(&quot;/order&quot;)
    fun order(
        @RequestBody() request: OrderRequest,
    ): Cup {
        val beverage = cafeService.executeOrder(request.type)
        return beverage
    }

    @GetMapping(&quot;/order/{orderId}&quot;)
    fun getOrder(
        @PathVariable(&quot;orderId&quot;) orderId: Long,
    ): Cup {
        return cafeService.getOrder(orderId)
    }
}

data class OrderRequest(
    val type: BeverageType,
)

@Service
class CafeService(
    private val repository: CafeRepository,
    private val coffeeRepository: CoffeeRepository,
    private val juiceRepository: JuiceRepository,
    private val smoothieRepository: SmoothieRepository,
) {

    fun executeOrder(type: BeverageType): Cup {
        if (type == BeverageType.COFFEE) return makeCoffee()
        if (type == BeverageType.JUICE) return makeJuice()
        if (type == BeverageType.SMOOTHIE) return makeSmoothie()

        throw RuntimeException()
    }

    fun makeCoffee(): CoffeeCup {

        val coffee = CoffeeCup(&quot;Coffee&quot;, 300, BeverageType.COFFEE)
        return coffeeRepository.save(coffee)
    }

    fun makeJuice(): JuiceCup {
        val juice = JuiceCup(&quot;Juice&quot;, 400, BeverageType.JUICE)
        return juiceRepository.save(juice)
    }

    fun makeSmoothie(): SmoothieCup {
        val smoothie = SmoothieCup(&quot;Smoothie&quot;, 500, BeverageType.SMOOTHIE)
        return smoothieRepository.save(smoothie)
    }

    fun getOrder(orderId: Long): Cup {
        val beverage = repository.findById(orderId).orElseThrow { throw RuntimeException() }
        // 여기서는 cast 를 통해 Entity를 직접 내려주지만 실제로는 DTO클래스로의 변환을 의미한다.
        if (beverage.type == BeverageType.COFFEE) return beverage as CoffeeCup
        if (beverage.type == BeverageType.JUICE) return beverage as JuiceCup
        if (beverage.type == BeverageType.SMOOTHIE) return beverage as SmoothieCup
        throw RuntimeException(&quot;Unknown type&quot;)
    }
}

enum class BeverageType {
    COFFEE,
    JUICE,
    SMOOTHIE
}</code></pre>
<pre><code class="language-kotlin">@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = &quot;dtype&quot;)
open class Cup(
    open var isTakeOut: Boolean,
    open val type:BeverageType
) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;id&quot;, nullable = false)
    open var id: Long? = null
}

// Cup 클래스를 상속받는 CoffeeCup 데이터 클래스를 정의
@Entity
@DiscriminatorValue(&quot;coffee&quot;)
open class CoffeeCup(
    var memo: String,
    val size: Int,
    type: BeverageType,
) : Cup(true, type)


@Entity
@DiscriminatorValue(&quot;juice&quot;)
open class JuiceCup(
    var memo: String,
    val size: Int,
    type:BeverageType
) : Cup(true, type)


@Entity
@DiscriminatorValue(&quot;smoothie&quot;)
open class SmoothieCup(
    var memo: String,
    val size: Int,
    type: BeverageType,
) : Cup(false, type)
</code></pre>
<h3 id="개선사항">개선사항</h3>
<p>위 service는 OCP를 만족할까?</p>
<p>우선 확장에 대해서 열려있지 않다고 생각했다. 신 메뉴가 출시되면 예를 들어 Latte 가 출시된다면 makeLatte() 라는 메서드는 당연히 새롭게 생성되어야 한다. 하지만 executeOrder() 메서드에서도 if조건문을 통해 makeLatte() 를 호출해야한다. 또한 makeLatte() 호출 결과를 LatteDTO ( 위 코드에서는 beverage as LatteCup 이 될 것이다) 로 변환 후 응답해야 한다.</p>
<p>여기서 3가지 문제가 발생할 수 있다고 본다.</p>
<ol>
<li>if 조건문의 조건식 에러 -&gt; 잘못된 BeverageType 비교</li>
<li>casting 에러 -&gt; 상위클래스 로의 캐스팅이라서 컴파일에러가 발생하지 않을 것</li>
<li>executeOrder 메서드에 로직 추가 빠짐</li>
</ol>
<p>위 문제점은 test code를 작성하면 적절하게 해소할 수는 있다.</p>
<p>하지만 executeOrder 메서드에 라떼 메뉴가 출시되면 makeLatte() 를 호출해야하는 구조를 개선하고자 했다. 또한 주문표를 통해 주문상태를 확인하는 GET api에 문제가 있다. api를 호출해보면 아래 쿼리를 발견할 수 있다.</p>
<pre><code>select
    c1_0.id,
    c1_0.dtype,
    c1_0.is_take_out,
    c1_0.type,
    c1_1.memo,
    c1_1.size,
    c1_2.memo,
    c1_2.size,
    c1_3.memo,
    c1_3.size 
from
    Cup c1_0 
left join
    Coffee_cup c1_1 
        on c1_0.id=c1_1.id 
left join
    Juice_cup c1_2 
        on c1_0.id=c1_2.id 
left join
    Smoothie_cup c1_3 
        on c1_0.id=c1_3.id 
where
    c1_0.id=?</code></pre><p>그럼 왜 이렇게 join절이 많이 발생했을까??</p>
<p>CoffeeCup, JuiceCup, SmoothieCup 등의 entity는 Cup이라는 상위 Entity를 상속받고 있다. 하지만 서버에서는 10번 이라는 orderId 만이 들어왔을 때, 어느 repository, 어느 table에서 10번 tuple을 찾아야 하는 지 알 수 없다. 그래서 모든 Cup 의 하위 Entity와 join 쿼리로 수행하게 된다. join 절이 하위 entity의 수 만큼 발생하고 있다. 앞으로 신 메뉴가 출시될 수록 join되는 테이블의 수는 더 많이 질 것이다. 이 쿼리는 구체적인 Cup 종류를 알 수 있다면(전달받는 다면) 최적화 시킬 수 있다.</p>
<p>그럼 이제 <code>Dynamic Factory Pattern</code> 을 적용해보자.</p>
<h2 id="dynamic-factory-pattern-리팩토링">Dynamic Factory Pattern 리팩토링</h2>
<pre><code class="language-kotlin">@Service
class CoffeeService(
    private val coffeeRepository: CoffeeRepository,
) : BeverageService {
    companion object {
        const val COFFEE_CAPACITY = 300
    }

    // ... some other methods specific to coffee...

    override fun getBeverageType(): BeverageType {
        return BeverageType.COFFEE
    }

    override fun makeBeverage(): CoffeeCup {
        val coffee = CoffeeCup(&quot;Coffee&quot;, COFFEE_CAPACITY, BeverageType.COFFEE)
        return coffeeRepository.save(coffee)
    }

    override fun getOrder(orderId: Long): Cup {
        return coffeeRepository.findById(orderId).orElseThrow { throw RuntimeException() }
    }
}

@Service
class JuiceService(
    private val juiceRepository: JuiceRepository,
) : BeverageService {
    companion object {
        const val JUICE_CAPACITY = 400
    }

    // ... some other methods specific to juice ...

    override fun getBeverageType(): BeverageType {
        return BeverageType.JUICE
    }

    override fun makeBeverage(): JuiceCup {
        val juice = JuiceCup(&quot;Juice&quot;, JUICE_CAPACITY, BeverageType.JUICE)
        return juiceRepository.save(juice)
    }

    override fun getOrder(orderId: Long): Cup {
        return juiceRepository.findById(orderId).orElseThrow { throw RuntimeException() }
    }
}

@Service
class SmoothieService(
    private val smoothieRepository: SmoothieRepository,
) : BeverageService {
    companion object {
        const val SMOOTHIE_CAPACITY = 500
    }

    // ... some other methods specific to smoothie ...

    override fun getBeverageType(): BeverageType {
        return BeverageType.SMOOTHIE
    }

    override fun makeBeverage(): SmoothieCup {
        val smoothie = SmoothieCup(&quot;Smoothie&quot;, SMOOTHIE_CAPACITY, BeverageType.SMOOTHIE)
        return smoothieRepository.save(smoothie)
    }

    override fun getOrder(orderId: Long): Cup {
        return smoothieRepository.findById(orderId).orElseThrow { throw RuntimeException() }
    }
}

@Service
class CafeService(
    private val repository: CafeRepository,
    private val beverageFactory: DynamicBeverageFactory,
) {
    fun executeOrder(type: BeverageType): Cup {
        return beverageFactory.makeBeverage(type)
    }

    fun getOrder(orderId: Long, type: BeverageType): Cup {
        return beverageFactory.getOrder(orderId, type)
    }
}

@Component
class DynamicBeverageFactory(
    private val beverageServices: List&lt;BeverageService&gt;,
) {
    private fun getServiceImpl(type: BeverageType): BeverageService {
        return beverageServices.first { it.getBeverageType() == type }
    }

    fun makeBeverage(type: BeverageType): Cup {
        val serviceImpl = getServiceImpl(type)
        return serviceImpl.makeBeverage()
    }

    fun getOrder(orderId: Long, type: BeverageType): Cup {
        val serviceImpl = getServiceImpl(type)
        return serviceImpl.getOrder(orderId)
    }
}

interface BeverageService {
    fun getBeverageType(): BeverageType

    fun makeBeverage(): Cup

    fun getOrder(orderId: Long): Cup
}</code></pre>
<p>기존의 CafeService에서 처리하던 것들이 각 음료에 맞는 책임만을 가진 BeverageService로 분리되었다.</p>
<p>DynamicBeverageFactory를 살펴보자.
각 BeverageService는 BeverageService라는 인터페이스의 구현체이다. 인터페이스의 규약 중 getBeverageType이 중심이 된다. 이 메서드를 구현함으로써 각 구현체들이 어떤 음료에 대한 책임을 가지는지 알 수 있게된다. Factory 클래스는 Bean으로 등록하게 되면서 beverageServices 를 통해 모든 <code>BeverageService</code>구현체들을 주입 받을 수 있다.</p>
<p>그럼 개선하고자 했던 점을 살펴보자</p>
<ol>
<li>executeOrder 메서드에 라떼 메뉴가 출시되면 makeLatte() 를 호출해야하는 구조를 개선 </li>
<li>주문표를 통해 주문상태를 확인하는 GET api에서 join 쿼리 개선</li>
</ol>
<p>1번문제는 Latte 메뉴가 출시된다면 우리는 LatteService를 당연히 구현하게 된다. 단순히 LatteService를 작성해주는 것 만으로, Factory 클래스에 구현체가 등록되니 makeLatte()호출 과 같은 추가작업을 생략할 수 있다.</p>
<p>그럼 쿼리는 최적화가 되었을까??</p>
<pre><code>select
    c1_0.id,
    c1_1.is_take_out,
    c1_1.type,
    c1_0.memo,
    c1_0.size 
from
    Coffee_cup c1_0 
join
    Cup c1_1 
        on c1_0.id=c1_1.id 
where
    c1_0.id=?</code></pre><p>커피를 조회한 쿼리이다. from절이 실제 Coffee로 바뀌었고, join절이 상위 Entity의 정보를 가져오기 위한 Cup Entity와만 join하고 있음을 볼 수 있다.</p>
<hr>
<p>이렇게 <code>Design Pattern</code>을 통해 개선을 해보았다. 위 예제들은 실제 Cafe Domain이라고 생각한다면 맞지 않는 부분이 있을 것이다. 하지만 Dynamic Factory Pattern 을 이용한 리팩토링 과정을 보여주기 위함이고, 개선방향에 초점을 맞춰 보면 좋을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@AuthenticationPrincipal를 위한 Custom HandlerMethodArgumentResolver]]></title>
            <link>https://velog.io/@nuh__d/AuthenticationPrincipal%EB%A5%BC-%EC%9C%84%ED%95%9C-Custom-HandlerMethodArgumentResolver</link>
            <guid>https://velog.io/@nuh__d/AuthenticationPrincipal%EB%A5%BC-%EC%9C%84%ED%95%9C-Custom-HandlerMethodArgumentResolver</guid>
            <pubDate>Sat, 17 Feb 2024 03:39:06 GMT</pubDate>
            <description><![CDATA[<p>우리는 Spring Secutiry의 Authentication을 사용해서 request를 보낸 유저정보를 관리한다. (적어도 reduck은..)</p>
<p>그럼 인증되지 않은 사용자가(이하 비 로그인 유저) request를 보냈을 때, Authentication는 어떻게 될까??</p>
<p>이번 포스팅에서는 JWT 를 인증방식으로 사용하고 있는 reduck에서 겪은 트러블 슈팅과, Authentication Principal에 대해서 적어보려고 한다.</p>
<hr>
<pre><code class="language-java">Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();

UserDetails userDetails = principal.getXXX();</code></pre>
<p>우리는 위 코드처럼 Security Context에 관리되는 principal 이라는 오브젝트로 유저정보를 받아 올 수 있다. ( Authentication을 어떻게 사용하냐에 따라 조금은 다를 것이다.)</p>
<p>만약, 여러 api에서 request를 보낸 사용자 정보가 필요하다면? 못해도 위의 3줄짜리 코드가 계속 쌓인다.
그래서 보통 <strong>@AuthenticationPricipal</strong> 이라는 어노테이션을 사용해 간단하게 Controller에서 인증된 사용자 정보를 받을 수 있게한다.</p>
<p>@AuthenticationPrincipal 을 가볍게 확인해보자. 
이번 트러블 슈팅에 있어 중요한 <strong>key point</strong>가 담겨 있으니,,</p>
<h2 id="authenticationprincipal">@AuthenticationPrincipal</h2>
<p><img src="https://velog.velcdn.com/images/nuh__d/post/a46b292c-0291-4e09-894d-e41ff9cecc24/image.png" alt=""></p>
<p>클래스 주석을 보면, <strong>Authentication.getPrincipal()</strong> 메소드로 뭔가를 받아오는 것 같다. 그리고 <strong>AuthenticationPrincipalArgumentResolver</strong> 라는 리졸버를 보라고 한다. 그럼 이 resolver도 가볍게 확인해보자!!</p>
<pre><code class="language-java">public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

    // another code ...

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        AuthenticationPrincipal annotation = findMethodAnnotation(AuthenticationPrincipal.class, parameter);
        String expressionToParse = annotation.expression();
        if (StringUtils.hasLength(expressionToParse)) {
            StandardEvaluationContext context = new StandardEvaluationContext();
            context.setRootObject(principal);
            context.setVariable(&quot;this&quot;, principal);
            context.setBeanResolver(this.beanResolver);
            Expression expression = this.parser.parseExpression(expressionToParse);
            principal = expression.getValue(context);
        }
        if (principal != null &amp;&amp; !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {
            if (annotation.errorOnInvalidType()) {
                throw new ClassCastException(principal + &quot; is not assignable to &quot; + parameter.getParameterType());
            }
            return null;
        }
        return principal;
    }

    // another code ...
}
</code></pre>
<p>지금은 3곳 정도만 보면된다.</p>
<ol>
<li>HandlerMethodArgumentResolver의 구현체 라는 것.</li>
<li>Security Context에서 Authentication객체를 가져오고, Principal를 꺼내오는 것.</li>
<li>parameter의 클래스와 Pricipal의 클래스의 일치 여부.</li>
</ol>
<blockquote>
<p>사실 이정도만 알아도 오늘의 트러블 슈팅은 사실 끝났다고 봐도 된다!</p>
</blockquote>
<hr>
<p>다시 처음으로 돌아가서, 비 로그인 유저가 보낸 request경우에는 Authentication Principal은 어떻게 될까??</p>
<p><code>당연하게도 인증되지 않았으니, NULL 일 것이다.</code></p>
<p>문제는 서비스 로직에서 Authentication.getPrincipal().getUser() 와 같이 로직을 작성하게 되면 NPE가 터진다는 것이다! 하지만 비 로그인 유저가 request를 요청하더라도 그에 맞는 서비스 로직이 있을 것이고, 올바르게 시스템은 유지되어야한다.</p>
<p>reduck을 예로 들자면, 본인의 게시글을 조회할 경우에 조회수 카운팅을 막고 있다. 또, 비 로그인 유저가 팔로우 목록을 조회하려고 한다면 <strong>401 Unauthorized</strong> 예외처리를 하고 있다.</p>
<p>이 문제를 <code>CustomHandlerMethodArgumentResolver</code>로 해결해보려고 한다.</p>
<blockquote>
<p>사실 우리가 해결하고자 하는 부분은 Authenticaion Principal이 Null인 것이다. 그러면 jwt filter단을 통해서도 가능하지 않을까??</p>
</blockquote>
<h2 id="jwt-filter의-책임">JWT Filter의 책임</h2>
<p>reduck에서는 UserDetails를 구현하는 CustomUserDetails라는 구현체를 통해 User 정보를 관리하고 있다.</p>
<pre><code class="language-java">public class CustomUserDetails implements UserDetails {

    private final User user;

    public final User getUser() {
        return user;
    }

    // another code ...
} </code></pre>
<p>그리고 jwt filter에서 UserDetailService를 이용해 Authentication Principal에 우리가 만든 CustomUserDetails 객체를 넣어 주고 있다.</p>
<pre><code class="language-java">if (jwt != null &amp;&amp; jwtProvider.validateToken(jwt)) {
  UserDetails customUserDetails = userDetailsService.loadUserByUsername(this.getAccount(token));
  Authentication auth = UsernamePasswordAuthenticationToken(customUserDetails, &quot;&quot;, customUserDetails.getAuthorities());
  SecurityContextHolder.getContext().setAuthentication(auth);
}

if ( jwt == null ) {
      SecurityContextHolder.getContext().setAuthentication({emptyAuthentication});
}</code></pre>
<p>그러면 이 순간에 jwt가 null 인경우 Security Context에 빈 껍데기 UserDetails 를 넣어주면 된다고 볼 수도 있지만, JWT Filter의 책임이 아니라고 생각한다.</p>
<p>jwt filter는 위 코드에서도 볼 수 있듯이, jwt의 유효성을 검증하는데에 있다고 생각한다. 따라서 filter에서 사용자의 인증 여부에 따라 context에 Authentication을 달리 넣는 로직은 범위를 벗어났다고 생각한다</p>
<h2 id="customhandlermethodargumentresolver">CustomHandlerMethodArgumentResolver</h2>
<p>CustomResolver의 구현은 생각보다 간단하다. 그냥 ctrl+c, ctrl+v 이다. 물론 동작과정을 이해가 우선이다.</p>
<pre><code class="language-java">public class CustomAuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

    // another code ...

    /**
     * access token이 존재하지 않는 요청이라면, empty CustomUserDetails 객체를 반환한다.
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {

        // 기존 resolver 와 동일.

        if (principal != null &amp;&amp; !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {
            if (annotation.errorOnInvalidType()) {
                throw new ClassCastException(principal + &quot; is not assignable to &quot; + parameter.getParameterType());
            }
            return new CustomUserDetails(null); // empty principal
        }
        return principal;
    }

    // another code ...

}</code></pre>
<p>이제 이 resolver를 bean으로 등록해주고 사용하면 된다.
실제 Controller에서 @AuthenticationPrincipal를 사용하면 null이 아닌 빈 깡통 Principal을 받을 수 있게된다.</p>
<blockquote>
<p>추가로 모두가 디버깅해보면 알겠지만, 내가 만든 @AuthenticaionPrincipal의 resovle과정에 대해 조금 써보겠다.
</br>
우선 HandlerMethodArgumentResolver의 구현체인 <strong>HandlerMethodArgumentResolverComposite</strong> 부터 시작하면 좋을 것 같다.</p>
</blockquote>
<pre><code class="language-java">    @Nullable
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        if (result == null) {
            for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
                if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, result);
                    break;
                }
            }
        }
        return result;
    }</code></pre>
<p>간단하다. 현재 등록되어있는 모든 resovler중에서 parameter를 지원하는 resolver를 찾아 cache에 넣고 사용하는 것이다. 따라서 우리가 만들었던 Custom Resolver에는 supportsParameter를 구현해야 했고, @AuthenticationPrincipal 어노테이션이 붙어있는 parameter인지 확인해주는 로직이 있으면 된다!</p>
<pre><code class="language-java">    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
    }</code></pre>
<p>아까 만들었던 우리의 resovler에서         <code>Authentication authentication = SecurityContextHolder.getContext().getAuthentication();</code>
Authentication 객체가 <strong>AnonymousAuthenticationToken</strong> 이라는 것을 알게된다.
</br>
갑자기 AnonymousAuthenticationToken?? 분명 난 이런 로직을 작성한 적이 없으니, 이미 구현되어있는 것 일테다. 그럼 어디서 context에 익명token을 넣고 있는지 찾아가자.
</br>
SecurityFilter는 <strong>SecurityContextPersistenceFilter</strong> 을 먼저 타고간다.(이전에 등록된 filter가 없다면) doFilter()에서 먼저 빈 security context를 생성한다.
<img src="https://velog.velcdn.com/images/nuh__d/post/b300f9e3-55da-40c8-83c5-affc928ffe56/image.png" alt="">
그 다음 chain의 doFilter는 reduck경우 jwt filter지만, jwt가 존재하지 않으므로 아무런 일을 하지 않고 다음 chain으로 넘어간다. jwt가 없으니, <strong>AnonymousAuthenticationFilter</strong>로 타고간다.
이 filter가 바로 AnonymousAuthenticationToken을 넣어주는 역할을 하게 된다.
<img src="https://velog.velcdn.com/images/nuh__d/post/216a1613-fc04-47c3-a43f-6d6f7294c713/image.png" alt="">
이제 우리는 AnonymousAuthenticationToken 가 갑자기 어디서 들어갔는지 알게되었다.
</br>
Security를 쫓아가다보니, 더 나은 방법이 생각났다. CustomResolver가 아닌 CustomAnonymousAuthenticationFilter이다. 그 이유는, Security의 AnonymousAuthenticationFilter 보면, 실제로 authentication의 null 여부에 따라 Security Context에 Authentication을 넣어주기 때문에 이 로직을 커스텀하여 Empty Authentication을 넣어주어도 상관없을 것 같다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[reDuck] https 적용 트러블 슈팅..?]]></title>
            <link>https://velog.io/@nuh__d/reDuck-https-%EC%A0%81%EC%9A%A9-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@nuh__d/reDuck-https-%EC%A0%81%EC%9A%A9-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Sun, 22 Oct 2023 10:54:40 GMT</pubDate>
            <description><![CDATA[<p>졸업프로젝트 https 를 적용시키던 중 계속 nginx가 실행이 안되는 문제가 발생했다.</p>
<p>아무것도 아니긴 하지만, 졸업프로젝트 중 생긴 이슈에 대해서 기록해보려고한다.</p>
<hr>
<p>백엔드의 도메인은 가비아를 통해 구성하고 있다. 그래서 certbot으로 간단하게 https 적용을 할 수 있었는데, 과거에 이미 https적용을 했었지만 3개월이 되어 ssl이 만료되었다. 이번에 다시 설정하게 되었다.</p>
<h2 id="원인">원인</h2>
<p>이미 3개월 전에 https 설정을 하느라 certbot을 이용해 pem을 받았었다. 따라서, 이번에 받게된 pem의 이름이 <code>{domain}-0003</code>/fullchain.pem 과 같이 number가 기록되었다.</p>
<p>이 사실을 뒤로한 채 다음과 같이 pem파일의 이름에 number을 빼고 설정하게 되었다.
<code>ssl_certificate /etc/letsencrypt/live/{domain}.site/fullchain.pem; # managed by Certbot</code></p>
<p><code>ssl_certificate_key /etc/letsencrypt/live/{domain}.site/privkey.pem; # managed by Certbot</code></p>
<h2 id="해결">해결</h2>
<p>nginx는 실행에 에러가 나면 로그파일이 기록된다.
<img src="https://velog.velcdn.com/images/nuh__d/post/63a8d4e7-6573-4346-a707-a56fd1bbdd81/image.PNG" alt="">
위 에러를 보면 pem파일에서 에러가 났다는 것을 보고 힌트를 얻어, 해당 pem 파일 이름에 0003 number을 추가하여 다시 설정해주었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 37] ordinal 인덱신 대신 EnumMap을 사용하라]]></title>
            <link>https://velog.io/@nuh__d/item-37</link>
            <guid>https://velog.io/@nuh__d/item-37</guid>
            <pubDate>Sun, 10 Sep 2023 14:07:30 GMT</pubDate>
            <description><![CDATA[<h2 id="ordinal-메서드로-인덱스-활용">Ordinal 메서드로 인덱스 활용</h2>
<pre><code class="language-java">Set&lt;Plant&gt;[] plantsByLifeCycle = (Set&lt;Plant&gt;[]) new Set[Plant.LifeCycle.values().length];
for(int i=0; i &lt; plantsByLifeCycle.length; i++)
    plantsByLifeCycle[i] = new HashSet&lt;&gt;();
for (Plant p : garden)
    plantsByLifeCycle[p.lifeCycle.oridinal()].add(p);
</code></pre>
<p><code>ordinal()</code> 메서드를 통해 인덱스로 활용하면 안된다고 말하고 있다. 그러면서 운이 좋으면 <code>ArrayIndexOutOfBoundsException</code>이 발생하거나, 잘못된 동작을 묵묵히 수행한다고 한다.
만약, Plant Enum에 상수가 하나 추가된다면, 그리고 그 상수의 ordinal에 접근하여 사용한다면ArrayIndexOutOfBoundsException이 발생하게 된다.
다른 상황으로는, Plant Enum 상수들의 순서가 바뀐경우 아무런 예외없이 잘못된 동작을 수행 할 것이다.</p>
<p>그래서 <code>EnumMap</code>을 사용하라고 전한다.</p>
<h2 id="enummap-사용">EnumMap 사용</h2>
<pre><code class="language-java">        EnumMap&lt;Plant.LifeCycle, Set&lt;Plant&gt;&gt; plantsByLifeCycle = 
        new EnumMap&lt;&gt;(Plant.LifeCycle.class); // EnumMap + 한정적 타입 토큰 이용

        for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
            plantsByLifeCycle.put(lc, new HashSet&lt;&gt;());
        }
        for (Plant p : garden
        ) {
            plantsByLifeCycle.get(p.lifeCycle).add(p);
        }
        System.out.println(plantsByLifeCycle);</code></pre>
<p>EnumMap을 사용하여 위 코드를 수정한 결과이다.
직접 index를 사용하지 않고, 관련 동작은 모두 Java 개발자에게 맡겼다. 또한, 성능도 좀 더 개선되었는데 내부에서 배열을 사용하기 때문이다. 즉, 성능+안정성을 얻어냈다.</p>
<p>주석을 보면 <code>한정적 타입 토큰</code>을 이용했다고 한다. EnumMap 생성자의 파라미터로 Class객체를 넘겨주는데 이는 중요한 역할을 한다. 내가 확인한바로는 3가지정도 역할이 있었다.</p>
<h2 id="enummap생성자의-타입-토큰의-역할">EnumMap생성자의 타입 토큰의 역할</h2>
<h3 id="1-배열의-크기-설정">1. 배열의 크기 설정</h3>
<p>EnumMap은 내부적으로 배열을 이용한다고 했다. 배열을 생성 할 때 그 크기를 결정해줘야하는데, 여기에 사용된다.</p>
<pre><code class="language-java">public EnumMap(Class&lt;K&gt; keyType) {
        this.keyType = keyType;
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
    }</code></pre>
<h3 id="2-타입-안전함-체크">2. 타입 안전함 체크</h3>
<p>한정적 타입 토큰의 본 역할을 수행한다.</p>
<pre><code class="language-java">public void putAll(Map&lt;? extends K, ? extends V&gt; m) {
        if (m instanceof EnumMap) {
            EnumMap&lt;?, ?&gt; em = (EnumMap&lt;?, ?&gt;)m;
            if (em.keyType != keyType) {
                if (em.isEmpty())
                    return;
                throw new ClassCastException(em.keyType + &quot; != &quot; + keyType);
            }

            for (int i = 0; i &lt; keyUniverse.length; i++) {
                Object emValue = em.vals[i];
                if (emValue != null) {
                    if (vals[i] == null)
                        size++;
                    vals[i] = emValue;
                }
            }
        } else {
            super.putAll(m);
        }
    }</code></pre>
<h3 id="3-성능-향상">3. 성능 향상</h3>
<p>가장 많이 사용할 <code>get</code> 메서드를 보면 <code>isValidKey</code>라는 메서드를 사용하고 있다.</p>
<pre><code class="language-java">public V get(Object key) {
        return (isValidKey(key) ?
                unmaskNull(vals[((Enum&lt;?&gt;)key).ordinal()]) : null);
    }

private boolean isValidKey(Object key) {
        if (key == null)
            return false;

        // Cheaper than instanceof Enum followed by getDeclaringClass
        Class&lt;?&gt; keyClass = key.getClass();
        return keyClass == keyType || keyClass.getSuperclass() == keyType;
    }</code></pre>
<p>여기서 isValidKey 메서드의 <code>Class&lt;?&gt; keyClass = key.getClass();</code> 라인의 주석에 힌트가 있다. 바로 리플렉션 메서드인 <code>getDeclaringClass</code> 보다 비용이 싸다는 장점이 있다. 일반적으로 리플렉션은 느리다고 알고 있다. 우리는 이미 EnumMap의 원소들의 타입 정보를 <code>keyType</code>을 통해 알고 있다. 따라서 리플렉션 메서드를 사용하지 않아도 클래스 타입을 비교할 수 있게된다.</p>
<p>만약 getDeclaringClass 메서드를 사용한다면 다음과 같은 형태일 것 같다.</p>
<pre><code class="language-java">Class&lt;?&gt; declaringClass = key.getClass().getDeclaringClass();
        if (keyType.isAssignableFrom(declaringClass)) {
            //
        }</code></pre>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://www.tutorialspoint.com/java/lang/class_getdeclaringclass.htm">https://www.tutorialspoint.com/java/lang/class_getdeclaringclass.htm</a></li>
<li><a href="https://ttl-blog.tistory.com/1232">https://ttl-blog.tistory.com/1232</a></li>
</ul>
<hr>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[reDuck 채팅 서비스 1]]></title>
            <link>https://velog.io/@nuh__d/reDuck-%EC%B1%84%ED%8C%85-%EC%84%9C%EB%B9%84%EC%8A%A4-1</link>
            <guid>https://velog.io/@nuh__d/reDuck-%EC%B1%84%ED%8C%85-%EC%84%9C%EB%B9%84%EC%8A%A4-1</guid>
            <pubDate>Mon, 21 Aug 2023 17:02:42 GMT</pubDate>
            <description><![CDATA[<p>졸업 프로젝트로 진행하고 있는 reDuck서비스의 채팅 시스템을 도입하며 고민했던 것을 기록하기 위해 작성합니다. 채팅 시스템는 우리에게 익숙한 카카오톡 서비스를 참고하여 1:1채팅을 구현하였습니다. 여러 기능 중 고민이 많았던 <code>채팅방 목록</code>, <code>채팅방 개설</code>, <code>채팅방 입장 &amp; 퇴장</code> 에 대해 적어보겠습니다.</p>
<p>더 좋은 설계나, 제가 한 방식들에 부족한 부분이 많이 있겠지만, 초기 단계를 기록하며 발전되어가는 흐름을 보는것에 의미가 있을 것 같아 작성하게 되었습니다.</p>
<p>STOMP (Simple Text Oriented Messaging Protocol)를 사용하였고, 현재는 내부 브로커를 이용하고 있습니다. 추후에 외부 브로커를 사용하려고 합니다.
채팅 내역은 지금은 MySQL에 저장하고 있어, 모든 설명은 MySQL기준입니다. NoSQL 에 채팅 내역들을 저장하고 여러 다른 기능들을 사용해야겠지만, 우선 빠르게 기능을 완성시킨 후 고도화할 예정입니다.</p>
<hr>
<h2 id="erd">ERD</h2>
<p>먼저 설계한 ER-다이어그램입니다. 채팅서비스와 관련된 필드들만 언급하였습니다.
<img src="https://velog.velcdn.com/images/nuh__d/post/de45e7e7-0337-4acc-ad73-3d2e2b30e1a3/image.png" alt=""></p>
<p>평상시에는 테이블간 관계를 FK를 통해 매핑하고 있었습니다. 최근에 실무에서는 FK사용을 안한다는 글을 읽고, 충격을 받았습니다. 데이터의 수정이나, 성능, lock과 관련하여 몇가지 이슈때문에 사용하지 않는다고 합니다. 이러한 부분을 보완할 수 있는 방법을 제시한 글도 보았지만, FK를 사용하지 않고 개발하면 어떤 점이 다를까 하고 이번 채팅 기능에서는 FK를 사용하지 않고 개발하였습니다.</p>
<p>위 ERD는 설계 단계이니, FK를 통해 테이블간 관계를 명시했습니다.</p>
<p>ERD를 그리며 신경 쓴 부분은 추후에 API를 사용하여 발생할 <code>UPDATE</code>쿼리의 최소화였습니다.
update쿼리의 사용은 불가피하다고 느꼈고, 그렇다면 최대한 <code>SELECT</code>를 이용하여 줄여보려고 노력했습니다. 그에따라, <code>chat_room_users</code>라는 테이블을 설계하게 되었고, 하나씩 설계이유를 적겠습니다.</p>
<h2 id="채팅방-목록-조회">채팅방 목록 조회</h2>
<p>카카오톡의 채팅방 목록을 보면, 마지막 채팅 메시지의 시간순으로 정렬되어 있습니다. 또한 내가 읽지 않은 메시지 수들도 보여주고 있습니다.
<img src="https://velog.velcdn.com/images/nuh__d/post/49dd30f2-ea09-4763-abfc-91eccab009cd/image.png" alt="">
이 기능을 개발하며 고민이 많았던 부분이 <code>안 읽은 메시지 개수</code>입니다. 처음엔 unread_message_size 필드를 두어, 이 필드를 <code>update</code> 해가며 계산하는 방식을 생각했습니다. 
<img src="https://velog.velcdn.com/images/nuh__d/post/60a8dc40-9368-4cc7-812f-a5d3e9130c0c/image.png" alt=""></p>
<p>제 PC환경에서 대략 800만개의 데이터에서 select는 약 0.8sec가 소요되지만, 800만번의 update는 약 220sec정도가 소요됩니다. 적합한 비교는 아니었을지라도 제 생각에는 unread_message_size필드는 메시지를 보낼 때마다 update요청을 해야하니 문제가 생길 것 이라고 생각했습니다.</p>
<h3 id="해결">해결</h3>
<p>chat_room_users테이블의 <code>last_chat_message</code>필드와 <code>안 읽은 메시지 수</code>의 제한 을 두어 해결합니다. 현재 카카오톡 서비스에서는 그룹챗의 경우 안 읽은 메시지 수를 <code>99개</code>로 제한을 두고 있습니다. 과거에는 300개로 기억하고 있었는데 업데이트됨에 따라 99개로 줄인 것 같습니다. 여기에서 힌트를 얻어, 300개로 제한을 한다면, 조회성능도 높일 수 있을 것 같았습니다.</p>
<p>last_chat_message필드란 사용자가 읽은 마지막 메시지의 id 값입니다. 최신의 300개의 메시지들을 조회 한 후 last_chat_message를 통해 더 최신의 메시지의 갯수를 계산한다면, 그 값이 <code>안 읽은 메시지 개수</code>가 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/nuh__d/post/952a1107-481a-453f-ad7a-c4a28b21f513/image.png" alt="">
사용자 A가 마지막으로 읽은 메시지인 last_chat_message_id=6이라고 해봅시다.</p>
<ol>
<li>최근 300개의 메시지를 모두 불러온다.</li>
<li>300개의 메시지id 값과 last_chat_message_id값을 비교.</li>
<li>비교 결과 더 큰 id값의 수를 계산.</li>
</ol>
<p>위와 같은 로직을 통해 사용자 A가 안 읽은 메시지 수는 2개 인 것을 알 수 있습니다.</p>
<h2 id="채팅방-입장--퇴장">채팅방 입장 &amp; 퇴장</h2>
<p>위 채팅방 목록 조회를 하기 위해 last_chat_message_id필드를 만들었습니다. 그럼 이 필드는 언제 update되어야 최소화 할 수 있을까에 대해 고민해보았습니다.</p>
<p>4가지 상황을 생각해 볼 수 있습니다.</p>
<ol>
<li>메시지를 읽고 있는 상태.</li>
<li>메시지를 읽지 않고 있는 상태.</li>
<li>채팅방에 입장하는 순간.</li>
<li>채팅방에서 퇴장하는 순간.</li>
</ol>
<p>첫 번째의 경우, last_chat_message_id를 업데이트 할 필요가 없습니다. 이 필드는 <code>내가 읽지 않은</code> 상대방의 메시지의 갯수를 계산하기 위해 존재합니다. 따라서, 상대방의 메시지를 계속해서 읽고 있는 상태라면 update할 필요가 없는 것입니다.</p>
<p>두 번째의 경우, 역시 update할 필요가 없습니다. 반대로 update를 한다는 것은 내가 상대방의 메시지를 읽었다는 뜻입니다. 그러니 내가 읽지 않고 있는 상태이므로 update를 해서는 안됩니다.</p>
<p>세 번째의 경우, 가장 마지막의 메시지로 update합니다. 첫 번째 경우를 보면 채팅방에 입장 후 상대방이 메시지를 보낼 경우 last_chat_message_id를 update하지 않습니다. 그렇다면 내 last_chat_message_id는 새로운 메시지 id보다 이전의 것이므로, 계속해서 <code>안 읽은 메시지 개수</code>가 누적될 것 입니다.</p>
<p>네 번째의 경우, 역시 가장 마지막의 메시지로 update합니다. 이유는 두 번째와 동일합니다.
우리가 update하지 않고 퇴장한다면, 상대가 메시지를 보낼 때 마다 이전의 안 읽은 메시지 개수에 누적하여 계산될 것입니다.</p>
<h2 id="채팅방-개설">채팅방 개설</h2>
<p>보통 카카오톡에서 1:1 채팅 아이콘을 누르면 해당 채팅방이 개설됩니다. 만약 기존에 채팅한 이력이 있다면 새로운 채팅방이 아닌 기존 채팅방으로 이동하게 됩니다. 
또한, 그룹채팅방의 경우 동일한 참여자들에 대해 채팅방을 개설하려고 하면, 채팅방으로 이동 또는 채팅방 만들기 라는 선택지를 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/nuh__d/post/26250203-5034-401b-87de-bb3208e4fe3b/image.png" alt=""></p>
<p>이러한 기능을 구현하기 위해 생각한 방법은 각 <code>사용자 id로 구성된 Alias</code>를 사용하는 것입니다. 사용자의 id는 유일하므로 참여자들이 동일하지 않은 채팅방의 alias또한 유일합니다.</p>
<p>현재는 단일 채팅을 지원하지만, 나중에 그룹채팅으로 확장할 계획입니다. 따라서, 사용자의 id값을 받을 때 List형태로 받아, 한번에 Alias로 변환하는 작업을 하고 있습니다.</p>
<blockquote>
<p>조금 나중에 알게 된 내용인데, <code>YAGNI</code>라는 원칙에 위배되는 설계인 것 같기도 합니다.
YAGNI원칙이란, <code>You aren&#39;t gonna need it</code>로 실제로 필요할 때 기능을 구현하라 라는 내용이라고 합니다.</p>
</blockquote>
<p>이 원칙이 클라이언트로부터 받아오는 파라미터에도 해당되는지 고민해볼 필요가 있을 것 같습니다.</p>
<pre><code class="language-java">public ResponseEntity&lt;Void&gt; create(@RequestHeader HttpHeaders headers,
@RequestBody ChatRoomDto chatRoomDto) {
        String redirectUrl = simpleChatService.createRoom(chatRoomDto);
        headers.setLocation(URI.create(&quot;/chat/room/&quot; + redirectUrl));
        return new ResponseEntity(headers, HttpStatus.FOUND);
    }</code></pre>
<p>채팅방 개설 API는 <code>채팅방 조회</code>로 리다이렉션 하고 있습니다. createRoom이라는 메소드를 통해 리다이렉션 할 url를 매핑해줍니다.</p>
<pre><code class="language-java">public String createRoom(ChatRoomDto chatRoomDto) {
        String userId = AuthenticationToken.getUserId();
        String roomId = chatRoomDto.getRoomId();
        List&lt;String&gt; participantIds = mergeParticipantIds(userId, chatRoomDto.getOtherIds());
        return createRoomIfAbsent(roomId, participantIds);
    }

private String createRoomIfAbsent(String roomId, List&lt;String&gt; participantIds) {
        String alias = createAlias(participantIds);
        Optional&lt;ChatRoom&gt; oldChatRoom = chatRoomRepository.findByAlias(alias);
        return oldChatRoom.isPresent() ?
                oldChatRoom.get().getRoomId() :
                createNewRoom(roomId, participantIds, alias);
    }</code></pre>
<p>만약, 기존에 존재하는 alias에 해당된 채팅방 개설 요청이라면 기존 채팅방으로 리다이렉션해주고
만약, 새로운 채팅방 개설 요청이라면 채팅방 개설 후 해당 채팅방으로 리다이렉션합니다.</p>
<hr>
<p>제가 처음으로 설계했던 방식입니다.
위의 방식은 아직 부족한 부분이 많습니다. 예를 들어, 정상적인 소켓 종료가 아닌 브라우저 창 닫기 같은 <code>비정상적으로 소켓이 종료</code>된다면 last_chat_message_id의 update작업이 올바르게 이루어지지 않습니다.</p>
<p>다음 reduck 채팅 서비스2에서는 STOMP헤더에 JWT를 담아 사용자 인증하는 것과, session을 사용하여 안 읽은 메시지 개수 계산하는 방법에 대해 적어보게 될 것 같습니다.</p>
<p>감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 34] int 상수 대신 열거 타입을 사용하라]]></title>
            <link>https://velog.io/@nuh__d/item-34-int-%EC%83%81%EC%88%98-%EB%8C%80%EC%8B%A0-%EC%97%B4%EA%B1%B0-%ED%83%80%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-34-int-%EC%83%81%EC%88%98-%EB%8C%80%EC%8B%A0-%EC%97%B4%EA%B1%B0-%ED%83%80%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</guid>
            <pubDate>Fri, 18 Aug 2023 09:52:15 GMT</pubDate>
            <description><![CDATA[<h2 id="열거타입">열거타입</h2>
<pre><code class="language-java">public enum Enum {
    ONE(1),
    TWO(2),
    THREE(3),;
    private int number;

    Enum(int number) {
        this.number = number;
    }
}</code></pre>
<p>열거타입 자체는 클래스이다.
상수 하나당 <code>자신의 인스턴스를 하나씩 만들어 public static final 필드</code>로 제공한다. 일반적인 클래스로 구현한다면 어떤 형태일지 한번 작성해보자.</p>
<pre><code class="language-java">public class RawEnum {

    static final RawEnum ONE = new RawEnum(1);
    static final RawEnum TWO = new RawEnum(1);
    static final RawEnum THREE = new RawEnum(1);

    private int number;

    public RawEnum(int number) {
        this.number = number;
    }
}</code></pre>
<p><code>public static final 필드</code>로 각 상수에 해당하는 인스턴스를 제공하고 있다.
이러한 클래스의 형태를 보면 열거 타입의 특징과도 일치한다.</p>
<p>열거 타입에 메서드나 필드를 추가하는 것도 가능하다.
당연히 열거 타입 자체는 클래스이니 가능한게 맞다.</p>
<p>두 enum원소의 숫자를 더하는 메서드와 Enum의 모든 원소를 더하는 메서드를 추가해보자.</p>
<pre><code class="language-java">public enum Enum {
    ONE(1),
    TWO(2),
    THREE(3),;
    private int number;

    Enum(int number) {
        this.number = number;
    }
    public int sum(Enum first, Enum second) {
        return first.number + second.number;
    }

    public int sumAll() {
        return Arrays.stream(values())
                .mapToInt(Enum::getNumber)
                .sum();    
    }

    private int getNumber() {
        return number;
    }
}</code></pre>
<p><code>sumAll</code> 메소드를 보면 <code>values()</code>라는 메소드를 사용하고 있다. 이 메소드는 Enum이 제공하는 정적 메소드이다.</p>
<p>하지만 Enum클래스를 봐도 어디에도 <code>values()</code> 라는 메소드는 존재하지 않는다. <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html">이 메소드는 컴파일러가 자동 생성해주는 특별한메소드이다.</a></p>
<p>만약 상수별로 다르게 동작하게 만들고 싶다. 이런 경우 <code>추상 메서드</code>를 선언하여 상수별 메서드 구현을 하면 된다.</p>
<p>계산기를 Enum으로 작성하고, 상수 별로 계산되는 로직을 다르게 작성해보자.</p>
<pre><code class="language-java">public enum Operation {
    PLUS(&quot;+&quot;) {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS(&quot;-&quot;) {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES(&quot;*&quot;) {
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE(&quot;/&quot;) {
        public double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        int a = num;
        return symbol;
    }

    public abstract double apply(double x, double y);

}</code></pre>
<p>매우 깔끔하다. apply는 추상메서드이므로, Operation의 상수가 apply를 재정의 하지 않으면 컴파일 오류가 나니 매우 안전하다.</p>
<h2 id="열거-타입의-정적필드와-생성자의-관계">열거 타입의 정적필드와 생성자의 관계</h2>
<p>Operation에 문자열을 해당 열거타입 상수로 변환해주는 <code>fromString</code>메서드를 만들어보자.</p>
<p>코드가 너무 길어지니, 추가된 부분만 작성해놓겠다.</p>
<pre><code class="language-java">public enum Operation {

    ...    

    private static final Map&lt;String, Operation&gt; stringToEnum =
           Stream.of(values())
           .collect(toMap(Object::toString, e -&gt; e));

    ...

    public static Optional&lt;Operation&gt; fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }

}</code></pre>
<p>stringToEnum에 대해 알아볼 부분이 있다.</p>
<ol>
<li>Operation 상수가 stringToEnum 맵에 추가되는 시점</li>
<li>열거타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다.</li>
<li>열거 타입 생성자에서 같은 열거 타입의 다른 상수에 접근 할 수 없다.</li>
</ol>
<p>2번과 3번은 같은 맥락에서 이해할 수 있다.
하나씩 알아보자.</p>
<h3 id="1-operation-상수가-stringtoenum-맵에-추가되는-시점">1. Operation 상수가 stringToEnum 맵에 추가되는 시점</h3>
<p>열거 타입 상수 생성 후 정적 필드가 초기화 될 때이다.
열거 타입의 각 상수는 결국 public static final 인 인스턴스라고 했다. 곧 정적필드이다. Java에서 정적 필드는 클래스에 작성된 순서대로 초기화된다. Enum은 상수 선언을 맨 위에 해야하고 다른 정적 필드를 그 밑에 선언한다. 따라서 당연히 <code>stringToMap</code>의 초기화 시점은 각 상수들이 <code>초기화 된 이후</code>이다.</p>
<h3 id="2--3-열거타입-생성자에서-정적필드의-접근-제한">2 &amp; 3 열거타입 생성자에서 정적필드의 접근 제한</h3>
<p>자바 8 이전 방식을 설명하며 빈 해시맵에 상수를 추가한다고 설명했다. 그리고 생성자에서는 상수 변수를 제외한 정적 필드에 접근 할 수 없다고 한다.</p>
<p>우선 stringToEnum 맵을 빈 해시맵으로 바꾸고, 생성자에서 추가해보도록 하자.</p>
<pre><code class="language-java">    private static final Map&lt;String, Operation&gt; stringToEnum =
//            Stream.of(values()).collect(
//                    toMap(Object::toString, e -&gt; e));
            new HashMap&lt;&gt;();

    Operation(String symbol) {
        this.symbol = symbol;
        stringToEnum.put(&quot;+&quot;, this.PLUS);
    }</code></pre>
<p>이렇게 작성하면 <code>stringToEnum.put(&quot;+&quot;, this.PLUE);</code> 에 오류가 날 것이다. 메시지를 확인해보면, <code>It is illegal to access static member &#39;stringToEnum&#39; from enum constructor or instance initializer</code> 즉 enum의 생성자에서 정적 필드에 접근할 수 없다고 한다.</p>
<p>왜 그럴까?</p>
<p><code>열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다.</code> 이라고 한다. 즉 생성자가 실행될 때에는 정적필드가 초기화 되기 전이라는 것이다.</p>
<p>사실 일반적인 클래스에서의 생성자에서는 정적필드에 접근이 가능하다. 아직 초기화되지 않은 정적필드에 접근하면 <code>null</code>로 대체된다.
하지만 Enum에서는 제한한다.</p>
<blockquote>
<p>나의 개인적인 생각으로는, enum이라는 특성에 그 이유가 있다고 생각한다.
Enum은 상수들의 집합이다. 이 상수들은 특정한 의미를 가져야한다. 만약 null로 대체가 가능하다면, 이 특정한 의미를 잃어버리게 되어 enum의 의미가 사라진다고 생각한다.</p>
</blockquote>
<p>따라서, 일반적인 클래스처럼 null로 대체할 수 없고 그러니 접근을 제한한 것으로 보인다. stringToEnum같은 정적필드를 통해 사용자가 어떠한 행위를 할 지 모르니 역시 접근을 제한한 것이라고 생각한다.</p>
<hr>
<p>참고</p>
<ul>
<li><a href="https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html">https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Intellij New-UI]]></title>
            <link>https://velog.io/@nuh__d/Intellij-New-UI</link>
            <guid>https://velog.io/@nuh__d/Intellij-New-UI</guid>
            <pubDate>Mon, 14 Aug 2023 13:33:31 GMT</pubDate>
            <description><![CDATA[<p>이번에 버전 업데이트 하면서 알게되었는데 23.7.26일날 23.2버전이 나왔더라구요.</p>
<p>UI가 변했는데 진짜 너무 깔끔하고 이쁩니다!!</p>
<p><img src="https://velog.velcdn.com/images/nuh__d/post/0a250746-2d53-4546-b4a4-c1d4887d7d65/image.png" alt="">
이런 느낌입니다. 자세한 정보는 <a href="https://www.jetbrains.com/idea/new-ui/">https://www.jetbrains.com/idea/new-ui/</a>
에서 보시면 됩니다</p>
<p>모르시는 분들도 계실 것 같아 글 남깁니다~</p>
<p><img src="https://velog.velcdn.com/images/nuh__d/post/f0b2add9-3fb5-4fc0-a1ea-e80c8258e498/image.png" alt="">
우측 상단의 <code>설정</code>버튼을 누르면</p>
<p><img src="https://velog.velcdn.com/images/nuh__d/post/c9ee49f0-bf21-4364-87b2-ecfb232c7966/image.png" alt="">
맨 아래에 <code>Enable New UI</code> 를 누르시면 됩니다!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라]]></title>
            <link>https://velog.io/@nuh__d/item-32-%EC%A0%9C%EB%84%A4%EB%A6%AD%EA%B3%BC-%EA%B0%80%EB%B3%80%EC%9D%B8%EC%88%98%EB%A5%BC-%ED%95%A8%EA%BB%98-%EC%93%B8-%EB%95%8C%EB%8A%94-%EC%8B%A0%EC%A4%91%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-32-%EC%A0%9C%EB%84%A4%EB%A6%AD%EA%B3%BC-%EA%B0%80%EB%B3%80%EC%9D%B8%EC%88%98%EB%A5%BC-%ED%95%A8%EA%BB%98-%EC%93%B8-%EB%95%8C%EB%8A%94-%EC%8B%A0%EC%A4%91%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sun, 06 Aug 2023 13:24:27 GMT</pubDate>
            <description><![CDATA[<h3 id="가변인수-메서드와-힙-오염">가변인수 메서드와 힙 오염</h3>
<p><a href="https://velog.io/@nuh__d/item-29-%EC%9D%B4%EC%99%95%EC%9D%B4%EB%A9%B4-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EB%9D%BC#generic-%EA%B8%B0%EB%B0%98-stack-1">item 29</a>에서 힙 오염에 대해 잠깐 언급했었다. 힙 오염은 <code>매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 발생한다</code>. </p>
<p>가변인수 메서드는 다음처럼 생겼다.</p>
<pre><code class="language-java">    static &lt;T&gt; T[] toArray(T... args) {

        return args;
    }</code></pre>
<p>여기에 제네릭을 적용해보면 <code>힙 오염</code>이 발생할 수도 있다.</p>
<pre><code class="language-java">public static void method(List&lt;String&gt;... strs) {
        List&lt;Integer&gt; intList = List.of(43);
        Object[] objects = strs;
        objects[0] = intList; // 힙 오염 발생
        String s = strs[0].get(0); // ClassCastException
    }</code></pre>
<blockquote>
<p>그럼 가변인수 메서드와 힙 오염이 어떻게 연관되어 있을까?</p>
</blockquote>
<p>가변인수 메서드의 파라미터를 보면 <code>List&lt;String&gt;...strs</code> 의 List&lt;string&gt;을 담기 배열이 만들어진다. 앞서, 실체화 불가 타입(제네릭, 매개변수화 타입 등)은 타입 소거로 인해 런타임에 타입 정보가 없다고 배웠다. 그래서 런타임에 생성되는 이 배열은 <code>List[]</code> 이다! 런타임에는 타입 정보가 없으므로 어떤 타입도 우선 들어올 수가 있어진다.
이 메서드를 컴파일하면 비검사 경고를 보내는데 <code>Possible heap pollution from ~</code> 라고 나온다.</p>
<p>위의 가변인수 메서드 method를 보며 생각해보자.
런타임에는 타입 정보가 없다는 것을 이용하면, 컴파일만 통과하면 된다. Object[]로 strs가변인수 배열에 List&lt;Integer&gt;를 담을 수 있게 된다. 그러면 <code>objects[0] = intList;</code>에서 힙오염이 발생하는데, 좀 더 자세히 말하면 <code>strs가 힙 오염</code>된다고 본다. </p>
<h3 id="safevarargs">@SafeVarargs</h3>
<p>이러한 비검사 경고를 없애기 위해서는 자바 7 이전에는 호출하는 곳마다 @SuppressWarnings(&quot;unchecked
&quot;)를 사용했다. 자바 7에서 @SafeVarargs가 추가된 이후로 번거로움을 없앨 수 있었다.</p>
<p>하지만, 경고를 없애는 행위이기 때문에 그 메서드가 <code>타입 안전함</code>을 보장한 뒤에 사용해야 한다.
2가지 기준으로 타입 안전함을 보장할 수 있다.</p>
<ul>
<li>가변인수를 담는 제네릭 배열에 아무것도 저장하지 않는다.</li>
<li>이 배열의 참조가 밖으로 노출되지 않는다.
즉, varargs 매개변수 배열이 순수하게 인수들을 전달하는 일만 하면 안전하다.</li>
</ul>
<p>varargs매개변수 배열에 아무것도 저장하지 않고 타입 안정성을 깰 수 있는데 확인해보자.</p>
<h3 id="varargs-매개변수-배열의-참조-노출">varargs 매개변수 배열의 참조 노출</h3>
<pre><code class="language-java">static &lt;T&gt; T[] toArray(T... args) {

        return args;
    }</code></pre>
<p>처음 보여주었던 가변인수 메서드이다. <code>이 메서드가 반환하는 배열의 타입은 컴파일타임에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 없어 타입을 잘 못 판단 할 수 있다.</code> 이 내용은 아래 예제를 통해 좀 더 말해보겠다.  이 메서드를 보면 args를 반환함으로써 배열의 참조를 노출 하고 있다.  </p>
<p>그럼 구체적으로 어떻게 문제가 되는지 보자.</p>
<pre><code class="language-java">    public static void main(String[] args) {
        String[] attributes = pickTwo(&quot;좋은&quot;, &quot;빠른&quot;, &quot;저렴한&quot;); // ClassCastException

    }

    static &lt;T&gt; T[] pickTwo(T a, T b, T c) {
        switch (ThreadLocalRandom.current().nextInt(3)) {
            case 0:
                return toArray(a, b);
            case 1:
                return toArray(a, c);
            case 2:
                return toArray(b, c);
        }
        throw new AssertionError();
    }</code></pre>
<p>이 상황에서 pickTwo메서드의 반환타입은 String[] 일까?</p>
<p>아니다. <code>Object[]</code> 이다. toArray메서드의 반환값을 그대로 반환하고 있으니, toArray를 보자.
컴파일 시점에서 toArray에 넘어오는 인수들은 T a, T b, T c로 이 타입을 String이 아닌 매개변수 타입 T이다. 아직은 컴파일러가 무슨 타입인지 정확히 모른다. 그러니까 toArray메서드 입장에서는 Object[]로 반환할 수 밖에 없다.</p>
<h4 id="그럼-구체적으로-명시하면-올바른-타입string-으로-반환할까">그럼 구체적으로 명시하면 올바른 타입(String[]) 으로 반환할까?</h4>
<blockquote>
<pre><code class="language-java">String[] array = toArray(&quot;좋은&quot;, &quot;빠른&quot;, &quot;저렴한&quot;);
        System.out.println(&quot;array = &quot; + array[0]);</code></pre>
</blockquote>
<pre><code>이렇게 직접 toArray메서드 정확한 타입으로 넘겨주면 올바르게 동작한다. 컴파일러가 이제는 충분한 정보(String)을 받았기 때문이다.


그래서 pickTwo메서드의 반환 타입은 항상 Object[] 타입 배열이다. 그러니 main메서드에서 Object[] 을 String[]로 형변환하게 되니 ClassCastException이 발생하며 실패한다.

이러한 상황이 바로 varargs매개변수 배열을 올바르게 사용했지만 문제가 발생한 예이다.

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[item 29] 이왕이면 제네릭 타입으로 만들라]]></title>
            <link>https://velog.io/@nuh__d/item-29-%EC%9D%B4%EC%99%95%EC%9D%B4%EB%A9%B4-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-29-%EC%9D%B4%EC%99%95%EC%9D%B4%EB%A9%B4-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EB%9D%BC</guid>
            <pubDate>Sun, 23 Jul 2023 08:35:33 GMT</pubDate>
            <description><![CDATA[<h2 id="object-기반-stack">Object 기반 Stack</h2>
<p>먼저 이전에 작성했던 Stack을 확인해보자</p>
<pre><code class="language-java">class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    private void ensureCapacity() {
        if (size &gt;= elements.length) {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

}</code></pre>
<p>지금 이 Stack은 위험이 내재되어있다. 런타임에 ClassCastException등의 오류가 발생 할 수 있다. 제네릭 타입으로 바꿔보자.</p>
<h2 id="generic-기반-stack-1">Generic 기반 Stack #1</h2>
<pre><code class="language-java">public class Stack&lt;E&gt; {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() { // 비검사 경고 발생. unchecked cast
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if (size == 0)
            throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    public static void main(String[] args) {
        Stack&lt;String&gt; stack = new Stack&lt;&gt;();
        for (String arg : args)
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}</code></pre>
<p>위 Stack에서 봐야할 점은 2가지다.</p>
<ul>
<li><h3 id="생성자의-비검사-경고">생성자의 <a href="https://velog.io/@nuh__d/item-27-%EB%B9%84%EA%B2%80%EC%82%AC-%EA%B2%BD%EA%B3%A0%EB%A5%BC-%EC%A0%9C%EA%B1%B0%ED%95%98%EB%9D%BC">비검사 경고</a></h3>
</li>
</ul>
<p>일반적으로 다운캐스팅은 <code>ClassCastException</code>이 발생한다. <code>String s = (String) new Object();</code> 와 같이 다운캐스팅하면 말이다. 하지만 생성자에 사용된 다운캐스팅은 예외가 발생하지는 않는다. 이에 대해 말하면, Object배열을 아직 모르는 타입의 배열로 다운캐스팅하 꼴이다. 그러니, <code>unchecked cast</code>라는 비검사 경고를 보여주는것이다.</p>
<p>하지만 우리는 이 비검사 경고에 대해 타입 안전함을 보장할 수 있다. push메소드에서 항상 E타입의 원소를 저장하고 있다. 따라서 배열에 저장될 원소들의 타입은 E 타입임이 확실하므로 <code>@SuppressWarnings(&quot;unchecked&quot;)</code> 로 경고를 숨겨도 된다.</p>
<ul>
<li><h3 id="힙-오염">힙 오염</h3>
</li>
</ul>
<p>힙 오염은 item32에서 다루고있는 내용이다. 먼저 간단히 말해서 힙 오염은, <a href="https://www.geeksforgeeks.org/what-is-heap-pollution-in-java-and-how-to-resolve-it/">a variable of parameterized type points to an object that is not of that parameterized type.</a> 즉, <code>매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.</code></p>
<p>위 elements 배열은 런타임에는 Object 배열이다. 하지만 컴파일타임에는 E[]배열이었다. 그러니, 런타임에 E[] 배열이 Object배열을 참조하는 <code>힙 오염</code>이 발생한다. 책에서는 위 코드에는 힙 오염이 해가 되지 않았다고 한다.</p>
<h4 id="그럼-해가-되게끔-코드를-수정해보자">그럼 해가 되게끔 코드를 수정해보자!</h4>
<blockquote>
<ol>
<li>우리가 만든 Stack에서 발생한 힙 오염의 원인은 <code>배열의 런타임 타입이 컴파일타임과 다른 Object라는 것이다.</code></li>
<li>Object타입이라 함은, 모든 타입을 수용한다.</li>
<li>그럼 String타입 Stack에, Integer타입을 넣는 행위를 유도할 수 있다.</li>
<li>이러한 행위는 컴파일타임에는 통과되고, 런타임에 문제가 생길 것이다.</li>
</ol>
</blockquote>
<p>코드로 바꾸면!</p>
<pre><code class="language-java">        Stack&lt;String&gt; stringSt = new Stack&lt;&gt;();
        Stack&lt;Integer&gt; intSt = (Stack&lt;Integer&gt;) (Object) stringSt; // Stack 으로부터 발생된 힙 오염!
        intSt.push(1);
        intSt.push(2);
        String pop = stringSt.pop(); //ClassCastException
        System.out.println(&quot;pop = &quot; + pop);</code></pre>
<p>intSt과 stringSt는 동일한 Stack이다. <code>intSt.push(1);</code>로 String타입 Stack에 Integer를 넣을 수 있다. 그 이유는, 런타임에는 Object[]배열이라는 것이다. <a href="https://docs.oracle.com/javase/tutorial/java/generics/why.html">제네릭은 컴파일타임에 타입을 지정하여 런타임시 발생할 수 있는 오류를 줄이는데에 목적이 있다.</a> 그런데 우리가 <code>Stack&lt;Integer&gt; intSt = (Stack&lt;Integer&gt;)(Object) stringSt</code> 라는 코드로 컴파일러가 stringSt변수가 참조하는 Stack객체의 타입을 올바르게 지정하였음에도 불구하고,  런타임에 Integer가 들어가게끔 만들어버린것이다. </p>
<p>따라서 런타임에 <code>ClassCastException</code> 가 발생하는 <code>해</code>를 만들었다!</p>
<h2 id="generic-기반-stack-2">Generic 기반 Stack #2</h2>
<pre><code class="language-java">class Stack&lt;E&gt; {
    private Object[] elements; // case 2
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY]; // case 2
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    private void ensureCapacity() {
        if (size &gt;= elements.length) {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        @SuppressWarnings(&quot;unchecked&quot;) E result = (E) elements[--size]; // case 2
        elements[size] = null;
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}</code></pre>
<p>두 번째 방법은 내부에 사용되는 배열의 타입을 <code>E[] -&gt; Object[]</code>로 바꾸는 것이다. 이 방법의 단점은 원소 하나를 꺼낼 때마다 형변환을 해야한다는 점이다. <a href="https://stackoverflow.com/questions/26335959/what-is-the-cost-of-casting-in-java-is-it-a-good-idea-to-avoid-it">형변환은 DownCasting이라면 좀 더 느리다고는 한다. 하지만 형변환을 지양해야하는 이유는 유지보수성 측면에 있다고 한다.</a></p>
<hr>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://velog.io/@semi-cloud/Effective-Java-%EC%95%84%EC%9D%B4%ED%85%9C-29-%EC%9D%B4%EC%99%95%EC%9D%B4%EB%A9%B4-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EB%9D%BC#%EF%B8%8F-%EC%B6%94%EA%B0%80-%ED%9E%99-%EC%98%A4%EC%97%BC%EC%9D%B4%EB%9E%80">https://velog.io/@semi-cloud/Effective-Java-%EC%95%84%EC%9D%B4%ED%85%9C-29-%EC%9D%B4%EC%99%95%EC%9D%B4%EB%A9%B4-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EB%9D%BC#%EF%B8%8F-%EC%B6%94%EA%B0%80-%ED%9E%99-%EC%98%A4%EC%97%BC%EC%9D%B4%EB%9E%80</a></li>
<li><a href="https://www.geeksforgeeks.org/what-is-heap-pollution-in-java-and-how-to-resolve-it/">https://www.geeksforgeeks.org/what-is-heap-pollution-in-java-and-how-to-resolve-it/</a></li>
<li><a href="https://docs.oracle.com/javase/tutorial/java/generics/why.html">https://docs.oracle.com/javase/tutorial/java/generics/why.html</a></li>
<li><a href="https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html">https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html</a></li>
<li><a href="https://stackoverflow.com/questions/26335959/what-is-the-cost-of-casting-in-java-is-it-a-good-idea-to-avoid-it">https://stackoverflow.com/questions/26335959/what-is-the-cost-of-casting-in-java-is-it-a-good-idea-to-avoid-it</a></li>
<li><a href="https://www.infoworld.com/article/2076555/java-performance-programming--part-2--the-cost-of-casting.html">https://www.infoworld.com/article/2076555/java-performance-programming--part-2--the-cost-of-casting.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 27] 비검사 경고를 제거하라]]></title>
            <link>https://velog.io/@nuh__d/item-27-%EB%B9%84%EA%B2%80%EC%82%AC-%EA%B2%BD%EA%B3%A0%EB%A5%BC-%EC%A0%9C%EA%B1%B0%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-27-%EB%B9%84%EA%B2%80%EC%82%AC-%EA%B2%BD%EA%B3%A0%EB%A5%BC-%EC%A0%9C%EA%B1%B0%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sat, 22 Jul 2023 15:05:12 GMT</pubDate>
            <description><![CDATA[<p>우선 비검사 경고는 간략하게 다음을 말한다.</p>
<blockquote>
<p>Runtime에 발생하고, 명시적인 예외처리를 하지 않는 Exception이다.</p>
</blockquote>
<p>이번 item에 나온 비검사 경고는 <code>ClassCastException</code>이다. 이 예외는 당연히 RuntimeException을 상속받고 있다.</p>
<pre><code class="language-java">public class ClassCastException extends RuntimeException {
    private static final long serialVersionUID = -9223365651070458532L;

    /**
     * Constructs a &lt;code&gt;ClassCastException&lt;/code&gt; with no detail message.
     */
    public ClassCastException() {
        super();
    }

    /**
     * Constructs a &lt;code&gt;ClassCastException&lt;/code&gt; with the specified
     * detail message.
     *
     * @param   s   the detail message.
     */
    public ClassCastException(String s) {
        super(s);
    }
}</code></pre>
<h3 id="unchecked-conversion">unchecked conversion</h3>
<blockquote>
<h4 id="compile-time-warning">Compile-time warning</h4>
</blockquote>
<h4 id="타입확인없이-로-타입으로-선언할-때-발생">타입확인없이 로 타입으로 선언할 때 발생</h4>
<pre><code class="language-java">Set&lt;Lark&gt; exaltation = new HashSet();</code></pre>
<p>위 코드는 <code>unchecked conversion</code> 라는 비검사 경고가 발생한다. 이러한 선언은 허용된다. 그 이유는 제네릭이 지원되지 않았던, 이전 Java버전과의 호환성을 위해서이다.
이 경고를 해결하기 위해서는, 올바른 타입 매개변수를 명시하거나, 다이아몬드 연산자(&lt;&gt;)를 사용하면된다.</p>
<pre><code class="language-java">// 방법 1
Set&lt;Lark&gt; exaltation = new HashSet&lt;Lark&gt;();
// 방법 2
Set&lt;Lark&gt; exaltation = new HashSet&lt;&gt;();</code></pre>
<h3 id="제어할-수-없는-비검사-경고">제어할 수 없는 비검사 경고</h3>
<p>그런데, 만약 이러한 비검사 경고를 우리가 제어할 수 없을때도 있을것이다. 예를 들어, 그럴일은 없겠지만 외부라이브러리의 잘못된 메소드를 사용하는 경우이다. 이 메소드에서 비검사 경고가 발생할 코드가 있다면, 즉 다음과 같은 메소드이다.</p>
<pre><code class="language-java">//외부 라이브러리 메소드.
public static List getRawList() {
        List result = new ArrayList();
        result.add(&quot;I am the 1st String.&quot;);
        result.add(&quot;I am the 2nd String.&quot;);
        result.add(&quot;I am the 3rd String.&quot;);
        return result;
    }

// 이 외부 라이브러리 메소드를 사용하는 클라이언트 코드.
List&lt;String&gt; rawList = getRawList();</code></pre>
<p>이 메소드는 <code>[unchecked] unchecked call to add(E) as a member of the raw type List</code>라는 비검사경고가 발생하고, 반환타입은 <code>List</code> 로 타입이다. 따라서 이를 사용하는 클라이언트에서 <code>List&lt;String&gt; rawList = getRawList();</code>와 같이 사용한다면, 자동 형변환되는 과정에서 <code>unchecked conversion</code> 경고가 발생할 것이다. 이러한 상황이 위에서 말한 제어할 수 없는 상황이다.</p>
<p>그럼 이럴 때엔 어떻게 비검사 경고를 제거할까??</p>
<h3 id="suppresswarningsunchecked">@SuppressWarnings(“unchecked”)</h3>
<p>이런경우 <code>@SuppressWarnings(“unchecked”)</code> 어노테이션을 통해 해결할 수 있다. 이때 주의해야 할 점은, <code>반드시 우리가 type safe함을 보장하는 경우에만 사용가능</code>하다라는 것이다. 위 외부라이브러리 메소드라면 내부에서 String만을 List에 담고있기때문에 어노테이션으로 경고를 숨겨주어도 괜찮다.</p>
<p>만약, 타입 안전함을 검증하지 않고 사용해버리면 어떻게 될까??
예를 들어, 다음과 같이 수정된 외부 라이브러리 메소드를 사용한다고 하자.</p>
<pre><code class="language-java">// 위험한 외부 라이브러리 메소드.
public static List getRawListWithMixedTypes() {
        List result = new ArrayList();
        result.add(&quot;I am the 1st String.&quot;);
        result.add(&quot;I am the 2nd String.&quot;);
        result.add(&quot;I am the 3rd String.&quot;);
        result.add(new Date());
        return result;
    }

// 이 외부 라이브러리 메소드를 사용하는 클라이언트 코드.
@SuppressWarnings(&quot;unchecked&quot;)
List&lt;String&gt; rawListWithMixedTypes = getRawListWithMixedTypes();
String s = rawListWithMixedTypes.get(3); // ClassCastException 발생.</code></pre>
<p>실제 클라이언트에서 형변환시에는 @SuppresssWarnings 어노테이션을 사용했으므로, 비검사 경고는 보이지 않는다. 그리고 반환받은 List의 데이터를 가져올 때, 그 타입은 당연히 <code>String</code>타입으로 받게 된다. 하지만 이 <code>rawListWithMixedTypes</code>에는 <code>String + Date</code>조합의 데이터이다. 따라서, 이 Date객체를 String으로 받으면서 <code>ClassCastException</code>예외가 발생하는데 이는 컴파일시점이 아닌 <code>런타임시점</code>에 알 수 있다.</p>
<p>따라서, @SuppressWarnings(“unchecked”) 를 사용할 땐 <code>Type Safe</code>를 보장해야한다. 이와 함께 <code>경고를 무시해도 안전한 이유를 항상 주석으로 남기자.</code></p>
<h3 id="결론">결론</h3>
<blockquote>
<h4 id="모든-비검사-경고는-런타임에-classcastexception의-발생가능성을-의미하니-최선을-다해-제거하라">모든 비검사 경고는 런타임에 ClassCastException의 발생가능성을 의미하니, 최선을 다해 제거하라.</h4>
</blockquote>
<h4 id="제거할-방법이-없다면-그-코드가-안전함을-증명하고-최소한의-범위로-suppresswarningsunchecked-어노테이션으로-숨겨라">제거할 방법이 없다면, 그 코드가 안전함을 증명하고 최소한의 범위로 @SuppressWarnings(&quot;unchecked&quot;) 어노테이션으로 숨겨라.</h4>
<h4 id="그리고-그-근거를-주석으로-남겨라">그리고 그 근거를 주석으로 남겨라.</h4>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://www.baeldung.com/java-unchecked-conversion">https://www.baeldung.com/java-unchecked-conversion</a></li>
<li><a href="https://www.baeldung.com/java-checked-unchecked-exceptions">https://www.baeldung.com/java-checked-unchecked-exceptions</a></li>
</ul>
<hr>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 25] 톱레벨 클래스는 한 파일에 하나만 담으라]]></title>
            <link>https://velog.io/@nuh__d/item-25-%ED%86%B1%EB%A0%88%EB%B2%A8-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94-%ED%95%9C-%ED%8C%8C%EC%9D%BC%EC%97%90-%ED%95%98%EB%82%98%EB%A7%8C-%EB%8B%B4%EC%9C%BC%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-25-%ED%86%B1%EB%A0%88%EB%B2%A8-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94-%ED%95%9C-%ED%8C%8C%EC%9D%BC%EC%97%90-%ED%95%98%EB%82%98%EB%A7%8C-%EB%8B%B4%EC%9C%BC%EB%9D%BC</guid>
            <pubDate>Wed, 28 Jun 2023 12:18:36 GMT</pubDate>
            <description><![CDATA[<p>톱 레벨 클래스란 <code>내부 클래스</code>와 반대로 생각하면 된다. 가장 바깥의 class를 의미한다.
Main.java</p>
<pre><code class="language-java">
public class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}</code></pre>
<p>Utensil.java</p>
<pre><code class="language-java">class Utensil {
    static final String NAME = &quot;PAN&quot;;
}
class Dessert{
    static final String NAME = &quot;CAKE&quot;;
}</code></pre>
<p>여기서 Utensil, Dessert클래스가 바로 톱레벨 클래스라고 볼 수 있다.
이 구조로 main을 실행시키면 <code>PANCAKE</code>가 출력된다.
지금은 아무 문제가 생기지는 않는다.</p>
<p>하지만 만약 Dessert.java라는 파일이 생성되고, 우연히도 class명이 겹친 class를 만들게되면 문제가된다.</p>
<p>Dessert.java</p>
<pre><code class="language-java">class Utensil {
    static final String NAME = &quot;PAN&quot;;
}
class Dessert{
    static final String NAME = &quot;CAKE&quot;;
}</code></pre>
<p>책에서는 <code>javac Main.java Dessert.java</code>명령어로 컴파일을 하면 <code>Utensil과 Dessert클래스가 중복되었다</code>고 알려준다고 한다.
하지만, 내가 해볼때는 계속 컴파일이 될 뿐이었다. 진짜 이거 때문에 몇일동안 계속 뭐가 잘못된건지 생각해봤는데, 내 결론은 <code>컴파일러</code>밖에 떠오르지 않았다.(아닐수도 있다. 내 추측이다.) 혹시 다른 분들도 책의 내용을 따라하다가 나처럼 다르게 동작할 수 있을 것 같아 언급해보았다.</p>
<blockquote>
<p>이펙티브 자바의 저자의 jdk는 <code>Zulu의 OpenJDK 9.0.0.15</code>라고 한다. 옮긴이 이신 <code>이복연</code>저자분의 환경은 어떻게 구성하신 지는 확인해보지 못했고, 현재 내 jdk는 Oracle의 jdk11.0.14을 사용하고 있다.</p>
</blockquote>
<p><code>Zulu의 jdk버전</code>이 한참 옜날 버전인 점도 있겠고, 어쨋든 서로 다른 jdk이기 때문에 차이가 발생하는 부분은 있을 것 이라 생각하기로 했다..
Zulu jdk 9.0.0.15로 설정하고 해보려고 했지만, aZule의 <a href="MTY4ODM2MzE1OC4xLjEuMTY4ODM2MzIwMC4xOC4wLjA">릴리즈 노트</a>를 보면 9.0.0.15버전을 찾을 수 없어 포기했다.</p>
<p>그럼 내 환경에서 실행한 결과에 대해 써보도록 하겠다.</p>
<p>Intellij IDE에서는 <code>Duplicated Class</code>라며 에러를 보여주고 있다.
우선 총 4가지의 컴파일 순서를 가지고 실행해보았다.</p>
<h4 id="javac-mainjava-dessertjava-------javac-dessertjava-mainjava">javac Main.java Dessert.java |      javac Dessert.java Main.java</h4>
<p>컴파일되고, main.class를 보면 <code>potpie</code>로 출력될 것 이라 컴파일 된다.</p>
<pre><code class="language-java">public class Main {
    public Main() {
    }

    public static void main(String[] var0) {
        System.out.println(&quot;potpie&quot;);
    }
}</code></pre>
<h4 id="javac-mainjava-utensiljava--javac-utensiljava-mainjava">javac Main.java Utensil.java | javac Utensil.java Main.java</h4>
<p>마찬가지로 compile은 되고 이번엔 <code>PANCAKE</code>으로 출력된다.</p>
<pre><code class="language-java">public class Main {
    public Main() {
    }

    public static void main(String[] var0) {
        System.out.println(&quot;PANCAKE&quot;);
    }
}</code></pre>
<h4 id="javac-mainjava">javac Main.java</h4>
<p>책에서는 <code>javac Main.java Utensil.java</code>와 동일하게 동작된다고 하지만 실제로 내 환경에서는 컴파일 오류가 발생했다. Symbol을 찾지 못한다고 한다.
<img src="https://velog.velcdn.com/images/nuh__d/post/65a8b691-33e8-4e52-8874-6abd2b0b940a/image.png" alt="">
Symbol오류는 내가 알기로는 Main클래스에 해당 symbol이 없어서 발생하는 거로 안다. 그래서 Main클래스에 Utensil, Dessert클래스를 작성하면 다시 컴파일 되는 것을 볼 수 있긴한데, 이렇게 작성하면 이 책에서 말하는 의도가 아니게 된다 생각한다. 이 부분에 관해서는 내가 java에 대해 더 공부하다 보면 이해할 수 있는 부분이라 생각하고 기억해둔 채로 넘어간다.</p>
<h4 id="javac-utensiljava-dessertjava--javac-dessertjava-utensiljava">javac Utensil.java Dessert.java | javac Dessert.java Utensil.java</h4>
<p>이번에는 Symbol오류가 아닌 duplicate class 오류가 발생했다.
<img src="https://velog.velcdn.com/images/nuh__d/post/a02692b5-418a-4753-ad7e-c755fc0d6518/image.png" alt=""></p>
<hr>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 24] 멤버 클래스 되도록 static으로 만들라]]></title>
            <link>https://velog.io/@nuh__d/item-24</link>
            <guid>https://velog.io/@nuh__d/item-24</guid>
            <pubDate>Sun, 11 Jun 2023 14:15:08 GMT</pubDate>
            <description><![CDATA[<p>item 24에서 <code>private static class</code>에 대해 말해준다. 언젠가 <code>내부 클래스는 private static class로 만들어야한다</code> 는 글을 본적이 있는데, 제목에서도 써있듯이 <code>항상</code> 그렇게 만들라는 것은 아니다. 이번 item을 통해 언제 그리고 왜 사용하는지에 대해 알 수 있을 것이다.</p>
<hr>
<p>중첩 클래스는 다른 클래스안에 정의된 클래스를 말하며, 자신을 감싼 바깥 클래스에서만 사용되어야 한다. 중첩 클래스는 4가지로 나눌 수 있다.</p>
<h3 id="정적-멤버-클래스">정적 멤버 클래스</h3>
<p>정적 멤버 클래스는 public 도우미 클래스로 쓰인다. 이 예시로 item34를 언급하며 Operation 열거 타입이 Calculator클래스이 public 정적 멤버 클래스가 되어야한다고 한다. 열거타입은 뒷 item의 내용이긴 하지만 <code>public 도우미 클래스</code>가 어떤 형태일지 이해를 돕기위해 간단하게 구현해보았다.</p>
<pre><code class="language-java">public class Calculator {
    private int battery;

    public Calculator() {
        this.battery = 100;
    }
    public double calculate(Operation op, double x, double y) {
        if(this.battery == 0) throw new RuntimeException(&quot;배터리 없음.&quot;);
        this.battery -= 1;
        return op.apply(x,y);
    }
    public static enum Operation {
        PLUS,
        MINUS,
        TIMES,
        DIVIDE,
        ;
        public double apply(double x, double y) {
            switch (this) {
                case PLUS:
                    return x + y;
                case MINUS:
                    return x - y;
                case TIMES:
                    return x * y;
                case DIVIDE:
                    return x / y;
            }
            throw new AssertionError(&quot;알수없는 연산 : &quot; + this);
        }
    }
}</code></pre>
<p>이는 계산기 클래스이다. 간단히 사칙연산을 지원하는 계산기이다. <code>calculate</code>함수는 Operation 도우미 클래스를 사용하여, 파라미터로 넘겨받은 Opertaion 상수에 따라 계산을 수행한다. 실제 Calculator 클라이언트에서는 다음처럼 사용할 것이다.</p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        //계산기 수행.
        Calculator calculator = new Calculator();
        double calculate = calculator.calculate(Calculator.Operation.PLUS, 1.3, 1.7);
        System.out.println(&quot;calculate = &quot; + calculate);

        //가능
        double apply = Calculator.Operation.DIVIDE.apply(3, 2);
        System.out.println(&quot;apply = &quot; + apply);
    }
}</code></pre>
<p>계산기를 사용할 때, 어떤 operation에 따라 수행되어야하는 로직이 다를 것이다. 이런 경우 <code>public static class(도우미 클래스)</code>로 enum클래스를 사용하여 원하는 연산을 쉽게 사용할 수 있을 것 같다.</p>
<h3 id="비정적-멤버-클래스">(비정적) 멤버 클래스</h3>
<p>즉, static이 아닌 멤버 클래스를 말한다. <code>이 클래스의 인스턴스는 바깥 클래스의 인스턴스로의 숨은 외부 참조를 갖는다</code> 이 관계는 멤버 클래스의 인스턴스안에 만들어져, 메모리도 더 차지하고, 생성시간도 오래걸린다. 그리고 가장 중요하게 GC가 이를 수집하지 못하는 <code>메모리 누수가 발생</code>할 수도 있다.</p>
<blockquote>
<p>숨은 외부 참조? 메모리 누수?</p>
</blockquote>
<pre><code class="language-java">public class Outer {
    private int[] arr;

    // 내부 클래스
    class Inner_Class {

    }
    // 외부 클래스 생성자
    public Outer(int size) {
        arr = new int[size];
        System.out.println(&quot;Outer 인스턴스 생성 : &quot; +this);
    }
     public void print() {
        System.out.println(&quot;Outer 인스턴스&quot;);
    }
}</code></pre>
<p>위 Outer클래스의 컴파일된 형태를 확인해보자. 그러면 다음과 같이 Inner_Class의 생성자에 매개변수로 Outer의 인스턴스가 넘어옴을 볼 수 있다.
<span style="color:gray">Command Prompt를 열고, 해당 패키지로 경로이동한 뒤에 <code>javac Outer.java -encoding UTF8</code>로 컴파일했다. encoding옵션은 내 intellij 설정을 잘못했었는지 한글 인식문제가 있어 UTF8로 지정한 것이다.</span></p>
<pre><code class="language-java">class Outer$Inner_Class {
    Outer$Inner_Class(Outer var1) {
        this.this$0 = var1;
    }
}</code></pre>
<p>body부분을 보면 <code>this.this$0 = Outer인스턴스</code> 형태로 참조를 가지게 된다. 이러한 방식은 코드 작성 시점에서는 눈으로 확인할 수 없으니 <code>숨은 외부 참조</code>라고 말하는 듯 싶다.</p>
<p>그럼 <code>메모리 누수</code>에 대해 확인해보자</p>
<pre><code class="language-java">
public class Main {
    public static void main(String[] args) {
        Outer.Inner_Class inner_class = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class1 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class2 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class3 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class4 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class5 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class6 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class7 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class8 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class9 = new Outer(100_000_000).new Inner_Class();
        Outer.Inner_Class inner_class10 = new Outer(100_000_000).new Inner_Class();
    }
}</code></pre>
<p>GC는 자바에서 사용되는 <a href="https://velog.io/@nuh__d/item-7-%EB%8B%A4-%EC%93%B4-%EA%B0%9D%EC%B2%B4-%EC%B0%B8%EC%A1%B0%EB%A5%BC-%ED%95%B4%EC%A0%9C%ED%95%98%EB%9D%BC#4%EA%B0%80%EC%A7%80%EC%9D%98-%EC%B0%B8%EC%A1%B0%EA%B4%80%EA%B3%84">4가지의 참조관계</a>에 따라 객체들을 수집한다.
이 코드만 보면 Outer인스턴스는 생성된 이후, 어떠한 참조도 갖지 못해 GC가 수집할 수 있을 것 처럼 보인다. 하지만 알고보면 위에서 말한 <code>숨은 외부 참조</code>로 인해 즉, 내부 Inner_Class인스턴스가 Outer인스턴스를 참조하고 있으므로 <code>강한 참조</code>관계를 가지게 되어 GC가 수집하지 않는것이다. 따라서, 어디에도 사용되지 않을 Outer인스턴스는 수집되는것이 더 좋겠지만, 그렇지 못해 <code>메모리 누수</code>로 이어지게 되는 것이다.</p>
<p>그래서 글의 초반부에 말한 <code>private static class</code>중 static에 대해 이유는 확실해졌다.
<code>멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 반드시 static을 붙여서 정적 멤버 클래스</code>로 만들어야한다. </p>
<p>이제는 private에 대해 생각해보자.</p>
<p><code>Map구현체</code>들을 생각해보면 이유를 찾을 수 있다. WeakHashMap을 생각해보자. 
여러 구현체들과 마찬가지로 <code>Entry라는 내부 클래스</code>로 key-value쌍을 표현하고 있다. 이 Entry클래스의 메소드들은 바깥클래스(WeakHashMap)의 정보에 직접 접근하지는 않는다. 그러니 당연히 static가 알맞겠다. 그리고 이전 item에서부터 계속 말해왔던 <code>불변</code>에 있어 안전해야하므로 private이 알맞을 것이다. 만약 바깥 클래스의 구성요소로 쓰이는 <code>멤버 클래스</code>가 public이라면 불변이 깨지는 문제가 발생한다. Outer클래스를 다음처럼 조금 수정해보았다.</p>
<pre><code class="language-java">public class Outer {
    private int[] arr;
    private 도우미클래스 helper[];
    private int size;

    public static class 도우미클래스 {
        int a = 1;
        private int b = 2;
        final int c = 3;
        static final int d = 4;
    }

    // 외부 클래스 생성자
    public Outer(int size) {
        arr = new int[size];
        helper = new 도우미클래스[100];
        size = 0;
        System.out.println(&quot;Outer 인스턴스 생성 : &quot; + this);
    }

    public 도우미클래스 push(도우미클래스 help) {
        helper[size++] = help;
        return help;
    }

    public 도우미클래스[] getHelper() {
        return this.helper;
    }
}</code></pre>
<p>public static class인 도우미 클래스를 배열형태로 내부 구성요소로 사용하고 있다.
push메소드를 통해 삽입 과정이 진행된다. public이므로 외부에서 이 도우미클래스를 직접 생성할 수 있을 것이다. 그럼 데이터 조작도 충분히 가능하고, 이러한 행위가 Outer인스턴스에도 영향을 끼치게 되는데 이를 확인해보겠다.</p>
<pre><code class="language-java">        Outer.도우미클래스 도우미클래스 = new Outer.도우미클래스();
        Outer outer = new Outer(100);
        outer.push(도우미클래스);
        System.out.println(outer.getHelper()[0].a);
        도우미클래스.a = -1;
        System.out.println(outer.getHelper()[0].a);</code></pre>
<p>우선 출력결과는 <code>1</code>과 <code>-1</code>이 출력된다. <code>도우미클래스</code>의 인스턴스를 조작한것이 Outer클래스까지 영향일 끼친 것이다. public으로 접근제한자를 지정하였기 때문에 외부에서 충분히 조작할 수 있게된다. 따라서 <code>바깥 클래스의 한 구성요소로 사용하기 위해서는 private 정적 멤버 클래스</code>로 사용해야함을 알 수 있다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9E%90%EB%B0%94%EC%9D%98-%EB%82%B4%EB%B6%80-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94-static-%EC%9C%BC%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%95%98%EC%9E%90">https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9E%90%EB%B0%94%EC%9D%98-%EB%82%B4%EB%B6%80-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94-static-%EC%9C%BC%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%95%98%EC%9E%90</a></li>
</ul>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 18] 상속보다는 컴포지션을 사용하라]]></title>
            <link>https://velog.io/@nuh__d/%EC%83%81%EC%86%8D%EB%B3%B4%EB%8B%A4%EB%8A%94-%EC%BB%B4%ED%8F%AC%EC%A7%80%EC%85%98%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/%EC%83%81%EC%86%8D%EB%B3%B4%EB%8B%A4%EB%8A%94-%EC%BB%B4%ED%8F%AC%EC%A7%80%EC%85%98%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sat, 10 Jun 2023 15:09:06 GMT</pubDate>
            <description><![CDATA[<p>이번 챕터를 공부하며 든 생각은 <code>내가 상속을 올바르게 구현 할 수 있을까?</code> 였다. 읽어보면 알겠지만, 고려할 점이 너무나 많다.</p>
<hr>
<h3 id="상속의-문제점">상속의 문제점</h3>
<h4 id="캡슐화를-깨뜨린다"><a href="https://velog.io/@nuh__d/%EC%BA%A1%EC%8A%90%ED%99%94">캡슐화</a>를 깨뜨린다.</h4>
<p>사실 깨뜨린다 보다는 <code>깨뜨릴수 있다</code> 가 좀 더 어울린다고 생각한다.
캡슐화를 꺠뜨릴 수 있다는 관점에서 보자면, 하위 클래스가 상위클래스에 의존적이게 된다.
이 책에서는 HashSet(InstrumentedHashSet)을 가지고 설명하고 있다.</p>
<p><code>InstrumentedHashSet</code>에서 <code>addAll()</code>과 <code>add()</code>를 재정의하여 사용하고 있다. 두 메소드 모두 addCount라는 변수를 증가시켜주고있다. 문제가 되는 부분은 <code>addAll()</code>의 사용에 있다.</p>
<pre><code class="language-java">        InstrumentedHashSet&lt;String&gt; set = new InstrumentedHashSet&lt;&gt;();
        set.addAll(List.of(&quot;틱&quot;, &quot;택&quot;, &quot;톡&quot;));
        System.out.println(&quot;set.getAddCount() = &quot; + set.getAddCount());</code></pre>
<p>우리는 위 코드를 실행하면 <code>3</code>이 출력될 것이라 예상하지만, 실제로는 <code>6</code>이 나온다.
이 원인은 <code>HashSet의 addAll 메소드가 add 메소드를 사용하여 구현</code>된데에 있다.
현재 <code>set</code>변수는 <code>InstrumendtedHashSet</code>의 인스턴스이다. 따라서 <code>set.addAll</code>메소드를 호출하면 <code>HashSet</code>의 addAll 메소드가 호출된 뒤, 이 메소드에서 <code>HashSet</code>의 add메소드가 아닌 <code>InstrumendtedHashSet</code>의 add 메소드가 호출될 것이다. 이 메소드는 우리가 재정의한 메소드이므로, addCount가 다시 증가되어 결과적으로 <code>6</code>이 나오게 된것이다.<span style="color : gray">
실제 동작과정은 직접 구현해보고 디버깅해보면 쉽게 알 수 있다.</span></p>
<p>상속과 캡슐화에 대해 찾아보면 <code>메소드 오버라이딩이 문제다</code>라고 소개되어 있는 내용을 찾아 볼 수 있다. 위 문제도 <code>메소드 재정의</code>가 원인이었다.
그래서 이 책에서 말해주는 해결법은 <code>컴포지션</code>이다.</p>
<h3 id="컴포지션을-사용하라">컴포지션을 사용하라.</h3>
<p>컴포지션이란 <code>기존 클래스가 새로운 클래스의 구성요소로 쓰이는 것</code>이다.</p>
<p>이제는, InstrumentedSet이라는 <code>Wrapper class</code>를 사용하게 된다. 이 클래스는 ForwardingSet 클래스를 상속받고 있다. 이 <code>ForwardingSet</code>클래스가 바로 컴포지션 방식이 적용된 클래스라고 본다. </p>
<blockquote>
<p>현재 우리 스터디에서, 이 클래스에 대한 issue가 올라왔었는데, <code>왜 굳이 Set을 구현(implements)하고 있나?</code>라는 내용이다.</p>
</blockquote>
<p>나는 이에 대해 다음처럼 생각한다.</p>
<ul>
<li>지금은 Instrumented<code>Set</code>이지만, 만약 예제처럼 계측 기능도 추가한 TreeSet을 사용하고 싶다면 Instrumented<code>TreeSet</code>으로 클래스 이름을 짓고, ForwardingSet은 <code>extends TreeSet</code>을 하는 것이 올바르다고 생각한다.</li>
<li>TreeSet을 상속받아야 모든 TreeSet의 메소드를 사용할 수 있다.</li>
<li>클래스 상속은 하위클래스에서 메소드 재정의를 하지 않아도 컴파일에러가 발생하지 않지만, 인터페이스의 구현은 다르다. 인터페이스에서 정의한 메소드를, 구현체에서 정의하지 않는다면 컴파일 에러가 발생한다.</li>
<li>이제 issue에 대해 생각해보면, 우리의 InstrumentedSet은 Set인터페이스에서 정의해놓은 메소드들은 모두 사용할 수 있는것이 당연하다. 만약 개발자의 실수로 인해 ForwardingSet에서 구현해놓지 않은 메소드가 있다면, 이 메소드는 클라이언트가 사용할 수 없다.</li>
<li>이는 InstrumentedSet의 설계의도에 어긋나게 된다. 이러한 상황을 방지하기 위해 implements키워드를 사용한 것이라고 본다. 만약 개발자가 실수로, 어떤 메소드를 구현하지 않게되면, 컴파일 에러가 발생하기 때문에 실수를 방지 할 수 있기 때문이다.</li>
</ul>
<p>그럼 다시 책으로 돌아와 InstrumentedSet 클래스를 <code>Wrapper class</code>라 하였는데, 이 래퍼클래스는 단점이 거의 없다. 단지 래퍼 클래스가 <code>콜백 프레임워크</code>와 어울리지 않다라는 점만 주의하면 된다. 나는 이 의미가 잘 이해되지 않아 구글링을 한 결과 좀 도움이 될만한 <a href="https://stackoverflow.com/questions/28254116/wrapper-classes-are-not-suited-for-callback-frameworks">글</a>을 보았다.</p>
<pre><code class="language-java">public class NotSuitableCallback {

    public static void main(String[] args) {
        SomeService   service       = new SomeService();
        WrappedObject wrappedObject = new WrappedObject(service);
        Wrapper       wrapper       = new Wrapper(wrappedObject);
        wrapper.doSomething();
    }
}
interface SomethingWithCallback {

    void doSomething();

    void call();

}


class WrappedObject implements SomethingWithCallback {

    private final SomeService service;

    WrappedObject(SomeService service) {
        this.service = service;
    }

    @Override
    public void doSomething() {
        service.performAsync(this);
    }

    @Override
    public void call() {
        System.out.println(&quot;WrappedObject callback!&quot;);
    }
}


class Wrapper implements SomethingWithCallback {

    private final WrappedObject wrappedObject;

    Wrapper(WrappedObject wrappedObject) {
        this.wrappedObject = wrappedObject;
    }

    @Override
    public void doSomething() {
        wrappedObject.doSomething();
    }

    void doSomethingElse() {
        System.out.println(&quot;We can do everything the wrapped object can, and more!&quot;);
    }

    @Override
    public void call() {
        System.out.println(&quot;Wrapper callback!&quot;);
    }
}

final class SomeService {

    void performAsync(SomethingWithCallback callback) {
        new Thread(() -&gt; {
            perform();
            callback.call();
        }).start();
    }

    void perform() {
        System.out.println(&quot;Service is being performed.&quot;);
    }
}
</code></pre>
<p>실행결과로 wrapper의 call()이 호출되어 <code>Wrapper callback</code>이 출력되어야겠지만, 실제로는 그렇지 않다. 이 책에서도 말하는 부분은 <code>내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신(this)의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다.</code>라고 하고 있다. 
예시 코드에서 바로 <code>WrappedObject</code> 클래스를 보면 된다. 이 내부 클래스는 래퍼의 존재를 모른다. 그래서 <code>doSomething</code> 메소드는 원래는 래퍼의 참조를 넘겨야 했겠지만, 실제로는 자기자신(WrappedObject)의 참조를 넘기게되어 결과적으로 내부 객체가 호출되는 문제가 발생한다.</p>
<h3 id="잘못-구현된-자바-플랫폼-라이브러리">잘못 구현된 자바 플랫폼 라이브러리</h3>
<p>대표적으로 stack과 Properties클래스를 소개하고 있다. 그러면서 두 사례 모두 컴포지션을 사용했다면 더 좋았을 것이라 말하고 있는데, 그래서 나는 <code>Stack</code>클래스를 컴포지션을 통해 구현해보았다.</p>
<blockquote>
<h4 id="우선-기존-stack의-문제점에-대해-말해보자">우선 기존 <code>Stack</code>의 문제점에 대해 말해보자.</h4>
<p>Vector를 확장하면 안되었다.
현재 Stack클래스는 FILO의 특성을 가지고 있고, 이는 한 방향으로만 삽입,삭제가 일어나야 한다. 하지만 Vector를 상속받고 있어서 Vector의 기능까지 사용가능하다.
그 중 set 메소드를 생각해보면 index를 지정하여 value를 덮어씌우게 된다. 그럼 FILO의 특성을 어긋나게 되는 문제가 발생한다.</p>
</blockquote>
<pre><code class="language-java">
public class CustomStack&lt;E&gt; {
    private final Vector&lt;E&gt; vector = new Vector&lt;&gt;();

    public CustomStack() {
    }

    public E push(E item) {
        vector.addElement(item);

        return item;
    }

    public synchronized E pop() {
        E obj;
        int len = vector.size();
        obj = peek();
        vector.removeElementAt(len - 1);
        return obj;
    }

    private E peek() {
        int len = vector.size();

        if (len == 0)
            throw new EmptyStackException();
        return vector.elementAt(len - 1);
    }

    public boolean empty() {
        return vector.size() == 0;
    }

    public synchronized int search(Object o) {
        int i = vector.lastIndexOf(o);

        if (i &gt;= 0) {
            return vector.size() - i;
        }
        return -1;
    }

    @Override
    public String toString() {
        return vector.toString();
    }

    private static final long serialVersionUID = 1224463164541339165L;

}</code></pre>
<p>이 처럼 Vector클래스를 내부 구성요소로 사용하는 컴포지션 방식을 사용한다.
그럼 자연스레 CustomStack에서 정의하지 않은 메소드들은 사용할 수 없다. (Vector클래스를 확장하지 않음)</p>
<hr>
<h3 id="결론">결론</h3>
<blockquote>
<p>상속은 is-a관계일 때만 사용하자.
그리고 상위클래스가 확정을 고려해 설계 되지 않았다면 상속받지 말자.(item 19)
상속대신 컴포지션을 사용하자.</p>
</blockquote>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/">https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/</a></li>
<li><a href="https://stackoverflow.com/questions/28254116/wrapper-classes-are-not-suited-for-callback-frameworks">https://stackoverflow.com/questions/28254116/wrapper-classes-are-not-suited-for-callback-frameworks</a></li>
</ul>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 15] 클래스와 멤버의 접근 권한을 최소화하라]]></title>
            <link>https://velog.io/@nuh__d/item-15-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EB%A9%A4%EB%B2%84%EC%9D%98-%EC%A0%91%EA%B7%BC-%EA%B6%8C%ED%95%9C%EC%9D%84-%EC%B5%9C%EC%86%8C%ED%99%94%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-15-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EB%A9%A4%EB%B2%84%EC%9D%98-%EC%A0%91%EA%B7%BC-%EA%B6%8C%ED%95%9C%EC%9D%84-%EC%B5%9C%EC%86%8C%ED%99%94%ED%95%98%EB%9D%BC</guid>
            <pubDate>Fri, 02 Jun 2023 07:37:45 GMT</pubDate>
            <description><![CDATA[<p>이번 item15는 정보 은닉을 올바르게 구현하기 위해 접근 제한자를 제대로 활용하는 방법에 대해 이야기 하고있다. 책에 적힌 모든 글자를 받아들이지는 못했지만, 이는 나중에 추가할 예정이고 이번 게시글에서는 이해한 내용을 바탕으로 작성할 것 이다.</p>
<hr>
<p>접근 제한자를 제대로 활용하는 것이 <code>정보 은닉</code>의 핵심이다.
이에 대한 원칙은 간단하게는 <code>모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.</code>이다. 이 책에서 뿐만 아니라 평상시에도 익히 들어온 말이다.</p>
<p>먼저 접근 제한자 4가지에 대해 알아보자.</p>
<blockquote>
</blockquote>
<h4 id="private">private</h4>
<p>멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.</p>
<h4 id="package-private">package-private</h4>
<p>멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준이다.(단, 인터페이스의 멤버는 기본적으로 public이다.) </p>
<h4 id="protected">protected</h4>
<p>package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.</p>
<h4 id="public">public</h4>
<p>모든 곳에서 접근할 수 있다.</p>
<p>책에서 중요한 말을 해주는데 바로 <code>권한을 풀어주는 일을 자주 하게 된다면 여러분 시스템에서 컴포넌트를 더 분해해야 하는 것은 아닌지 다시 고민해보자.</code> 이다. 이후에 말할 내용과도 관련이 있는 말이기도하다.</p>
<p>이 책의 p.98을 보면 <code>public클래스의 protected멤버는 공개API</code> 라는 말이 나온다. 위에서 말한 protected의 접근 수준에 대한 설명을 보면 외부 패키지에서 접근이 불가능함을 알 수 있다. 공개 API라 함은 public과 같이 어디서나 접근 가능한것 아닌가?</p>
<blockquote>
<p>그런데 왜 <code>공개 API</code>라고 하는걸까?</p>
</blockquote>
<h3 id="왜-public클래스의-protected멤버는-공개-api인걸까">왜 public클래스의 protected멤버는 공개 API인걸까?</h3>
<p>우선 내 생각은 이러하다.
결론부터 말하자면, <code>public class를 상속받고 있는 어떤 class가 있다면, 이 클래스의 인스턴스를 통해 상위클래스의 protected멤버에 접근이 가능하기 때문이다.</code>
그리고, 상위 클래스가 public class이기 때문에 외부 패키지에서도 인스턴스 생성이 가능하다. 따라서 외부 패키지에서 생성된 인스턴스를 통해 protected멤버에 접근이 가능해지므로 이를 보고 <code>공개 API</code>라고 말하지 않았나 싶다. 
간단한 예시 코드를 통해 이를 확인해보자.</p>
<pre><code class="language-java">package item15;

public class item15 {
    public int publicX=1;
    private int privateY=2;
    int defaultZ=3;
    protected int protectW=4;

    protected  void star(){
        System.out.println(toString());
        System.out.println(&quot;star&quot;);
    }

    @Override
    public String toString() {
        return &quot;item15{&quot; +
                &quot;publicX=&quot; + publicX +
                &quot;, privateY=&quot; + privateY +
                &quot;, defaultZ=&quot; + defaultZ +
                &quot;, protectW=&quot; + protectW +
                &#39;}&#39;;
    }

}</code></pre>
<p>public class인 item15가 있을 때, 이를 상속받고있는 클래스를 구현해보자.</p>
<pre><code class="language-java">package item15test;

import item15.item15;

public class main extends item15{

    public static void main(String[] args) {
        item15 item15 = new main();
        item15.star(); // 불가능
        main item151 = (main) item15;
        item151.star();

        main main = new main();
        main.star();
    }
}</code></pre>
<p>여기서 주의해야 할 부분은 main 인스턴스를 생성하는 방법이다. main의 인스턴스를 생성할 때 <code>main main = new main();</code>처럼 인스턴스를 생성하면 당연히 protected인 star()메소드를 호출 할 수 있다. 하지만 <code>item15 item15 = new main();</code> 과 같이 인스턴스를 생성할 때는 불가능하다. <span style ='color:gray'> 왜 안될까? 하고 고민해보았는데, 이 내용은 좀 더 탐구해보고 <code>삽질기</code>에 써보도록 하겠다.</span> </p>
<p>간단하게 생각해보면, <code>main main = new main();</code>으로 생성된 인스턴스는 상속받은 클래스인 main의 인스턴스이다. 위에서 protected 접근 제한자의 정의를 보면 <code>하위 클래스에서 접근 가능</code>하므로 당연하게도 가능할 것 이다. 하지만 <code>item15 item15 = new main();</code>으로 생성된 인스턴스는 그 타입이 item15이다. 결국 이 인스턴스는 하위 클래스인 main의 인스턴스이긴 하지만 item15타입이기 때문에, 묵시적 형변환을 통해 그냥 item15의 인스턴스랑 별 다를바 없을 것 이다. 그러므로 당연하게도 <code>외부 패키지에서 생성된 item15의 인스턴스</code> 이니, protected멤버에 접근이 불가능한 것이다.</p>
<p>다시 <code>왜 공개 API라고 하는걸까?</code>질문으로 돌아가면 내가 생각해본대로 실제로 외부 패키지에서 public클래스를 상속받고 있는 class는 상위 클래스의 인스턴스를 통해 문제없이 protected 멤버에 접근하고있다. 따라서 이와 같은 방법을 사용한다면 외부 패키지라도 충분히 접근 가능하므로 <code>공개 API</code>라고 볼 수 있을 것이다.</p>
<hr>
<p>public 클래스의 인스턴스 필드는 되도록 public이면 안된다.
public이라면 다른  곳에서 아무렇게나 접근하여 해당 필드를 수정할 수 있게된다. 이는 그 필드와 관련된 모든 것은 불변식을 보장할 수 없게 되는 것이다.</p>
<blockquote>
<p>그럼 final로 재할당을 막아버리면 해결이 되지 않을까?</p>
</blockquote>
<p>이전 item에서도 보았듯이 아쉽게도 안된다.. 예를 들자면 public final로 선언된 배열이 있다. 이 배열에 대한 접근자 메서드를 통해 다시 재할당하는것은 안되겠지만, 그 내부는 충분히 변경할 수 있다. 다음 예시 코드를 보자.</p>
<pre><code class="language-java">public class PublicFinalArray {
    public final int array[];

    public PublicFinalArray(int[] array) {
        this.array = array;
    }

    public int[] getArray() {
        return this.array;
    }
}</code></pre>
<p>아래 코드를 통해 PublicFinalArray의 내부 배열 필드를 수정할 수 있는가 테스트해보자.</p>
<pre><code class="language-java">        int [] array = {1,2,3,4,5};
        PublicFinalArray pfArray = new PublicFinalArray(array);
        System.out.println(pfArray);
        int[] newArray = new int[5]     ;
        pfArray.array = newArray; // final키워드로 인해 재할당 불가.
        int[] get = pfArray.getArray();
        get[0] = -1;
        get[1] = -2;
        get[2] = -3;
        get[3] = -4;
        get[4] = -5;
        System.out.println(pfArray);</code></pre>
<p>이를 실행하면 수정되는것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/nuh__d/post/b3756fc3-8223-42c8-ad8a-7dec7d75ed79/image.png" alt="">
이렇게 final로 재할당은 막았지만, 배열의 내부 요소들에 대한 수정은 막을 수 없다. 이 필드를 private final로 바꿔보아도 getArray()와 같은 접근자 메서드를 제공한다면 의미없다. 따라서 이 책에서는 <code>클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.</code>라고 말해주고 있다.</p>
<p>해결책으로 2가지를 알려주고있다.</p>
<h4 id="불변-리스트-사용">불변 리스트 사용</h4>
<pre><code class="language-java">private static final int[] PRIVATE_ARRAYS = { ... };
public static final List&lt;Integer&gt; ARRAYS = Collections.unmodifiableList(Arrays.asList(PRIVATE_ARRAY));</code></pre>
<h4 id="방어적-복사">방어적 복사</h4>
<pre><code class="language-java">private static final int[] PRIVATE_ARRAYS = { ... };
public static final int[] valuse() { return PRIVATE_ARRAYS.clone(); }</code></pre>
<hr>
<p> 이후에는 <code>모듈 시스템</code> 을 말하고 있다. 맨 처음에 받아들이지 못한 글자가 바로 이에대한 것이다. 이 내용은 앞으로 공부해나가면서 차근차근 정리해서 수정해보도록 하겠다.</p>
<h3 id="결론">결론</h3>
<blockquote>
<p>프로그램 요소의 접근성은 가능한 한 최소로 하자.
꼭 필요한 것만 골라 최소한의 public API를 설계하자.
public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드를 가져서는 안된다.
public static final 필드가 참조하는 객체가 불변인지 확인하라.</p>
</blockquote>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 12] toString을 항상 재정의하라]]></title>
            <link>https://velog.io/@nuh__d/item-12-toString%EC%9D%84-%ED%95%AD%EC%83%81-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-12-toString%EC%9D%84-%ED%95%AD%EC%83%81-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sun, 28 May 2023 10:52:54 GMT</pubDate>
            <description><![CDATA[<p>item12를 들어가면 곧 바로 <code>간결하면서 사람이 읽기 쉬운 형태의 유익한 정보</code>를 반환해야 한다. 는 문장을 볼 수 있다. 이는 이후에 보여줄 Enum의 toString()메소드의 문서 주석에 나오는 <code>programmer-freindly</code> 키워드와 동일한 의미를 가진다고 생각한다.
결국 이번 item12을 정리하게 되면 <code>toString()메소드의 재정의 기준이 이 programmer-freindly에 맞춰서 재정의한다.</code> 인 것 같다. 따라서 이번 게시글은 item12을 공부하며 알게된 여러 클래스의 toString()에 대해 조금 적어보겠다.</p>
<hr>
<p>우선 책을 보면 <code>대다수의 컬렉션 구현체는 추상 컬렉션 클래스들의 toString()메소드를 상속해 쓴다</code> 고 되어있다. 그럼 흔히 사용하는 HashMap과, HashSet을 확인 해 보자. Enum의 toString() 메소드도 확인해 볼 것이다.</p>
<h3 id="hashmap">HashMap</h3>
<p>HashMap은 AbstractMap을 상속받고있다. 그리고 실제로도 HashMap클래스에는 toString()이 재정의 되어있지 않고, <code>AbstractMap의 toString()</code>을 그대로 사용하고 있음을 확인 할 수 있다.
그리고 여러 Map의 구현체, WeakHashMap, TreeMap, LinkedHashMap, EnumMap 들도 AbstractMap의 toString()을 사용하고 있다.
AbstractMap의 toString()은 다음과 같다.</p>
<pre><code class="language-java">public String toString() {
        Iterator&lt;Entry&lt;K,V&gt;&gt; i = entrySet().iterator();
        if (! i.hasNext())
            return &quot;{}&quot;;

        StringBuilder sb = new StringBuilder();
        sb.append(&#39;{&#39;);
        for (;;) {
            Entry&lt;K,V&gt; e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            sb.append(key   == this ? &quot;(this Map)&quot; : key);
            sb.append(&#39;=&#39;);
            sb.append(value == this ? &quot;(this Map)&quot; : value);
            if (! i.hasNext())
                return sb.append(&#39;}&#39;).toString();
            sb.append(&#39;,&#39;).append(&#39; &#39;);
        }
    }</code></pre>
<p>toString()을 출력해보면 우리에게 익숙한 형태임을 알 수 있다.
<img src="https://velog.velcdn.com/images/nuh__d/post/be06cd73-a217-4bcb-860e-b6ea5f8697b8/image.png" alt=""></p>
<h3 id="hashset">HashSet</h3>
<p>HashSet은 AbstractSet을 상속받고 있다. 그리고 AbstractSet은 AbstractCollection을 상속받고 있다. 그래서 이 <code>AbstractCollection의 toString()</code>메소드를 사용하게 된다.
AbstractCollection의 toString()메소드는 다음과 같다.</p>
<pre><code class="language-java">public String toString() {
        Iterator&lt;E&gt; it = iterator();
        if (! it.hasNext())
            return &quot;[]&quot;;

        StringBuilder sb = new StringBuilder();
        sb.append(&#39;[&#39;);
        for (;;) {
            E e = it.next();
            sb.append(e == this ? &quot;(this Collection)&quot; : e);
            if (! it.hasNext())
                return sb.append(&#39;]&#39;).toString();
            sb.append(&#39;,&#39;).append(&#39; &#39;);
        }
    }</code></pre>
<p>HashSet, LinkedHashSet, TreeSet등을 확인 해보자.
toString()을 출력해보면 Set도 익숙한 형태로 보여진다.
<img src="https://velog.velcdn.com/images/nuh__d/post/0cca7e5c-299f-40fd-933e-cbaa4b5052b8/image.png" alt=""></p>
<h3 id="enum">Enum</h3>
<p>java.lang패키지에 있는 Enum클래스의 toString()은 다음과 같다.</p>
<pre><code class="language-java">    /**
     * Returns the name of this enum constant, as contained in the
     * declaration.  This method may be overridden, though it typically
     * isn&#39;t necessary or desirable.  An enum type should override this
     * method when a more &quot;programmer-friendly&quot; string form exists.
     *
     * @return the name of this enum constant
     */
    public String toString() {
        return name;
    }</code></pre>
<p>단순히 name을 반환하고 있다. 문서 주석을 보면 잘 재정의 하지는 않지만, <code>programmer-friendly한 string이 있는 경우 재정의된다.</code>고 되어있다. 이번 item12와 연결지어 <code>간결하면서 사람이 읽기 쉬운 형태의 유익한 정보</code>의 형태가 바로 <code>programmer-freindly</code> 정도로 이해할 수 있을 것이다.</p>
<p>여기서 한번 생각해 볼만한 점은 toString()메소드가 단순히 name를 반환하고 있고, Enum클래스에는 name()이라는 메소드도 존재하는데 이 또한 단순히 name을 반환 해주고 있다.</p>
<pre><code class="language-java">/**
     * Returns the name of this enum constant, exactly as declared in its
     * enum declaration.
     *
     * &lt;b&gt;Most programmers should use the {@link #toString} method in
     * preference to this one, as the toString method may return
     * a more user-friendly name.&lt;/b&gt;  This method is designed primarily for
     * use in specialized situations where correctness depends on getting the
     * exact name, which will not vary from release to release.
     *
     * @return the name of this enum constant
     */
    public final String name() {
        return name;
    }</code></pre>
<p>문서주석을 보면 <code>toString()메소드는 좀 더 user-freindly한 name을 반환해줄 것 이고, name()메소드는 정확한 name비교 등에 사용될 것 이다.</code> 라는 것을 볼 수 있다. 다시 말하면 name()메소드는 final키워드가 붙어 더이상 재정의를 할 수 없고, toString()은 재정의가 가능하다. 따라서, 필요에 맞게 toString()을 재정의하여 name을 출력하게끔 사용하게 될 것이다.</p>
<hr>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[삽질기 시리즈]]></title>
            <link>https://velog.io/@nuh__d/%EC%82%BD%EC%A7%88%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88</link>
            <guid>https://velog.io/@nuh__d/%EC%82%BD%EC%A7%88%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88</guid>
            <pubDate>Sun, 28 May 2023 07:53:07 GMT</pubDate>
            <description><![CDATA[<p>여러가지 공부를 하며 삽질한 내용을 적을 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라]]></title>
            <link>https://velog.io/@nuh__d/item-16</link>
            <guid>https://velog.io/@nuh__d/item-16</guid>
            <pubDate>Sun, 28 May 2023 07:45:35 GMT</pubDate>
            <description><![CDATA[<p>평상시 public class에서 아무런 고민없이 당연하게 private필드로 작성하고 있었다.
이번 챕터들을 읽어보며 이러한 습관에 대해 다시 생각해보고 그 이유를 찾아볼수 있다. item 16에서 말하는 내용은 그리 많지 않아 짧은 글이 될 예정이다.</p>
<hr>
<pre><code class="language-java">class Point {
    public double x;
    public double y;
}</code></pre>
<p>이런 클래스는 필드에 직접 접근(public)할 수 있어서 <code>캡슐화</code>의 이점이 깨진다.</p>
<p>따라서 이런 클래스가 있다면, public을 <code>private</code>로 제한하고 접근자(getter)를 추가한다. getter도 문제점을 가지고 있는데, 이러한 경우는 배열같은 필드를 getter를 통해 반환하면, private필드라도 충분히 수정가능하다는 문제가 생긴다.</p>
<p>이러한 getter의 문제는 필드를 그대로 반환 하지 말고, <code>Collections.unmodifiableList</code>같은 것을 사용하여 새로운 객체로 반환해주면 된다.</p>
<p>만약 <code>package-private</code>클래스이거나, <code>private 중첩</code>클래스라면 필드를 노출해도 괜찮다. 어차피 패키지 내부에서만 사용되어, 외부 코드가 영향을 줄 수 없어서 라고 이해했다.</p>
<p>java.awt의 Point클래스가 이렇게 잘못 만들어진 클래스라고 하는데, 실제로 public으로 필드를 열어두고 있다.
<img src="https://velog.velcdn.com/images/nuh__d/post/82f4571f-5dbf-49ec-99e1-aab8d916a492/image.png" alt=""></p>
<p>이렇게 public으로 열어두었을 때의 또 다른 문제점은 API를 변경하지 않고는 표현방식을 바꿀수 없다라는 것이다.
이는 public으로 열어두었을 때 클라이언트 코드가 이러한 field들을 직접 접근하여 사용하고 있었을 것이다. 나중에 이를 알고, private으로 변경하려면 클라이언트 코드들을 모두 에러가 발생할 것이다. </p>
<p>여기서 API를 변경해야한다는 의미를 생각해 볼 수 있다.</p>
<hr>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 9] try-finally보다 try-with-resources를 사용하라.]]></title>
            <link>https://velog.io/@nuh__d/item9</link>
            <guid>https://velog.io/@nuh__d/item9</guid>
            <pubDate>Tue, 23 May 2023 12:06:41 GMT</pubDate>
            <description><![CDATA[<p>앞선 item에서도 말했듯이, 프로그래머가 놓치기 쉬운 작업들로 인해 예측할 수 없는 성능문제로 이어질 수 있다. 결국 이번 item에서도 말하는 것은 <code>프로그래머가 직접 관리하는 것보다 Java가 제공해주는 기능을 적극적으로 사용하자</code> 인 것 같다.</p>
<p>item9 에서 <code>try-finally</code>에 대한 설명과 간단한 예제를 통해 문제점을 설명하고 있다. 그리고 이를 보완하는 방법으로 <code>try-with-resources</code>방법을 알려주고 있다.</p>
<hr>
<h2 id="try-finally">try-finally</h2>
<pre><code class="language-java">    static void firstLineOfFile(String path) throws Exception {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return;
            br.readLine();
        }finally {
            br.close();
        }
    }</code></pre>
<p>일반적으로 위와 같은 방법으로 close를 보장해주었다.
만약, 기기에 물리적 문제가 생긴다면 readLine() 메서드가 예외를 던지고 그리고 close() 메서드도 실패하게 된다. 이때 두번 째 예외가 첫번째 예외를 덮어씌우게돼 스택 추적 내역에 첫번 째 예외에 대한 정보가 남지 않는다.</p>
<p>이러한 문제점을 해결하기 위해 Java 7에서 <code>try-with-resources</code> 구조를 제공한다.</p>
<hr>
<h2 id="try-with-resources">Try-With-Resources</h2>
<p>이 구조를 사용하려면 <code>자원이 AutoCloseable을 구현</code>하고 있어야한다. 예제 코드를 통해 확인해보자.
먼저 AutoCloseable을 구현하고 있지 않은 자원을 try-with-resources 구조로 생성하려고 할 때이다.</p>
<pre><code class="language-java">public class NotAutoClose {
    public void task(){
        System.out.println(&quot;NotAutoClose task~&quot;);
    }
}</code></pre>
<p>이와 같이 단순한 클래스가 있다고 했을 때, try-with-resources 구조는 다음과 같은 것이다.</p>
<pre><code class="language-java">try (NotAutoClose not = new NotAutoClose()) {
            not.task();
        }</code></pre>
<p>위 코드를 작성해보면 알겠지만 빨간 줄이 그어지는 것을 볼 수 있고, 실행시키면 다음의 메시지를 보게 될 것이다. 메시지만 봐도 AutoCloseable을 구현해야 한다는 것을 알 수 있을것이다.
<img src="https://velog.velcdn.com/images/nuh__d/post/1e5c2698-463f-45dd-b3e2-b93865795291/image.png" alt="">
그럼 이번엔 AutoCloseable을 구현한 자원을 생성해보자.</p>
<pre><code class="language-java">public class AutoClose implements AutoCloseable {
    public void task(){
        System.out.println(&quot;some task~&quot;);
    }
    @Override
    public void close() throws Exception {
        System.out.println(&quot;auto close&quot;);
    }
}</code></pre>
<pre><code class="language-java">static void runTry_With_Resources() throws Exception {
        try (AutoClose auto = new AutoClose()) {
            auto.task();
        }
}</code></pre>
<p>위 코드를 실행하면 <code>some task</code> 가 출력된 뒤 <code>auto close</code>가 출력됨을 볼 수 있다. try-with-resources구조를 사용하여 자원을 생성하면, finally로 resource.close() 메소드를 생략할 수 있게되는 것이다.</p>
<p><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html">공식문서</a>를 보면 <code>The close() method of an AutoCloseable object is called automatically when exiting a try-with-resources block for which the object has been declared in the resource specification header.</code> 알 수 있듯이, AutoCloseable 객체의 close()메서드가 자동으로 호출됨을 확인 할 수 있다.
<code>try-with-resource</code>에 대한 좀 더 자세한 내용은 <a href="https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html">이 공식문서</a>를 보면 좋을 것 같다.</p>
<hr>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html">https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html</a></li>
<li><a href="https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html">https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html</a></li>
</ul>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[item 7] 다 쓴 객체 참조를 해제하라]]></title>
            <link>https://velog.io/@nuh__d/item-7-%EB%8B%A4-%EC%93%B4-%EA%B0%9D%EC%B2%B4-%EC%B0%B8%EC%A1%B0%EB%A5%BC-%ED%95%B4%EC%A0%9C%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@nuh__d/item-7-%EB%8B%A4-%EC%93%B4-%EA%B0%9D%EC%B2%B4-%EC%B0%B8%EC%A1%B0%EB%A5%BC-%ED%95%B4%EC%A0%9C%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sun, 14 May 2023 15:33:38 GMT</pubDate>
            <description><![CDATA[<h2 id="stack의-메모리-누수">Stack의 메모리 누수</h2>
<p>책에서 사용한 예제 Stack클래스에서 메모리 누수가 발생하는 부분은 pop() 메소드이다.</p>
<pre><code class="language-java">    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }</code></pre>
<p>위 코드를 사용하면 기능상 문제는 없을 것 같다. 하지만 숨겨져 있는 문제는 <code>메모리 누수</code>이다. 메모리 누수가 발생하는 부분은 <code>return elements[--size];</code>에서 발생할 것이다. 위 pop() 메소드에서는 pop() 하기 전의 가장 위에있던 객체가 여전히 stack클래스의 elements[] 배열에 담겨있으면서 살아있기 때문이다. 따라서 여전히 이 스택이 참조하고 있기 때문에 gc가 회수 하지 않는다. </p>
<blockquote>
<p>가비지 컬렉터(gc)는 객체 참조 하나를 살려두면 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다. 이는 아래에서 설명할 java의 4가지 reference type을 생각해보면 알 수 있을 것이다.</p>
</blockquote>
<p>제대로 구현한 pop메소드는 다음과 같다.</p>
<pre><code class="language-java">    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }</code></pre>
<p><code>다 쓴 참조를 null처리하는 것이다.</code> 이렇게 null 처리하면 메모리 관리와 더불어 다른 이점도 있다. 바로 null처리한 참조의 사용을 막을 수 있다. null처리한 참조를 사용하려 하면 NullPointException을 던지며 종료하게 될 것이다.</p>
<p>그럼 실제로 Java에 구현된 Stack 클래스에도 위와 같이 null 처리하여 메모리 누수에 주의를 기울였는지 확인해보았다. <del>(물론 당연히도 그렇게 구현 해놓았겠지만,,,공부해야지..?!)</del></p>
<pre><code class="language-java">    /**
     * Removes the object at the top of this stack and returns that
     * object as the value of this function.
     *
     * @return  The object at the top of this stack (the last item
     *          of the {@code Vector} object).
     * @throws  EmptyStackException  if this stack is empty.
     */
    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }</code></pre>
<p>우선 pop()메소드 안에서 removeElementAt()을 호출 하고 있는 것으로 보아 여기서 실제 로직이 이루어진다고 예상해 볼 수 있다.</p>
<pre><code class="language-java">    /**
     * Deletes the component at the specified index. Each component in
     * this vector with an index greater or equal to the specified
     * {@code index} is shifted downward to have an index one
     * smaller than the value it had previously. The size of this vector
     * is decreased by {@code 1}.
     *
     * &lt;p&gt;The index must be a value greater than or equal to {@code 0}
     * and less than the current size of the vector.
     *
     * &lt;p&gt;This method is identical in functionality to the {@link #remove(int)}
     * method (which is part of the {@link List} interface).  Note that the
     * {@code remove} method returns the old value that was stored at the
     * specified position.
     *
     * @param      index   the index of the object to remove
     * @throws ArrayIndexOutOfBoundsException if the index is out of range
     *         ({@code index &lt; 0 || index &gt;= size()})
     */
    public synchronized void removeElementAt(int index) {
        if (index &gt;= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + &quot; &gt;= &quot; +
                                                     elementCount);
        }
        else if (index &lt; 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j &gt; 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        modCount++;
        elementCount--;
        elementData[elementCount] = null; /* to let gc do its work */
    }</code></pre>
<p>Stack클래스는 Vector클래스를 상속하고 있다. 위 removeElementAt메소드에서 사용되는 변수에 대해 간략히 말하자면,</p>
<ul>
<li>elementCount : Stack클래스에서 관리하고 있는 객체 수</li>
<li>elementData : Object타입 배열로, 실제 객체들을 관리하고 있는 배열</li>
</ul>
<p>실제로 위 메소드의 마지막 라인을 보면 <code>elementData[elementCount] = null;</code>로 null 처리하여 참조해제 하고 있다. 또한 주석을 보면 gc를 실행하도록 하고 있음을 설명해준다.</p>
<hr>
<p>앞으로 얘기할 내용을 위해서 우선 java에서 사용하는 4가지의 <code>참조 관계</code> 에 대해 알아야 하는데, 간략히 정리해보겠다.</p>
<h2 id="4가지의-참조관계">4가지의 참조관계</h2>
<blockquote>
<h4 id="stronghard-reference">Strong(Hard) Reference</h4>
<p>참조 타입의 dafault타입이다. (우리가 가장 자주 사용하는 참조이지 않을까 싶다.)
어떤 객체에 대한 참조가 살아있는 한, gc는 이를 처리하지 않는다.</p>
</blockquote>
<h4 id="soft-reference">Soft Reference</h4>
<p>gc의 재량에 따라 처리된다.
즉, JVM의 메모리가 부족하다고 판단 될 떄 gc가 이를 처리할 것이다.
(이 떄, 오래된 soft참조 부터 처리한다...)<code>공식문서를 제대로 이해했는지는 잘 모르겠다...혹시 이에 대해 잘 아시는 분이 있으시면 댓글로 알려주시면 감사하겠습니다</code>
따라서, 메모리에 민감한 캐시를 구현하는데에 많이 사용된다.
해당 객체에 강한 참조가 있다면 gc는 이를 처리하지 않는다.</p>
<h4 id="weak-reference">Weak Reference</h4>
<p>strong, soft reference가 없어지면 그때 gc가 처리한다.
WeakReference를 사용한 대표적인 예시는 weakHashMap이다.</p>
<h4 id="phantom-reference">Phantom Reference</h4>
<p>phantom reference는 get메소드로 참조를 찾을 수 없다. 따라서 PhantomReference 클래스를 확장하여 사용하는 것이 일반적이다.
PhantomReference는 finaliztion 로직을 구현할 때 유용하다.</p>
<h2 id="캐시-사용에서의-메모리-누수">캐시 사용에서의 메모리 누수</h2>
<h3 id="hashmap">HashMap&lt;&gt;</h3>
<p>우선 일반적인 HashMap&lt;&gt;을 사용하여 메모리 누수를 확인해보았다. 테스트에 사용된 코드는 다음과 같다. <span style ='color:gray'>예시 코드에서는 gc처리 여부를 isEmpty() 메소드를 통해 확인해보고 있다. 이렇게 테스트해봄으로써 삽질을 하게 되었는데 이는 삽질기 시리즈에 담아보겠다.</span></p>
<pre><code class="language-java">    Object key = new Object();
    Object value = new Object();
    Map&lt;Object, Object&gt; cache = new HashMap&lt;&gt;();
    cache.put(key, value);
    key = null;
    System.gc();
    TimeUnit.SECONDS.sleep(5);
    System.out.println(&quot;cache.isEmpty() = &quot; + cache.isEmpty());</code></pre>
<p> 실행결과는 <code>cache.isEmpty() = false</code> 이다. HashMap에서 어떻게 key, value를 삽입하는지 알아야 한다. <code>putVal</code>메소드에서는  2가지의 방식으로 삽입하고 있다.</p>
<ul>
<li>tab[i] = newNode(hash, key, value, null);</li>
<li>p.next = newNode(hash, key, value, null);</li>
</ul>
<p>두 삽입하는 방식을 보면 <code>key에 의해 value가 gc에 수집될지가 결정되는 형태</code>가 아니기 때문에 key = null; 하더라도 아무 의미가 없을 것 이라는 것을 생각 해 볼 수 있다.</p>
<p>심지어 key = null; 을 하더라도 <code>cache에 있는 key가 null로 참조해제가 되는 것은 아니다.</code> 이 이유는 위에서 말한 Strong Reference도 고려해 생각해 볼만 하다. 현재 key = new Object() 로 생성한 Object 객체를 참조하고 있는 변수들은 <code>main의 key</code>와 <code>cache내부에서 사용하는 key</code>가 있을 것 이다. 그러므로 여전히 cache내부의 key가 이 객체를 Strong Reference타입으로 참조하고 있기 때문에, cache 내부의 key가 null이 되는것은 아니다.</p>
<p>*<em>결국엔, cache내부에는 key, value 객체가 그대로 살아있게된다. *</em></p>
<blockquote>
<p>System.gc(); 이후에         values.stream().forEach(val -&gt; System.out.println(val)); 로 출력해보면 진짜 value객체가 출력됨을 볼 수 있다.</p>
</blockquote>
<h3 id="weakhashmap">WeakHashMap&lt;&gt;</h3>
<p>그럼 WeakHashMap에서는 gc가 객체를 수집할지, 그리고 어떻게 메모리 누수를 방지하는지 알아보자.
우선 테스트에 사용될 코드는 다음과 같다.</p>
<pre><code class="language-java">    Object key2 = new Object();
    Object value2 = new Object();
    Map&lt;Object, Object&gt; cache2 = new WeakHashMap&lt;&gt;();
    cache2.put(key2, value2);
    key2 = null;
    System.gc();
    TimeUnit.SECONDS.sleep(5);
    System.out.println(&quot;cache2.isEmpty() = &quot; + cache2.isEmpty());</code></pre>
<p>실행결과는 <code>cache2.isEmpty() = true</code> 이다. </p>
<blockquote>
<p>isEmpty() 메소드를 확인해보면 내부에서 expungeStaleEntries() 메소드를 호출하고 있는데 이는 위에서 말한 <code>삽질기</code> 에서 정리해보도록 하겠다.</p>
</blockquote>
<p>HashMap은 key, value를 Node형태로 사용했다면 WeakHashMap은 Entry를 사용하고있다. 이 Entry클래스가 WeakReference를 상속받고 있어, gc가 객체를 수집할 수 있게 된다. 그럼 Entry클래스를  간단하게 확인해보자.</p>
<pre><code class="language-java">    /**
     * The entries in this hash table extend WeakReference, using its main ref
     * field as the key.
     */
    private static class Entry&lt;K,V&gt; extends WeakReference&lt;Object&gt; implements Map.Entry&lt;K,V&gt; {
        V value;
        final int hash;
        Entry&lt;K,V&gt; next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue&lt;Object&gt; queue,
              int hash, Entry&lt;K,V&gt; next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
        ```중략```
    }</code></pre>
<p>Entry의 생성자를 보면 <code>super(key, queue)</code>로 WeakReference생성자에 key를 넘기는 것을 볼 수 있다. WeakReference에 대한 좀 더 자세한 내용은 <code>삽질기</code>에서 작성하겠다. 이렇게 WeaKHashMap에서 key는 WeakReference타입으로 되어있다.</p>
<p>이제 처음에 말한 java에서 사용하는 4가지 참조관계 중, <code>Weak Reference</code>을 알아야 했던 이유가 나온다. Weak Reference에 다시 말하자면, Strong, Soft Reference가 사라지면 gc가 수집한다고 하였는데, 이는 곧 <code>Weak Reference만 남아있을 때 gc가 수집</code>하는 것이다. </p>
<p>여기서 key는 이제 main메소드에서의 key는 Strong Reference참조이고, WeakHashMap내에서는 Weak Reference참조관계를 가진다.
그러면 main메소드에서 <code>key = null;</code> 로 인해 Strong Reference참조가 해제 되므로 WeakHashMap내부의 Weak Reference타입 key만 남게된다. 이는 곧 gc가 수집하여 WeakHashMap에서 key가 삭제된다.</p>
<blockquote>
<h4 id="그럼-key는-gc가-처리할-수-있다는-것은-알았는데-isempty의-반환값이-왜-true일까">그럼 key는 gc가 처리할 수 있다는 것은 알았는데, isEmpty()의 반환값이 왜 true일까?</h4>
<p>WeakHashMap에서는 key = null; 을 하게되면 key뿐만 아니라 Entry까지 삭제된다. Entry까지 삭제되는 이유에 대한 설명은 공식문서에서 찾아 볼 수 있었다.
<code>Because the garbage collector may discard keys at any time, a WeakHashMap may behave as though an unknown thread is silently removing entries.</code> 즉,알려지지 않은 쓰레드가 그냥 entries를 지운다는 것인데, 이에 대해 알고 계신 분이 있으시면 댓글달아주시면 감사드립니다.</p>
</blockquote>
<hr>
<h2 id="리스너--콜백-사용에서의-메모리-누수">리스너 | 콜백 사용에서의 메모리 누수</h2>
<p>클라이언트가 콜백을 등록만하고 명확히 해지 하지 않는다면, 콜백은 계속 쌓여간다.
이럴때 WeakHashMap을 사용하면 쉽게 관리 할 수 있다.
이또한 간단한 예제를 통해 이해해보는게 쉬울 것 같다.</p>
<pre><code class="language-java">interface Listener {
    void event();
}
public class Callback {
    public static void main(String[] args) {
        ApiUser user = new ApiUser();
        user.registerListener(); // 리스너 등록
        user= null; // ApiUser객체 삭제 
        System.gc(); // gc처리
        API.fireListeners();

    }
}
class ApiUser{
    Listener listener = null;
    public void registerListener() {
        listener = new Listener() {
            @Override
            public void event() {
                System.out.println(&quot;Some task was working~~&quot;);
            }
        };
        API.registerListener(listener);
    }
}

class API {
    static WeakHashMap&lt;Listener, Object&gt; listeners = new WeakHashMap();

    static void registerListener(Listener listener) {
        listeners.put(listener, null);
    }
    static void fireListeners() {
        for (Listener listener : listeners.keySet()) {
            if (listener != null) {
                listener.event();

            }
        }
    }
}
</code></pre>
<p>위 코드대로 실행시키게 되면 여태까지 설명했듯이 WeakReference로 감싸진 key는 gc가 처리할 것 이므로 main메소드에서 fireListeners()호출 시 listeners라는 WeakHashMap에는 key가 존재하지 않기 때문에 아무런 출력문이 확인할 수 없을 것이다. 이에 대한 설명은 바로 위 WeakHashMap에 대해 설명하면서 설명했으므로 넘어가겠다. 한번 실행시켜보면서 다시 확인해보면 좋을 것 같다.</p>
<hr>
<h2 id="결론">결론</h2>
<p>이번 item의 주제는 <code>다 쓴 객체 참조를 해제하라</code> 이다. 결국 메모리를 관리해야한다 인데, 이 책에서 언급한 메모리 누수의 원인은 다음과 같다.</p>
<ul>
<li>캐시의 사용</li>
<li>리스너 혹은 콜백의 사용<blockquote>
<p>자기 메모리를 직접 관리하는 클래스 라면 프로그래머는 항시 메모리 누수에 주의해야 한다.</p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://mongsil1025.github.io/book/effective-java/item7/">https://mongsil1025.github.io/book/effective-java/item7/</a></li>
<li><a href="https://stackoverflow.com/questions/18406703/when-will-a-string-be-garbage-collected-in-java">https://stackoverflow.com/questions/18406703/when-will-a-string-be-garbage-collected-in-java</a></li>
<li><a href="https://www.geeksforgeeks.org/types-references-java/">https://www.geeksforgeeks.org/types-references-java/</a></li>
<li><a href="https://www.baeldung.com/java-reference-types">https://www.baeldung.com/java-reference-types</a></li>
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/ref/Reference.html">https://docs.oracle.com/javase/8/docs/api/java/lang/ref/Reference.html</a></li>
<li><a href="https://www.baeldung.com/java-soft-references">https://www.baeldung.com/java-soft-references</a></li>
<li><a href="https://www.baeldung.com/java-weak-reference">https://www.baeldung.com/java-weak-reference</a></li>
<li><a href="https://www.baeldung.com/java-phantom-reference">https://www.baeldung.com/java-phantom-reference</a></li>
<li><a href="https://www.youtube.com/watch?v=JqV2BmNDqe8">https://www.youtube.com/watch?v=JqV2BmNDqe8</a></li>
</ul>
<p>effective-java스터디에서 공유하고 있는 전체 item에 대한 <a href="https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94">정리글</a></p>
]]></description>
        </item>
    </channel>
</rss>