<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Ruin.log</title>
        <link>https://velog.io/</link>
        <description>어디까지 올라갈지 궁금한 하루</description>
        <lastBuildDate>Sun, 03 Dec 2023 12:39:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Ruin.log</title>
            <url>https://velog.velcdn.com/images/french_ruin/profile/3769e8ec-e229-4f84-940d-f811d1c3e602/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Ruin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/french_ruin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[KOTLIN] JWT때문에 애좀먹었습니다..😂]]></title>
            <link>https://velog.io/@french_ruin/KOTLIN-JWT%EB%95%8C%EB%AC%B8%EC%97%90-%EC%95%A0%EC%A2%80%EB%A8%B9%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@french_ruin/KOTLIN-JWT%EB%95%8C%EB%AC%B8%EC%97%90-%EC%95%A0%EC%A2%80%EB%A8%B9%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Sun, 03 Dec 2023 12:39:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/french_ruin/post/2ee34b3c-8188-4243-8265-463fc9d1f911/image.png" alt="허허허"></p>
<hr>
<h2 id="사이드-프로젝트로-jwt를-도입하고자했다">사이드 프로젝트로 JWT를 도입하고자했다..</h2>
<p>이것이 제일 큰 문제였던가..?</p>
<p>Kotlin SpringBoot로 많은 서비스를 포함한 LuckyFind라는 프로젝트를 진행하고있습니다.
뭐 이것저것 추가하면서 공부하고있었는데..</p>
<p>여기서 Spring Security 라이브러리를 추가하고 FormLogin 방식으로 진행하고있었는데 문득 JWT가 눈에 띄는겁니다? 그래서 어? 나도 한번 써봐야겠다!! 해서 시작을했는데!!</p>
<p>약 8시간동안 헤매기만하고 내가 뭘한건지 싶네요..</p>
<blockquote>
<p>결론적으로는 해결아닌 해결을 했지만, 대부분 그렇듯 코드 몇줄로 해결될 일을 질질 끌었다는 것이죠??</p>
</blockquote>
<p>덕분에 SpringSecurity관련 공부를 더 깊게 해야겠다는 생각이 강하게 들었습니다 ㅎㅎ</p>
<p>일단 소스로 제가 어떤것 때문에 헤맸는지 알려드릴게요</p>
<hr>
<h3 id="securityconfig">SecurityConfig</h3>
<pre><code class="language-kotlin">@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val authenticationConfiguration: AuthenticationConfiguration,
    private val jwtUtils: JWTUtils,
    private val userRepository: UserRepository,
) {

    // Cors Filter Custom
    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val config = CorsConfiguration()
        config.allowCredentials = true
        config.allowedOrigins = listOf(&quot;*&quot;)
        config.allowedMethods = listOf(&quot;*&quot;)
        config.allowedHeaders = listOf(&quot;*&quot;)
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration(&quot;/**&quot;, config)
        return source
    }

    // Filter Chain
    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
//        http.cors {
//            corsConfigurationSource()
//        }
        http.invoke {
            csrf {
                disable()
            }
            headers {
                frameOptions {
                    sameOrigin = true
                }
            }
            formLogin{
                disable()
            }
            authorizeHttpRequests {
                authorize(&quot;/swagger-ui/**&quot;, permitAll)
                authorize(PathRequest.toH2Console(), permitAll)
                authorize(&quot;/h2-console/**&quot;, permitAll)
                authorize(&quot;/assets/**&quot;, permitAll)
                authorize(&quot;/login&quot;, permitAll)
                authorize(&quot;/api/v1/**&quot;, permitAll)
                authorize(&quot;/api/v1/user/signUp&quot;, permitAll)
                authorize(&quot;/register&quot;, permitAll)
                authorize(anyRequest, authenticated)
            }
            sessionManagement {
                // 세션을 사용하지 않고 JWT를 사용할 예정
                sessionCreationPolicy = SessionCreationPolicy.STATELESS
            }
        }
        http.addFilterAt(
            JwtAuthenticationFilter(authenticationManager(), jwtUtils), &lt;&lt; 이부분..
            UsernamePasswordAuthenticationFilter::class.java
        )
        http.addFilterAt(
            JwtAuthorizationFilter(userRepository, jwtUtils), &lt;&lt; 이부분..
            BasicAuthenticationFilter::class.java
        )
        // JWT token
        return http.build()!!
    }

    @Bean
    fun authenticationManager(): AuthenticationManager =
        authenticationConfiguration.authenticationManager

    // Password Encoder
    @Bean
    fun passwordEncoder() = BCryptPasswordEncoder()
}</code></pre>
<p>먼저 부연설명을 하자면!
 JWT 토큰을 발행하기 위해서 통행증개념의 Authentication 및 Authorization이 필요했습니다.
 그래서 <code>UsernamePasswordAuthenticationToken</code> 을 대신할 <code>JwtAuthenticationFilter</code>와, 
 이 사용자가 유효한 토큰을 지니고 있는지 확인하는 <code>JwtAuthorizationFilter</code>를 생성했습니다.</p>
<h3 id="jwtauthenticationfilter">JwtAuthenticationFilter</h3>
<pre><code class="language-kotlin"> class JwtAuthenticationFilter(
    private val authenticationManager: AuthenticationManager,
    private val jwtUtils: JWTUtils,
) : UsernamePasswordAuthenticationFilter() {


    // UsernamePasswordAuthenticationFilter getAuthenticationManager is null problem solve.
    override fun getAuthenticationManager(): AuthenticationManager =
        authenticationManager


    override fun attemptAuthentication(request: HttpServletRequest?, response: HttpServletResponse?): Authentication {

        // username &amp;&amp; password parameter
        println(request?.getParameter(this.usernameParameter))
        println(request?.getParameter(this.passwordParameter))

        val authenticationToken =
            UsernamePasswordAuthenticationToken(
                request?.getParameter(this.usernameParameter),
                request?.getParameter(this.passwordParameter)
            )
        println(authenticationToken)                // authentication Token
        println(authenticationToken.principal)      // username
        println(authenticationToken.credentials)    // password

        println(&quot;로그인 시도 ==========================&quot;)
        val authentication: Authentication = authenticationManager.authenticate(authenticationToken) // 로그인 시도
        println(authentication.isAuthenticated)
        println(&quot;로그인 시도 종료 ==========================&quot;)
        // Authentication return
        return authentication
    }

    // 인증성공
    override fun successfulAuthentication(
        request: HttpServletRequest?,
        response: HttpServletResponse?,
        chain: FilterChain?,
        authResult: Authentication?
    ) {
        println(&quot;인증 완료 ==============&quot;)
        val userResponse: UserResponse = with((authResult?.principal as User)) {
            UserResponse(
                username = this.username,
                password = this.password,
                authorities = this.authorities!!,
                userId = this.userId!!,
                enabled = this.enabled
            )
        }
        println(userResponse) // 인증정보
        val token = jwtUtils.createToken(userResponse)
        println(token)

        SecurityContextHolder.getContext().authentication = authResult
        // 토큰 헤더에 추가
        response?.addHeader(&quot;Authorization&quot;, &quot;Bearer $token&quot;)
//        response?.sendRedirect(&quot;/notice&quot;)
        super.successfulAuthentication(request, response, chain, authResult)
    }

    override fun unsuccessfulAuthentication(
        request: HttpServletRequest?,
        response: HttpServletResponse?,
        failed: AuthenticationException?
    ) {
        println(&quot;실패??&quot;)
        super.unsuccessfulAuthentication(request, response, failed)
    }

}</code></pre>
<blockquote>
<p>이곳의 코드에서도 많이 헤매었지만, 별거 아니었습니다.</p>
</blockquote>
<ul>
<li><p>단순하게, 사용자가 로그인을 시도하면, 해당 아이디와 패스워드가 동일한지 체크하고, 그것들을 기반으로 로그인 처리와 토큰을 발급해서 건네주는 방식이었습니다.</p>
</li>
<li><p>세션방식은 사용하지 않고, 헤더에 토큰을 넣어 건네주었습니다.</p>
</li>
<li><p>print를 찍어보면서 제대로 아이디와 패스워드가 넘어가고, 토큰까지 발행되는것을 확인했습니다.</p>
<p>그런데..</p>
<p>로그인을 통해서 토큰이 발급되었는데 왠지 모르게 권한이없다면서 밀어내는것입니다..</p>
<p>도저히 생각이 안떠올라서 몇시간동안 코드 수정해가면서, Filter가 문제인가, 아니면 AuthenticationManager 주입을 잘못했나 싶기도하고 머리를 좀 쥐어짰습니다.. 😂😂😂</p>
</li>
</ul>
<p>무한히 검색하면서 찾아본 결과로는 적절한 방법인지는 아직 모르겠으나
<code>BasisAuthenticationFilter</code> 처리를 하는것이였습니다.</p>
<blockquote>
<p>제 견해로는.. 모든 요청에 BasicAuthenticationFilter를 거치게 되는데, 
기존에는 Session 방식으로 로그인을 하여서 세션에 사용자의 정보와 권한이 담겨있었지만, 
JWT를 도입하면서 Session을 버리고 단순히 헤더에 넣어서 유저의 정보와 권한을 건네주기때문에 이사람이 적절한 권한을 가지고있는지, 또 올바르게 접근을 했는지 판단을 하지 못한것 같습니다.</p>
</blockquote>
<p>그래서 BasisAuthenticationFilter를 Custom하게 생성하여 SecurityConfig에 주입해주었습니다.</p>
<hr>
<h3 id="basicauthenticationfilter">BasicAuthenticationFilter</h3>
<pre><code class="language-kotlin">class JwtAuthorizationFilter(
    private val userRepository: UserRepository,
    private val jwtUtils: JWTUtils,
) : OncePerRequestFilter() {

    // 요청 제외 url
    private val excludeUrls =
        listOf(
            &quot;/assets&quot;,
            &quot;/login&quot;,
            &quot;/templates&quot;,
            &quot;/scripts&quot;,
            &quot;/swagger-ui/&quot;,
//            &quot;/v3/api-docs&quot;
        )

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        println(request.servletPath) // 요청 path

//        val header = request.getHeader(&quot;Authorization&quot;)
//
//        if (header == null || !header.startsWith(&quot;Bearer &quot;)) {
//            filterChain.doFilter(request, response)
//            return
//        }

        val user = userRepository.findByUsername(&quot;admin&quot;)
        val authentication: Authentication = UsernamePasswordAuthenticationToken(
            user, user!!.password, user.authorities
        )
        SecurityContextHolder.getContext().authentication = authentication

        filterChain.doFilter(request, response);
    }

    override fun shouldNotFilter(request: HttpServletRequest): Boolean {
        return excludeUrls.stream().anyMatch {
            request.servletPath.contains(it)
        }
    }
}</code></pre>
<blockquote>
<p>아니 BasicAuthenticationFilter라면서 왜 OncePerRequestFilter 에요??</p>
</blockquote>
<p>이게 BasicAuthenticationFilter의 경우에는 모든 요청에 대해서 필터를 거치지만,
<code>OncePerRequestFilter</code> 의 경우에는 필터내부 로직을 처리할지 말지를 결정할 <code>shouldNotFilter</code> 를 가지고있기때문에 보다 부하를 줄일 수 있다는 생각이 들었습니다.</p>
<p>그래서 해당 필터로 선택하였고, 아직 내부 로직은 개선 중입니다 😊😊</p>
<p>덕분에 수많은 블로그와 문서를 읽으면서 필터별 역할도 아주 달달달 기억이 날정도인 것 같아요 ㅎㅎ</p>
<hr>
<p>이번 글은 제가 오늘 하루종일 삽질하면서 느끼고 뭔가 훈장처럼 남기고 싶어서 적는 글이고
프로젝트가 마무리될쯤에 제가 알고있는내용들을 다 정리하면서 다시 포스팅할 예정입니다.</p>
<p>혹시 제 프로젝트 진행상황이 궁금하신분들을 위해 깃헙소스는 아래 링크에 추가하겠습니다.</p>
<p><a href="https://github.com/FrenchRuin/LuckyFind">https://github.com/FrenchRuin/LuckyFind</a></p>
<p>아 그리고, 소스를 혹시 보신다면 개선해야될점들을 찾아주신다면 매우 감사드리겠습니다 👍👍👍👍</p>
<p>감사합니다 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 사이드프로젝트를 진행하면서 느낀점]]></title>
            <link>https://velog.io/@french_ruin/Kotlin-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%A7%84%ED%96%89%ED%95%98%EB%A9%B4%EC%84%9C-%EB%8A%90%EB%82%80%EC%A0%90</link>
            <guid>https://velog.io/@french_ruin/Kotlin-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%A7%84%ED%96%89%ED%95%98%EB%A9%B4%EC%84%9C-%EB%8A%90%EB%82%80%EC%A0%90</guid>
            <pubDate>Sun, 26 Nov 2023 10:43:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/french_ruin/post/08f5898c-6f29-4c69-af88-390cb95edf73/image.png" alt=""></p>
<hr>
<h3 id="호기심에-사이드프로젝트를-진행하고-있습니다">호기심에 사이드프로젝트를 진행하고 있습니다.</h3>
<p>LuckyFind라는 사이드 프로젝트를 진행하고 있습니다.</p>
<blockquote>
<p>여기 깃헙 링크! <a href="https://github.com/FrenchRuin/LuckyFind">Lucky Find 개인 프로젝트</a> </p>
</blockquote>
<p>주제는 <code>개발자들의 사이드 프로젝트 팀원 모집</code>서비스 입니다.
이런 주제를 고른 이유는 제 경험에 빗대어 볼 수 있을 것 같습니다.</p>
<ol>
<li>재직중에 시간이 알맞는 팀원을 구하기 힘들었다.</li>
<li>나의 실력이 과연 팀프로젝트를 진행할 때 문제가 되지 않을까?</li>
</ol>
<p>위의 두가지가 저에게 크게 와닿았던것 같습니다.</p>
<hr>
<h3 id="진행하면서-무엇을-느꼈는가">진행하면서 무엇을 느꼈는가?</h3>
<p>원래는 JAVA로 프로젝트를 진행할까 했었지만, 어리석게도 프로젝트를 해야지..해야지 하고 시작했다가 도중에 날린 소스들이 여럿있습니다.
2~3개의 프로젝트들은 그저 깃헙 Repo 어딘가 깊숙하게 숨어있죠 ㅎㅎ</p>
<p>많은 기업들이 Java 에서 Kotlin으로 변환하려 시도중인 점도 있고, Java와 호환되면서 간결한 문법들이 존재하는 Kotlin에 매력을 느꼈네요 😊</p>
<p>그래서 Kotlin과 Spring Boot로 사이드 프로젝트를 진행하게되었습니다.</p>
<h4 id="자료의-부족">자료의 부족??</h4>
<p>확실히 Java에 관련된 정보보다는 현저히 적다는 것을 느꼈습니다.
어느정도 Java에 대해서 잘 알고 있지만, Kotlin에서는 어떻게 풀어 나갔나 궁금하기도 해서 검색을 해보면 제가 원하는 정보들은 그렇게 많지 않더군요!</p>
<p>그래서 공식문서들을 계속계속 읽었던것같습니다.
Spring Boot 버전에 따라서 호환되는점들도 많이 바뀌다보니 까다로웠습니다 ㅎㅎ</p>
<p>그래도 제일 많이 참고했던 블로그는 <a href="https://colabear754.tistory.com/">개발하는 곰돌이</a> &lt;&lt; 이분의 블로그가 제일 최신이면서 아주 잘 설명해주셨습니다. 너무 감사합니다 👍👍</p>
<h4 id="npe가-없어도-너무-없네요-👏">NPE가 없어도 너무 없네요 👏</h4>
<p>평소였으면 NPE가 떨어졌을 Java 에서 확실히 Kotlin에서는 NPE 처리가 간결하다보니 너무너무너무 편했습니다.
가끔 깜짝깜짝 놀랄때가 있습니다.</p>
<p>물론 제 실력도 문제겠지만, Java에서는 예기치못한곳에서 NPE가 떨어지잖아요??
근데 Kotlin에서는 떨어지는 수가 현저히 적었습니다.. 너무 감탄스럽습니다. 괜히 Kotlin을 칭찬하는게 아니였더군요!!!!</p>
<h4 id="spring-security의-변화">Spring Security의 변화</h4>
<p>제가 Spring Security를 사용한지는 약 1년전 정도 된거같은데, 이제 다시 사용하려고 하니, 문법이나 Depreceated된것들이 좀 있더군요.</p>
<p>override했던 메소드들은 없어지고, 직접 Bean으로 등록해줘야하고,
Bean으로 등록한 SecurityFilterChain은 DML문법으로 바뀌고..
이것저것 메꾸느라 아주 시간을 많이 보냈습니다 ^^ </p>
<hr>
<h3 id="개발자로서-느낀점">개발자로서 느낀점?</h3>
<h4 id="백엔드-프론트엔드-구분-필요없다">백엔드 프론트엔드 구분? 필요없다</h4>
<p>개인프로젝트여서 그런지 몰라도, 프론트엔드쪽에 관심이 가더군요?
LuckyFind라는 프로젝트에서는 BootStrap을 사용해서 최대한 Kotlin에 많은 시간을 쏟고 싶었는데, 의외로 BootStrap인데도 불구하고 손이 많이 가더라구요.</p>
<p>그래서 <code>과연 Vue나 React였으면 어땠을까?</code> 라는 생각이 들었습니다.
특히, Vue에 호기심이 생겼습니다. 양방향 바인딩, MVVC패턴? 정도만 알고있는데 맞는지도 모르겠네요 😂</p>
<p>이번 사이드프로젝트가 끝나면 Vue와 Kotlin을 사용해서 프로젝트를 진행해 보고 싶습니다.</p>
<h4 id="너무-소홀했다">너무 소홀했다.</h4>
<p>블로그에 글을 올리고 싶었는데, 아무래도 회사일정이 빠듯하다보니 나의 시간도 없이 매일 8~9시에 퇴근하는 하루를 보내다 보니 아무런 소식을 올리지도 못했습니다..</p>
<p><img src="https://velog.velcdn.com/images/french_ruin/post/db922a66-8520-4354-a780-ed2df2adbe67/image.png" alt=""></p>
<p>이번에 포트폴리오도 만들어보고, 이력서도 수정해보고, 사이드 프로젝트를 진행하면서 글을 꾸준히 올릴예정입니다. </p>
<h4 id="이직-예정자">이직 예정자?</h4>
<p>그리고 이직할겁니다 이직!! 😉😉
세상을 너무 좁게본다는 느낌이 들었습니다. 
얼마전에 제로베이스에서 주관하는 <code>백엔드 이직</code> 관련 강의를 들었습니다.
멘토분 한분이 나오셔서 설명해주시는데, 너무 유익하기도 했고, 얼른 이런 환경에서 벗어나자는 생각이 강하게 들었습니다.</p>
<p>아직 나이도 어리기도 하고, 현재 있는곳에서의 경력은 별로 도움이 되지 않을것같다는 생각이 들어서 도전을 많이 해볼겁니다. </p>
<p><strong>세상을 넓게 봐야죠</strong></p>
<hr>
<h3 id="결론">결론</h3>
<p>너무 두서없이 글을 쓴것같네요 😎</p>
<p>한번쯤 제 깃헙이나 개인프로젝트에 오셔서 피드백주시면 감사하겠습니다.</p>
<p>더 좋은 모습으로 위에서 봤으면 좋겠습니다!! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Diary] 비전공자 2023년 9월 회고]]></title>
            <link>https://velog.io/@french_ruin/Diary-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-2023%EB%85%84-9%EC%9B%94-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@french_ruin/Diary-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-2023%EB%85%84-9%EC%9B%94-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 24 Sep 2023 11:33:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/french_ruin/post/8e7dd0b8-40c7-4a83-9e3f-710fbdf7a319/image.png" alt=""></p>
<hr>
<h1 id="2023년-9월-어느-개발자의-회고">2023년 9월 어느 개발자의 회고...</h1>
<h3 id="q1--어떻게-지내셨나요">Q1 . 어떻게 지내셨나요?</h3>
<p>이번달은 아주 재밌게 흘러간 한달이었습니다.
예비군도 갔다오고, 감기도 걸리고, 곧 추석도 다가오니 워킹데이가 상당히 적은 한달이네요 👍</p>
<h3 id="q2--어떤-일들을-하고-지내셨나요">Q2 . 어떤 일들을 하고 지내셨나요?</h3>
<p>이번달들어서 이직 준비를 실행하고있습니다. 
요즘 개발자들의 취업시장은 겨울이라는 소식을 많이 접했어요. 그래서 겁이 나기도 하고, 내 위치에 상당히 감사해야하는점도 있는것같고, 매우 복합적인 생각이 들었습니다.</p>
<p>그래도 이직은 해야겠다는 생각이 들었습니다.
왜냐하면 현재 있는 회사에서 혼자 일한다는 느낌이 강하게 들었습니다
상무님과 둘이서 한 파트를 맡고 개발을 진행하는데, 상무님의 자리비움이 지속되면서, 일처리에 있어서 속도도 안나오고 미결된 사항만 늘어나고 답답한 상황이 지속되고 있어요.. </p>
<blockquote>
<p>제일 중요한점은 제가 성장할 수 있는 환경이 아니라는 생각이 들었어요.</p>
</blockquote>
<p>앞으로 개발자를 계속 할 사람인데, 성장없이 허송세월을 보낼 수는 없잖아요??
주니어 개발자라 배울것이 많고, 이끌어주는 사람이 있어야 한다고 생각을 해요.</p>
<h3 id="q3--그럼-어떻게-준비를-하고-계세요">Q3 . 그럼 어떻게 준비를 하고 계세요?</h3>
<p>현재 있는 회사가 SI이다보니, 업무특성상 코드리뷰보다 생산성리뷰가 더 강한 느낌입니다. 하루에 하나의 산출물을 가져간다는것이 이번 프로젝트의 계획입니다..
뼈가 저리죠..😂</p>
<p>그래서 앞으로의 미래를 위해서 서비스 회사로 중고신입으로 이직할 계획입니다.</p>
<blockquote>
<p>먼 미래에 저만의 서비스를 개발할 능력이 필요하다고 생각합니다.</p>
</blockquote>
<p>그래서 잡플래닛과 원티드 등등 사람인을 제외하고 이력서작성과 노션 포트폴리오를 작성하고있어요.
알고리즘 문제도 1일 1문제하고, Git 역시 1일 1커밋을 하고있습니다.</p>
<p>물론 남들이 말하는 네카라쿠배당토? 는 거들떠 보지 않습니다. 아.직.은
비전공자이다보니, 전공자보다 실력은 현저히 떨어지고, 이제까지 해온 일들은 완전 동떨어진 일이였으니까요 ^^</p>
<h3 id="q4--그나저나-개발-공부는-하고계세요">Q4 . 그나저나 개발 공부는 하고계세요??</h3>
<p>아 물론 하고있습니다.
최근에 Kotlin에 대해 관심이 크게 갔습니다. 그래서 홧김에 Kotlin관련 강의를 구매해서 듣고 있습니다.
Java와는 다른매력에 매우  끌렸습니다. 그리고 Nest.js 관련해서도 관심이 가더군요.</p>
<p>Kotlin 개인 프로젝트도 동행하면서, 프로젝트가 얼추 마무리되면 Nest.js도 배워서 적용을 해볼까합니다.</p>
<p>일단, 이직을 위해서 Java공부를 더 열심히해야겠죠??ㅎㅎ</p>
<blockquote>
<p>CS 관련 지식이 가장 중요하더군요.</p>
</blockquote>
<p>신입의 경우, 판별 기준이 전공여부, CS지식, 개인 또는 팀프로젝트 이정도 밖에 없는것같더군요..
그래서 CS지식도 꾸준히 공부하고있습니다.
아무래도 전공자의 CS지식을 따라갈려면 엄청 노력해야하니까요 ✌</p>
<h3 id="q5--언제쯤-성과가-나올까요">Q5 . 언제쯤 성과가 나올까요?</h3>
<p>으음 솔직히 잘 모르겠습니다.
현재 취업시장이 어렵기도 하고, 신입을 뽑는 경우가 별로 없으니까요..</p>
<p>그래도 이번 프로젝트가 끝나기전에는 성과를 보여줄 예정입니다.
빨리 쓸데없는 시간 낭비 없이 방향성을 잡고 성장하고싶어요..</p>
<p><img src="https://velog.velcdn.com/images/french_ruin/post/9ca4b09c-920a-4ce3-afef-16a9ad5574c6/image.png" alt=""></p>
<blockquote>
<p>솔직히 핑계일수도 있을것같네요..
누군가는 진득하게 다닐 수 있는 환경인거같은데, 누군가에게는 그저 일하기 싫어하는 청년의 핑계일 수도 있겠다는 생각도 듭니다.
하지만, 제 인생 제가 책임질텐데, 저도 겪어보고 부딪히면서 알아가는거 아니겠습니까?</p>
</blockquote>
<h3 id="q6-말이-많은데-마무리-하실까요">Q6. 말이 많은데, 마무리 하실까요?</h3>
<p>네 그러시죠.
다음달의 회고에는 상황이 많이 변해있을수도있습니다.
이직을 성공했을 수도 있고, 이직을 포기했을수도있고.. 잘 모르겠네요 ^^</p>
<p>아무튼 이제까지 회고 들어주셔서 감사하고, 
공부하는 모습을 틈틈히 공유하겠다고 했는데 지키지 못했다는것이 조금 걸리네요 ?
앞으로 공부하는것도 자주 올려야겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[KOTLIN] 코틀린! 자바랑 뭐가달라?]]></title>
            <link>https://velog.io/@french_ruin/KOTLIN-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9E%90%EB%B0%94%EB%9E%91-%EB%AD%90%EA%B0%80%EB%8B%AC%EB%9D%BC</link>
            <guid>https://velog.io/@french_ruin/KOTLIN-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9E%90%EB%B0%94%EB%9E%91-%EB%AD%90%EA%B0%80%EB%8B%AC%EB%9D%BC</guid>
            <pubDate>Mon, 11 Sep 2023 10:07:13 GMT</pubDate>
            <description><![CDATA[<h2 id="드디어-코틀린-시작">드디어 코틀린 시작!</h2>
<p>오늘 자기계발을 위해서 코틀린 관련 강의를 수강하기 시작했습니다.
1년전에 자바,스프링 관련 강의를 시작으로 한번의 강의도 듣지 않았는데, 초심을 되찾기 위해 큰맘(?) 먹고 결제했습니다..😊</p>
<hr>
<h2 id="그래서-주제가-뭔데">그래서 주제가 뭔데?</h2>
<p>그래서 앞으로 매일 공부하면서 알고있는것 들이나, 여러분과 공유하고싶은 지식을 포스팅할겁니다. 공부도 하고! 글도 쓰고! 아주 훌륭하다고 생각합니다.</p>
<p>주로 코틀린관련해서 포스팅할 예정이며, 개인프로젝트도 진행하면서 배운것들을 포스팅 할거에요!</p>
<hr>
<h2 id="코틀린은-자바랑-뭐가달라">코틀린은 자바랑 뭐가달라?</h2>
<p>이게 바로 주제입니다.
강의 자료에도 나와있는 정보이지만, 제가 스스로 정리하면서 다시한번 공부한다는 느낌으로 가봅시다!</p>
<h3 id="checked-exception--체크드-익셉션-">Checked Exception [ 체크드 익셉션 ]</h3>
<ol>
<li>자바의 Exception 계층과 차이점이 있습니다.<ul>
<li>Throwable : 예외 계층의 상위클래스</li>
<li>Error : 시스템에 비정상적인 상황이 발생. 예측이 어렵고 기본적으로 복구가 불가능 함<ul>
<li>e.g. OutOfMemoryError, StackOverflowError, etc</li>
</ul>
</li>
<li>Exception : 시스템에서 포착 가능하여(try-catch) 복구 가능. 예외 처리 강제<pre><code>  - IOException, FileNotFoundException, etc
  - @Transactional 에서 해당 예외가 발생하면 기본적으론 롤백이 동작하지 않음
  - rollbackFor를 사용해야함</code></pre><ul>
<li>RuntimeException : 런타임시에 발생하는 예외. 예외 처리를 강제하지 않음<ul>
<li>e.g. NullPointerException, ArrayIndexOutOfBoundsException, etc</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/french_ruin/post/8d7585cb-856e-45be-99a2-8f697bdda54b/image.png" alt=""></p>
<blockquote>
<p>자바에서 체크드 익셉션의 경우에는 try-catch로 감싸거나 throw로 예의를 전파해야합니다.</p>
</blockquote>
<pre><code class="language-java">try {
Thread.sleep(1);
} catch (InterruptedException e) {
// 예외 처리
}</code></pre>
<p>하지만, 대부분의 소스들은 catch 안에서 아무런 처리를 하지 않습니다.</p>
<pre><code class="language-java">try {
log.append(message)
} catch(IOException e) {
// Do nothing 흔하게 볼 수 있는 코드
}
try {
File file = FileUtils.get(filename);
// ...
} catch (FileNotFoundException e) {
// 파일이 없는데 어떤 처리를 하지?
}
try {
return objectMapper.readValue(json, clazz);
} catch (IOException e) {
// 단순 에러 로그 출력
logger.error(e.getMessage(), e);
}</code></pre>
<blockquote>
<p>코틀린은 체크드 익셉션을 강제하지 않습니다.</p>
</blockquote>
<pre><code class="language-kotlin">Thread.sleep(1);
// 물론 원하는 경우에는 try-catch 가능
try {
Thread.sleep(1)
} catch (e: Exception) {
// 예외 처리
}</code></pre>
<hr>
<h3 id="정적-멤버">정적 멤버</h3>
<p>자바에서는 static 키워드로 정적멤버를 선언합니다.</p>
<pre><code class="language-java">pubic class JavaClass {

  static int i = 0;

  public static void staticMethod() {
  // ...
  }
}
</code></pre>
<p>하지만 코틀린에서는,  <code>compainon object</code> 로 대체합니다.</p>
<pre><code class="language-kotlin">class KotlinClass {

  companion object {
    val i: Int = 0
    fun function() {
    // ...
    }
  }

}</code></pre>
<hr>
<h3 id="삼항-연산자">삼항 연산자</h3>
<pre><code class="language-java">
// 자바의 경우에는 삼항연산자
String animalSound = &quot;호랑이&quot;.equals(animal) ? &quot;어흥&quot; : &quot;야옹&quot;;

// 코틀린의 경우에는 if-else로 대체 
val animalSound: String = if (&quot;호랑이&quot; == animal) &quot;어흥&quot; else &quot;야옹&quot;</code></pre>
<hr>
<h3 id="확장">확장</h3>
<p>개발자가 임의로 객체의 함수나 프로퍼티를 확장해서 사용할 수 있다</p>
<pre><code class="language-kotlin">MyStringExtensions.kt

fun String.first(): Char {
    return this[0]
}

fun String.addFirst(char: Char): String {
    return char + this.substring(0)
}

fun main() {
    println(&quot;ABCD&quot;.first()) // 출력 : A
    println(&quot;ABCD&quot;.addFirst(&#39;Z&#39;)) // 출력 : ZABCD
}
</code></pre>
<hr>
<h3 id="npe-null-pointer-exception">NPE [Null Pointer Exception]</h3>
<ul>
<li>자바에서 가장 많이 발생하는 예외는 NullPointerException 줄여서 NPE</li>
<li>자바의 옵셔널(Optional)은 값을 래핑하기 때문에 객체 생성에 따른 오버헤드가 발생하고, 컴파일 단계에서 Null 가능성을 검사하지 않음</li>
<li>코틀린은 언어적 차원에서 NPE가 발생할 가능성을 제거한다</li>
</ul>
<pre><code class="language-kotlin">val a : String = null // 컴파일 오류
var b : String = &quot;aabbcc&quot;
b = null // 컴파일 오류

// Nullable 참조는 컴파일 단계에서 널 안정성을 제공

var a : String? = null
a.length // 컴파일 오류
a?.length // safe-call 정상
a!!.length // Null이 아니라고 확신하는 경우</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<p>아래에 추가적으로 차이점이 존재하지만, 나중에 공부하면서 다루게될 예정이므로 여기서는 PASS!!</p>
<ul>
<li>스마트 캐스트</li>
<li>실드 클래스 (Jdk15 추가)</li>
<li>위임</li>
<li>중위 표현식</li>
<li>연산자 오버로딩</li>
<li>코루틴</li>
<li>etc </li>
</ul>
<p>일단 기본적으로 차이점에 대해서 알아보았습니다.
수정해야될 정보나, 보완해야할 점이 있다면 말씀해주세요 바로 수정하겠습니다.👍👍</p>
<p>그럼..이만!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] 약수 구하기]]></title>
            <link>https://velog.io/@french_ruin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%95%BD%EC%88%98-%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@french_ruin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%95%BD%EC%88%98-%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 03 Sep 2023 05:20:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/french_ruin/post/30818180-4582-43e0-b5fd-7ba49a8ce279/image.png" alt="알고리즘"></p>
<h2 id="약수구하기">약수구하기</h2>
<p>약 5개월전까지만 해도 알고리즘에 대해서 꾸준히 공부를 진행해왔었다.
하지만 다른회사로 이직을 하게되면서, 업무에 적응과 회사분위기에 적응이 필요하기때문에 어느정도 공부는해왔지만, 글을 작성해오지는 않았습니다..</p>
<p>그래서 이것은 알아둘 필요가 있겠다 싶어 작성합니다.</p>
<hr>
<p>약수구하기
아주 간단하게 생각하면 아래와 같은 로직이 구성된다.</p>
<pre><code class="language-java">int n = 100;

for(int i = 1; i &lt;= n; i++){
    if(n % i == 0){
        System.out.println(i + &quot;는 약수 입니다.&quot;);
    }
}</code></pre>
<blockquote>
<p>단순하게 1부터 n까지 반복하면서 index가 나누어지면 약수로 취급한다.
하지만 알고리즘을 조금 알고있다면 시간복잡도에 관해 생각이 들것이다.</p>
</blockquote>
<p>기본적으로 for문은 시간복잡도가 O(n)이기 때문에 조금 비효율적일수 있다.</p>
<hr>
<h2 id="그렇다면-어떻게-해">그렇다면 어떻게 해?</h2>
<p>제곱근을 통해서 시간을 단축시키고 효율성을 증대시켜보자..</p>
<pre><code class="language-java">int n = 100; // 입력 값
int sqrt = (int) Math.sqrt(n); // 100의 제곱근은 10
ArrayList&lt;Integer&gt; arr = new ArrayList&lt;&gt;(); // 약수 받을 ArrayList

for(int i = 1; i &lt;= sqrt; i++){
    if(n % i == 0){ // 약수 중 작은 수 저장
        arr.add(i);
        if(n / i != i){ // 약수 중 큰 수 저장
            arr.add(n / i);
        }
    }
}</code></pre>
<p>간단하게 위를 살펴보면</p>
<ul>
<li>100의 제곱근을 구한다 .  = 10</li>
<li>여기서 우리가 알수 있는것은, 10을 i 로 나누어서 떨어진다면,  i와 몫은 모두 10의 약수로 판단할수가있다.</li>
<li>그렇기에, 위와 같이 두번의 플래그를 통해 가져온다.</li>
</ul>
<hr>
<p>실행결과는 보여주기 어렵지만, 프로그래머스 알고리즘 레벨1 문제를 풀다가
알아두면 매우 좋을것같다는 생각이 들어 간단하게 작성해봅니다.</p>
<p>혹시 잘못된정보가 있다면, 수정하겠습니다. 😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Diary] 개발자로써 떠오르는 생각]]></title>
            <link>https://velog.io/@french_ruin/Diary-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C%EC%8D%A8-%EB%96%A0%EC%98%A4%EB%A5%B4%EB%8A%94-%EC%83%9D%EA%B0%81</link>
            <guid>https://velog.io/@french_ruin/Diary-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C%EC%8D%A8-%EB%96%A0%EC%98%A4%EB%A5%B4%EB%8A%94-%EC%83%9D%EA%B0%81</guid>
            <pubDate>Sat, 19 Aug 2023 11:03:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/french_ruin/post/d162a123-c1af-4642-9197-a2d71f9ef6f5/image.webp" alt=""></p>
<hr>
<h2 id="왜그래-너-뭐돼">왜그래?? 너 뭐돼?</h2>
<h3 id="모르겠다-속히-말하는-현자타임이-왔나보다">모르겠다.. 속히 말하는 현자타임이 왔나보다..</h3>
<p>4월부터 시작한 프로젝트가 있다.
개발을 착수하기 전에 준비기간이 2달정도 있었다.
주구장창 문서만 작성하고.. 개발을 하고싶다는 생각이 강하게 들었었다.
이때부터 시작이였던것같다. 일종의 무기력감과 현자타임과 번아웃이 동시에 온 느낌을 받았다.</p>
<p>나름 나는 워커홀릭이라고 생각한다.
그저 컴퓨터앞에 앉아서 코드를 작성하고, 성과물을 낸다는것 자체가 기분이 좋았다.
막말로, 하루종일 일을 시켜도 군말없이 할 수 있는 사람이다 나는</p>
<p>하지만, 모두가 공감하겠지만 모든 서비스나 회사는 나 혼자만을 기준으로 굴러가지 않는다.
누군가와는 얘기를 통해 협의를 해야하고, 피드백도 받아야하며, 나혼자만 잘한다고 제대로 흘러가지 않는다.</p>
<hr>
<h3 id="하고싶은말이-뭐야">하고싶은말이 뭐야??</h3>
<p>글쎄.. 단순히 혼자 썩힌다는 느낌이 든다.
어딘가에는 표출하고 싶고, 회사에서는 나를 의지하는 분위기이기 때문에 함부로 불만을 표출할 수 는 없다.</p>
<p>얼마전에, 회사의 상무님이 나의 분위기를 눈치를 챘는지, ** 혹시 월급에 대해서 만족스럽지 않으면 자신에게 말해달라** 는 말씀을 하셨다.</p>
<p>그 말을 듣기 전까지 이직할 회사를 계속 찾아본것도 사실이다.
기존의 찍어내기 식의 개발이 아닌 하나의 서비스를 깊게 파고들어가고싶은 욕망이 있다.</p>
<p>그래도 한편으로는 프로젝트를 위해서라도, 회사 팀원들을 생각해서라도 나의 역할을 제대로 하는것이 분명한 정답이라는 것은 알고있다.</p>
<hr>
<h3 id="그럼-계속-다니면-되잖아">그럼 계속 다니면 되잖아!!</h3>
<h4 id="피드백-피드백">피드백.. 피드백!!!</h4>
<p>5월말 부터 개발에 들어갔다.
개발을 진행하면서 피드백이 간절했다.
내가 제대로 개발을 하고 있는건지, 로직은 제대로 구성했는지..
혼자만의 시선이 아닌, 남이 봐줘야 문제점이 더 잘보이고 더 탄탄한 성과물이 나온다고 생각한다.
물론, 피드백없이도 혼자 척척 알아서 해내는 사람들이 있는것도 알고 있다.
<strong>나는 개인적으로 그런사람을 &#39;신&#39;이라고 부르고싶다.</strong></p>
<p>그런 사람들은 회사 입장에서는 최고의 가성비 직원이 아닐까 싶다.
하지만 경력도 적은 내게 모든것을 떠맡기려는 느낌을 강하게 받았다.</p>
<p>다른것은 다 필요없다.
나는 피드백을 강하게 원하는 것 같다.</p>
<hr>
<h3 id="두서없이-말하니까-속이-후련해">두서없이 말하니까 속이 후련해??</h3>
<p>솔직히 글을 쓰면서 조금 후련해진다.</p>
<p>어제 번개로 회사에서 맥주한잔을 했다.
차장님께서 다들 나를 걱정하고 있다고 하셨다. 언제 번아웃이 모를지 모른다는 걱정이라고 하신다.
정말 벅차고 힘들면 자신에게 말을 해달라고 하신다. </p>
<p>그래서 월요일에 진지하게 말씀을 드려볼까한다.
물론 나의 능력부족도 맞다. 1인분을 해내지 못한것이나 다름없으니까.</p>
<p>그래도 회사에 피해를 주지 않기 위해서는 나의 능력부족을 회사에 인정시켜야 한다고 생각한다.
나도 회사에 의존해보고 싶다.</p>
<hr>
<h2 id="아무튼-그렇다고">아무튼 그렇다고..</h2>
<p>내가 요즘 제일 좋아하는 단어가 <strong>Anyway</strong> 다.
하여튼 이라는 뜻인데, 매우 맘에드는 단어다. 
왠지 걱정들을 모두 각설하고 현생을 살게해주는 단어같은 느낌이든다.</p>
<p>혹시라도 이 글을 읽으신분들은 감사드리고, 별것도없는 하소연을 읽어주신것도 감사드립니다. ^^</p>
<hr>
<blockquote>
<p>그래서 개발 공부는 언제하지..? ㅠㅠ
프로젝트가 매우 바쁘다..
곧 찾아뵙겠습니다 😂</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[KOTLIN] 드디어 생겼네요.. JPA 문제가.. 😂 ]]></title>
            <link>https://velog.io/@french_ruin/KOTLIN-%EB%93%9C%EB%94%94%EC%96%B4-%EC%83%9D%EA%B2%BC%EB%84%A4%EC%9A%94..-JPA-%EB%AC%B8%EC%A0%9C%EA%B0%80</link>
            <guid>https://velog.io/@french_ruin/KOTLIN-%EB%93%9C%EB%94%94%EC%96%B4-%EC%83%9D%EA%B2%BC%EB%84%A4%EC%9A%94..-JPA-%EB%AC%B8%EC%A0%9C%EA%B0%80</guid>
            <pubDate>Sun, 13 Aug 2023 11:37:57 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="생각해보니까-문제가-있더라">생각해보니까 문제가 있더라..</h2>
<p>첫 입사한 회사에서 자바+스프링으로 개발을 시작했는데.. 물론 대한민국에서는 자바 스프링이 제일 대중적으로 쓰인다고 생각합니다.</p>
<p>하지만, 연차는 적지만서도 나중을 생각하면 다른 Skill도 필요하겠다 라는 생각이 강하게 들었다.
자바와 100% 호환된다는 Kotlin을 선택했다.</p>
<p>그래서 기존의 Java 사이드 프로젝트들을 기반으로 Kotlin으로 프로젝트를 진행해보고자 했다.</p>
<p>기본적인 Controller의 경우는 문제가 전혀 없었다.  그.러.나.</p>
<blockquote>
<p>아무리 100% 호환이라고는 하지만, Java Spring에서 잘 적용되던 Entity형식의 코드들이 Kotlin에서는 유의할점으로 남아있다는 것이다.</p>
</blockquote>
<hr>
<h2 id="문제라면-해결해야지">문제라면 해결해야지!</h2>
<p>물론 맞는말이다. 문제라면 해결하면 그만이다.
하지만, 프로젝트에 그대로 코드만 복붙해서 가느니, 한번쯤은 이론과 중요성에 대해서 알아보고자 합니다요.</p>
<h3 id="어이-jpa-뭘-원하는데">어이 JPA 뭘 원하는데??</h3>
<p>일단 뭐, <a href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#entity">하이버네이트 사용가이드</a>에서는 이렇다 저렇다 하는데, 여기서는 그 중에서 몇개만 다루어 보고자 합니다.</p>
<ul>
<li>엔티티 클래스는 매개변수가 없는 <code>public</code>또는 <code>protected</code>생성자를 가져야한다.</li>
<li>지연로딩을 사용하려면 엔티티 클래스는 final이 아니여야 한다.</li>
</ul>
<hr>
<h2 id="문제-해석-들어가자">문제 해석 들어가자!</h2>
<h3 id="첫번째">첫번째</h3>
<p>일단 기본적으로 코틀린에서 Class 는 문법상 Property를 선언하면서 자동으로 생성되는 기본 생성자를 사용하는 경우가 대부분일것이다.
그리고 추가적인 생성자는 반드시 기본생성자를 상속해야하는 코틀린의 문법 특징으로 코틀린에서는 매개변수가 없는 생성자를 작성하는것이 <strong>굉장히 번거롭다.</strong></p>
<blockquote>
<p>이를 위해서 코틀린에서는 Gradle 또는 Maven에서 추가적인 plugIn을 지정하는 것으로 <code>@Entity</code> <code>@Embeddable</code> <code>@MappedSuperClass</code> 어노테이션이 붙은 모든 클래스에 자동으로 매개변수가 없는 생성자를 만들 수 있다.</p>
</blockquote>
<p><strong>참고로 필자는 Gradle(Kotlin) 이기에 &lt;&lt; 이 기준을 따라서 작성할거에요~</strong></p>
<pre><code class="language-kotlin">plugins {
    ..
    kotlin(&quot;plugin.jpa&quot;) version &quot;{Kotlin버전}&quot;
    ..
}
</code></pre>
<blockquote>
<p>위의 Gradle 설정에 추가하면, 코틀린을 자바코드로 디컴파일 했을때 매개변수가 없는 생성자가 추가가 되어있을 겁니다. 
참고로 이 플러그인은 Spring JPA 를 추가했다면 추가할 필요는 없습니다.</p>
</blockquote>
<h3 id="두번째">두번째</h3>
<p>코틀린의 모든 클래서는 기본적으로 <code>final</code>이죠? 
엔티티 클래스가 final 클래스라고 해도 JPA를 사용하는 것 자체는 문제가 없지만!! 지연 로딩 (Lazy) 을 제대로 사용할 수가 없어요..</p>
<p><code>@OneToMany</code>라면 별도의 걱정없이 지연로딩을 사용할 수 있지만!
<code>@ManyToOne</code>이라면 엔티티 클래스가 final 클래스라면 지연 로딩이 동작을 하지 않습니다.
<strong>따라서 엔티티 클래스들을 모두 OPEN 해줄 필요가 있습니다.</strong></p>
<p>하지만! 엔티티 클래스마다 일일이 OPEN 키워드를 붙여주는것은 매우매우 귀찮은 일이죠?
그래서 이 또한 Gradle에서 설정을 해줄 수가 있습니다.</p>
<p><strong>Gradle(Spring Boot 3.0.0 이상)</strong></p>
<pre><code class="language-kotlin">plugins {
    ..
    // Gradle(Groovy)
    id &#39;org.jetbrains.kotlin.plugin.allopen&#39; version &#39;{Kotlin버전}&#39;    

    // Gradle(Kotlin)
    kotlin(&quot;plugin.allopen&quot;) version &quot;{Kotlin버전}&quot;    
    ..
}

allOpen {
    annotation(&quot;jakarta.persistence.Entity&quot;)
    annotation(&quot;jakarta.persistence.Embeddable&quot;)
    annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
}</code></pre>
<p><strong>Gradle(Spring Boot 3.0.0 미만)</strong></p>
<pre><code class="language-kotlin">plugins {
    ..
    // Gradle(Groovy)
    id &#39;org.jetbrains.kotlin.plugin.allopen&#39; version &#39;{Kotlin버전}&#39;    

    // Gradle(Kotlin)
    kotlin(&quot;plugin.allopen&quot;) version &quot;{Kotlin버전}&quot;    
    ...
}

allOpen {
    annotation(&quot;javax.persistence.Entity&quot;)
    annotation(&quot;javax.persistence.Embeddable&quot;)
    annotation(&quot;javax.persistence.MappedSuperclass&quot;)
}</code></pre>
<p>이렇게 설정해주면 되는데.. 주의할 점은~!
<strong>스프링부트 3.0.0부터는 <code>org.springframework.boot:spring-boot-starter-data-jpa</code>의 JPA 어노테이션들의 최상위 패키지가 <code>javax</code>에서 <code>jakarta</code>로 변경되었기 때문에 스프링부트 버전에 따라서 allOpen에 설정할 어노테이션의 패키지 명을 잘 작성해야 한다.</strong></p>
<hr>
<h2 id="네-그렇습니다">네 그렇습니다..</h2>
<p>이렇게 해서 일단은 일단락이 된 기분인데.. 아마 개인프로젝트를 진행하면서 계속 들락날락 할것같아요~..</p>
<p>유익했다면 댓글 한번만 달아주시고, 혹시 틀린정보가 있다면 말씀해주시면 수정해서 다시 포스팅하겠습니다 😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대역죄인 복귀합니다.. :)]]></title>
            <link>https://velog.io/@french_ruin/%EB%8C%80%EC%97%AD%EC%A3%84%EC%9D%B8-%EB%B3%B5%EA%B7%80%ED%95%A9%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@french_ruin/%EB%8C%80%EC%97%AD%EC%A3%84%EC%9D%B8-%EB%B3%B5%EA%B7%80%ED%95%A9%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Sun, 13 Aug 2023 08:27:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/french_ruin/post/96c8aad7-93fb-48d7-9f57-cd420cf6f0a4/image.png" alt=""></p>
<h2 id="나의-처절한-복귀">나의 처절한 복귀..</h2>
<blockquote>
<p>잠시 자본주의의 노예가 되어 수익을 창출할 수 있는 플랫폼인 Tistory에 발을 디디고 왔습니다.</p>
</blockquote>
<p>결론부터 말씀드리면, 다시 velog에서 활동을 하고자 합니다.</p>
<p>이유는 즉슨,</p>
<ul>
<li>수익을 창출하기 위해서는 광고를 집어넣어야하는데, SEO나, 광고 인증등을 하기가 너무 버거로웠습니다.. 저에게는..</li>
</ul>
<hr>
<h3 id="velog도-충분히-나에게-걸맞다">Velog도 충분히 나에게 걸맞다</h3>
<pre><code>광고의 이유만 제외하면 저에게는 velog가 아주 적.절.한 플랫폼입니다.</code></pre><p> 디자인이 깔끔함과 더불어, 광고에 신경쓰지 않아도되고 충분히 글을 올리고 보고, 커뮤니티를 생성할 수 있을것 같다는 생각이 들었습니다.</p>
<p> 나중의 포트폴리오나 이력서에 첨부할 만한 글거리를 작성하고, 제가 공부하고 생각해온것들을 공유할까합니다.</p>
<p> 추후에는 뭐, 개발자들과의 커뮤니티를 생성하고 의견을 주고받을 수 있을지는 모르겠네요..ㅎㅎ</p>
<hr>
<h3 id="anyway">AnyWay</h3>
<p> 아무튼 저의 복귀를 환영해주시면 감사하겠습니다. [ 비록 올린글은 쥐꼬리지만.. ]</p>
<p> 앞으로 좋은 글과 의견들을 포스팅하는 Dev_Lee가 되겠습니다. ^^</p>
<h3 id="see-ya-">see ya :)</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SPRING] JPA - Persistence Aware Issue]]></title>
            <link>https://velog.io/@french_ruin/SPRING-JPA-Persistence-Aware-Issue</link>
            <guid>https://velog.io/@french_ruin/SPRING-JPA-Persistence-Aware-Issue</guid>
            <pubDate>Mon, 08 Aug 2022 08:00:16 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>영속성 컨텍스트 이슈가 발생하는 이유는 &#39;동일성&#39; 때문에 발생합니다.</p>
<blockquote>
<ul>
<li>수정 혹은 생성은 주의</li>
</ul>
</blockquote>
</blockquote>
<ul>
<li><p>@Transactional  의 경우에 모든 메서드가 끝나는 시점에 영속성 컨텍스트가 DB에 반영</p>
<ul>
<li>JPA 조회를 하면 DB 쿼리 조회보다 영속성 컨텍스트에서 먼저 조회</li>
<li>save ( ) 는 DB 저장이 아닌 영속성 컨텍스트에 저장</li>
</ul>
<hr>
<h2 id="entity">Entity</h2>
<pre><code class="language-java">@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@DynamicInsert                                   //여기도 집중하자~~~
@EqualsAndHashCode(callSuper = true)
public class Comment extends BaseEntity {

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 private String comment;

 @ManyToOne
 @ToString.Exclude
 private Review review;

 @Column(columnDefinition = &quot;datetime&quot;)     // 집중!!!!!!!!!!!!!!!!!!!!!!!!// 
 private LocalDateTime commentedAt;
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>- comment 라는 Entity를 만들어준다.
  - 집중해야할 부분은 commentedAt 이다.
- @Column 어노테이션으로 datetime 을 명시해준다

---
### JPA Repo
```java
public interface CommentRepository extends JpaRepository&lt;Comment,Long&gt; {
}
</code></pre><p>간단하게 선언해준다.</p>
<hr>
<h2 id="test">Test</h2>
<pre><code class="language-java">    @DisplayName(&quot;1. commentTest &quot;)
    @Test
    @Transactional
    void test_1(){
        Comment comment = new Comment();
        comment.setComment(&quot;별로....&quot;);
        comment.setCommentedAt(LocalDateTime.now());

        commentRepository.save(comment);

        commentRepository.findAll().forEach(System.out::println);
    }</code></pre>
<p>  간단하게 해보자 실행 시켜본다.</p>
<pre><code class="language-c">   id=4, comment=별로...., commentedAt=2022-08-08T16:55:03.005</code></pre>
<p>  위와 같은 결과가 나온다. 근데 만약 </p>
<pre><code class="language-java">    @DisplayName(&quot;1. commentTest &quot;)
    @Test
    @Transactional
    void test_1(){
        Comment comment = new Comment();
        comment.setComment(&quot;별로....&quot;);
        comment.setCommentedAt(LocalDateTime.now());

        commentRepository.save(comment);

        entityManager.clear();     // 짜짠!

        commentRepository.findAll().forEach(System.out::println);
    }</code></pre>
<p>  clear ( ) 를 시켜서 캐시를 지워준다면??</p>
<pre><code class="language-c">  id=4, comment=별로...., commentedAt=2022-08-08T16:57:14</code></pre>
<blockquote>
<p>결과가 다르게 나오는것을 볼 수 있다.
  아주 사소한 차이긴 하지만 나중에는 영속성 컨텍스트의 캐시 문제로 데이터가 잘 내려오지 않거나 다르게 내려온다면
  골치가 아파진다...</p>
</blockquote>
<hr>
<h2 id="">+@</h2>
<p>  일단 이 문제는 더 겪어보고 내가 원하는대로 데이터가 내려오지않을때 해봐야할것같다.</p>
<p>  나중에 추가로 진행해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - Phone Book V4]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-V4</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-V4</guid>
            <pubDate>Mon, 08 Aug 2022 06:56:26 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>V4 에서는 이름, 번호, 이메일, 그룹 을 추가하고. 추가양식을 나타내는 기능을 구현했다.</p>
</blockquote>
<hr>
<h2 id="main--">Main ( )</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;

#define CAPACITY 100
#define BUFFER_LENGTH 100
#define _CRT_SECURE_NO_WARNINGS // 혹시 모를 fopen 오류 무시

typedef struct person {   // 이름 번호 이메일 그룹 struct
    char *name;
    char *number;
    char *email;
    char *group;
} Person;

Person directory[CAPACITY];     // directory person 배열

int read_line(FILE *fp, char str[], int n);            // 명령어 읽기
void load(char *fileName);       // 파일 로드
void add(char *name, char *number, char *email, char *group);   // 이름 번호 이메일 그룹 추가
void save(char *fileName);        // 파일 저장
void print_person(Person p);      // 출력
int search(char * name);          // 검색 -1 return
void remove_item(char*name);      // 삭제
void find(char*name);             // 이름으로 검색
void status();                    // 전체 조회
void handle_add(char *name);      // 데이터 생성 양식
int compose_name(char str[], int limit);    // 이름만 뺴오기
int n = 0;                // 인원 숫자

int main() {
    char command_line[BUFFER_LENGTH];
    char *command, *arg;
    char name_str[BUFFER_LENGTH];

    while (1) {
        printf(&quot;$ &quot;);
        if (read_line(stdin, command_line, BUFFER_LENGTH) &lt;= 0) {
            continue;
        }

        command = strtok(command_line, &quot; &quot;);
        if (strcmp(command, &quot;read&quot;) == 0) {
            arg = strtok(NULL, &quot; &quot;);
            if (arg == NULL) {
                printf(&quot;Invalid arguments.\n&quot;);
                continue;
            }
            load(arg);

        } else if (strcmp(command, &quot;add&quot;) == 0) {
            if (compose_name(name_str, BUFFER_LENGTH) &lt;= 0) {
                printf(&quot;Name required.\n&quot;);
                continue;
            }
            handle_add(name_str);
        } else if (strcmp(command, &quot;find&quot;) == 0) {
            if (compose_name(name_str, BUFFER_LENGTH) &lt;= 0) {
                printf(&quot;Name required.\n&quot;);
                continue;
            }
            find(name_str);
        } else if (strcmp(command, &quot;save&quot;) == 0) {
            arg = strtok(NULL, &quot; &quot;);
            if (strcmp(arg, &quot;as&quot;) != 0) {
                printf(&quot;Invalid arguments.\n&quot;);
                continue;
            }
            arg = strtok(NULL, &quot; &quot;);
            if (arg == NULL) {
                printf(&quot;Invalid arguments.\n&quot;);
                continue;
            }
            save(arg);
        } else if (strcmp(command, &quot;status&quot;) == 0) {
            status();
        } else if (strcmp(command, &quot;delete&quot;) == 0) {
            if (compose_name(name_str, BUFFER_LENGTH) &lt;= 0) {
                printf(&quot;Invalid arguments.\n&quot;);
                continue;
            }
            remove_item(name_str);
        } else if (strcmp(command, &quot;exit&quot;) == 0) {
            break;
        }
    }
    return 0;
}</code></pre>
<blockquote>
<p>모두 비슷합니다만..
전체 명령을 command_line에 입력받습니다. 그다음에 strtok 함수를 이용해서 단어들을 공백기준으로 나눈후, 
진행합니다.</p>
</blockquote>
<hr>
<h3 id="read_line--">Read_Line ( )</h3>
<pre><code class="language-c">int read_line(FILE *fp, char str[], int n) {
    int ch, i = 0;

    while ((ch = fgetc(fp)) != &#39;\n&#39; &amp;&amp; ch != EOF) {
        if (i &lt; n) {
            str[i++] = ch;
        }
    }
    str[i] = &#39;\0&#39;;
    return i;
}</code></pre>
<ul>
<li>위의 Main 함수에서 볼수 있듯이, stdin 을 사용하여 파일 뿐만아니라 키보드의 입력도 적용할 수 있게 했습니다.<ul>
<li>파일과, 파일의 내용을 담을 str 배열, 그리고 n ( Buffer size ) 을 받습니다.</li>
<li>V3 에서는 getchar( ) 를 사용했지만, V4 에서는 fgetc( ) 로 파일의 내용을 읽어옵니다. 
여기서도 또한 int를 return</li>
</ul>
</li>
</ul>
<hr>
<h3 id="load--">Load ( )</h3>
<pre><code class="language-c">void load(char *fileName) {
    char buffer[BUFFER_LENGTH];
    char *name, *number, *email, *group;

    FILE *fp = fopen(fileName, &quot;r&quot;);
    if (fp == NULL) {
        printf(&quot;Open Failed.\n&quot;);
        return;
    }

    while (1) {
        if (read_line(fp, buffer, BUFFER_LENGTH) &lt;= 0) {
            break;
        }
        name = strtok(buffer, &quot;#&quot;);
        number = strtok(NULL, &quot;#&quot;);
        email = strtok(NULL, &quot;#&quot;);
        group = strtok(NULL, &quot;#&quot;);
        add(name, number, email, group);
    }
    fclose(fp);
}</code></pre>
<ul>
<li>파일을 읽어오는 로직, 파일이 없다면 open failed <ul>
<li>만약 파일의 내용이 없다면 break;</li>
<li>파일의 한문장을 # 기준으로 나누고 add함수로 저장</li>
</ul>
</li>
</ul>
<hr>
<h3 id="add--">Add ( )</h3>
<pre><code class="language-c">void add(char *name, char *number, char *email, char *group) {
    int i = n - 1;
    while (i &gt;= 0 &amp;&amp; strcmp(directory[i].name, name) &gt; 0) {
        directory[i + 1] = directory[i];
        i--;
    }
    directory[i + 1].name = strdup(name);
    directory[i + 1].number = strdup(number);
    directory[i + 1].email = strdup(email);
    directory[i + 1].group = strdup(group);
    n++;
}</code></pre>
<ul>
<li>이름 번호 이메일 그룹을 인자로 받는 add 함수 생성<ul>
<li>strcmp 함수로 저장되어있던 이름과 입력받은 이름을 비교하여 정렬하여 저장</li>
<li>그리고 지역변수로 사라질 name, number, email, group을 directory배열내부의 각각 넣어줌</li>
</ul>
</li>
</ul>
<hr>
<h3 id="handle_add--">Handle_Add ( )</h3>
<pre><code class="language-c">void handle_add(char *name)
{
    char number[BUFFER_LENGTH], email[BUFFER_LENGTH], group[BUFFER_LENGTH];
    char empty[] = &quot; &quot;;

    printf(&quot;  Phone:  &quot;);
    read_line(stdin, number, BUFFER_LENGTH);

    printf(&quot;  Email:  &quot;);
    read_line(stdin, email, BUFFER_LENGTH);

    printf(&quot;  Group:  &quot;);
    read_line(stdin, group, BUFFER_LENGTH);

    add(name,(char*)(strlen(number)&gt;0 ? number : empty),
                (char*)(strlen(email)&gt;0 ? email : empty),
                (char*)(strlen(group)&gt;0 ? group : empty)
        );
}</code></pre>
<ul>
<li>입력받을 number와 email, group 배열 변수 선언<ul>
<li>만약 데이터가 없을경우의 넣어줄 emtpy선언</li>
<li>순서대로 입력후,  add 함수를 이용하여 추가 <ul>
<li>삼항 조건식 ( null 일경우에 빈칸으로 입력 )</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="compose-name--">Compose Name ( )</h3>
<pre><code class="language-c">int compose_name(char str[], int limit) {
    char *ptr;
    int length = 0;

    ptr = strtok(NULL, &quot; &quot;);
    if (ptr == NULL) {
        return 0;
    }

    strcpy(str, ptr);
    length += strlen(ptr);

    while ((ptr = strtok(NULL, &quot; &quot;)) != NULL) {
        if (length + strlen(ptr) + 1 &lt; limit) {
            str[length++] = &#39; &#39;;
            str[length] = &#39;\0&#39;;
            strcat(str, ptr);
            length += strlen(ptr);
        }
    }
    return length;
}</code></pre>
<blockquote>
<p>나름 이해하기 어려웠던 로직..</p>
<blockquote>
<p>ptr 포인터 변수에 다음으로 입력받을 데이터를 공백으로 나눈다.
 만약 입력된것이 없다면 return;</p>
<blockquote>
<p>있다면, str에 ptr에 들어간 데이터를 복사한후, ptr의 길이만큼 legnth에 더합니다.
    그다음에 ptr의 데이터가 strtok 으로 나눈후 의 데이터가 null 이 아닌동안에
    길이가 limit을 넘지 않는다면, strtok 으로 나눈 문자열의 맨끝에 공백을, 
    문자열의 마지막은 \0으로 지어준다.</p>
<blockquote>
<p>마지막으로 length 를 return</p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<hr>
<h3 id="find--">Find ( )</h3>
<pre><code class="language-c">void find(char*name)
{
    int index = search(name);
    if (index == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.\n&quot;, name);
    } else {
        print_person(directory[index]);
    }
}</code></pre>
<hr>
<h3 id="search--">Search ( )</h3>
<pre><code class="language-c">int search(char * name){
    int i;
    for (i = 0; i &lt; n; i++) {
        if (strcmp(name, directory[i].name) == 0) {
            return i;
        }
    }
    return -1;
}</code></pre>
<blockquote>
<p>find함수와 search 함수는 V3 에서 확인.</p>
</blockquote>
<hr>
<h3 id="print_person--">Print_person ( )</h3>
<pre><code class="language-c">void print_person(Person p)
{
    printf(&quot;%s:\n&quot;, p.name);
    printf(&quot;   Phone: %s\n&quot;, p.number);
    printf(&quot;   Email: %s\n&quot;, p.email);
    printf(&quot;   Group: %s\n&quot;, p.group);
}</code></pre>
<hr>
<h3 id="remove_item----delete">remove_item ( ) &gt;&gt; delete</h3>
<pre><code class="language-c">void remove_item(char*name)
{
    int i = search(name);
    if (i == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.\n&quot;, name);
        return;
    }
    int j =i;
    for (; j &lt; n - 1; j++) {
        directory[j] = directory[j + 1];
    }
    n--;
    printf(&quot;&#39;%s&#39; was deleted successfully.\n&quot;, name);
}</code></pre>
<hr>
<h3 id="status--">Status ( )</h3>
<pre><code class="language-c">
void status(){
int i ;
    for (i = 0; i &lt; n; i++) {
        print_person(directory[i]);
    }
    printf(&quot;Total %d persons.\n&quot;, n);
}</code></pre>
<hr>
<h3 id="save--">Save ( )</h3>
<pre><code class="language-c">void save(char *fileName)
{
    int i;
    FILE *fp = fopen(fileName, &quot;w&quot;);
    if (fp == NULL) {
        printf(&quot;Open Failed.\n&quot;);
        return;
    }
    for (i = 0;  i&lt;n ; i++) {
        fprintf(fp, &quot;#%s#&quot;, directory[i].name);
        fprintf(fp, &quot;#%s#&quot;, directory[i].number);
        fprintf(fp, &quot;#%s#&quot;, directory[i].email);
        fprintf(fp, &quot;#%s#\n&quot;, directory[i].group);
    }
    fclose(fp);
}</code></pre>
<blockquote>
<p>설명이 없는 부분은 이미 이전 포스트에서 다루었다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - Phone Book V3]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-V3</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-V3</guid>
            <pubDate>Mon, 08 Aug 2022 02:26:51 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>Phone Book V2 는 파일을 읽어오고 조회하고 삭제하는 기능을 추가시켰습니다.
 이번 V3 에서는 사용자가 명령어를 잘못입력했을때 하는 처리와 최대한 간결화 시킨상태의 로직을 구성해볼것입니다.</p>
</blockquote>
<hr>
<h2 id="main--">Main ( )</h2>
<pre><code class="language-c"> #include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;

#define INIT_CAPACITY 3
#define BUFFER_SIZE 50

char **names;
char **numbers;

int capacity = INIT_CAPACITY ;
int n = 0;

char delim[] = &quot; &quot;;

void init_directory();       // 데이터 배열 생성

void process_command();      // 명령어 처리 함수

int read_line(char str[], int limit);     // 명령어 입력
void load(char * fileName);              // 파일 로드
void add(char *name, char *numbers);   // 추가
void delete(char *name);         //삭제
void save(char *fileName);    // 저장
int search(char *name);      // 검색 -1 return
void find();               // 찾기
void status();            // 현 정보 조회
void reallocate();        // 재할당


int main() {

    init_directory();     // 배열 크기 할당
    process_command();    // 명령어 입력 함수

    return 0;
}</code></pre>
<blockquote>
<p>char** 이중포인터 배열을 사용합니다.</p>
<blockquote>
<pre><code class="language-c">int * name[10] ; 
int **name ; 
위와 같은 의미
우리는 배열을 할당하여 사용할 것이기에 이중포인터배열을 사용합니다.</code></pre>
</blockquote>
</blockquote>
<pre><code>
---
### Init Directory( ) 
```c
void init_directory() {
    names = (char **) malloc(INIT_CAPACITY *sizeof(char *));
    numbers = (char **) malloc(INIT_CAPACITY*sizeof(char *));
}</code></pre><ul>
<li>V2 와 동일하게 malloc 함수로 배열을 동적으로 할당합니다.</li>
</ul>
<hr>
<h3 id="read_line--">Read_Line ( )</h3>
<pre><code class="language-c">int read_line(char str[], int limit) {
    int ch, i = 0;

    while ((ch = getchar()) != &#39;\n&#39;) {
        if (i &lt; limit - 1) {
            str[i++] = ch;
        }
    }

    str[i] = &#39;\0&#39;;

    return i;
}</code></pre>
<ul>
<li>getchar ( ) 의 경우 리턴값은 int 이다 ( 나중에 다룰 예정 )<ul>
<li>엔터를 치기 전까지 글자수 제한 전의 글자를 모두 읽어 str 배열에 저장한다.</li>
<li>그다음 i 의 값을 return</li>
</ul>
</li>
</ul>
<hr>
<h3 id="process_command--">Process_Command ( )</h3>
<pre><code class="language-c">void process_command() {
    char command_line[BUFFER_SIZE];
    char *command, *arg1, *arg2;

    while (1) {
        printf(&quot;$ &quot;);

        if (read_line(command_line, BUFFER_SIZE) &lt;= 0)       //명령줄을 통째로 읽는다.
            continue;

        command = strtok(command_line, delim);
        if (command == NULL)
            continue;

        if (strcmp(command, &quot;read&quot;) == 0) {

            arg1 = strtok(NULL, delim);
            if (arg1 == NULL) {
                printf(&quot;File name required\n&quot;);
                continue;
            }

            load(arg1);
        } else if (strcmp(command, &quot;add&quot;) == 0) {
            arg1 = strtok(NULL, delim);
            arg2 = strtok(NULL, delim);

            if (arg1 == NULL || arg2 == NULL) {
                printf(&quot;Invalid Arguments\n&quot;);
                continue;
            }
            add(arg1, arg2);

            printf(&quot;%s was added successfully\n&quot;, arg1);
        } else if (strcmp(command, &quot;find&quot;) == 0) {
            arg1 = strtok(NULL, delim);
            if (arg1 == NULL) {
                printf(&quot;Invalid Arguments\n&quot;);
                continue;
            }
            find(arg1);
        } else if (strcmp(command, &quot;status&quot;) == 0) {
            status();
        } else if (strcmp(command, &quot;delete&quot;) == 0) {
            arg1 = strtok(NULL, delim);
            if (arg1 == NULL) {
                printf(&quot;Invalid Arguments\n&quot;);
                continue;
            }
            delete(arg1);
        } else if (strcmp(command,&quot;save&quot;)==0) {
            arg1 = strtok(NULL, delim);
            arg2 = strtok(NULL, delim);

            if (arg1 == NULL || strcmp(&quot;as&quot;, arg1) != 0 || arg2 == NULL) {
                printf(&quot;Invalid command format\n&quot;);
                continue;
            }
            save(arg2);

        } else if (strcmp(command, &quot;exit&quot;) == 0) {
            break;
        }

    }
}</code></pre>
<blockquote>
<p>strcmp 함수를 이용하여 두개의 단어가 동일하면 0을 return 하여 그안의 함수 실행</p>
</blockquote>
<hr>
<h3 id="load--">Load ( )</h3>
<pre><code class="language-c">void load(char * fileName)
{
    char buf1[BUFFER_SIZE];
    char buf2[BUFFER_SIZE];

    FILE *fp = fopen(fileName, &quot;r&quot;);
    if (fp == NULL) {
        printf(&quot;Open Failed\n&quot;);
        return;
    }

    while ((fscanf(fp, &quot;%s&quot;, buf1) != EOF)) {
        fscanf(fp, &quot;%s&quot;, buf2);
        add(buf1, buf2);
    }
    fclose(fp);
}</code></pre>
<hr>
<h3 id="add--">Add ( )</h3>
<pre><code class="language-c">void add(char *name, char *number)
{
    if (n &gt;= capacity) {
        reallocate();
    }

    int i = n-1;
    while (i &gt;= 0 &amp;&amp; strcmp(names[i], name) &gt; 0) {
        names[i + 1] = names[i];
        numbers[i + 1] = numbers[i];
        i--;
    }
    names[i + 1] = strdup(name);
    numbers[i + 1] = strdup(number);
    n++;
}</code></pre>
<blockquote>
<p>reallocate ( )</p>
<blockquote>
<pre><code class="language-c">    void reallocate()
    {    
    int i ;
    capacity *= 2;
    char **tmp1 = (char **) malloc(capacity * sizeof(char *));
    char **tmp2 = (char **) malloc(capacity * sizeof(char *));
    for (int i = 0; i &lt; n; ++i) {
        tmp1[i] = names[i];
        tmp2[i] = numbers[i];
    }
    free(names);
    free(numbers);
    names = tmp1;
    numbers = tmp2;
    }</code></pre>
</blockquote>
</blockquote>
<pre><code>&gt;&gt;&gt;용량을 2배로 늘리고,. tmp1 과 tmp2 의 동적할당 후 ,
tmp1과 tmp2 에 names와 numbers의 데이터를 넣고,
free 함수로 필요없는 공간을 버리고,
다시 names와 numbers 에 tmp1 과 tmp2 를 넣는다.

---

### Find ( )
```c
void find()
{
    char namesTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, namesTmp);
    int index = search(namesTmp);
    if (index == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.  \n&quot;, namesTmp);
    } else
        printf(&quot;%s\n&quot;, numbers[index]);
}</code></pre><blockquote>
<p>Search ( ) </p>
<blockquote>
<pre><code class="language-c">     int search(char *name)
{
    int i;
    for (; i &lt; n; ++i) {
        if (strcmp(name, names[i]) == 0) {
            return i;
        }
    }
    return -1;
}</code></pre>
</blockquote>
</blockquote>
<pre><code>&gt;&gt;&gt;V2 와 동일합니다.

----
### Status ( )
```c
void status()
{
    int i ;
    for (int i = 0; i &lt; n; ++i) {
        printf(&quot;%s %s\n&quot;, names[i], numbers[i]);
    }
    printf(&quot;Total %d persons \n&quot;, n);
}
</code></pre><hr>
<h3 id="delete--">Delete ( )</h3>
<pre><code class="language-c">void delete(char *name)
{
    int i = search(name);
    if (i == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.\n&quot;, name);
        return;
    }
    int j = i;
    for (; j &lt; n - 1; ++j) {
        names[j] = names[j + 1];
        numbers[j] = numbers[j + 1];
    }
    n--;
    printf(&quot;&#39;%s&#39; was deleted successfully\n&quot;,name);
}</code></pre>
<hr>
<h3 id="save--">Save ( )</h3>
<pre><code class="language-c">void save(char *fileName)
{
    int i;
    FILE *fp = fopen(fileName, &quot;w&quot;);
    if (fp == NULL) {
        printf(&quot;Open Failed\n&quot;);
        return;
    }

    for (int i = 0; i &lt; n; ++i) {
        fprintf(fp, &quot;%s %s\n&quot;, names[i], numbers[i]);
    }
    fclose(fp);
}</code></pre>
<hr>
<blockquote>
<p>V2 와 동일한 내용들은 설명을 생략했습니다~!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SPRING] JPA - N + 1 Issue]]></title>
            <link>https://velog.io/@french_ruin/SPRING-JPA-N-1-Issue</link>
            <guid>https://velog.io/@french_ruin/SPRING-JPA-N-1-Issue</guid>
            <pubDate>Sun, 07 Aug 2022 09:14:06 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="n--1-issue-">N + 1 Issue ?</h2>
<blockquote>
<p>N+1 문제란 만약 1번의 쿼리를 날렸을 때 필요하지 않은 쿼리가 N번 만큼 추가적으로 실행된다는것을 의미합니다.
우리가 단순한 프로젝트를 할때는 많아봤자 10개정도의 쿼리가 실행되겠지만
만약 대형서비스를 진행할때 100번 아니 천번 만번 불필요한 쿼리가 실행된다면 
끔찍한 상황일 것이죠?!</p>
<blockquote>
<p>이러한 N+1 의 상황은 OneToMany 관계나 ManyToOne 관계에서 엔티티를 조회할 때 발생합니다.</p>
<blockquote>
<p>EAGER 전략으로 데이터를 조회하거나, LAZY 전략으로 데이터를 조회하여 하위 엔티티를 사용할때 다시 조회하면서 발생합니다.</p>
</blockquote>
</blockquote>
</blockquote>
<hr>
<h3 id="eager--lazy">EAGER / LAZY</h3>
<ul>
<li><p>EAGER ( 즉시 로딩 )</p>
<ul>
<li><p>JPQL 에서 만든 SQL을 통해서 조회</p>
<ul>
<li>EAGER 전략으로 조회하면서 하위엔티티들을 추가조회 하게 되면서 N +1 이슈 발생</li>
</ul>
</li>
<li><p>LAZY ( 지연 로딩 ) </p>
<ul>
<li>JPQL 에서 만든 SQL을 통해서 조회</li>
<li>LAZY 전략의 경우 추가조회는 발생하지 않습니다.<ul>
<li>그러나 하위 엔티티들을 데리고 작업하다 보면 추가조회로 인해서 N+1 이슈 발생</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="problem">Problem</h3>
<blockquote>
<p>먼저 ManyToOne 의 경우 기본전략은 EAGER
OneToMany 의 경우에는 LAZY가 Default 값이다.</p>
</blockquote>
<pre><code class="language-java">@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Comment extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String comment;

    @ManyToOne
    @ToString.Exclude
    private Review review;
}</code></pre>
<p>위와 같은 Comment 라는 Entity를 만든다.</p>
<pre><code class="language-java">@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Review extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    private float score;

    @ManyToOne
    private User user;

    @ManyToOne
    private Book book;

    @OneToMany
    @JoinColumn(name = &quot;review_id&quot;)
    private List&lt;Comment&gt; comments;

}</code></pre>
<p>그다음 Review 라는 Entity도 생성한다.</p>
<pre><code class="language-java">insert into review(`id`,`title`,`content`,`score`,`user_id`,`book_id`) values (1,&#39;내 인생을 바꾼책&#39;,&#39;너무너무 좋았어요&#39;,5.0 ,1,1);

insert into review(`id`,`title`,`content`,`score`,`user_id`,`book_id`) values (2,&#39;조금 속도가 빨라요&#39;,&#39;별로였던거 같기도하고?&#39;,3.0,2,2);

insert into comment(`id`,`comment`,`review_id`) values (1, &#39;저도 좋았어요&#39;,1);

insert into comment(`id`,`comment`,`review_id`) values (2, &#39;저는 그냥 그랬어요&#39;,1);
insert into comment(`id`,`comment`,`review_id`) values (3, &#39;아니 왜만든거에요???&#39;,2);</code></pre>
<p>Test를 위해서 Data.sql 에 위의 쿼리문장을 추가해준다.
( 나머지의 Value 들은 JPA 포스팅을 천천히 따라오다보면 된다 ) </p>
<hr>
<h3 id="test">Test</h3>
<pre><code class="language-java">    @Test
    @Transactional
    void reviewTest() {
        List&lt;Review&gt; reviews = reviewRepository.findAll();

        reviews.forEach(System.out::println);
    }</code></pre>
<blockquote>
<p>그냥 단순하게 위와 같은 테스트를 진행한다</p>
</blockquote>
<pre><code class="language-java">Hibernate: 
    select
        review0_.id as id1_6_,
        review0_.created_at as created_2_6_,
        review0_.updated_at as updated_3_6_,
        review0_.book_id as book_id7_6_,
        review0_.content as content4_6_,
        review0_.score as score5_6_,
        review0_.title as title6_6_,
        review0_.user_id as user_id8_6_ 
    from
        review review0_
Hibernate: 
    select
        book0_.id as id1_1_0_,
        book0_.created_at as created_2_1_0_,
        book0_.updated_at as updated_3_1_0_,
        book0_.author_id as author_i4_1_0_,
        book0_.category as category5_1_0_,
        book0_.deleted as deleted6_1_0_,
        book0_.name as name7_1_0_,
        book0_.publisher_id as publishe9_1_0_,
        book0_.status as status8_1_0_,
        publisher1_.id as id1_5_1_,
        publisher1_.created_at as created_2_5_1_,
        publisher1_.updated_at as updated_3_5_1_,
        publisher1_.name as name4_5_1_,
        bookreview2_.id as id1_3_2_,
        bookreview2_.created_at as created_2_3_2_,
        bookreview2_.updated_at as updated_3_3_2_,
        bookreview2_.average_review_score as average_4_3_2_,
        bookreview2_.book_id as book_id6_3_2_,
        bookreview2_.review_count as review_c5_3_2_ 
    from
        book book0_ 
    left outer join
        publisher publisher1_ 
            on book0_.publisher_id=publisher1_.id 
    left outer join
        book_review_info bookreview2_ 
            on book0_.id=bookreview2_.book_id 
    where
        book0_.id=? 
        and (
            book0_.deleted = 0
        )
Hibernate: 
    select
        user0_.id as id1_7_0_,
        user0_.created_at as created_2_7_0_,
        user0_.updated_at as updated_3_7_0_,
        user0_.company_city as company_4_7_0_,
        user0_.company_address_detail as company_5_7_0_,
        user0_.company_district as company_6_7_0_,
        user0_.company_zip_code as company_7_7_0_,
        user0_.email as email8_7_0_,
        user0_.home_city as home_cit9_7_0_,
        user0_.home_address_detail as home_ad10_7_0_,
        user0_.home_district as home_di11_7_0_,
        user0_.home_zip_code as home_zi12_7_0_,
        user0_.name as name13_7_0_,
        userhistor1_.user_id as user_id14_8_1_,
        userhistor1_.id as id1_8_1_,
        userhistor1_.id as id1_8_2_,
        userhistor1_.created_at as created_2_8_2_,
        userhistor1_.updated_at as updated_3_8_2_,
        userhistor1_.company_city as company_4_8_2_,
        userhistor1_.company_address_detail as company_5_8_2_,
        userhistor1_.company_district as company_6_8_2_,
        userhistor1_.company_zip_code as company_7_8_2_,
        userhistor1_.email as email8_8_2_,
        userhistor1_.home_city as home_cit9_8_2_,
        userhistor1_.home_address_detail as home_ad10_8_2_,
        userhistor1_.home_district as home_di11_8_2_,
        userhistor1_.home_zip_code as home_zi12_8_2_,
        userhistor1_.name as name13_8_2_,
        userhistor1_.user_id as user_id14_8_2_ 
    from
        user user0_ 
    left outer join
        user_history userhistor1_ 
            on user0_.id=userhistor1_.user_id 
    where
        user0_.id=?
Hibernate: 
    select
        book0_.id as id1_1_0_,
        book0_.created_at as created_2_1_0_,
        book0_.updated_at as updated_3_1_0_,
        book0_.author_id as author_i4_1_0_,
        book0_.category as category5_1_0_,
        book0_.deleted as deleted6_1_0_,
        book0_.name as name7_1_0_,
        book0_.publisher_id as publishe9_1_0_,
        book0_.status as status8_1_0_,
        publisher1_.id as id1_5_1_,
        publisher1_.created_at as created_2_5_1_,
        publisher1_.updated_at as updated_3_5_1_,
        publisher1_.name as name4_5_1_,
        bookreview2_.id as id1_3_2_,
        bookreview2_.created_at as created_2_3_2_,
        bookreview2_.updated_at as updated_3_3_2_,
        bookreview2_.average_review_score as average_4_3_2_,
        bookreview2_.book_id as book_id6_3_2_,
        bookreview2_.review_count as review_c5_3_2_ 
    from
        book book0_ 
    left outer join
        publisher publisher1_ 
            on book0_.publisher_id=publisher1_.id 
    left outer join
        book_review_info bookreview2_ 
            on book0_.id=bookreview2_.book_id 
    where
        book0_.id=? 
        and (
            book0_.deleted = 0
        )
Hibernate: 
    select
        user0_.id as id1_7_0_,
        user0_.created_at as created_2_7_0_,
        user0_.updated_at as updated_3_7_0_,
        user0_.company_city as company_4_7_0_,
        user0_.company_address_detail as company_5_7_0_,
        user0_.company_district as company_6_7_0_,
        user0_.company_zip_code as company_7_7_0_,
        user0_.email as email8_7_0_,
        user0_.home_city as home_cit9_7_0_,
        user0_.home_address_detail as home_ad10_7_0_,
        user0_.home_district as home_di11_7_0_,
        user0_.home_zip_code as home_zi12_7_0_,
        user0_.name as name13_7_0_,
        userhistor1_.user_id as user_id14_8_1_,
        userhistor1_.id as id1_8_1_,
        userhistor1_.id as id1_8_2_,
        userhistor1_.created_at as created_2_8_2_,
        userhistor1_.updated_at as updated_3_8_2_,
        userhistor1_.company_city as company_4_8_2_,
        userhistor1_.company_address_detail as company_5_8_2_,
        userhistor1_.company_district as company_6_8_2_,
        userhistor1_.company_zip_code as company_7_8_2_,
        userhistor1_.email as email8_8_2_,
        userhistor1_.home_city as home_cit9_8_2_,
        userhistor1_.home_address_detail as home_ad10_8_2_,
        userhistor1_.home_district as home_di11_8_2_,
        userhistor1_.home_zip_code as home_zi12_8_2_,
        userhistor1_.name as name13_8_2_,
        userhistor1_.user_id as user_id14_8_2_ 
    from
        user user0_ 
    left outer join
        user_history userhistor1_ 
            on user0_.id=userhistor1_.user_id 
    where
        user0_.id=?
Hibernate: 
    select
        comments0_.review_id as review_i5_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.created_at as created_2_4_1_,
        comments0_.updated_at as updated_3_4_1_,
        comments0_.comment as comment4_4_1_,
        comments0_.review_id as review_i5_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
Review(super=BaseEntity(createdAt=2022-08-07T18:02:12.703757, updatedAt=2022-08-07T18:02:12.703757), id=1, title=내 인생을 바꾼책, content=너무너무 좋았어요, score=5.0, user=User(super=BaseEntity(createdAt=2022-08-07T18:02:12, updatedAt=2022-08-07T18:02:12), id=1, name=martin, email=martin@fastcampus.com, homeAddress=null, companyAddress=null), book=Book(super=BaseEntity(createdAt=2022-08-07T18:02:12.690928, updatedAt=2022-08-07T18:02:12.690928), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false, status=BookStatus(code=100, description=판매종료)), comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:02:12.710414, updatedAt=2022-08-07T18:02:12.710414), id=1, comment=저도 좋았어요), Comment(super=BaseEntity(createdAt=2022-08-07T18:02:12.716371, updatedAt=2022-08-07T18:02:12.716371), id=2, comment=저는 그냥 그랬어요)])
Hibernate: 
    select
        comments0_.review_id as review_i5_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.created_at as created_2_4_1_,
        comments0_.updated_at as updated_3_4_1_,
        comments0_.comment as comment4_4_1_,
        comments0_.review_id as review_i5_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
Review(super=BaseEntity(createdAt=2022-08-07T18:02:12.707035, updatedAt=2022-08-07T18:02:12.707035), id=2, title=조금 속도가 빨라요, content=별로였던거 같기도하고?, score=3.0, user=User(super=BaseEntity(createdAt=2022-08-07T18:02:12, updatedAt=2022-08-07T18:02:12), id=2, name=dennis, email=denis@fastcampus.com, homeAddress=null, companyAddress=null), book=Book(super=BaseEntity(createdAt=2022-08-07T18:02:12.694357, updatedAt=2022-08-07T18:02:12.694357), id=2, name=Spring Security 초격차 패키지, category=null, authorId=null, deleted=false, status=BookStatus(code=200, description=판매중)), comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:02:12.719883, updatedAt=2022-08-07T18:02:12.719883), id=3, comment=아니 왜만든거에요???)])
2022-08-07 18:02:15.352  INFO 4928 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@68c9133c testClass = ReviewRepositoryTest, testInstance = com.example.jpa.bookmanager.repository.ReviewRepositoryTest@24b38e8f, testMethod = reviewTest@ReviewRepositoryTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@7a35b0f5 testClass = ReviewRepositoryTest, locations = &#39;{}&#39;, classes = &#39;{class com.example.jpa.bookmanager.BookmanagerApplication}&#39;, contextInitializerClasses = &#39;[]&#39;, activeProfiles = &#39;{}&#39;, propertySourceLocations = &#39;{}&#39;, propertySourceProperties = &#39;{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}&#39;, contextCustomizers = set[org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@41fbdac4, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5f058f00, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1ab3a8c8, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@479d31f3, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@6aba2b86, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@24273305], resourceBasePath = &#39;src/main/webapp&#39;, contextLoader = &#39;org.springframework.boot.test.context.SpringBootContextLoader&#39;, parent = [null]], attributes = map[&#39;org.springframework.test.context.web.ServletTestExecutionListener.activateListener&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents&#39; -&gt; false]]</code></pre>
<p>위의 결과처럼 아주 난리부르스가 난다....</p>
<p>그렇다면 어떻게 수정을 해주어야 불필요한 쿼리들과 비용을 줄일 수 있을까...???</p>
<hr>
<h2 id="solution">Solution</h2>
<pre><code class="language-java">@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Review extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    private float score;

    @ManyToOne(fetch = FetchType.LAZY)
    @ToString.Exclude
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @ToString.Exclude
    private Book book;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;review_id&quot;)
    private List&lt;Comment&gt; comments;

}</code></pre>
<blockquote>
<p>불필요한 ToString 의 중복을 제거하고  (  @ ToString.Exclude ) 
조회 순간마다 엔티티들을 조회하는것이아닌 한번의 조회로 모든결과를 내준다
( LAZY 지연 로딩) </p>
</blockquote>
<pre><code class="language-java">    @Query(&quot;select distinct r from Review r join fetch r.comments&quot;)
    List&lt;Review&gt;  findAllByFetchJoin();

    @EntityGraph(attributePaths = &quot;comments&quot;)
    @Query(&quot;select r from Review r&quot;)
    List&lt;Review&gt; findAllByEntityGraph();</code></pre>
<blockquote>
<p>그리고 Repository 에 위와 같은 쿼리메소드를 생성해준다.
Fetch 와 Entity Graph를 통해 N+  1 이슈를 해결해준다.
( Fetch 의 경우 Inner Join // Entity Graph 는 left Outer Join )</p>
</blockquote>
<h3 id="fetch-join">Fetch Join</h3>
<pre><code class="language-java"> @Test
    @Transactional
    void reviewTest() {
        List&lt;Review&gt; reviews = reviewRepository.findAllByFetchJoin();
        reviews.forEach(System.out::println);
    }</code></pre>
<p>  위의 테스트를 실행후 결과</p>
<pre><code class="language-java"> 2022-08-07 18:08:30.396  INFO 10868 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@68c9133c testClass = ReviewRepositoryTest, testInstance = com.example.jpa.bookmanager.repository.ReviewRepositoryTest@43e7f104, testMethod = reviewTest@ReviewRepositoryTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@7a35b0f5 testClass = ReviewRepositoryTest, locations = &#39;{}&#39;, classes = &#39;{class com.example.jpa.bookmanager.BookmanagerApplication}&#39;, contextInitializerClasses = &#39;[]&#39;, activeProfiles = &#39;{}&#39;, propertySourceLocations = &#39;{}&#39;, propertySourceProperties = &#39;{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}&#39;, contextCustomizers = set[org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@41fbdac4, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5f058f00, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1ab3a8c8, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@479d31f3, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@6aba2b86, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@24273305], resourceBasePath = &#39;src/main/webapp&#39;, contextLoader = &#39;org.springframework.boot.test.context.SpringBootContextLoader&#39;, parent = [null]], attributes = map[&#39;org.springframework.test.context.web.ServletTestExecutionListener.activateListener&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents&#39; -&gt; false]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4c02899]; rollback [true]
Hibernate: 
    select
        distinct review0_.id as id1_6_0_,
        comments1_.id as id1_4_1_,
        review0_.created_at as created_2_6_0_,
        review0_.updated_at as updated_3_6_0_,
        review0_.book_id as book_id7_6_0_,
        review0_.content as content4_6_0_,
        review0_.score as score5_6_0_,
        review0_.title as title6_6_0_,
        review0_.user_id as user_id8_6_0_,
        comments1_.created_at as created_2_4_1_,
        comments1_.updated_at as updated_3_4_1_,
        comments1_.comment as comment4_4_1_,
        comments1_.review_id as review_i5_4_1_,
        comments1_.review_id as review_i5_4_0__,
        comments1_.id as id1_4_0__ 
    from
        review review0_ 
    inner join
        comment comments1_ 
            on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2022-08-07T18:08:28.166819, updatedAt=2022-08-07T18:08:28.166819), id=1, title=내 인생을 바꾼책, content=너무너무 좋았어요, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:08:28.171181, updatedAt=2022-08-07T18:08:28.171181), id=1, comment=저도 좋았어요), Comment(super=BaseEntity(createdAt=2022-08-07T18:08:28.176519, updatedAt=2022-08-07T18:08:28.176519), id=2, comment=저는 그냥 그랬어요)])
Review(super=BaseEntity(createdAt=2022-08-07T18:08:28.168033, updatedAt=2022-08-07T18:08:28.168033), id=2, title=조금 속도가 빨라요, content=별로였던거 같기도하고?, score=3.0, comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:08:28.179968, updatedAt=2022-08-07T18:08:28.179968), id=3, comment=아니 왜만든거에요???)])
2022-08-07 18:08:30.598  INFO 10868 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@68c9133c testClass = ReviewRepositoryTest, testInstance = com.example.jpa.bookmanager.repository.ReviewRepositoryTest@43e7f104, testMethod = reviewTest@ReviewRepositoryTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@7a35b0f5 testClass = ReviewRepositoryTest, locations = &#39;{}&#39;, classes = &#39;{class com.example.jpa.bookmanager.BookmanagerApplication}&#39;, contextInitializerClasses = &#39;[]&#39;, activeProfiles = &#39;{}&#39;, propertySourceLocations = &#39;{}&#39;, propertySourceProperties = &#39;{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}&#39;, contextCustomizers = set[org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@41fbdac4, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5f058f00, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1ab3a8c8, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@479d31f3, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@6aba2b86, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@24273305], resourceBasePath = &#39;src/main/webapp&#39;, contextLoader = &#39;org.springframework.boot.test.context.SpringBootContextLoader&#39;, parent = [null]], attributes = map[&#39;org.springframework.test.context.web.ServletTestExecutionListener.activateListener&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents&#39; -&gt; false]]</code></pre>
<h3 id="entity-graph">Entity Graph</h3>
<pre><code class="language-java"> @Test
    @Transactional
    void reviewTest() {
        List&lt;Review&gt; reviews = reviewRepository.findAllByEntityGraph();
        reviews.forEach(System.out::println);
    }</code></pre>
<p>  위의 테스틑 실행후 결과</p>
<pre><code class="language-java"> 2022-08-07 18:10:49.099  INFO 9404 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@68c9133c testClass = ReviewRepositoryTest, testInstance = com.example.jpa.bookmanager.repository.ReviewRepositoryTest@43e7f104, testMethod = reviewTest@ReviewRepositoryTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@7a35b0f5 testClass = ReviewRepositoryTest, locations = &#39;{}&#39;, classes = &#39;{class com.example.jpa.bookmanager.BookmanagerApplication}&#39;, contextInitializerClasses = &#39;[]&#39;, activeProfiles = &#39;{}&#39;, propertySourceLocations = &#39;{}&#39;, propertySourceProperties = &#39;{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}&#39;, contextCustomizers = set[org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@41fbdac4, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5f058f00, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1ab3a8c8, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@479d31f3, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@6aba2b86, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@24273305], resourceBasePath = &#39;src/main/webapp&#39;, contextLoader = &#39;org.springframework.boot.test.context.SpringBootContextLoader&#39;, parent = [null]], attributes = map[&#39;org.springframework.test.context.web.ServletTestExecutionListener.activateListener&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents&#39; -&gt; false]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4c02899]; rollback [true]
Hibernate: 
    select
        review0_.id as id1_6_0_,
        comments1_.id as id1_4_1_,
        review0_.created_at as created_2_6_0_,
        review0_.updated_at as updated_3_6_0_,
        review0_.book_id as book_id7_6_0_,
        review0_.content as content4_6_0_,
        review0_.score as score5_6_0_,
        review0_.title as title6_6_0_,
        review0_.user_id as user_id8_6_0_,
        comments1_.created_at as created_2_4_1_,
        comments1_.updated_at as updated_3_4_1_,
        comments1_.comment as comment4_4_1_,
        comments1_.review_id as review_i5_4_1_,
        comments1_.review_id as review_i5_4_0__,
        comments1_.id as id1_4_0__ 
    from
        review review0_ 
    left outer join
        comment comments1_ 
            on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2022-08-07T18:10:46.270307, updatedAt=2022-08-07T18:10:46.270307), id=1, title=내 인생을 바꾼책, content=너무너무 좋았어요, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:10:46.272685, updatedAt=2022-08-07T18:10:46.272685), id=1, comment=저도 좋았어요), Comment(super=BaseEntity(createdAt=2022-08-07T18:10:46.274947, updatedAt=2022-08-07T18:10:46.274947), id=2, comment=저는 그냥 그랬어요)])
Review(super=BaseEntity(createdAt=2022-08-07T18:10:46.271508, updatedAt=2022-08-07T18:10:46.271508), id=2, title=조금 속도가 빨라요, content=별로였던거 같기도하고?, score=3.0, comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:10:46.281339, updatedAt=2022-08-07T18:10:46.281339), id=3, comment=아니 왜만든거에요???)])
2022-08-07 18:10:49.301  INFO 9404 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@68c9133c testClass = ReviewRepositoryTest, testInstance = com.example.jpa.bookmanager.repository.ReviewRepositoryTest@43e7f104, testMethod = reviewTest@ReviewRepositoryTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@7a35b0f5 testClass = ReviewRepositoryTest, locations = &#39;{}&#39;, classes = &#39;{class com.example.jpa.bookmanager.BookmanagerApplication}&#39;, contextInitializerClasses = &#39;[]&#39;, activeProfiles = &#39;{}&#39;, propertySourceLocations = &#39;{}&#39;, propertySourceProperties = &#39;{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}&#39;, contextCustomizers = set[org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@41fbdac4, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5f058f00, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1ab3a8c8, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@479d31f3, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@6aba2b86, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@24273305], resourceBasePath = &#39;src/main/webapp&#39;, contextLoader = &#39;org.springframework.boot.test.context.SpringBootContextLoader&#39;, parent = [null]], attributes = map[&#39;org.springframework.test.context.web.ServletTestExecutionListener.activateListener&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder&#39; -&gt; true, &#39;org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents&#39; -&gt; false]]</code></pre>
<blockquote>
<p>Feth Join 과 Entity Graph의 차이는 Inner Join 과 left Outer Join 말고는 없다.</p>
</blockquote>
<hr>
<h2 id="">+@</h2>
<p>우리가 기본적으로 JPARepository 에서 findAll()을 재정의해서 사용할 수 있다.</p>
<pre><code class="language-java"> @EntityGraph(attributePaths = &quot;comments&quot;)
    List&lt;Review&gt; findAll();</code></pre>
<p>  위와 같이 정의 해준후 테스트를 실행하면 동일한 결과가 나온다.</p>
<pre><code class="language-java">  Hibernate: 
    select
        review0_.id as id1_6_0_,
        comments1_.id as id1_4_1_,
        review0_.created_at as created_2_6_0_,
        review0_.updated_at as updated_3_6_0_,
        review0_.book_id as book_id7_6_0_,
        review0_.content as content4_6_0_,
        review0_.score as score5_6_0_,
        review0_.title as title6_6_0_,
        review0_.user_id as user_id8_6_0_,
        comments1_.created_at as created_2_4_1_,
        comments1_.updated_at as updated_3_4_1_,
        comments1_.comment as comment4_4_1_,
        comments1_.review_id as review_i5_4_1_,
        comments1_.review_id as review_i5_4_0__,
        comments1_.id as id1_4_0__ 
    from
        review review0_ 
    left outer join
        comment comments1_ 
            on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2022-08-07T18:13:06.779017, updatedAt=2022-08-07T18:13:06.779017), id=1, title=내 인생을 바꾼책, content=너무너무 좋았어요, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:13:06.784070, updatedAt=2022-08-07T18:13:06.784070), id=1, comment=저도 좋았어요), Comment(super=BaseEntity(createdAt=2022-08-07T18:13:06.786952, updatedAt=2022-08-07T18:13:06.786952), id=2, comment=저는 그냥 그랬어요)])
Review(super=BaseEntity(createdAt=2022-08-07T18:13:06.782331, updatedAt=2022-08-07T18:13:06.782331), id=2, title=조금 속도가 빨라요, content=별로였던거 같기도하고?, score=3.0, comments=[Comment(super=BaseEntity(createdAt=2022-08-07T18:13:06.788297, updatedAt=2022-08-07T18:13:06.788297), id=3, comment=아니 왜만든거에요???)])</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SPRING] JPA - Embedded / Embeddable]]></title>
            <link>https://velog.io/@french_ruin/SPRING-JPA-Embedded-Embeddable</link>
            <guid>https://velog.io/@french_ruin/SPRING-JPA-Embedded-Embeddable</guid>
            <pubDate>Sat, 06 Aug 2022 12:57:48 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>우리가 만약에 사용자의 주소를 저장해야 한다고 생각해 봅시다.
물론 Enum  으로 제공할 수 도 있습니다.
하지만 Embedded Type 으로 객체를 만들어서 사용한다면???</p>
</blockquote>
<hr>
<h2 id="embeddable">Embeddable</h2>
<pre><code class="language-java">@Embeddable
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {

    private String city;

    private String district;

    private String detail;

    private String zipCode;

}</code></pre>
<p>위와 같은 Address 라는 클래스를 생성해 줍니다.</p>
<ul>
<li>@Embeddable 은 Embedded 가 가능하게 한다는 의미.</li>
</ul>
<hr>
<h2 id="embedded">Embedded</h2>
<pre><code class="language-java">    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = &quot;city&quot;,column = @Column(name = &quot;home_city&quot;)),
            @AttributeOverride(name = &quot;district&quot;,column = @Column(name = &quot;home_district&quot;)),
            @AttributeOverride(name = &quot;detail&quot;,column = @Column(name = &quot;home_address_detail&quot;)),
            @AttributeOverride(name = &quot;zipCode&quot;,column = @Column(name = &quot;home_zip_code&quot;)),
    }
    )
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = &quot;city&quot;,column = @Column(name = &quot;company_city&quot;)),
            @AttributeOverride(name = &quot;district&quot;,column = @Column(name = &quot;company_district&quot;)),
            @AttributeOverride(name = &quot;detail&quot;,column = @Column(name = &quot;company_address_detail&quot;)),
            @AttributeOverride(name = &quot;zipCode&quot;,column = @Column(name = &quot;company_zip_code&quot;)),
    }
    )
    private Address companyAddress;</code></pre>
<ul>
<li>User의 HomeAddress 와 CompanyAddress를 나타내고 싶습니다</li>
</ul>
<blockquote>
<p>우리가 @Embedded 어노테이션을 사용해서 Address 클래스를 Embedded 해올수 있으며,
@AttributeOverrides로 컬럼들의 컬럼명을 설정해줄수 있습니다.
( Ex - Address 클래스에 존재하는 city라는 컬럼을 HomeAddress 컬럼에서는 home_city로 컬럼명을 받겠다 )</p>
</blockquote>
<hr>
<h2 id="test-1">Test 1</h2>
<pre><code class="language-java">   @DisplayName(&quot;1. embedTest&quot;)
    @Test
    void test_1(){

        User user = new User();
        user.setName(&quot;steve&quot;);
        user.setHomeAddress(new Address(&quot;서울시&quot;, &quot;강서구&quot;, &quot;코딩아파트&quot;, &quot;763487&quot;));
        user.setCompanyAddress(new Address(&quot;서울시&quot;, &quot;마곡동&quot;, &quot;자바아파트&quot;, &quot;347894&quot;));

        userRepository.save(user);

        userRepository.findAll().forEach(System.out::println);

    }</code></pre>
<blockquote>
<p>user 를 생성한후 , user의 HomeAddress와 CompanyAddress 를 생성자로 받아서 입력하여 
 UserRespository에 저장해주고 출력해줍니다.</p>
</blockquote>
<blockquote>
<pre><code> homeAddress=Address(city=서울시, district=강서구, detail=코딩아파트, zipCode=763487), 
 companyAddress=Address(city=서울시, district=마곡동, detail=자바아파트, zipCode=347894))</code></pre></blockquote>
<pre><code>출력결과는 다음과 같이 나오게 됩니다.

---
## Test 2
```java
  @DisplayName(&quot;1. embedTest&quot;)
    @Test
    void test_1(){
        User user1 = new User();
        user1.setName(&quot;joshua&quot;);
        user1.setHomeAddress(null);
        user1.setCompanyAddress(null);
        userRepository.save(user1);

        User user2 = new User();
        user2.setName(&quot;jordan&quot;);
        user2.setHomeAddress(new Address());
        user2.setCompanyAddress(new Address());
        userRepository.save(user2);


        userRepository.findAll().forEach(System.out::println);

    }</code></pre><blockquote>
<p>그렇다면 Address의 값들을  null로 생성한다면</p>
</blockquote>
<p>1) Address Set을 Null 값 입력
2) Address 인자없는 생성자를 Set</p>
<p>Result --</p>
<pre><code class="language-java">    homeAddress=null, companyAddress=null // 1

    homeAddress=Address(city=null, district=null, detail=null, zipCode=null),
    companyAddress=Address(city=null, district=null, detail=null, zipCode=null //2</code></pre>
<p>위와 같이 다른 결과값을 도출하게 됩니다.</p>
<hr>
<h1 id="so-what">So What?</h1>
<blockquote>
<p>과연 정보를 담을때 Null 값을 어떻게 처리해야할지?</p>
</blockquote>
<blockquote>
<p>Address  와 같이 Home 과 Company를 구분지어 생성할수 있지만
@AttributeOverride 를 통해 재정의 한다고는 하지만 코드가 지저분하지는 않은지?</p>
</blockquote>
<p>위와 같은 고민들을 생각해가며 로직을 짜가야 하는것이 맞다고 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SPRING] JPA - Converter]]></title>
            <link>https://velog.io/@french_ruin/SPRING-JPA-Converter</link>
            <guid>https://velog.io/@french_ruin/SPRING-JPA-Converter</guid>
            <pubDate>Sat, 06 Aug 2022 12:06:09 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="converter">Converter</h2>
<blockquote>
<p>컨버터를 사용하면 Entity의 Data를 변환해서 DB에 저장할 수 있습니다.</p>
<blockquote>
<p>ENUM 이나 Boolean 값에 따라 0 이나 1 대신 Y 또는 N을 저장하고 싶다면 Converter를 사용하면됩니다.</p>
</blockquote>
</blockquote>
<hr>
<h2 id="status">Status</h2>
<pre><code class="language-java">@Data
public class BookStatus {
    private int code;
    private String description;

    public BookStatus(int code) {
        this.code = code;
        this.description = parseDescription(code);
    }

    private String parseDescription(int code) {
        switch (code) {
            case 100:
                return &quot;판매종료&quot;;
            case 200:
                return &quot;판매중&quot;;
            case 300:
                return &quot;판매보류&quot;;
            default:
                return &quot;미지원&quot;;
        }
    }

    public boolean isDisplayed() {
        return code == 200;
    }
}</code></pre>
<blockquote>
<p>위와 같이 BookStatus라는 현재 책의 판매 상태를 나타내는 클래스를 생성합니다.
(Getter 와 Setter를 사용하기 위해 @DATA 어노테이션 사용)</p>
</blockquote>
<hr>
<h3 id="converter-class">Converter class</h3>
<pre><code class="language-java">@Converter
public class BookStatusConverter implements AttributeConverter&lt;BookStatus, Integer&gt; {

    @Override
    public Integer convertToDatabaseColumn(BookStatus attribute) {
        return null;
    }

    @Override
    public BookStatus convertToEntityAttribute(Integer dbData) {
        return null;
    }
}</code></pre>
<blockquote>
<p>먼저 converter를 사용할 수 있도록 BookStatusConverter라는 클래스를 만든후
AttributeConverter&lt;BookStatus, Integer&gt; 를 implements 한후 
convertToDatabaseColumn 와 convertToEntityAttribute 메소드를 정의  해 주어야 합니다.</p>
</blockquote>
<pre><code class="language-java">    @Override
    public Integer convertToDatabaseColumn(BookStatus attribute) {
        return attribute.getCode();
    }

    @Override
    public BookStatus convertToEntityAttribute(Integer dbData) {
        return dbData != null ? new BookStatus(dbData) : null;
    }</code></pre>
<p>  @Converter 어노테이션과 함께 
  위와 같은 로직으로 작성해줍니다.</p>
<p>  그 이후에 Converter를 적용할 컬럼에</p>
<pre><code class="language-java">      @Convert(converter = BookStatusConverter.class)
    private BookStatus status; // 판매상태</code></pre>
<h2 id="convert-어노테이션과-converter-클래스를-적용시켜줍니다">  @Convert 어노테이션과 Converter 클래스를 적용시켜줍니다.</h2>
<h2 id="result">Result</h2>
<pre><code class="language-java">    @DisplayName(&quot;1. converterTest&quot;)
    @Test
    void test_5(){

        bookRepository.findAll().forEach(System.out::println);
    }
</code></pre>
<p>  위와 같은 Test생성하고 실행시키면</p>
<pre><code class="language-java">  status=BookStatus(code=100, description=판매종료)
  status=BookStatus(code=200, description=판매중)</code></pre>
<blockquote>
<p>위의 결과로 결과가 저장 되어지는것을 확인 할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - Phone Book - v2]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-v2</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-v2</guid>
            <pubDate>Sat, 06 Aug 2022 09:19:19 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>이전에 Phone Book - v1 을 통해서 기본적인 전화번호부 조회 삭제 로직을 만들어 보았습니다.
이번에는 파일을 읽고 조회하고 삭제하고 추가하여 저장하는 것까지 해볼까합니다.</p>
<blockquote>
<p>데이터의 배열을 이름순으로 정렬</p>
</blockquote>
</blockquote>
<hr>
<h2 id="main--">Main ( )</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

#define CAPACITY 100
#define BUFFER_SIZE 20

char *names[CAPACITY];
char *numbers[CAPACITY];
int n = 0;

void add();
void find();
void status();
void delete();
void load();
void save();
int search(char *name);

int main() {
    char command[BUFFER_SIZE];
    while (1) {
        printf(&quot;$   &quot;);
        scanf(&quot;%s&quot;, command);

        if ((strcmp(command, &quot;add&quot;)) == 0) {
            add();
        } else if ((strcmp(command, &quot;find&quot;)) == 0) {
            find();
        } else if ((strcmp(command, &quot;delete&quot;)) == 0) {
            delete();
        } else if ((strcmp(command, &quot;status&quot;)) == 0) {
            status();
        } else if ((strcmp(command, &quot;read&quot;)) == 0) {
            load();
        } else if ((strcmp(command, &quot;save&quot;)) == 0) {
            save();
        } else if ((strcmp(command, &quot;exits&quot;)) == 0)
            break;
    }
    return 0;
}</code></pre>
<blockquote>
<p>이전 Phone Book - v1 과 구성은 비슷합니다.
load ( ) 와  save ( ) 함수를 추가하고, 
search  ( )  함수도 추가했습니다. 자세한 사항은 아래에서 설명하겠습니다.</p>
</blockquote>
<hr>
<h3 id="load---">Load  ( )</h3>
<pre><code class="language-c">void load() {
    char fileName[BUFFER_SIZE];
    char namesTmp[BUFFER_SIZE];
    char numbersTmp[BUFFER_SIZE];

    scanf(&quot;%s&quot;, fileName);

    FILE *fp = fopen(fileName, &quot;r&quot;);
    if (fp == NULL) {
        printf(&quot;Open failed.\n&quot;);
        return;
    }

    while ((fscanf(fp, &quot;%s&quot;, namesTmp)) != EOF) {
        fscanf(fp, &quot;%s&quot;, numbersTmp);
        names[n] = strdup(namesTmp);
        numbers[n] = strdup(numbersTmp);
        n++;
    }
    fclose(fp);
}</code></pre>
<ul>
<li>파일 이름을 입력받을 fileName 배열을 선언<ul>
<li>파일을   &quot; r &quot; 로 읽기모드로 열고, 파일이 없으면 return 시킵니다.</li>
<li>fsacnf ( ) 함수로 파일내부에 이름들을 모두 읽고 namesTmp 배열에 이름을 모두 넣을 때까지 while 문을 실행<ul>
<li>또 fscanf ( ) 함수로 파일내부에있는 번호들을 numbersTmp 배열에 순서대로 넣어준다.<ul>
<li>names 와 numbers 포인터 배열에 namesTmp 와 numbersTmp 배열을 복사하여 순서대로 넣어준다.</li>
<li>마지막으로 n 을 증가시켜 갯수를 증가시킨다.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>마지막으로 fclose( ) 로 파일을 닫아준다. </li>
<li><em>( 파일을 열었으면 닫아줘야 한다 )*</em></li>
</ul>
<hr>
<h3 id="add--">Add ( )</h3>
<pre><code class="language-c">void add()
{
    char namesTmp[BUFFER_SIZE], numbersTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, namesTmp);
    scanf(&quot;%s&quot;, numbersTmp);

    int i = n-1;
    while (i &gt;= 0 &amp;&amp; strcmp(names[i], namesTmp) &gt; 0) {
        names[i + 1] = names[i];
        numbers[i + 1] = numbers[i];
        i--;
    }
    names[i + 1] = strdup(namesTmp);
    numbers[i + 1] = strdup(numbersTmp);

    n++;
    printf(&quot;%s was added successfully.\n&quot;, namesTmp);
}</code></pre>
<ul>
<li>추가할 이름과 번호를 입력받을 namesTmp 와 numbersTmp  배열을 선언합니다<ul>
<li>int i  = n -1 ; &gt;&gt; 배열은 인덱스가 0 부터 시작하기때문에 총 인원에서 - 1 
( ex - 10명인 경우 배열은 0 ~ 9 가 됨 )</li>
<li>while 문을 통해서 입력받은 이름을 넣어줄 공간을 확보한다.<ul>
<li>strcmp ( ) 는 단순히 문자열을 비교하는것이 아니라 , strcmp ( x , y )   &gt; x가 y 보다 더 크다면 양수를 return 하고 더 작으면 음수를 return</li>
</ul>
</li>
</ul>
</li>
<li>그 다음 입력받은 이름과 번호를 names 와 numbers 포인터배열에 넣어준다<ul>
<li>n ++ 로 인원 증가</li>
</ul>
</li>
</ul>
<hr>
<h3 id="delete--">Delete ( )</h3>
<pre><code class="language-c">void delete()
{
    char namesTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, namesTmp);

    int index = search(namesTmp);
    if (index == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.\n&quot;, namesTmp);
        return;
    }
    int j = index;
    for (; j &lt; n-1 ; j++) {
        names[j] = names[j + 1];
        numbers[j] = numbers[j + 1];
    }
    n--;
    printf(&quot;&#39;%s&#39; was deleted successfully.  \n&quot;, namesTmp);
}</code></pre>
<ul>
<li><p>삭제할 데이터의 이름을 입력받을 namesTmp 배열 선언</p>
<ul>
<li><p>search ( )</p>
<ul>
<li>```c 
int search(char *name)
{
int i;
for (int i = 0; i &lt; n; i++){<pre><code> if(strcmp(name, names[i]) == 0 ){
    return i;
}</code></pre>}
return -1;
}</li>
<li>strcmp( ) 함수로 만약 입력받은 이름이 names 포인터 배열에 있는 이름과 같다면 i 를 return
그게 아니면 -1 return</li>
</ul>
</li>
<li><p>index 값이 -1 이면 찾을 수 없는것이므로 return</p>
</li>
<li><p>해당되는 데이터를 찾았다면 데이터의 자리를 한칸씩 앞으로 당겨서 덮어 저장한다.</p>
</li>
<li><p>n -- 로 인원 1 감소</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="find--">Find ( )</h3>
<pre><code class="language-c">void find()
{
    char namesTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, namesTmp);
    int index = search(namesTmp);
    if (index == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.  \n&quot;, namesTmp);
    } else
        printf(&quot;%s\n&quot;, numbers[index]);
}</code></pre>
<ul>
<li>찾을 이름을 입력받을 namesTmp 배열 선언</li>
<li>delete ( ) 와 마찬가지로 search( ) 함수로 index값 추출</li>
<li>만약 찾을 수없다면 찾을수 없다는 메시지 출력 
찾으면 해당 index의 numbers를 출력</li>
</ul>
<hr>
<h3 id="status---">Status  ( )</h3>
<pre><code class="language-c">void status()
{
    int i ;
    for (int i = 0; i &lt; n; ++i) {
        printf(&quot;%s %s\n&quot;, names[i], numbers[i]);
    }
    printf(&quot;Total %d persons \n&quot;, n);
}</code></pre>
<ul>
<li>Phone book -v1 과 동일</li>
</ul>
<hr>
<h2 id="complete">Complete</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

#define CAPACITY 100
#define BUFFER_SIZE 20

char *names[CAPACITY];
char *numbers[CAPACITY];
int n = 0;

void add();
void find();
void status();
void delete();
void load();
void save();
int search(char *name);

int main() {
    char command[BUFFER_SIZE];
    while (1) {
        printf(&quot;$   &quot;);
        scanf(&quot;%s&quot;, command);

        if ((strcmp(command, &quot;add&quot;)) == 0) {
            add();
        } else if ((strcmp(command, &quot;find&quot;)) == 0) {
            find();
        } else if ((strcmp(command, &quot;delete&quot;)) == 0) {
            delete();
        } else if ((strcmp(command, &quot;status&quot;)) == 0) {
            status();
        } else if ((strcmp(command, &quot;read&quot;)) == 0) {
            load();
        } else if ((strcmp(command, &quot;save&quot;)) == 0) {
            save();
        } else if ((strcmp(command, &quot;exits&quot;)) == 0)
            break;
    }
    return 0;
}

void load() {
    char fileName[BUFFER_SIZE];
    char namesTmp[BUFFER_SIZE];
    char numbersTmp[BUFFER_SIZE];

    scanf(&quot;%s&quot;, fileName);

    FILE *fp = fopen(fileName, &quot;r&quot;);
    if (fp == NULL) {
        printf(&quot;Open failed.\n&quot;);
        return;
    }

    while ((fscanf(fp, &quot;%s&quot;, namesTmp)) != EOF) {
        fscanf(fp, &quot;%s&quot;, numbersTmp);
        names[n] = strdup(namesTmp);
        numbers[n] = strdup(numbersTmp);
        n++;
    }
    fclose(fp);
}

void save()
{
    int i;
    char fileName[BUFFER_SIZE];
    char tmp[BUFFER_SIZE];

    scanf(&quot;%s&quot;, tmp);
    scanf(&quot;%s&quot;,fileName);

    FILE *fp = fopen(fileName, &quot;w&quot;);
    if (fp == NULL) {
        printf(&quot;Open failed.\n&quot;);
        return;
    }
    for (int i = 0; i &lt; n; ++i) {
        fprintf(fp, &quot;%s %s\n&quot;, names[i], numbers[i]);
    }

    fclose(fp);
}

void add()
{
    char namesTmp[BUFFER_SIZE], numbersTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, namesTmp);
    scanf(&quot;%s&quot;, numbersTmp);

    int i = n-1;
    while (i &gt;= 0 &amp;&amp; strcmp(names[i], namesTmp) &gt; 0) {
        names[i + 1] = names[i];
        numbers[i + 1] = numbers[i];
        i--;
    }
    names[i + 1] = strdup(namesTmp);
    numbers[i + 1] = strdup(numbersTmp);

    n++;
    printf(&quot;%s was added successfully.\n&quot;, namesTmp);
}

void delete()
{
    char namesTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, namesTmp);

    int index = search(namesTmp);
    if (index == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.\n&quot;, namesTmp);
        return;
    }
    int j = index;
    for (; j &lt; n-1 ; j++) {
        names[j] = names[j + 1];
        numbers[j] = numbers[j + 1];
    }
    n--;
    printf(&quot;&#39;%s&#39; was deleted successfully.  \n&quot;, namesTmp);
}

void find()
{
    char namesTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, namesTmp);
    int index = search(namesTmp);
    if (index == -1) {
        printf(&quot;No person named &#39;%s&#39; exists.  \n&quot;, namesTmp);
    } else
        printf(&quot;%s\n&quot;, numbers[index]);
}

int search(char *name)
{
    int i;
    for (int i = 0; i &lt; n; ++i) {
        if (strcmp(name, names[i]) == 0) {
            return i;
        }
    }
    return -1;
}

void status()
{
    int i ;
    for (int i = 0; i &lt; n; ++i) {
        printf(&quot;%s %s\n&quot;, names[i], numbers[i]);
    }
    printf(&quot;Total %d persons \n&quot;, n);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - Phone Book - v1]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-v1</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Phone-Book-v1</guid>
            <pubDate>Fri, 05 Aug 2022 05:39:41 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>간단하게 이름과 번호를 Console  창에 입력하여 저장하고 조회하고, 삭제하는 로직을 만들어 볼까 합니다.</p>
</blockquote>
<hr>
<h3 id="main--">Main ( )</h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;ctype.h&gt;
#include &lt;string.h&gt;

#define CAPACITY 100
#define BUFFER_SIZE 100

char *names[CAPACITY];      /* names */
char *numbers[CAPACITY];    /* phone numbers */
int n = 0;                  /* number of people in phone directory */

void add();
void find();
void delete();
void status();

int main() {

    char command[BUFFER_SIZE];
    while (1) {
        printf(&quot;$   &quot;);
        scanf(&quot;%s&quot;, command);

        if (strcmp(command, &quot;add&quot;) == 0) {
            add();
        } else if (strcmp(command, &quot;find&quot;) == 0) {
            find();
        } else if (strcmp(command, &quot;status&quot;) == 0) {
            status();
        } else if (strcmp(command, &quot;delete&quot;) == 0) {
            delete();
        } else if (strcmp(command, &quot;exit&quot;) == 0)
            break;
    }
    return 0;
};</code></pre>
<ul>
<li>strcmp ( ) 를 통해서 입력한 문자열과 비교하여 맞는 단어가 나오면 0을 리턴하게 한후 ,
그에 맞는 메소드를 실행시키는 로직</li>
</ul>
<hr>
<h3 id="생성--add-">생성 ( Add )</h3>
<pre><code class="language-c">void add(){
    char nameTmp[BUFFER_SIZE], numberTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, nameTmp);
    scanf(&quot;%s&quot;, numberTmp);

    names[n] = strdup(nameTmp);
    numbers[n] = strdup(numberTmp);
    n++;

    printf(&quot;%s was added successfully.\n&quot;, nameTmp);
}</code></pre>
<ul>
<li>지역변수로 nameTmp 와 numberTmp str 배열을 선언하고 
배열에 값을 입력한다.</li>
<li>그다음 strdup ( ) 를 통해서 지역변수 배열의 값을 복사해서 names 와 numbers 포인터 배열에 값을 넣는다.</li>
<li>n++  을 통해서 정보의 갯수를 증가시키고<ul>
<li>nameTmp 에 입력한 이름을 출력한다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="조회--find-">조회 ( Find )</h3>
<pre><code class="language-c">void find(){
    char findTmp[BUFFER_SIZE];

    scanf(&quot;%s&quot;, findTmp);

    int i ;
    for (int i = 0; i &lt; n; ++i) {
        if (strcmp(findTmp, names[i]) == 0) {
            printf(&quot;%s\n&quot;, numbers[i]);
            return;
        }
    }
    printf(&quot;No person name &#39;%s&#39; exists.\n&quot;, findTmp);
}</code></pre>
<ul>
<li><p>조회할 이름을 입력받을 findTmp 배열을 선언</p>
</li>
<li><p>for 문을 통해서 findTmp에 입력한 값과 names 포인터 배열에 있는 값과 동일한지 파악
(동일하면 0 을 return)</p>
<ul>
<li>만약 동일하다면 해당하는 이름의 전화번호를 출력하고, 그게 아니라면 아무도 없다라는 메시지를 출력한다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="상태--status-">상태 ( Status )</h3>
<pre><code class="language-c">void status(){
    int i ;
    for (int i = 0; i &lt; n; ++i) {
        printf(&quot;%s  %s\n&quot;, names[i], numbers[i]);
    }
    printf(&quot;Total %d persons.\n&quot;, n);
}</code></pre>
<ul>
<li>for 문을 돌면서 출력한다.</li>
</ul>
<hr>
<h3 id="삭제--delete-">삭제 ( Delete )</h3>
<pre><code class="language-c">void delete(){
    char deleteTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, deleteTmp);

    int i ;
    for (int i = 0; i &lt; n; ++i) {
        if (strcmp(deleteTmp, names[i]) == 0) {
            names[i] = names[n - 1];
            numbers[i] = numbers[n - 1];
            n--;
            printf(&quot;&#39;%s&#39; was deleted successfully.  \n&quot;, deleteTmp);
            return;
        }
    }
    printf(&quot;No person name &#39;%s&#39; exists.\n&quot;, deleteTmp);
}</code></pre>
<ul>
<li>조회할 이름을 입력받을 deleteTmp 배열을 선언<ul>
<li>deleteTmp 에 입력한 이름과 names에 있는 이름과 같으면 </li>
<li><em>맨뒤에 있는 값을 입력한 이름의 값이 있는곳으로 덮어쓴다.*</em></li>
<li>그다음 성공했다는 메시지를 출력하고, 만약 해당 이름이 존재하지 않는다면 없다는 메시지를 출력한다.</li>
</ul>
</li>
</ul>
<hr>
<h4 id="complete-code">Complete Code</h4>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;ctype.h&gt;
#include &lt;string.h&gt;

#define CAPACITY 100
#define BUFFER_SIZE 100

char *names[CAPACITY];      /* names */
char *numbers[CAPACITY];    /* phone numbers */
int n = 0;                  /* number of people in phone directory */

void add();
void find();
void delete();
void status();

int main() {

    char command[BUFFER_SIZE];
    while (1) {
        printf(&quot;$   &quot;);
        scanf(&quot;%s&quot;, command);

        if (strcmp(command, &quot;add&quot;) == 0) {
            add();
        } else if (strcmp(command, &quot;find&quot;) == 0) {
            find();
        } else if (strcmp(command, &quot;status&quot;) == 0) {
            status();
        } else if (strcmp(command, &quot;delete&quot;) == 0) {
            delete();
        } else if (strcmp(command, &quot;exit&quot;) == 0)
            break;
    }
    return 0;
};

void add(){
    char nameTmp[BUFFER_SIZE], numberTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, nameTmp);
    scanf(&quot;%s&quot;, numberTmp);

    names[n] = strdup(nameTmp);
    numbers[n] = strdup(numberTmp);
    n++;

    printf(&quot;%s was added successfully.\n&quot;, nameTmp);
}

void find(){
    char findTmp[BUFFER_SIZE];

    scanf(&quot;%s&quot;, findTmp);

    int i ;
    for (int i = 0; i &lt; n; ++i) {
        if (strcmp(findTmp, names[i]) == 0) {
            printf(&quot;%s\n&quot;, numbers[i]);
            return;
        }
    }
    printf(&quot;No person name &#39;%s&#39; exists.\n&quot;, findTmp);
}

void status(){
    int i ;
    for (int i = 0; i &lt; n; ++i) {
        printf(&quot;%s  %s\n&quot;, names[i], numbers[i]);
    }
    printf(&quot;Total %d persons.\n&quot;, n);
}

void delete(){
    char deleteTmp[BUFFER_SIZE];
    scanf(&quot;%s&quot;, deleteTmp);

    int i ;
    for (int i = 0; i &lt; n; ++i) {
        if (strcmp(deleteTmp, names[i]) == 0) {
            names[i] = names[n - 1];
            numbers[i] = numbers[n - 1];
            n--;
            printf(&quot;&#39;%s&#39; was deleted successfully.  \n&quot;, deleteTmp);
            return;
        }
    }
    printf(&quot;No person name &#39;%s&#39; exists.\n&quot;, deleteTmp);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - getchar( ) ]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-getchar</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-getchar</guid>
            <pubDate>Fri, 05 Aug 2022 03:22:13 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>이전 <a href="https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-String">String</a> 에서 알게 된것을 토대로 getchar( ) 를 활용해 
예제를 하나 해볼까 합니다.</p>
</blockquote>
<hr>
<h2 id="앞뒤-공백-제거">앞뒤 공백 제거</h2>
<pre><code class="language-c">다음과 같은 문자열을 입력합니다.

  your     capacity    to    enjoy       life

 앞뒤 공백을 제거 하는 로직을 생성하여 아래와 같은 결과를 도출할 것입니다.

 your capacity to enjoy life</code></pre>
<hr>
<h3 id="logic">Logic</h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;ctype.h&gt;

#define BUFFER_SIZE 80


int read_line_with_compression(char compressed[], int limit);

int main() {
    char line[BUFFER_SIZE];
    while (1) {
        printf(&quot;$  &quot;);
        int length = read_line_with_compression(line, BUFFER_SIZE);
        printf(&quot;%s:%d\n&quot;, line, length);
    }
    return 0;
};

int read_line_with_compression(char compressed[], int limit) {
//
    int ch, i = 0;
    while ((ch = getchar()) != &#39;\n&#39;) {
        if (i &lt; limit - 1 &amp;&amp; (!isspace(ch) || i &gt; 0 &amp;&amp; !isspace(compressed[i - 1])))
            compressed[i++] = ch;
    }

    if (i &gt; 0 &amp;&amp; isspace(compressed[i - 1]))
        i--;
    compressed[i] = &#39;\0&#39;;
    return i;

}</code></pre>
<ul>
<li>buffer-size 가 80 인 line 배열을 선언합니다.</li>
<li>read_line_with_compression 함수를 선언합니다.<ul>
<li>getchar( ) 를 활용해 줄바꿈이 입력될때까지 문자를 입력받습니다.</li>
<li>isspace ( ) 를 활용하여 공백이 있는지를 체크하고 공백을 제거합니다.<ul>
<li>그리고 맨 뒤의 공간을 isspace( ) 를 활용해 파악하고, 
문자열 맨끝을 &#39;\0&#39; 로 마무리 지어줍니다.</li>
</ul>
</li>
</ul>
</li>
<li>그다음 i 를 리턴받아 입력된 문자열을 공백을 제거한 상태로
출력하고, 문자열의 길이를 length로 받아 출력합니다.</li>
</ul>
<hr>
<h3 id="result">Result</h3>
<pre><code>$   your     capacity    to    enjoy       life
  your capacity to enjoy life:27</code></pre><p>  위와 같은 결과가 도출 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - String]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-String</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-String</guid>
            <pubDate>Fri, 05 Aug 2022 01:49:11 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="string">String</h2>
<blockquote>
<p>문자열은 char타입의 배열의 각 칸마다 문자 하나씩 저장이 됩니다.</p>
</blockquote>
<pre><code class="language-c">     char str[6];
    str[0] = &quot;h&quot;;
    str[1] = &quot;e&quot;;
    str[2] = &quot;l&quot;;
    str[3] = &quot;l&quot;;
    str[4] = &quot;o&quot;;
    str[5] = &quot;\0&quot;; // 문자열 끝을 표시하는 역할 &amp; 배열의 크기는 문자 길이보다 1커야함</code></pre>
<p>  이렇게 일일이 저장하는 일은 매우 귀찮은 일입니다.
  그래서 C 언어에서는 문자열을 생성하는 편리한 방법을 제공합니다.</p>
<pre><code class="language-c">  char str[] = &quot;hello&quot;;
  or
  char *str = &quot;hello&quot;;</code></pre>
<p>  하지만 <strong>포인터 변수로 선언한 String 은 수정이 불가능</strong>합니다.
  이것을 <strong>String Literal</strong> 이라고 부릅니다.</p>
<hr>
<h2 id="stringh-라이브러리-method">string.h 라이브러리 Method</h2>
<ul>
<li>string.h 라이브러리는 문자열을 다루는 다양한 함수를 제공합니다.<ul>
<li>strcpy = 문자열 복사<ul>
<li>strlen = 문자열의 길이</li>
<li>strcat = 문자열 합치기</li>
<li>strcmp = 문자열 비교</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="문자열-저장">문자열 저장</h2>
<ul>
<li>여러개의 단어들을 포인터를 이용하여 저장을 해봅시다!</li>
</ul>
<pre><code class="language-c">#include &lt;stdio.h&gt;

#define BUFFER_SIZE 100

int main(void) {

    char *words[100];
    int n = 0;
    char buffer[BUFFER_SIZE];

    while (n &lt; 4 &amp;&amp; scanf(&quot;%s&quot;, buffer) != EOF) {
        words[n] = buffer;
        n++;
    }

    for (int i = 0; i &lt; 4; ++i) {
        printf(&quot;%s\n&quot;, words[i]);
    }

    return 0;
};</code></pre>
<blockquote>
<p>words 의 포인터 배열을 선언하고
while 문을 통해서 buffer에 입력한 값을 words에 넣어줍니다.
그다음 for문을 통해서 words의 값들을 순서대로 출력합니다</p>
</blockquote>
<pre><code class="language-c">first
second
third
fourth
//입력값

fourth
fourth
fourth
fourth
//출력값</code></pre>
<p>위와 같은 출력이 됩니다.
이렇게 된 이유는 words에 계속 같은주소에 덮어쓰기가 되었다고 생각하시면 됩니다.</p>
<h3 id="then">then</h3>
<p>그렇다면 입력된값을 words에 복사를 진행한다면? </p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

#define BUFFER_SIZE 100

int main(void) {

    char *words[100];
    int n = 0;
    char buffer[BUFFER_SIZE];

    while (n &lt; 4 &amp;&amp; scanf(&quot;%s&quot;, buffer) != EOF) {
        words[n] = strcpy(words[n], buffer);
        n++;
    }

    for (int i = 0; i &lt; 4; ++i) {
        printf(&quot;%s\n&quot;, words[i]);
    }

    return 0;
};</code></pre>
<p>출력값을 볼 필요도 없이 exit code에서 에러가 발생합니다.</p>
<h3 id="sucess">sucess</h3>
<pre><code class="language-c"> #include &lt;stdio.h&gt;
#include &lt;string.h&gt;

#define BUFFER_SIZE 100

int main(void) {

    char *words[100];
    int n = 0;
    char buffer[BUFFER_SIZE];

    while (n &lt; 4 &amp;&amp; scanf(&quot;%s&quot;, buffer) != EOF) {
        words[n] = strdup(buffer);
        n++;
    }

    for (int i = 0; i &lt; 4; ++i) {
        printf(&quot;%s\n&quot;, words[i]);
    }

    return 0;
};</code></pre>
<blockquote>
<p>strcpy 함수가 아닌
strdup 함수를 사용하여 words 포인터 배열에 값을 복사하여 넣는다면!</p>
</blockquote>
<pre><code class="language-c">first
second
third
fourth
//입력값

first
second
third
fourth
//출력값</code></pre>
<p>다음과 같이 정상적으로 값이 입력된 순서대로 출력되는것을 확인하고,
words 포인터 배열에 올바르게 값이 넣어진것 또한 확인 할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - Dynamic Memory Allocation]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Dynamic-Memory-Allocation</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Dynamic-Memory-Allocation</guid>
            <pubDate>Thu, 04 Aug 2022 13:30:19 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="동적메모리-할당">동적메모리 할당</h2>
<blockquote>
<p>보통은 변수로 메모리를 할당 하지만, 
그것 대신 프로그램의 요청으로 메모리를 할당 할 수 있습니다.
그것이 바로 <strong>동적 메모리 할당(Dynamic Memory Allocation)</strong> 입니다.</p>
<blockquote>
<p>malloc 함수를 호출하여 동적메모리 할당을 요청하면 요구하는 크기만큼의 메모리를 할당하고
그 <strong>시작 주소를 반환</strong> 합니다.</p>
</blockquote>
</blockquote>
<pre><code class="language-c">    int * p;
    p = (int *) malloc(40);
    if (p == null) {
        // 동적 메모리 할당이 실패
        // 적절한 조치를 취하는 로직 생성
    }

    p[0] = 12;
    p[1] = 24;
    *(p+2) = 36;</code></pre>
<ul>
<li>P라는 포인터 변수를 선언</li>
<li>malloc 함수를 통해서 40바이트 만큼의 메모리를 할당</li>
<li><strong>malloc 으로 할당받은 메모리는 보통의 배열처럼 사용이 가능</strong><ul>
<li>( int * )  는 작성하지 않아도 되지만, 표시해주는 것이 가독성이 좋음</li>
</ul>
</li>
</ul>
<hr>
<h2 id="배열-키우기">배열 키우기</h2>
<blockquote>
<p>여기서 동적메모리 할당의 주된 특징은
동적으로 할당된 배열의 공간이 부족할 경우에 더 큰 배열을 할당하여 사용이 가능합니다.</p>
</blockquote>
<pre><code class="language-c">    int *array = (int *) malloc(4 * sizeof(int)); // == malloc(16);

    //    배열 array의 크키가 부족한 상황이 발생한다.

    int *tmp = (int *) malloc(8 * sizeof(int));
    int i;
    for (int i = 0; i &lt; 4; ++i) {
        tmp[i] = array[i];
    }
    array = tmp;</code></pre>
<ul>
<li>malloc( 4 * sizeof ( int ) ) ; 로 할당한 이유는 int의 크기가 4바이트가 아닌 다른 크기일 수도 있기때문에 
동적으로 변경될 수 있게 선언한것</li>
<li>tmp 라는 32바이트 크기의 포인터 변수 선언<ul>
<li>tmp배열에 array의 값을 순서대로 주입해준다</li>
<li>그다음, array 의 배열을 tmp 로 변경한다.</li>
</ul>
</li>
</ul>
<p>그러나 이러한 방법이 반복된다면 array같은 버려지는 배열은 쌓이면서 프로그램의 속도를 저하 시킬 수 있다.
그것을 처리하는 방법은 나중에 다루겠다.</p>
<hr>
<h2 id="question">Question!!!</h2>
<blockquote>
<p>아니 위에 로직에서 </p>
</blockquote>
<pre><code class="language-c"> int *array = (int *) malloc(4 * sizeof(int));   //  1
 int array[4];                                      //  2</code></pre>
<blockquote>
<p>??? : 아니 그러면 1번 * array로 선언하지 말고 , 2번 array[4] 로 선언해도 상관없는거 아닌가요??</p>
<blockquote>
<p>앞서 이전 포스팅 <a href="https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Pointer-Array">( Pointer &amp; Array )</a> 을 참고 하면!
array[4] 로 선언할 경우 같은 배열이 나오는 것은 맞다 .
하지만! <strong>array라는 배열의 주소값을 가진 포인터 변수는 변경이 불가</strong> 하다는 점이 있다.</p>
<blockquote>
<p>그리고 또 하나는 array[4]로 선언했을 경우 
LifeTime 에 대한 Problem이 발생하게 된다. 이 또한 나중에 다루도록 하겠습니다.</p>
</blockquote>
</blockquote>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C 언어] - Pointer Arithmetic]]></title>
            <link>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Pointer-Arithmetic</link>
            <guid>https://velog.io/@french_ruin/C-%EC%96%B8%EC%96%B4-Pointer-Arithmetic</guid>
            <pubDate>Thu, 04 Aug 2022 09:21:21 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="pointer-arithmetic">Pointer Arithmetic</h2>
<p>결론만 말하자면</p>
<pre><code class="language-c">int a[10] 이라는 배열이 선언되었다면!

*a 와 a[0]은 동일한 의미
*a[i] 은 *(a+1)과 동일 , a[i] 는 *(a+i)와 동일하다.</code></pre>
<p>풀어서 설명하겠습니다.</p>
<ul>
<li><p>int a[10] 이라는 배열이 선언되면 a는 배열의 이름을 가진 포인터변수입니다.
(주소값을 가지는 값 a)</p>
<ul>
<li>그렇다면 *a 는 a가 가진 주소값을 가진 곳의 값을 가지겠죠?
(즉, 배열의 처음값의 주소를 저장한 값이 a)</li>
</ul>
</li>
</ul>
<blockquote>
<p>여기서 중요한 것은!
   *(a + 1) 은 단순히 a의 값에서 1이 증가한 값이 아닙니다.
   자료형의 바이트 값만큼 증가합니다.</p>
<blockquote>
<p>무슨 말이냐 하면~
   int는 4바이트가 되겠죠? 
   여기서 1의 증가는 4 바이트 (메모리 값) 의 증가가 됩니다.</p>
<blockquote>
<p>만약 a의 값이 1004 라면 *(1004 + 4) 가 되겠죠?
     그렇다면 1008의 주소에 위치한 값을 가져오게 되는겁니다.</p>
<blockquote>
<p>결과는? a[1]의 값을 가져오게 되는것이죠....</p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>   말을 너무 길게 했네요..ㅎㅎ</p>
<p>   제가 이해하려고 한겁니다.</p>
<blockquote>
<p>그렇다면 결론은
 a[ i ]  = <em>(a + i )가 맞는 결론이 나오고!
 이러한 수식을 우리는 *</em>Pointer Arithmetic** 이라고 부른답니다!</p>
</blockquote>
<hr>
<h3 id="">+@</h3>
<p>그렇다면 자료형 마다 바이트가 다르니~</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {

    int data[] = {1, 2, 3};
    char data2[] = {&#39;a&#39;, &#39;b&#39;, &#39;c&#39;};
    long long data3[] = {1, 2, 3};


    int *p = &amp;data[0];
    char *q = &amp;data2[0];
    long long *r = &amp;data3[0];


    printf(&quot;%d %x\n&quot;, p, p);
    printf(&quot;%d %x\n&quot;, p + 1, p + 1);
    printf(&quot;%d %x\n&quot;, p + 2, p + 2);

    printf(&quot;%d %x\n&quot;, q, q);
    printf(&quot;%d %x\n&quot;, q + 1, q + 1);
    printf(&quot;%d %x\n&quot;, q + 2, q + 2);


    printf(&quot;%d %x\n&quot;, r, r);
    printf(&quot;%d %x\n&quot;, r + 1, r + 1);
    printf(&quot;%d %x\n&quot;, r + 2, r + 2);


    return 0;
};</code></pre>
<blockquote>
<p>각 포인터 변수 p ,q, r 에 배열의 주소값을 담았다.
long long 의 경우 8 바이트.
printf 의 경우 10진수와 16진수 모두 출력</p>
</blockquote>
<p>위와 같은 예제를 만들고! 실행시켜주면<del>~</del></p>
<pre><code class="language-bash">-2044725156 861ff85c
-2044725152 861ff860
-2044725148 861ff864
//p 

-2044725159 861ff859
-2044725158 861ff85a
-2044725157 861ff85b
//q

-2044725184 861ff840
-2044725176 861ff848
-2044725168 861ff850
//r</code></pre>
<p>참고로 주소값이 음수입니다.</p>
<blockquote>
<p>int 는 4바이트씩 
char 은 1바이트씩
long long 은 8 바이트씩 증가하는 모습을 볼수 있습니다.</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>