<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>어제의 최선</title>
    <link>https://bbbicb.tistory.com/</link>
    <description>.</description>
    <language>ko</language>
    <pubDate>Thu, 25 Jun 2026 10:08:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>비비빅B</managingEditor>
    <item>
      <title>SpringBoot JPA 에서 기본키, 자연키 vs 대리키</title>
      <link>https://bbbicb.tistory.com/77</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 로그인ID를 이메일 말고 사번으로 변경해달라는 요구사항이 있었다. 기본키를 대리키로 설계했던 터라 별 무리없이 변경할 수 있을 듯 했는데, 자세히 살펴보니 대부분의 코드가 변경되어야 하는 대작업이었다. 분명 이런 상황을 대비해서 대리키를 채택한 것인데, 뭐가 잘못된걸까?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;JPA 기본키&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity를 DB에서 식별할 수 있는 값으로, 중복(null 포함)일 수 없고 변하지 않는 값이어야 한다. 개발이 어느정도 진행되고 난 다음에는 수정하기가 매우 어렵기 때문에, 설계 초기에 확실하게 규칙을 정하고 가는 것이 옳다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;1159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oOq7q/btsMFlQo2hI/UBFepEg7GKRo2HK7LwJ0pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oOq7q/btsMFlQo2hI/UBFepEg7GKRo2HK7LwJ0pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oOq7q/btsMFlQo2hI/UBFepEg7GKRo2HK7LwJ0pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoOq7q%2FbtsMFlQo2hI%2FUBFepEg7GKRo2HK7LwJ0pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;387&quot; height=&quot;433&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;1159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;자연키 vs 대리키&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.589147%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 45.077519%;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;자연키(Natural Key)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.333333%;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;대리키(Surrogate Key)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.589147%;&quot;&gt;&lt;b&gt;정의&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.077519%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;업무적으로 의미가 있는 실제 데이터 값을 키로 사용&lt;br /&gt;(예: 주민등록번호, 사업자번호, ISBN)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.333333%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;임의로 생성된 식별 값을 키로 사용&lt;br /&gt;(예: auto_increment, 시퀀스, UUID)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.589147%;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.077519%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;bull; 별도 키 생성이 필요 없음&lt;br /&gt;&amp;bull; &lt;b&gt;자체적으로 업무적 의미 포함&lt;/b&gt;&lt;br /&gt;&amp;bull; 데이터 중복 방지 효과&lt;br /&gt;&amp;bull; &lt;b&gt;조인 시 의미 있는 연결 제공&lt;/b&gt;&lt;br /&gt;&amp;bull; 인덱스 추가 생성 불필요&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.333333%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;bull; 업무 규칙 변경에 영향 받지 않음&lt;br /&gt;&amp;bull; &lt;b&gt;성능에 최적화&lt;/b&gt;(보통 정수형으로 간단)&lt;br /&gt;&amp;bull; 복합키 대신 &lt;b&gt;단일키&lt;/b&gt; 사용 가능&lt;br /&gt;&amp;bull; 물리적 크기가 작아 조인 성능 향상&lt;br /&gt;&amp;bull; 숨겨진 의존성 없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.589147%;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.077519%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;bull; &lt;b&gt;업무 규칙 변경 시 키 변경 위험&lt;/b&gt;&lt;br /&gt;&amp;bull; &lt;b&gt;복합키&lt;/b&gt;일 경우 관리 복잡&lt;br /&gt;&amp;bull; 키 길이가 길 수 있어&lt;b&gt; 성능 저하&lt;/b&gt;&lt;br /&gt;&amp;bull; 다른 시스템과 통합 시 충돌 가능성&lt;br /&gt;&amp;bull; 실제 데이터 노출로 &lt;b&gt;보안 위험&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.333333%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;bull; &lt;b&gt;의미 없는 값&lt;/b&gt;으로 직관적 이해 어려움&lt;br /&gt;&amp;bull; &lt;b&gt;별도 인덱스&lt;/b&gt; 관리 필요&lt;br /&gt;&amp;bull; 추가적인 저장 공간 필요&lt;br /&gt;&amp;bull; &lt;b&gt;자연키 제약조건 별도 구현 필요&lt;/b&gt;&lt;br /&gt;&amp;bull; 데이터 이관 시 매핑 작업 필요&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.589147%;&quot;&gt;&lt;b&gt;적합상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.077519%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;bull; 절대 변하지 않는 자연스러운 식별자가 있을 때&lt;br /&gt;&amp;bull; 업무적 의미가 명확히 필요한 경우&lt;br /&gt;&amp;bull; 레거시 시스템과의 호환성이 중요할 때&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.333333%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;bull; 데이터 변경 가능성이 있는 경우&lt;br /&gt;&amp;bull; 높은 성능이 요구되는 시스템&lt;br /&gt;&amp;bull; 마이그레이션이 빈번한 환경&lt;br /&gt;&amp;bull; 보안이 중요한 시스템&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 변경에 다소 안전하고, 성능이 좋다는 이유로 대리키를 PK로 선택한다. 특히 JPA를 사용하는 경우, 복합키가 다소 복잡하기 때문에 대부분 대리키를 사용하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;대리키의 종류&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;table style=&quot;color: #000000; text-align: start; border-collapse: collapse; width: 100%; height: 181px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 13.139534%; height: 19px;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.511628%; height: 19px;&quot;&gt;&lt;b&gt;정수형 키(시퀀스, AUTO_INCREMENT, IDENTITY)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.232559%; height: 19px;&quot;&gt;&lt;b&gt;UUID/GUID&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px; width: 13.139534%;&quot;&gt;&lt;b&gt;크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 41.511628%;&quot;&gt;일반적으로 4바이트(INT) 또는 8바이트(BIGINT)&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 45.232559%;&quot;&gt;16바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;height: 36px; width: 13.139534%;&quot;&gt;&lt;b&gt;생성 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; width: 41.511628%;&quot;&gt;데이터베이스에서 순차적으로 자동 생성&lt;/td&gt;
&lt;td style=&quot;height: 36px; width: 45.232559%;&quot;&gt;알고리즘을 통해 무작위 또는 시간 기반으로 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;height: 36px; width: 13.139534%;&quot;&gt;&lt;b&gt;성능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; width: 41.511628%;&quot;&gt;매우 좋음&lt;/td&gt;
&lt;td style=&quot;height: 36px; width: 45.232559%;&quot;&gt;상대적으로 나쁨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 54px;&quot;&gt;
&lt;td style=&quot;height: 54px; width: 13.139534%;&quot;&gt;&lt;b&gt;저장 공간 효율성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 54px; width: 41.511628%;&quot;&gt;높음&lt;/td&gt;
&lt;td style=&quot;height: 54px; width: 45.232559%;&quot;&gt;낮음 (4배 정도 더 많은 공간 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px; width: 13.139534%;&quot;&gt;&lt;b&gt;보안성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 41.511628%;&quot;&gt;낮음 (쉽게 추측 가능)&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 45.232559%;&quot;&gt;높음 (추측 어려움)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대리키의 종류별 장단점을 충분히 검토하지 못하고, 무작정 익숙하면서 성능이 좋은 정수형 키로 선택한 것이 시작이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 요구사항으로 보안적인 측면이 강조되면서, 쉽게 추측이 가능하다는 보안상의 이유로 API 응답으로 PK를 전달하지 않도록 개발되었다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;지금 생각해보면 이 시점에서 API 응답에 PK를 빼는 것이 아닌 PK를 UUID로 바꿨어야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 다음과 같은 문제가 발생했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트에서는 PK(대리키)에 접근할 수 없음&lt;/li&gt;
&lt;li&gt;클라이언트는 대상을 식별하기 위해 자연키(로그인 ID)만 사용할 수 있음&lt;/li&gt;
&lt;li&gt;자연스럽게 백엔드 API도 자연키를 인자로 받도록 구현됨&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1741507344403&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 대리키(이렇게 개발되었어야했지만)
public UserDto findByLoginId(Long pk) {
	User user = repository.findById(pk).orElseThrow();
    ...
}

// 자연키(이렇게 됨)
public UserDto findByLoginId(String loginId) {
	User user = repository.findByLoginId(loginId).orElseThrow();
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 개발이 진행되다 보니, 최종적으로는 아래와 같은 결과가 발생했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;자연키에 추가 인덱스를 생성&lt;/li&gt;
&lt;li&gt;테이블 간 조인에서도 자연키를 사용&lt;/li&gt;
&lt;li&gt;결과적으로 &quot;대리키를 사용하는 척하면서 실제로는 자연키에 의존하는&quot; 최악의 설계&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;올바른 접근법&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;노출되더라도 예측할 수 없는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;UUID를 대리키를 사용해서 보안을 강화하면서, PK는 그대로 응답값에 포함시켜야 했다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 UUID를 사용하면 성능이 감소하고 정렬하기가 어렵다는 문제점이 있다. 하지만&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;성능 문제&lt;/b&gt;: 데이터가 엄청 크지 않는 이상 성능 차이는 크지 않다. 최신 DB 엔진은 UUID 인덱싱 최적화가 잘 되어 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정렬 문제&lt;/b&gt;: UUID 대신 &lt;b&gt;ULID(Universally Unique Lexicographically Sortable Identifier)&lt;/b&gt;를 사용하면 해결할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;사실 대리키를 정수형과 UUID 2개로 생각하는 것 자체가 틀렸다. 비즈니스와 관련이 없는 ID 생성방식은 모두 대리키다. 예로 일반 기업에서 채택한 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Snowflake ID 같은 것도 대리키인 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;추가 고려사항&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;+) Equals 동등비교는 무슨 필드로?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대리키를 사용하면 Equals 비교 필드를 대리키에 해야하나 생각이 들 수도 있지만, 동등 비교는 비즈니스와 관련된 자연키에 하는 것이 옳다. 대리키를 동등 비교 필드로 사용하면, 새로운 아이템을 등록한다고 했을 때, DB에 persist하기 전과 후가 다른 객체라고 뱉어낼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;+) 공통코드 테이블은 자연키vs대리키?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통코드 테이블은 보통 다른 테이블의 조회 쿼리에서 조인해서 사용된다. 공통코드 테이블 PK를 대리키로 사용할 경우에는 정말 의미없는 조인 조건이 사용되거나 조인을 2번해야하는 경우가 생긴다. 따라서 코드성 테이블은 자연키를 PK로 하는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1741509054124&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 
    O.ORDER_ID,
    O.ORDER_DATE,
    O.STATUS_CODE,
    C.CODE_NAME AS STATUS_NAME
FROM 
    ORDERS O
-- 대리키여서 2번 조인
LEFT JOIN
    CODE_GROUP P ON P.GROUP_ID = 'ORDER_STATUS'
LEFT JOIN 
    CODE_DETAIL C ON C.GROUP_ID = G.GROUP_ID AND O.STATUS_CODE = C.CODE_ID

-- 자연키였으면 아래처럼 한번만 조인
LEFT JOIN 
    CODE_DETAIL C ON O.STATUS_CODE = C.CODE_ID AND C.GROUP_ID = 'ORDER_STATUS'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 요새 코드는 Enum으로 빼는 것이 트렌드라, Enum으로 사용하는 것이 제일 좋아보이긴 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 하고 싶은 말은, 무조건적으로 대리키가 좋다는 말이 아니라, 상황에 맞게 선택하는 것이 제일 좋다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 JPA에서는 `@NaturalId`로 자연키를 선언하면 자연키로 조회할 수 있도록 메소드를 지원해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1741510781799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class User {
    @Id
    private UUID id;
    
    @NaturalId
    private String loginId;
    
    // ...
}

// 사용 예시
User user = session.byNaturalId(User.class)
    .using(&quot;loginId&quot;, &quot;user123&quot;)
    .load();&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Backend/Java</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/77</guid>
      <comments>https://bbbicb.tistory.com/77#entry77comment</comments>
      <pubDate>Sun, 23 Feb 2025 18:04:16 +0900</pubDate>
    </item>
    <item>
      <title>Spring XSS</title>
      <link>https://bbbicb.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;보안테스트 결과 XSS 취약점이 발견돼 조치를 하면서, 약간 고생을 해서 경험을 공유해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;XSS(Cross Site Scripting)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;악의적인 사용자가 공격하려는 사이트에 스크립트를 넣는 기법&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;XSS Filter&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 백엔드 프로젝트가 Rest API로 구성돼, 대부분의 요청과 응답이 json 방식으로 요청 중이다. Spring XSS를 검색하면 가장 많이 노출되는 &lt;a href=&quot;https://github.com/naver/lucy-xss-servlet-filter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lucy Filter&lt;/a&gt;가 있지만, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@ResponseBody&lt;/span&gt;로 응답되는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;application/json&lt;/span&gt; 타입은 처리하지 못한다고 해서 &lt;b&gt;Jackson 라이브러리의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;CharacterEscape&lt;/span&gt;를 설정&lt;/b&gt;해 &lt;b&gt;&amp;lt;, &amp;gt;, ', &quot; 총 4개의 문자를 치환&lt;/b&gt;했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;관련된 설정은 &lt;a style=&quot;color: #666666;&quot; href=&quot;https://jojoldu.tistory.com/470&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jojoldu.tistory.com/470&lt;/a&gt;을 참고했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 설정으로 DB에 저장될 때는 그대로 저장되지만, 클라이언트에게 응답할 때 Jackson 라이브러리의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;MappingJackson2HttpMessageConverter&lt;/span&gt;가 따옴표와 꺾쇠를 치환하면서 대부분의 XSS 공격을 무효화할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;취약점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 사용자들이 글을 작성할 수 있는 게시판 같은 곳에서 &lt;b&gt;웹에디터(ckeditor5)&lt;/b&gt;를 사용하고 있는데, HTML 기반으로 작동하다 보니 화면에서 보여줄 때&lt;b&gt; HTML escape 된 내용을 다시 풀어줘야지 작성된 내용이 정상적으로 보인다&lt;/b&gt;(XSS Filter에서 치환된 문자를, 다시 원복 해야 함).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 보니 &lt;b&gt;웹에디터를 사용한 곳에서 XSS 공격에 노출되는 문제점&lt;/b&gt;이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;XSS Util&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ckeditor.com/docs/ckeditor4/latest/guide/dev_disallowed_content.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;웹에디터 자체적인 기능&lt;/a&gt;도 있고, 다른 &lt;a href=&quot;https://github.com/cure53/DOMPurify&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;자바스크립트 오픈소스 라이브러리&lt;/a&gt;를 사용을 추천하는 글도 있었다. 하지만 현재 프로젝트에서는 프론트 서버가 따로 없다 보니 &lt;b&gt;클라이언트에서 모두 조작이 가능해 위험&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국은 &lt;b&gt;백엔드 자바 서버에서 필터링&lt;/b&gt;해야 하는 것이라, 웹에디터 내용을 응답하는 로직에서 사용할 XSS 유틸을 만들어서 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694351098501&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	@ParameterizedTest
	@ValueSource(strings = {
		&quot;&amp;lt;p&amp;gt;&amp;gt;XSS&amp;gt;&amp;lt;b class='red' onclick=alert(1)&amp;gt;attack&amp;lt;/b&amp;gt;&amp;lt;/p&amp;gt;&quot;,
		&quot;&amp;lt;p class='red' onpointerdown= 'alert(1)'&amp;gt;&amp;lt;/p&amp;gt;&quot;,
		&quot;&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;iframe class='red' src='www.naver.com' width=500 height=500;&quot;
	})
	@DisplayName(&quot;xss 위험한 단어는 제거한다.&quot;)
	void prevent_xss(String attack) {
		String cleaned = this.cleanXss(attack);
		System.out.println(cleaned);
		Assertions.assertThat(cleaned)
			.contains(&quot;&amp;lt;p&quot;, &quot;&amp;lt;/p&amp;gt;&quot;, &quot;class='red'&quot;)
			.doesNotContain(&quot;onclick&quot;, &quot;alert&quot;, &quot;iframe&quot;);
	}

	private String cleanXss(String text) {
		Pattern onEventPattern = Pattern.compile(&quot;(&amp;lt;\\s?[^&amp;gt;]+\\s)&quot; +
			&quot;(on[\\w\\s]+?=\\s*'[^']*'&quot; +
			&quot;|on[\\w\\s]+?=\\s*\&quot;[^\&quot;]*\&quot;&quot; +
			&quot;|on[\\w\\s]+?=[^&amp;gt;]*)&quot; +
			&quot;([^&amp;gt;]*&amp;gt;)&quot;);
		Pattern blacklist = Pattern.compile(&quot;script|iframe|frame(set)|eval|javascript&quot;, Pattern.CASE_INSENSITIVE); // 위험한 태그들, etc...

		String unescaped = StringEscapeUtils.unescapeHtml(text);
		String eventRemoved = onEventPattern.matcher(unescaped)
			.replaceAll(&quot;$1$3&quot;);

		return blacklist.matcher(eventRemoved)
			.replaceAll(&quot;&quot;);
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;cleanXss&lt;/span&gt;라는 함수를 유틸로 제공했었는데, 간단히 내용을 설명하면 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내용을 &lt;b&gt;unescape&lt;/b&gt;(&amp;amp;lt;을 &amp;lt;로 치환)&lt;/li&gt;
&lt;li&gt;태그 안의 &lt;b&gt;on... 이벤트를 제거&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위험한 태그명을 제거&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xOE1k/btstsZ7tO2w/5Qvyin5zgQJnhMFhtQq15k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xOE1k/btstsZ7tO2w/5Qvyin5zgQJnhMFhtQq15k/img.png&quot; data-alt=&quot;잘되는군&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xOE1k/btstsZ7tO2w/5Qvyin5zgQJnhMFhtQq15k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxOE1k%2FbtstsZ7tO2w%2F5Qvyin5zgQJnhMFhtQq15k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1722&quot; height=&quot;152&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잘되는군&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 앞서 말했듯이, 보안테스트 결과 XSS 공격에 노출됐다.&lt;b&gt; 범인은 공백&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;취약점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정규식에서 &lt;b&gt;&amp;amp;nbsp;&lt;/b&gt;는 공백이 아니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 on 이벤트를 제거하는 정규식에서 공백을 의미하는 &lt;b&gt;\s 표현식&lt;/b&gt;을 사용했었다. 당연히 모든 공백을 매칭할 줄 알았는데 &lt;b&gt;&amp;amp;npsp;는 HTML 파싱 될 때만 공백으로 인식하는 특정 문자열로 정규식에서는 일반문자로 취급된다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 아래와 같은 XSS 공격에 무방비하게 노출 돼버려 취약점에 걸린 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceV0BO/btstx7jaYYS/5abaqH7vx83vCRebP4SN80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceV0BO/btstx7jaYYS/5abaqH7vx83vCRebP4SN80/img.png&quot; data-alt=&quot;공백을 &amp;amp;amp;nbsp; 바꿔서 공격하자 테스트 실패&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceV0BO/btstx7jaYYS/5abaqH7vx83vCRebP4SN80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceV0BO%2Fbtstx7jaYYS%2F5abaqH7vx83vCRebP4SN80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;452&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공백을 &amp;amp;nbsp; 바꿔서 공격하자 테스트 실패&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;\p{Z}&lt;/b&gt;&amp;nbsp;or&amp;nbsp;&lt;b&gt;\p{Separator}&lt;/b&gt;: any kind of whitespace or invisible separator.&lt;br /&gt;&lt;a href=&quot;https://www.regular-expressions.info/unicode.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.regular-expressions.info/unicode.html&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 기존 공백 정규식과 함께 &amp;amp;nbsp;도 같이 인식할 수 있는 표현식이 있다. &lt;b&gt;\s로 작성된 부분을 \p{Z}로 변경&lt;/b&gt;하면 &amp;amp;nbsp;도 공백패턴으로 인식할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694353473649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Pattern onEventPattern = Pattern.compile(&quot;(&amp;lt;\\p{Z}?[^&amp;gt;]+\\p{Z})&quot; +
	&quot;(on[\\w\\p{Z}]+?=\\p{Z}*'[^']*'&quot; +
	&quot;|on[\\w\\p{Z}]+?=\\p{Z}*\&quot;[^\&quot;]*\&quot;&quot; +
	&quot;|on[\\w\\p{Z}]+?=[^&amp;gt;]*)&quot; +
	&quot;([^&amp;gt;]*&amp;gt;)&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wUb4U/btstwmOQOsv/qstdiYySkU3vpsibqXbLl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wUb4U/btstwmOQOsv/qstdiYySkU3vpsibqXbLl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wUb4U/btstwmOQOsv/qstdiYySkU3vpsibqXbLl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwUb4U%2FbtstwmOQOsv%2FqstdiYySkU3vpsibqXbLl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1812&quot; height=&quot;342&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 XSS의 경우 잘 만들어진 &lt;a href=&quot;https://jsoup.org&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;오픈소스 라이브러리&lt;/a&gt;가 많기 때문에 직접 정규식을 작성하는 것은 상대적으로 위험하다. 나같은 경우에는 요구사항이 on으로 시작하는 이벤트만 제거해달라는 요구사항이 있어서 불가피하게 정규식을 작성했지만, 대부분의 상황에서는 라이브러리를 사용하는게 생산성과 안전성, 그리고 건강에 좋을듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;XSS 공격에 성공한 문구를 전달 받았을 때 &amp;amp;nbsp;가 스페이스바(공백)과 똑같이 보이는 상태였는데, 이를 복붙하다보니 원인을 파악하는데만 2시간 정도 걸렸다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Backend/Java</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/76</guid>
      <comments>https://bbbicb.tistory.com/76#entry76comment</comments>
      <pubDate>Sun, 10 Sep 2023 22:48:37 +0900</pubDate>
    </item>
    <item>
      <title>Spring Actuator로 Property 변경하기</title>
      <link>https://bbbicb.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 가동중에 프로퍼티를 변경할 수 있으면, 서비스를 배포하지 않아도 되기 때문에 빠르고 유연한 서비스를 운영할 수 있다. 스프링에서 제공하는 spring actuator를 의존하는 것만으로도 쉽게 적용할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;의존성 추가&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1693238504440&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# build.gradle
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-actuator'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;프로퍼티 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1693238585596&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
management:
  endpoints:
    web:
      exposure:
        include: refresh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게만 설정하면 끝이다. 프로퍼티를 변경할 일이 생기면 application.yml을 변경하고 actuator에서 제공하는 엔드포인트를 호출하기만 하면 적용된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;기존 프로퍼티&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1693238083975&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
jwt:
  token:
    expiration-minutes: 120m
    secret: user-secret-secure-token-more-than-256-user-service​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;변경 프로퍼티&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1693239140801&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
jwt:
  token:
    expiration-minutes: 10m
    secret: user-secret-secure-token-more-than-256-user-service&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1693239499141&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http POST localhost:8000/actuator/refresh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 JWT 정책에 변경이 필요해 유효시간을 10분으로 줄이고 싶은 상황이라 해보자. 프로퍼티를 변경 후 위의 refresh 엔드포인트를 호출하면 어플리케이션 재배포없이 런타임 중에 성공적으로 JWT 정책을 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693239890824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@Slf4j
public class Controller {

  private final Environment environment;

  @GetMapping(&quot;/properties&quot;)
  public String healthCheck() {
    return &quot;&quot;&quot;
       ===this application with properties===
       jwt-token-expiration-minutes: %s
       jwt-token-secret: %s
      &quot;&quot;&quot;
      .formatted(
        environment.getProperty(&quot;jwt.token.expiration-minutes&quot;),
        environment.getProperty(&quot;jwt.token.secret&quot;),
      );
  }
}
/*
...
===this application with properties===
jwt-token-expiration-minutes: 10m
jwt-token-secret: user-secret-secure-token-more-than-256-user-service
*/&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;유의사항&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;@ConfigurationProperties&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1693237882845&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;jwt&quot;)
public record JwtProperty(Token token) {

  public JwtProperty(Token token) {
    this.token = Objects.requireNonNullElse(token, new Token(null, null));
  }

  public record Token(@DurationUnit(ChronoUnit.MINUTES) Duration expirationMinutes, String secret) {

    @ConstructorBinding
    public Token(Duration expirationMinutes, String secret) {
      this.expirationMinutes = Objects.requireNonNullElse(expirationMinutes, Duration.ofDays(1L));
      this.secret = Objects.requireNonNullElse(secret, &quot;secret&quot;);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 현업에 가면 프로퍼티를 직접 꺼내쓰기보다는 자바 프로퍼티 객체를 별도로 정의하고 사용할 것이다. 나도 스프링에서 제공해주는 @ConfigurationProperties를 사용해 개발자들이 함부로 변경하지 못하도록 &lt;b&gt;record 불변 객체&lt;/b&gt;로 정의했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로퍼티 바인딩까지는 잘 동작했지만, 프로퍼티 변경 후 refresh 했을 때 객체의 값이 변경되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 알아서 변경해주지 못하는가 싶어서 @RefreshScope도 선언해봤지만 효과는 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 불변 프로퍼티 객체는 actuator refresh 대상이 아니라는 것이다. 만약 필요하다면 Bean으로 등록해 사용하라고 권장한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;We have no plans to support refresh of read only properties (ie constructor binding)&lt;br /&gt;&amp;nbsp;If you want a refresh scope configuration properties, use an @Bean&amp;nbsp;method in configuration.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/spring-cloud/spring-cloud-commons/issues/846&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/spring-cloud/spring-cloud-commons/issues/846&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Java</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/75</guid>
      <comments>https://bbbicb.tistory.com/75#entry75comment</comments>
      <pubDate>Tue, 29 Aug 2023 01:41:18 +0900</pubDate>
    </item>
    <item>
      <title>오라클 - 마이바티스 날짜형 데이터 맵핑</title>
      <link>https://bbbicb.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보다 보니&lt;b&gt; 날짜를 의&lt;/b&gt;&lt;b&gt;미하는 데이터를 마이바티스-&amp;gt;어플리케이션 맵핑하는 방식&lt;/b&gt;이 다양한 것을 봤다. 내 기억으로는 특정 마이바티스 버전부터는 자동으로&lt;b&gt; [LocalDateTime, LocalDate, LocalTime]&lt;/b&gt;으로 맵핑해 주는 걸로 알고 있었는데 활용하지 못한 것처럼 보여 의아했다. 이유를 알아보니 기존 레거시 코드를 그대로 옮기다 보니 그런 것도 있지만, &lt;b&gt;현재 환경이 위의 Java 타입으로 맵핑이 깔끔하게 되지는 않기 때문이었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;데이트 타입 맵핑 장애물&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;오라클DB 날짜 타입&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 환경은 오라클과&amp;nbsp;비슷한 국산DB 티베로를 사용하고 있다(이 글에서 말하려는 특징은 동일하기 때문에 오라클로 설명하겠다). &lt;a href=&quot;https://docs.oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG238&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;오라클 날짜 형식의 데이터는 4가지로 분류된다.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DATE&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;연, 월, 일, 시, 분, 초를 저장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;연월일까지만 저장할 경우, 해당 날의 자정의 시간으로 저장&lt;/li&gt;
&lt;li&gt;연월까지만 저장할 경우, 해달 월의 1일 자정의 시간으로 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TIMESTAMP&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DATE 타입에서 소수점 2자리 초까지 저장&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TIMESTAMP WITH TIME ZONE&lt;/li&gt;
&lt;li&gt;TIMESTAMP WITH LOCAL TIME ZONE&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &lt;b&gt;시간 없이 날짜만, 혹은 날짜 없이 시간만 저장하는 데이터를 오라클에서는 지원해 주지 않는다.&lt;/b&gt; 이런 이유에서 DB팀에서는 날짜 혹은 시간을 저장해야 하는 경우, CHAR(6)로 문자열 데이터('230717', '123054')로 저장하기로 기준을 잡았다. 그러다 보니 데이터 타입 변환이 특정 시점에 필요했고, 대부분 크게 아래의 3가지 경우로 분류할 수 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;날짜를 다루는 로직이 없어서, 그냥 문자열(String)로 받아서 화면에 전달&lt;/li&gt;
&lt;li&gt;날짜를 다루는 로직이 있어서, DB에서 Date타입으로 변환 후 Java 날짜 타입으로 맵핑&lt;/li&gt;
&lt;li&gt;날짜를 다루는 로직이 있어서, 일단 문자열로 받은 후 자바 라이브러리로 타입 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;마이바티스 타입 핸들러 지원 범위&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복적으로 나타나는 타입 변환 코드는 읽기도 힘들고 지저분하게 만들어서, 프레임워크 영역에서 처리해주고 싶었다. 현재 환경은 &lt;a href=&quot;https://mybatis.org/mybatis-3/ko/configuration.html#typeHandlers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3.4.5 버전 이상의 마이바티스&lt;/a&gt;를 쓰고 있었기 때문에, &lt;b&gt;Java 날짜 타입 변환을 &lt;b&gt;자동으로 &lt;/b&gt;지원해주고 있었다. 그래서 6자리 CHAR를 LocalDate, LocalTime으로 &lt;b&gt;자동 &lt;/b&gt;변환시켜주길 기대했지만 그렇지 못했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2682&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckx5uh/btsnqTwQiO7/vu3M87x5Xlz9JS2GuvH131/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckx5uh/btsnqTwQiO7/vu3M87x5Xlz9JS2GuvH131/img.png&quot; data-alt=&quot;yyyyMMdd CHAR(6) -&amp;amp;gt; Java LocalDate로 변환 실패&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckx5uh/btsnqTwQiO7/vu3M87x5Xlz9JS2GuvH131/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckx5uh%2FbtsnqTwQiO7%2Fvu3M87x5Xlz9JS2GuvH131%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2682&quot; height=&quot;478&quot; data-origin-width=&quot;2682&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;yyyyMMdd CHAR(6) -&amp;gt; Java LocalDate로 변환 실패&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2572&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLesmi/btsnkLtYQAd/ZNgnv3bI6yUEdcMf9lMWn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLesmi/btsnkLtYQAd/ZNgnv3bI6yUEdcMf9lMWn1/img.png&quot; data-alt=&quot;HHmmss CHAR(6) -&amp;amp;gt; &amp;amp;nbsp;java LocalTime 변환 실패&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLesmi/btsnkLtYQAd/ZNgnv3bI6yUEdcMf9lMWn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLesmi%2FbtsnkLtYQAd%2FZNgnv3bI6yUEdcMf9lMWn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2572&quot; height=&quot;398&quot; data-origin-width=&quot;2572&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HHmmss CHAR(6) -&amp;gt; &amp;nbsp;java LocalTime 변환 실패&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 해보니 오라클 DATE 데이터를 Java [LocalDateTime, LocalDate, LocalTime)으로 변환은 성공적으로 됐으나,&lt;b&gt; 오라클 CHAR(6) 데이터를 Java [LocalDate, LocalTime]으로 변환하는데 실패&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Mybatis Custom TypeHandler&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 DATE 타입에서 Java 날짜 타입(LocalDateTime, LocalDate, LocalTime)으로 변환은 잘 됐기 때문에, 해당 기능을 유지하면서 추가적인 CHAR(6) 타입을 변환할 기능이 필요했다. 마이바티스에서 자동으로 타입 변환이 가능하도록, CHAR(6)에서 Java 날짜 타입(LocalDate, LocalTime)으로 변환시킬 마이바티스 타입핸들러를 정의했다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;LocalDateTypeHandler&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1689525555048&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.date.typehandler;

public class LocalDateTypeHandler extends BaseTypeHandler&amp;lt;LocalDate&amp;gt; {

  private final DateTimeFormatter localDateFormatter = new DateTimeFormatterBuilder()
    .optionalStart()
    .appendPattern(&quot;yyyyMMdd&quot;)
    .optionalEnd()
    .optionalStart()
    .appendPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;)
    .toFormatter();

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType) throws SQLException {
    ps.setObject(i, parameter);
  }

  @Override
  public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return LocalDate.parse(rs.getString(columnName), localDateFormatter);
  }
 ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;LocalTimeTypeHandler&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1689525656950&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.date.typehandler;

public class LocalTimeTypeHandler extends BaseTypeHandler&amp;lt;LocalTime&amp;gt; {

  private final DateTimeFormatter localTimeFormatter = new DateTimeFormatterBuilder()
    .optionalStart()
    .appendPattern(&quot;HHmmss&quot;)
    .optionalEnd()
    .optionalStart()
    .appendPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;)
    .toFormatter();

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, LocalTime parameter, JdbcType jdbcType) throws SQLException {
    ps.setObject(i, parameter);
  }

  @Override
  public LocalTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return LocalTime.parse(rs.getString(columnName), localTimeFormatter);
  }
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Mybatis 타입핸들러 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1689525907087&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mybatis.type-handlers-package= com.example.date.typehandler // 생성한 타입핸들러 패키지 선언&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalDate 타입의 필드로 선언될 경우, DateTimeFormatter이 'yyyyMMdd'와 'yyyy-MM-dd HH:mm:ss' 문자열을 파싱 하도록 작성했다. LocalTime의 경우 'HHmmss'와 'yyyy-MM-dd HH:mm:ss' 형식의 문자열 파싱을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개의 타입핸들러를 작성 후, &amp;nbsp;application.properties에 마이바티스 설정을 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;자동 타입 변환 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1689526400150&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Mapper
public interface DateMapper {

  @Select(&quot;select sysdate from dual&quot;)
  LocalDateTime selectNow();
  @Select(&quot;select '20230711' from dual&quot;)
  LocalDate selectDate();
  @Select(&quot;select '123001' from dual&quot;)
  LocalTime selectTime();

  @Select(&quot;select sysdate from dual&quot;)
  LocalDate selectDateAsLocalDate();
  @Select(&quot;select sysdate from dual&quot;)
  LocalTime selectDateAsLocalTime();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1689526319005&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@MybatisTest
@TestPropertySource(value = &quot;/application-test.properties&quot;)
public class DateMappingTest {

  @Autowired
  DateMapper dateMapper;

  @Test
  @DisplayName(&quot;CHAR6타입을 LocalDate로 매핑한다.&quot;)
  void whenChar6InDBAndLocalDateInJavaType_success() {
    LocalDate localDate = dateMapper.selectDate();
    System.out.println(&quot;localDate = &quot; + localDate);
    assertThat(localDate).isInstanceOf(LocalDate.class);
    assertThatNoException();
  }

  @Test
  @DisplayName(&quot;CHAR6타입을 LocalTime로 매핑한다.&quot;)
  void whenChar6InDBAndLocalTimeInJavaType_success() {
    LocalTime localTime = dateMapper.selectTime();
    System.out.println(&quot;localTime = &quot; + localTime);
    assertThat(localTime).isInstanceOf(LocalTime.class);
    assertThatNoException();
  }

  @Test
  @DisplayName(&quot;Date타입을 LocalDateTime로 매핑한다.&quot;)
  void whenDateInDBAndLocalDateTimeInJavaType_success() {
    LocalDateTime localDateTime = dateMapper.selectNow();
    System.out.println(&quot;localDateTime = &quot; + localDateTime);
    assertThat(localDateTime).isInstanceOf(LocalDateTime.class);
    assertThatNoException();
  }

  @Test
  @DisplayName(&quot;Date타입을 LocalTime로 매핑한다.&quot;)
  void whenDateInDBAndLocalTimeInJavaType_success() {
    LocalDate localDate = dateMapper.selectDateAsLocalDate();
    System.out.println(&quot;localDate = &quot; + localDate);
    assertThat(localDate).isInstanceOf(LocalDate.class);
    assertThatNoException();
  }

  @Test
  @DisplayName(&quot;Date타입을 LocalDate로 매핑한다.&quot;)
  void whenDateInDBAndLocalDateInJavaType_success() {
    LocalTime localTime = dateMapper.selectDateAsLocalTime();
    System.out.println(&quot;localTime = &quot; + localTime);
    assertThat(localTime).isInstanceOf(LocalTime.class);
    assertThatNoException();
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbtM1m/btsnOrG10SF/J58VCzn1XrrzvQZFY4QFCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbtM1m/btsnOrG10SF/J58VCzn1XrrzvQZFY4QFCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbtM1m/btsnOrG10SF/J58VCzn1XrrzvQZFY4QFCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbtM1m%2FbtsnOrG10SF%2FJ58VCzn1XrrzvQZFY4QFCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1294&quot; height=&quot;376&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Date -&amp;gt; LocalDateTime, LocalDate, LocalTime&lt;/li&gt;
&lt;li&gt;CHAR(6) -&amp;gt; LocaDate, LocalTime&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5가지 케이스를 모두 만족하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 하기 전까지는 사실 데이터팀에서 날짜, 시간을 CHAR(6) 타입으로 쓰는 이유에 대해서 깊게 생각해보지 않았다. 단순히 AS-IS가 그렇게 돼있어서 그런 줄 알았는데 DB에서 지원하는 날짜 타입이 없어서 그런 것인 줄은 생각 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 오라클이 null을 빈값과 동일하게 처리한다는 것에 대해서도 MySQL과 달라 당황했던 기억이 새록새록 났다. 이런 데이터베이스 간의 차이점도 잘 기억해 둬야겠다.&lt;/p&gt;</description>
      <category>Backend/Java</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/74</guid>
      <comments>https://bbbicb.tistory.com/74#entry74comment</comments>
      <pubDate>Wed, 12 Jul 2023 20:51:53 +0900</pubDate>
    </item>
    <item>
      <title>Vim 입문</title>
      <link>https://bbbicb.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;Vim으로 통일&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chaPf3/btsljhV3Wef/KXQKsgVeiAXTsYDixsQUok/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chaPf3/btsljhV3Wef/KXQKsgVeiAXTsYDixsQUok/img.webp&quot; data-alt=&quot;모르면 나갈 수도 없는 불친절한 vim...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chaPf3/btsljhV3Wef/KXQKsgVeiAXTsYDixsQUok/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchaPf3%2FbtsljhV3Wef%2FKXQKsgVeiAXTsYDixsQUok%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;513&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모르면 나갈 수도 없는 불친절한 vim...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자에게도 점점 데브옵스적인 역량이 요구되면서, 리눅스 서버에 터미널로 접근하게 되는 경우가 많아졌다. 간단하게는 로그나 파일이 존재하는지, 또는 설정파일을 변경하거나 프로세스가 살아있는지 등등 여러 가지 이유에서 말이다. 그럴 때마다 리눅스 터미널 단축키를 구글링 하면서 하곤 했는데, 한동안 안 하면 까먹고 다시 찾아보고... 무한 반복이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 일이 잦아지면서, &lt;b&gt;리눅스를 공부&lt;/b&gt;해야겠다고 계획했다. &lt;b&gt;에디터 조작을 vim 단축키로 통일하는 것은 그 과정 중 하나&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 vim을 사용하는 건 굳이라고 생각을 했었다. 하지만 최근 들어서 에디터를 통일할 필요성을 점점 느끼게 됐다. 회사에서는 윈도우OS에 이클립스와 vscode를 사용하고, 개인적으로는 맥OS에 인텔리제이랑 웹스톰을 사용하다 보니 단축키가 완전 뒤죽박죽이었다. 이왕 통일할 거 처음에 조금 불편하더라도 vim으로 정착하자고 마음먹었다. 들어가긴 어렵지만 나오기도 어렵다는 게 vim이라고 하니깐 좀 참고 익숙해져보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;IDE Vim Plugin&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vim 자체를 코딩하는데 사용하는 건 솔직히 좀 무리인 것 같고, 현재 &lt;b&gt;IDE + Vim Plugin 조합&lt;/b&gt;으로 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워낙 유명하고 커뮤니티가 활발하다 보니 대부분의 IDE에서 Vim 플러그인 설치하면 간단하게 vim을 덧붙여 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/164-ideavim&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;ideaVim&lt;/span&gt;&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Intellij, WebStorm IDE 플러그인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=vscodevim.vim&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Vim&lt;/span&gt;&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;VSCode IDE 플러그인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vrapper.sourceforge.net/home/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Vrapper&lt;/span&gt;&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Eclipse IDE 플러그인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE 플러그인 외에도 &lt;a href=&quot;https://www.makeuseof.com/best-vim-plugins/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Vim 자체의 유용한 플러그인&lt;/a&gt;은 훨씬 더 많이 존재한다. 나는 괄호 작성을 편하게 도와주는 `&lt;a href=&quot;https://github.com/tpope/vim-surround&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;surround.vim&lt;/a&gt;`만 추가 설치해서 사용 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Vim 연습하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;백문이 불여일타&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vim에 익숙해지려면, 자주 사용해야 한다. 근데 사실 vim 자체는 그다지 친절한 편은 아니라서, 아무 생각 없이 사용하기만 하면 생산성을 높일 수 없고 오히려 떨어진다고 본다. 계속 hjkl만 누르면서 코딩할 순 없다. ㅠ vim도 공부해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 vim을 잘 사용하진 못하지만, 연습할 때 도움이 됐던 것을 2가지 소개하려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vimtutor&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vim 에디터 튜토리얼&lt;/li&gt;
&lt;li&gt;터미널에서 `vimtutor`을 입력하면 됨&lt;/li&gt;
&lt;li&gt;회사에서 심심할 때 하기 좋음&lt;/li&gt;
&lt;li&gt;기초적인 조작법을 파악&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9zqCr/btslufJaMJJ/njw1fx1Inko6PYncZkvjd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9zqCr/btslufJaMJJ/njw1fx1Inko6PYncZkvjd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9zqCr/btslufJaMJJ/njw1fx1Inko6PYncZkvjd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9zqCr%2FbtslufJaMJJ%2Fnjw1fx1Inko6PYncZkvjd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;370&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.vimgolf.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;VimGolf&lt;/b&gt;&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최소한의 타수로 목표한 텍스트 모양으로 만드는 게임&lt;/li&gt;
&lt;li&gt;자신의 점수보다 높은 사람들의 방법을 보면서, 효율적인 단축키를 익힐 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HCDQg/btsluV4JY0U/DZZ9ukMLmW2LMUmyxWGafk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HCDQg/btsluV4JY0U/DZZ9ukMLmW2LMUmyxWGafk/img.png&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;1224&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;30.92&quot; style=&quot;width: 30.196883%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HCDQg/btsluV4JY0U/DZZ9ukMLmW2LMUmyxWGafk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHCDQg%2FbtsluV4JY0U%2FDZZ9ukMLmW2LMUmyxWGafk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1106&quot; height=&quot;1224&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4ThJg/btslne5Eneq/A7VdBcPR4KiVzwsvPDDzW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4ThJg/btslne5Eneq/A7VdBcPR4KiVzwsvPDDzW1/img.png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;1124&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;33.73&quot; style=&quot;width: 32.942903%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4ThJg/btslne5Eneq/A7VdBcPR4KiVzwsvPDDzW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4ThJg%2Fbtslne5Eneq%2FA7VdBcPR4KiVzwsvPDDzW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1108&quot; height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vOGry/btsljhhxbBw/eOnWmIaBGztErJY9kzKOS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vOGry/btsljhhxbBw/eOnWmIaBGztErJY9kzKOS0/img.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;1078&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;35.35&quot; style=&quot;width: 34.534633%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vOGry/btsljhhxbBw/eOnWmIaBGztErJY9kzKOS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvOGry%2FbtsljhhxbBw%2FeOnWmIaBGztErJY9kzKOS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1114&quot; height=&quot;1078&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;start file을 end file로 최소한의 타수로 만들어야 한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Vim 사용후기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vim을 사용한 지 3달 정도 돼가는데, 사실 아직 IDE 단축키에 비해 큰 장점은 모르겠다. 마우스에 손이 덜 가서 조금 편한 정도? 그래도 더 익숙해지면 확실히 편할 것 같긴 하다. 그것 외에도 막힘없이 Vim을 사용하다 보면 그냥 기분이 좋다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/emQsk8/btslw7jju0S/K16S7aHM3TWdrwD0HNms2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/emQsk8/btslw7jju0S/K16S7aHM3TWdrwD0HNms2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/emQsk8/btslw7jju0S/K16S7aHM3TWdrwD0HNms2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FemQsk8%2Fbtslw7jju0S%2FK16S7aHM3TWdrwD0HNms2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;379&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;819&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Vim으로 통일하면서 마크다운 에디터도 Vim모드가 지원되는 &lt;a href=&quot;https://obsidian.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Obsidian&lt;/a&gt;으로 변경했다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Tools</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/73</guid>
      <comments>https://bbbicb.tistory.com/73#entry73comment</comments>
      <pubDate>Mon, 26 Jun 2023 21:03:08 +0900</pubDate>
    </item>
    <item>
      <title>순수 JS로 모듈 컴포넌트 만들기</title>
      <link>https://bbbicb.tistory.com/72</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVWNRR/btslghtFLtt/MHyJIZIcvdrqEQKxF6XFd1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVWNRR/btslghtFLtt/MHyJIZIcvdrqEQKxF6XFd1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVWNRR/btslghtFLtt/MHyJIZIcvdrqEQKxF6XFd1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVWNRR%2FbtslghtFLtt%2FMHyJIZIcvdrqEQKxF6XFd1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;515&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;span style=&quot;caret-color: #009a87;&quot;&gt;&lt;b&gt;중복되는 컴포넌트&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/188655&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;컴포넌트&lt;/a&gt; 중복이 가장 많은 곳은 프론트영역이다. 달력, 입력박스, 안내창 등은 대부분의 화면에서 공통적인 기능으로 사용된다. 스타일은 물론 로직(이벤트처리)이 같은 컴포넌트도 많을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 공통 컴포넌트를 개발하고 관리하는 것이 중요하지만, 현재 진행 중인 프로젝트에서는 그러지 못했다. 서버에서 관리하는 컴포넌트의 경우 &lt;a href=&quot;https://docs.oracle.com/cd/E19159-01/819-3669/6n5sg7b50/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jsp tag 라이브러리&lt;/a&gt;를 이용해 어느정도 관리가 됐지만, 화면에서 관리해야 하는 컴포넌트(Clinent Side Rendering)의 경우 전혀 관리되지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같은 기능을 하는 컴포넌트들이 각자 개발되다보니, 작은 변경 하나도 모든 화면을 수정해야 했다. 유지보수 지옥이나 다름없었다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이렇게까지 된 이유는 별다른 방법이 없다고 생각했기 때문이다. React, Vue와 같은 라이브러리 없이 컴포넌트를 공통으로 관리하는 것은 힘들다고 생각했다. 또한 현 프로젝트에서는 IE11 버전 지원, &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모듈 스크립트&lt;/a&gt; 작성 불가능(솔루션 라이브러리 사용 조건)으로 인해 `import`, `export` 구문을 사용 못하다 보니, 별다른 방안이 생각나지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 최근에 재미로 지원했던 &lt;a href=&quot;https://prgms.tistory.com/139&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프로그래머스 프론트 과제 테스트&lt;/a&gt;에서 그 해답을 찾아 글로 정리해보려 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;span style=&quot;caret-color: #009a87;&quot;&gt;&lt;b&gt;컴포넌트 모듈 만들기&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;개별 컴포넌트&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직원들의 이메일을 입력받는 입력박스가 있어야 한다고 가정해 보자. 입력이 끝나면 간단하게 이메일형식을 확인해 알려주는 기능도 필요하다. 개별적으로 개발한다면 대부분 아래와 같이 개발될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687702541583&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;bbbicb&amp;lt;/title&amp;gt;
  &amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html&quot; charset=&quot;UTF-8&quot;&amp;gt;
  &amp;lt;style&amp;gt;
    .input__non_valid {
      background-color: red;
    }
  &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;main class=&quot;App&quot;&amp;gt;
    &amp;lt;form&amp;gt;
      &amp;lt;input type=&quot;email&quot; id=&quot;emailBox&quot; name=&quot;email&quot;&amp;gt;
    &amp;lt;/form&amp;gt;
  &amp;lt;/main&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;script&amp;gt;
  document.getElementById(&quot;emailBox&quot;)?.addEventListener(&quot;input&quot;, (e) =&amp;gt; {
    console.count(&quot;email input event&quot;);
    const emailRegexPattern = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
    const nonValidClazz = &quot;input__non_valid&quot;

    const inputNode = e.target
    const isValid = inputNode.value.match(emailRegexPattern);
    if (!isValid) {
      inputNode.className = nonValidClazz;
    } else {
      inputNode.classList.remove(nonValidClazz);
    }
  })
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Jun-25-2023 23-24-32.gif&quot; data-origin-width=&quot;176&quot; data-origin-height=&quot;32&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnANIG/btsldO6irhF/xJLSOOq9rGKKGSrlWef8W0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnANIG/btsldO6irhF/xJLSOOq9rGKKGSrlWef8W0/img.gif&quot; data-alt=&quot;이메일 형식 검사까지 잘 되는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnANIG/btsldO6irhF/xJLSOOq9rGKKGSrlWef8W0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bnANIG/btsldO6irhF/xJLSOOq9rGKKGSrlWef8W0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;176&quot; height=&quot;32&quot; data-filename=&quot;Jun-25-2023 23-24-32.gif&quot; data-origin-width=&quot;176&quot; data-origin-height=&quot;32&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이메일 형식 검사까지 잘 되는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 기능은 제대로 동작하더라도, 유지보수의 관점에서 보면 상당히 골치 아픈 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드가 모든 화면에 있다고 생각하면, &lt;b&gt;작은 수정에도 전체 화면을 수정하고 테스트해야 하는 상황이 돼버린다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 요구사항이 추가로 생겼다고 가정해 보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빈값이면 &quot;이메일을 입력해 주세요&quot;라는 문구를 넣어주세요.&lt;/li&gt;
&lt;li&gt;이메일 도메인 셀렉트박스를 옆에 보여주고, &quot;직접입력&quot;을 기본으로 해주세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 개발했다면 이런 요구사항을 반영하기 위해서, &lt;b&gt;모든 화면을 하나하나 수정해야 해서 유지보수 지옥이나 다름없다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;공통 컴포넌트&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;기존기능 구현&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 이전에 개별적으로 구현했던 기능을 공통 컴포넌트로 변경했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687707347604&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// EmailInput.js

export default function EmailInput({target, initialState}) {
  const EMAIL_REGEX_PATTERN = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;

  this.elelemt = document.createElement(&quot;form&quot;);
  target.appendChild(this.elelemt);

  this.state = initialState;
  this.setState = (nextState) =&amp;gt; {
    this.state = {...this.state, ...nextState};
    this.render();
  }

  this.render = () =&amp;gt; {
    this.elelemt.innerHTML = `
      &amp;lt;input type=&quot;email&quot; class=&quot;${this.state.clazz}&quot; id=&quot;${this.state.id}&quot; name=&quot;${this.state.name}&quot; value=&quot;${this.state.value ?? &quot;&quot;}&quot;&amp;gt;
    `;
  }

  this.render();
  this.elelemt.addEventListener(&quot;input&quot;, e =&amp;gt; {
    console.count(&quot;email input event&quot;);
    const inputNode = e.target
    const isValid = inputNode.value.match(EMAIL_REGEX_PATTERN);
    inputNode.className = isValid ? &quot;&quot; : &quot;input__non_valid&quot;;
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다른 화면에서 아래와 같이 모듈을 생성하기만 하면 동일하게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687708537004&quot; class=&quot;xml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
&amp;lt;body&amp;gt;
  &amp;lt;main class=&quot;App&quot;&amp;gt;&amp;lt;/main&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;script type=&quot;module&quot; &amp;gt;
  import EmailInput from './EmailInput.js'
  new EmailInput({
    target: document.querySelector(&quot;.App&quot;),
    initialState: {
      id: &quot;emailBox&quot;,
      name: &quot;email&quot;
    }});
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;요구사항 반영&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 컴포넌트로 구현해 놓았기 때문에, 모든 화면을 수정할 필요 없이 &lt;b&gt;`EmailInput` 컴포넌트만 수정하면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687708272418&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// EmailInput.js
  this.render = () =&amp;gt; {
    this.elelemt.innerHTML = `
      &amp;lt;input type=&quot;email&quot; class=&quot;${this.state.clazz}&quot; id=&quot;${this.state.id}&quot; name=&quot;${this.state.name}&quot; value=&quot;${this.state.value ?? &quot;&quot;}&quot;
        placeholder=&quot;이메일을 입력해주세요.&quot;&amp;gt;
      &amp;lt;select&amp;gt;
        &amp;lt;option value=&quot;&quot;&amp;gt;직접입력&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;@gmail.com&quot;&amp;gt;구글&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;@naver.com&quot;&amp;gt;네이버&amp;lt;/option&amp;gt;
      &amp;lt;/select&amp;gt;
    `;
  }

  this.render();
  this.elelemt.addEventListener(&quot;input&quot;, e =&amp;gt; {
    console.count(&quot;email input event&quot;);
    const inputNode = this.elelemt.querySelector(&quot;input&quot;);
    const selectNode = this.elelemt.querySelector(&quot;select&quot;);
    const isValid = (inputNode.value + selectNode[selectNode.selectedIndex].value).match(EMAIL_REGEX_PATTERN);
    inputNode.className = isValid ? &quot;&quot; : &quot;input__non_valid&quot;;
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nUmEw/btslqh7pxhT/5tHAS9ihdy6LcYco45mdSk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nUmEw/btslqh7pxhT/5tHAS9ihdy6LcYco45mdSk/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;244&quot; data-origin-height=&quot;32&quot; data-filename=&quot;Jun-26-2023 00-53-06.gif&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.418605%; margin-right: 10px;&quot; height=&quot;46&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nUmEw/btslqh7pxhT/5tHAS9ihdy6LcYco45mdSk/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnUmEw%2Fbtslqh7pxhT%2F5tHAS9ihdy6LcYco45mdSk%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;244&quot; height=&quot;32&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZhnY6/btslahhHkyA/6H52STOcIX3l8QT0wkV7N1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZhnY6/btslahhHkyA/6H52STOcIX3l8QT0wkV7N1/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;244&quot; data-origin-height=&quot;32&quot; data-filename=&quot;Jun-26-2023 00-52-45.gif&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.418605%;&quot; height=&quot;46&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZhnY6/btslahhHkyA/6H52STOcIX3l8QT0wkV7N1/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZhnY6%2FbtslahhHkyA%2F6H52STOcIX3l8QT0wkV7N1%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;244&quot; height=&quot;32&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;이메일 형식 검사를 셀렉트박스에 맞게 하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;기본기의 중요성&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 스타일이 리액트 라이브러리와 비슷하다는 것에서 볼 수 있듯이, &lt;b&gt;리액트에서 제공하는 상태기반 컴포넌트를 바닐라 자바스크립트로 구현한 것이다.&lt;/b&gt; 모듈 방식의 자바스크립트 지식이 부족하다 보니, 리액트를 사용하지 않고는 컴포넌트를 관리하는 게 어렵다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행하는 프로젝트에서 초반에 이걸 알고 정리했다면, 좀 더 좋은 유지보수 환경을 만들 수 있었을 것 같은데 아쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 리액트 배우기 전에 자바스크립트로 먼저 해보라는 말을 강의에서 듣곤 하는데, 이번에 그 이유를 확실히 알 수 있었다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/72</guid>
      <comments>https://bbbicb.tistory.com/72#entry72comment</comments>
      <pubDate>Mon, 12 Jun 2023 00:45:55 +0900</pubDate>
    </item>
    <item>
      <title>Java에서 Null을 다루는 방식</title>
      <link>https://bbbicb.tistory.com/71</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;널널(null null)하게 개발하지 마세요&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어마다 약간의 차이가 있지만, 자바에서는 &lt;b&gt;존재하지 않는 객체 메모리 주소&lt;/b&gt;를 `null`이라 표현한다. 따라서 원시타입을 제외한 객체를 가리키는 모든 변수는, 초기에 null 상태라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZWvAw/btsgEKudAjW/URErw2zg9oiYHoP88atauK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZWvAw/btsgEKudAjW/URErw2zg9oiYHoP88atauK/img.webp&quot; data-alt=&quot;int와 Integer 변수 선언의 차이&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZWvAw/btsgEKudAjW/URErw2zg9oiYHoP88atauK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZWvAw%2FbtsgEKudAjW%2FURErw2zg9oiYHoP88atauK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;250&quot; height=&quot;214&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;int와 Integer 변수 선언의 차이&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null을 참조하려고 하면 자바는 &lt;b&gt;NPE(Null Point Exception)&lt;/b&gt;라는 런타임 오류를 뱉어내는데&lt;b&gt; 해결하기 쉬우면서도 어려운 오류&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null safe하게 코드를 작성하려고 노력하지만, 항상 예상하지 못한 곳에서 NPE가 발생한다. 런타임 오류기 때문에 코드가 배포돼서 서비스되기 전까지는 모르기 때문에 치명적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;널 레퍼런스를 처음 만든 토니 호어가 &lt;b&gt;'10억 불짜리 실수'(billion dollar mistake)&lt;/b&gt;였다고 회고한 적 있다. 널 포인터를 체크하지 않고 사용해서 발생하는 버그가 엄청나게 많기 때문. 때문에 최신 언어들은 null reference를 배제하려는 움직임을 보인다. - 나무위키&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;span style=&quot;caret-color: #009a87;&quot;&gt;&lt;b&gt;Null Safe 방법&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null을 안전하게 다루는 여러 방법이 있지만, 내가 알고 있던 정보로는&lt;b&gt;&amp;nbsp;`Optional`이 제일 트렌디하고 최선이라고 생각했다.&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하지만 사용하다보니 생각보다 불편한 점이 있었고, 다른 곳에서는 어떻게 사용하는지 찾아보던 중 몰랐던 방법을 찾아 글로 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684682739087&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Box {
  Message getMessage() {
    if (somtimes()) {
      return null;
    }
    return new Message();
  }
}

class Message {
  String message;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가끔씩 null을 반환하는 Box를 null safe 하게&lt;/b&gt; 만들 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;if문 처리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;가장 기본적인 방법으로, &lt;b&gt;직접 if문에서 null 예외처리&lt;/b&gt; 하는 것이다. 보통 라이브러리를 사용하거나 공통 함수를 사용해 처리한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684682586540&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void nullSafe(Box box) {

    Message msg = box.getMessage();
    if (msg == null) {
      msg = new Message();
    }
    if (Objects.isNull(msg)) {
      msg = new Message();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개발자들이&amp;nbsp;&lt;b&gt;가끔씩(제법 자주) 예외처리하는 것을 까먹는다는 문제점&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;Optional&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;Scala 언어의 영향을 받아 자바 8부터 도입된 &lt;b&gt;Optional 객체를 활용&lt;/b&gt;하는 것이다. null일수도 있다는 것을 객체로 알려줄 뿐만 아니라, 값을 꺼내기 위해서 null 예외처리를 강제하도록 인터페이스가 설계됐다. or,&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt; orElse, orElseThrow, orElseGet,... 다양한 메서드를 지원한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684683504692&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Box {
  Optional&amp;lt;Message&amp;gt; getOptionalMessage() {
    if (sometimes()) {
      return Optional.empty();
    }
    return Optional.of(new Message());
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1684683593625&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  void nullSafe(Box box) {
    Message msg = box.getOptionalMessage().orElse(new Message());
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 여러 방식으로 최적화를 하지만 Optional도 결국 객체기 때문에, 메모리를 차지한다는 성능 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;a href=&quot;https://www.beyondjava.net/optionals-guidelines&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;특정 상황에서는 가독성이 떨어지고, Optional의 잘못된 사용으로 null 문제를 해결 못하고 있다는 주장&lt;/a&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;물론 Optional은 잘못이 없다.&lt;/b&gt; 개발자들의 잘못된 사용이 위의 문제를 야기하는 것이다.&lt;br /&gt;&lt;a href=&quot;https://www.latera.kr/blog/2019-07-02-effective-optional/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Optional의 올바른 사용법에 대해 여러 가지 논의가 있다.&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;@Nullable&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`@Nullable` 어노테이션을 적극 활용해서 IDE 혹은 정적분석도구의 도움을 받는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;IDE 지원&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Intellij의 경우, NPE가 발생할 수 있을만한 코드를 어노테이션 기반으로 분석하고 알려주는 옵션이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYxSpW/btsg0WHy7Gj/icZ9WtChy4Z5OtJQDbPSd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYxSpW/btsg0WHy7Gj/icZ9WtChy4Z5OtJQDbPSd0/img.png&quot; data-alt=&quot;해당 옵션을 활성화하면 IDE에서 NPE 위험한 곳을 알려준다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYxSpW/btsg0WHy7Gj/icZ9WtChy4Z5OtJQDbPSd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYxSpW%2Fbtsg0WHy7Gj%2FicZ9WtChy4Z5OtJQDbPSd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1326&quot; height=&quot;566&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;해당 옵션을 활성화하면 IDE에서 NPE 위험한 곳을 알려준다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPLI5X/btsg0XzJzk7/KxvMlKsm691QnLE7Pvqk8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPLI5X/btsg0XzJzk7/KxvMlKsm691QnLE7Pvqk8K/img.png&quot; style=&quot;width: 48.840916%; margin-right: 10px;&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;374&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;49.42&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPLI5X/btsg0XzJzk7/KxvMlKsm691QnLE7Pvqk8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPLI5X%2Fbtsg0XzJzk7%2FKxvMlKsm691QnLE7Pvqk8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pQFqc/btsg25cGWKt/kH8fc41LrDtGDE6KqZaiL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pQFqc/btsg25cGWKt/kH8fc41LrDtGDE6KqZaiL0/img.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;372&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;50.58&quot; style=&quot;width: 49.996293%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pQFqc/btsg25cGWKt/kH8fc41LrDtGDE6KqZaiL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpQFqc%2Fbtsg25cGWKt%2FkH8fc41LrDtGDE6KqZaiL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;448&quot; height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;@Nonnull 어노테이션이 붙은 메소드에서 null을 반환하자 노란색 경고표시로 알려준다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모든 곳에 어노테이션을 작성하기에는 번거롭고 가독성도 해치기 때문에, &lt;b&gt;`&lt;a href=&quot;https://www.baeldung.com/java-package-info&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;package-info.java&lt;/a&gt;` 파일에 패키지레벨 어노테이션을 설정하는 것이 일반적&lt;/b&gt;이라고 한다. 만약 @Nonnull을 패키지레벨 어노테이션으로 설정했다면, @Nullable한 곳에서만 어노테이션을 override 하면 된다.&lt;br /&gt;
&lt;pre id=&quot;code_1684774939248&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package-info.java
@Nonnull
package com.example.nulltrend;

import jakarta.annotation.Nonnull;​&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsLKmm/btsg3VgD9I8/KqTK2jzvqNinlRHVahaHU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsLKmm/btsg3VgD9I8/KqTK2jzvqNinlRHVahaHU0/img.png&quot; data-alt=&quot;이제 어노테이션이 없어도 @Nonnull로 간주하고 null 경고표시를 해준다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsLKmm/btsg3VgD9I8/KqTK2jzvqNinlRHVahaHU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsLKmm%2Fbtsg3VgD9I8%2FKqTK2jzvqNinlRHVahaHU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;426&quot; height=&quot;302&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이제 어노테이션이 없어도 @Nonnull로 간주하고 null 경고표시를 해준다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 IDE에서 지원해 주는 것은 단순한 경고이기 때문에, &lt;b&gt;코드를 빌드하는 과정에서는 경고를 무시해도 전혀 문제가 없다.&lt;/b&gt; 따라서 만약 개발자가 IDE의 경고를 무시하고 mainline 소스에 통합한다면, 이는 여전히 null-safe 하지 못한 소스가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;정적분석도구&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정적분석도구를 활용하면 런타임오류를 빌드 오류로 변환시켜, 일종의 컴파일 오류로 쉽게 예방할 수 있다.&lt;/b&gt; nullable 정적 분석도구는 다양하지만 그중 uber에서 만든 `nullaway`를 적용시켜 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684778955834&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.0'
    id 'io.spring.dependency-management' version '1.1.0'
    id &quot;net.ltgt.errorprone&quot; version &quot;3.0.0&quot;
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    errorprone &quot;com.google.errorprone:error_prone_core:2.19.0&quot;
    annotationProcessor &quot;com.uber.nullaway:nullaway:0.10.10&quot;
}

tasks.named('test') {
    useJUnitPlatform()
}

tasks.withType(JavaCompile).configureEach {
    options.errorprone {
        option(&quot;NullAway:AnnotatedPackages&quot;, &quot;com.example&quot;)
    }
}
tasks.compileJava {
    options.errorprone.error(&quot;NullAway&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1684778875126&quot; class=&quot;groovy&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/Users/hyojip/Desktop/repo/NullTrend/src/main/java/com/example/nulltrend/NullTrendApplication.java:36: error: [NullAway] returning @Nullable expression from method with @NonNull return type
      return null;
      ^
    (see http://t.uber.com/nullaway )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 #getMessage 함수에서 null을 리턴하던 것을 정적분석도구가 발견해 컴파일에 실패했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684779224534&quot; class=&quot;aspectj&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Nullable
  Message getMessage() {
    if (sometimes()) {
      return null;
    }
    return new Message();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Nullable 어노테이션을 추가한 뒤, 컴파일하면 아래와 같이 빌드에 성공하는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1684779284769&quot; class=&quot;gradle&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; Task :compileJava
&amp;gt; Task :processResources UP-TO-DATE
&amp;gt; Task :classes
&amp;gt; Task :resolveMainClassName
&amp;gt; Task :bootJar
&amp;gt; Task :jar
&amp;gt; Task :assemble
&amp;gt; Task :compileTestJava UP-TO-DATE
&amp;gt; Task :processTestResources NO-SOURCE
&amp;gt; Task :testClasses UP-TO-DATE

&amp;gt; Task :test

&amp;gt; Task :check
&amp;gt; Task :build

BUILD SUCCESSFUL in 2s
7 actionable tasks: 5 executed, 2 up-to-date
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
3:13:04: 실행이 완료되었습니다 'build'.&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=oJpVeKKrgKI&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/eDZcL/hySG7lRhrR/KProCLKXp8bUeHc9Eft80K/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/oJpVeKKrgKI&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Java</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/71</guid>
      <comments>https://bbbicb.tistory.com/71#entry71comment</comments>
      <pubDate>Mon, 22 May 2023 01:11:04 +0900</pubDate>
    </item>
    <item>
      <title>커밋 주기는 어느정도가 적당할까?</title>
      <link>https://bbbicb.tistory.com/70</link>
      <description>&lt;h2 style=&quot;color: #009a87;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;브랜치는 자주 분기하고 커밋 단위는 깔끔하게!&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무 컨텍스트가 바뀌면 브랜치를 분기하고, 작업 후 여러 커밋들을 `squash`해서 유의미한 커밋 메시지 단위를 유지하는 것이 좋다고 생각했다. 실제로 그렇게 했을 때 `revert`하기도 편하고 커밋히스토리도 비교적 깔끔하게 관리되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 최근에 브랜치 전략을 설명할 일이 생기면서, 다른 회사의 브랜치 전략들을 찾아봤는데 잘못 알고 있던 정보들이 많아 글로 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;커밋은 적어도 하루에 한 번&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유의미한 커밋을 만들어 내는 것은 좋지만, 그것보다&lt;b&gt; 내가 작업한 결과물을 동료들에게 빠르게 공유하는 것이 훨씬 중요하다&lt;/b&gt;. 왜냐하면 소스 통합 시 발생하는 충돌(conflict)때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 충돌을 많이 경험하진 못했지만, 발생할 때마다 스트레스는 상당했다. 충돌된 소스와 내 소스 중 뭐가 더 맞는 소스인지 확인하려면 컨텍스트(무엇때문에 변경했는지, 뭐가 올바른 로직인 건지)를 알고 있어야 했기 때문이다. 실제로 변경 규모가 크고, 오랫동안 통합하지 않았다면 충돌 해결에만 반나절 정도 소요되는 경우도 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌이 자주 발생해서 부정적인 경험이 쌓이게 되면, 개발자들은 소스 통합을 미루게 되고 결국 악순환에 굴레에 들어서게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 다른 사람의 소스에서 변경할 부분이 눈에 보여도 충돌의 공포 때문에 변경하지 않게 된다. 당장의 리팩토링 기회도 놓칠뿐더러 적극적으로 문제를 해결하는 개발 문화에도 악영향을 미친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합을 잦게 하면 동료 개발자들이 내 변경사항을 알게 돼 충돌을 피할 수 있고, 만약 충돌 나더라도 매우 작은 부분이기 때문에 시간이 별로 소요되지 않을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;소스 통합 전략&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 내가 알고 있던 지식이랑 충돌이 발생한다. 개발자들은 어떤 추가적인 기능을 개발할 때 feature 브랜치를 따서 하는 게 좋은 브랜치 전략 아니었나? 브랜치를 따서 작업하되 중간에 지속적으로 메인 소스에 병합하라는 말인가?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;mainline&lt;/b&gt;은 항상 &lt;b&gt;healthy 해야&lt;/b&gt; 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;mainline&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자들에게 공유되는 단 하나의 브랜치로 개발소스의 최신버전을 가짐&lt;/li&gt;
&lt;li&gt;흔히 git에서 master or main 브랜치라고 말하지만 엄밀히 구분하면 조금 다름&lt;/li&gt;
&lt;li&gt;개인 repository로 fork 하는 경우 자신의 repo master 브랜치는 mainline이 아님&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;healthy&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 소스로 당장 배포되더라도 문제없이 정상 서비스되는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 통합 할 때 가장 중요한 원칙이다. 이 원칙이 지켜졌을 경우에만 &lt;a href=&quot;https://martinfowler.com/bliki/ContinuousDelivery.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Continuous Delivery&lt;/b&gt;&lt;/a&gt; 된다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;Feature Branch Integration&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기능 개발 시작할 때 개인 브랜치를 분기해서 진행하고 완료 후 병합하는 전략&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 흔히 사용하는 통합 패턴이다. 예를 들어 oauth2 로그인 기능이 신규로 된다면 mainline에서 `feat/oauth2`를 분기해 작업을 시작한다. 작업이 완료되면 병합하기 전 테스트를 거치고 mainline에 반영해 healthy 한 상태를 유지한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기능 개발이 완료되는 시점에 mainline에 반영해 동료들에게 공유하기 때문에&lt;b&gt; 소스가 명확하다는 장점&lt;/b&gt;이 있지만, 위에서 언급한 &lt;b&gt;커밋 주기가 길다는 단점&lt;/b&gt;이 있다. 짧게는 하루에서 길게는 한 달 넘게 걸리는 기능이 있기 때문에, &lt;b&gt;병합 시 충돌의 위험성이 크다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;Continuous Integration&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;별도의 브랜치에서 작업하지 않고 mainline에 수정사항을 바로 통합하는 전략&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;최소 하루에 한 번은 mainline에 통합하는 것을 목표로 짧은 커밋 주기를 가지는 전략이다. 커밋 단위가 작은 만큼 충돌의 위험성이 작다는 장점이 있다. 하지만 healthy 한 mainline을 유지하기 위해 매 커밋마다 테스트가 필수적이며, 이를 위해 꼼꼼한 단위테스트 작성이 필요하다는 단점이 있다. 또한 새로운 기능의 개발 중인 소스가 mainline에 반영되기 때문에 &lt;b&gt;Feature Toggle&lt;/b&gt;이라는 추가적인 전략이 필요하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Feature Toggle 이란 개발 완료되지 않은 기능이 클라이언트에 노출되는 것을 막기 위해, 코드 수정 없이 시스템의 동작을 바꾸는 기술이다.&lt;/blockquote&gt;
&lt;figure id=&quot;og_1683546020733&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;매일 배포하는 팀이 되는 여정(2) &amp;mdash; Feature Toggle 활용하기&quot; data-og-description=&quot;효율적이고 안정적인 배포를 위해 고민했던 것 중 하나인 Feature Toggle(Feature Flag)에 대한 이야기&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/daangn/매일-배포하는-팀이-되는-여정-2-feature-toggle-활용하기-b52c4a1810cd&quot; data-og-url=&quot;https://medium.com/daangn/%EB%A7%A4%EC%9D%BC-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%8A%94-%ED%8C%80%EC%9D%B4-%EB%90%98%EB%8A%94-%EC%97%AC%EC%A0%95-2-feature-toggle-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-b52c4a1810cd&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/P5qIE/hySyfv6ooy/wVJ6l60sK54Dd5Qo5OcHh0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://medium.com/daangn/매일-배포하는-팀이-되는-여정-2-feature-toggle-활용하기-b52c4a1810cd&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/daangn/매일-배포하는-팀이-되는-여정-2-feature-toggle-활용하기-b52c4a1810cd&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/P5qIE/hySyfv6ooy/wVJ6l60sK54Dd5Qo5OcHh0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;매일 배포하는 팀이 되는 여정(2) &amp;mdash; Feature Toggle 활용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;효율적이고 안정적인 배포를 위해 고민했던 것 중 하나인 Feature Toggle(Feature Flag)에 대한 이야기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Code Review&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 코드리뷰 시점이 애매해진다는 단점이 있다. 브랜치 통합 전략의 경우 github의 pull-request 기능과 결합하여, feature 브랜치가 mainline에 병합되기 전에 코드리뷰를 할 수 있었다. 하지만 Continuous Integration은 미완성된 소스가 계속 mainline에 반영되는데 코드리뷰를 어느 시점에 할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드리뷰에 대해서도 개발자들이 처리해야 할 큰 업무로 다가오면서 일종의 부채로 느껴지기 시작한 지 오래됐다고 한다. 그러면서 코드리뷰의 단점에 대해서도 많이 논의된 상황인데 정리하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 코드리뷰 처리량&lt;/li&gt;
&lt;li&gt;코드리뷰 과정에서 발생하는 불화&lt;/li&gt;
&lt;li&gt;지연되는 코드리뷰
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드리뷰가 승인될 동안 개발자의 업무가 불명확해짐&lt;/li&gt;
&lt;li&gt;시간이 지날수록 Context가 희미해져 효율이 낮아짐&lt;/li&gt;
&lt;li&gt;병합 시 충돌 가능성이 커짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리뷰어에게 코드 품질을 의존하는 구조
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀원을 신뢰하지 않는 분위기를 조장할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 문제점 때문에 점차 코드리뷰를 줄이는 추세라고 한다. 나는 코드리뷰 문화를 경험해보지도 못했는데 뭔가 억울하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신&lt;b&gt; 짝 프로그래밍(pair programming) 혹은 `&lt;a href=&quot;https://martinfowler.com/articles/ship-show-ask.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ship/Show/Ask&lt;/a&gt;`를 통해 코드리뷰의 빈자리를 보완한다고 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;무조건 Continuous Integration?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'featrue 브랜치는 레거시 하고 나쁜 걸까?'라고 생각이 들 수도 있지만 그렇지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전략의 핵심은 통합 주기를 짧게 해서 충돌을 최소화해 개발자들의 스트레스를 줄이면서, 서비스를 성공적으로 배포하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 단위 테스트 코드가 부족하거나, 주니어 개발자들이 많아 강제적인 코드리뷰가 꼭 필요하는 등 여러 이유로 인해 mainline의 healthy를 유지하기가 힘든 상황이 있을 수도 있다. 그렇다면 Continuous Integration을 과감히 포기하고 `release` 브랜치를 따로 구성해, 배포 가능한 소스를 확보해 놓는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은탄환은 없다지만 현재 여건(신뢰할 수 있는 동료)이 된다면 브랜치를 최소화하는 것이 제일 좋은 방향이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 알려진 git-flow는 여러 배포 버전을 관리해야 하는 서비스를 대상으로 만든 전략이다. 현재 그 의도와 맞지 않게 서비스에 비해 브랜치만 과도하게 않은 케이스가 많다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 버전을 관리할 필요 없는 웹 서비스라면&lt;b&gt; github-flow&lt;/b&gt;와 &lt;b&gt;Trunk-Based Development&lt;/b&gt; 전략 중에 하나를 선택하는 것이 현재로선 최선으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;figure id=&quot;og_1683548956150&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Patterns for Managing Source Code Branches&quot; data-og-description=&quot;Mainline, Feature Branching, Continuous Integration, Release Branch and a clutch of other handy patterns.&quot; data-og-host=&quot;martinfowler.com&quot; data-og-source-url=&quot;https://martinfowler.com/articles/branching-patterns.html&quot; data-og-url=&quot;https://martinfowler.com/articles/branching-patterns.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d7VbHl/hySypZPGOc/TNht2tukCGMwZRWUOgvk61/img.png?width=560&amp;amp;height=300&amp;amp;face=0_0_560_300,https://scrap.kakaocdn.net/dn/bAYmKM/hySyhgrbJc/PhUTOAOz0PaOw30LQNwKlk/img.png?width=564&amp;amp;height=396&amp;amp;face=0_0_564_396,https://scrap.kakaocdn.net/dn/TyrKo/hySydZnEpb/gM6eKpKsdkASKY1EkLxk40/img.png?width=714&amp;amp;height=202&amp;amp;face=0_0_714_202&quot;&gt;&lt;a href=&quot;https://martinfowler.com/articles/branching-patterns.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://martinfowler.com/articles/branching-patterns.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d7VbHl/hySypZPGOc/TNht2tukCGMwZRWUOgvk61/img.png?width=560&amp;amp;height=300&amp;amp;face=0_0_560_300,https://scrap.kakaocdn.net/dn/bAYmKM/hySyhgrbJc/PhUTOAOz0PaOw30LQNwKlk/img.png?width=564&amp;amp;height=396&amp;amp;face=0_0_564_396,https://scrap.kakaocdn.net/dn/TyrKo/hySydZnEpb/gM6eKpKsdkASKY1EkLxk40/img.png?width=714&amp;amp;height=202&amp;amp;face=0_0_714_202');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Patterns for Managing Source Code Branches&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Mainline, Feature Branching, Continuous Integration, Release Branch and a clutch of other handy patterns.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;martinfowler.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Tools/git</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/70</guid>
      <comments>https://bbbicb.tistory.com/70#entry70comment</comments>
      <pubDate>Mon, 8 May 2023 21:29:28 +0900</pubDate>
    </item>
    <item>
      <title>공통 lib -&amp;gt; spring-boot-starter로 바꾸기(3)</title>
      <link>https://bbbicb.tistory.com/69</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bbbicb.tistory.com/64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2023.02.13 - [Backend/Java] - 공통 lib -&amp;gt; spring-boot-starter로 바꾸기(1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bbbicb.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2023.03.20 - [Backend/Java] - 공통 lib -&amp;gt; spring-boot-starter로 바꾸기(2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;span style=&quot;caret-color: #009a87;&quot;&gt;&lt;b&gt;조금 다른, 새로운 프로젝트 등장&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 커져가면서,&lt;b&gt; 기능을 분리해 새로운 프로젝트로 관리&lt;/b&gt;해야 했다. 기존에 공통 기능 프로젝트(master)와 업무 프로젝트(slave)를 잘 분리했기에 손쉽게 프로젝트를 구성할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o2uLX/btsbUPNBDkt/7IlhJNfykTJo9s4KRLRQ0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o2uLX/btsbUPNBDkt/7IlhJNfykTJo9s4KRLRQ0k/img.png&quot; data-alt=&quot;프로젝트 의존성 다이어그램 - 프로젝트가 커져갈 수록 slave 프로젝트는 점점 많아질 것이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o2uLX/btsbUPNBDkt/7IlhJNfykTJo9s4KRLRQ0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo2uLX%2FbtsbUPNBDkt%2F7IlhJNfykTJo9s4KRLRQ0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;394&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 의존성 다이어그램 - 프로젝트가 커져갈 수록 slave 프로젝트는 점점 많아질 것이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 업무 프로젝트(slave)마다 필요한 설정이 조금씩 다를 것이다. 특별하게 필요한 설정은 각 프로젝트에서 설정해 사용하면 되지만, 문제는 &lt;b&gt;master 프로젝트에서 제공하는 공통 기능이 특정 프로젝트에서는 필요 없는 상황이 생길 수도 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 기존에 공통으로 처리하던 어떤 웹에 의존적인 설정(필터나 로그인 인터셉터 Bean)들은 배치프로젝트에서는 필요 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;master 프로젝트의 Bean을 선택적으로 적용할 수 있어야 한다.&lt;/b&gt; 하지만 현재 설정된 방식으로는 Bean을 스캔하는 package를 똑같이 맞춰 놓았기 때문에 특정 Bean을 제외할 좋은 방법이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 설정된 방식은 &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://bbbicb.tistory.com/64&quot;&gt;spring-boot-starter로 바꾸기(1)&lt;/a&gt;을 참고하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해서는 &lt;b&gt;프로젝트 설정을 spring-autoconfigure 방식으로 변경해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;spring-boot-starter&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;프로젝트 구조&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;master-spring-boot-starter&lt;/b&gt;: 2개의 하위 모듈을 묶어주는 root 프로젝트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;master-spring-boot-autoconfigure&lt;/b&gt;: 스프링 Context에 자동으로 Bean 등록하는 설정 프로젝트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;worker&lt;/b&gt;: 공통 라이브러리 기능의 프로젝트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;slave&lt;/b&gt;: starter 프로젝트를 의존 주입받아 사용하는 프로젝트&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;프로젝트 명은 유니크한 gav 가지기 위해 스프링에서 {module-name}-spring-boot-{module-type}을 권장하고 있다.&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/1.5.11.RELEASE/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-naming&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-boot/docs/1.5.11.RELEASE/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-naming&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sviio/btscxbvgfK4/L5wFzHcLuzh5p3K4fCv6k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sviio/btscxbvgfK4/L5wFzHcLuzh5p3K4fCv6k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sviio/btscxbvgfK4/L5wFzHcLuzh5p3K4fCv6k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsviio%2FbtscxbvgfK4%2FL5wFzHcLuzh5p3K4fCv6k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;392&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;worker project&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 인사말을 주입받아 인사하는 로봇 클래스를 하나 만들었다. 이 로봇이 대부분의 프로젝트에서 필요한 객체라고 가정하자.&lt;/p&gt;
&lt;pre id=&quot;code_1682335715177&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package me.bbbicb.worker;

public class HelloBot {

  private final String helloComment;

  public HelloBot(String helloComment) {
    this.helloComment = helloComment;
  }

  public String hello() {
    return this.helloComment;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;master-spring-boot-autoconfigure project&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1682335796110&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package me.bbbicb.masterspringbootautoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = &quot;master&quot;)
public class MasterProperty {
  private String hello = &quot;hi&quot;; // 프로퍼티 설정이 없을 경우 적용될 default값

  public String getHello() {
    return hello;
  }

  public void setHello(String hello) {
    this.hello = hello;
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1682336562913&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package me.bbbicb.masterspringbootautoconfigure;

import me.bbbicb.worker.HelloBot;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
@AutoConfiguration
@EnableConfigurationProperties(MasterProperty.class)
public class HelloAutoConfigure {

  @Bean
  @ConditionalOnMissingBean
  public HelloBot helloBot(MasterProperty masterProperty) {
    return new HelloBot(masterProperty.getHello());
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1682338315950&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package me.bbbicb.masterspringbootautoconfigure;

import me.bbbicb.worker.HelloBot;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import static org.assertj.core.api.Assertions.assertThat;

class MasterSpringBootAutoconfigureApplicationTests {

  private final ApplicationContextRunner acr = new ApplicationContextRunner()
    .withConfiguration(AutoConfigurations.of(HelloAutoConfigure.class));

  @Test
  @DisplayName(&quot;hello함수 실행시 'MasterProperty' hello값을 반환한다.&quot;)
  void whenDefaultHelloReturnMasterPropertyValue() {
    acr.run(ct -&amp;gt; {
      assertThat(ct).hasSingleBean(HelloBot.class);
      assertThat(ct).hasSingleBean(MasterProperty.class);
      MasterProperty masterProperety = ct.getBean(MasterProperty.class);
      assertThat(ct.getBean(HelloBot.class).hello()).isEqualTo(masterProperety.getHello());
      assertThat(masterProperety.getHello()).isEqualTo(&quot;hi&quot;);
    });
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;autoconfigure 프로젝트에서 worker에서 정의된 `HelloBot` 빈을 프로퍼티 설정 값으로 자동 등록한다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여기서 proxyBeanMethods는 싱글톤을 보장하기 위한 설정으로 기본값이 true인데, 성능을 위해 대부분의 자동설정에서는 false로 설정한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`@ConditionalOn` 어노테이션으로 같은 타입의 Bean이 Context에 없을 경우에만, 정의한 `HelloBot` Bean이 자동 등록되도록 했다. autoconfigure 프로젝트에 등록된 Conditional Bean은 마지막 순서에 로드하는 것을 스프링에서 보장하므로, 혹여나 Bean 로드 순서가 잘못될 일은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;classpath:/resources/META-INF/spring/&lt;/b&gt; 경로에 &lt;b&gt;org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/b&gt; 파일을 만들고, 정의한 AutoConfigure 클래스를 정의하면 된다. &lt;b&gt;스프링부트가 실행될 때, 파일에 명시된 클래스들을 추가적으로 스캔할 것이다&lt;/b&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1682338120385&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
me.bbbicb.masterspringbootautoconfigure.HelloAutoConfigure&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존에는 &lt;b&gt;META-INF/spring.factories&lt;/b&gt;였지만 스프링부트 3 버전부터 위의 경로로 변경됐다.&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.0.0/reference/html/features.html#features.developing-auto-configuration.locating-auto-configuration-candidates&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-boot/docs/3.0.0/reference/html/features.html#features.developing-auto-configuration.locating-auto-configuration-candidates&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;slave project&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;span style=&quot;caret-color: #009a87;&quot;&gt;&lt;b&gt;slave1&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1682338365697&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package me.bbbicb.slave;

import me.bbbicb.worker.HelloBot;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SlaveApplicationTests {

  @Autowired
  HelloBot helloBot;

  @Test
  @DisplayName(&quot;기본으로, `HelloBot`을 등록하고 'hi'라고 인사한다.&quot;)
  void default_registerHelloBotBean() {
    Assertions.assertThat(helloBot.hello()).isEqualTo(&quot;hi&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slave1 프로젝트에서는 HelloBot이 필요해 &lt;b&gt;별다른 설정을 하지 않았고, 성공적으로 빈이 등록&lt;/b&gt;되고 동작하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;slave2&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1682338619986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package me.bbbicb.slave2;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class Slave2ApplicationTests {

  @Autowired
  BeanFactory factory;

  @Test
  @DisplayName(&quot;`HelloBot` Bean을 관리하지 않는다.&quot;)
  void doseNotRegisterHelloBotBean() {
    Assertions.assertThat(factory.containsBean(&quot;helloBot&quot;)).isFalse();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyaEuG/btscvZPGCU7/4beUhScUvV0ZuU6aazH2G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyaEuG/btscvZPGCU7/4beUhScUvV0ZuU6aazH2G0/img.png&quot; data-alt=&quot;별다른 설정을 하지 않아, HelloBot이 등록된 상태. 테스트 실패&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyaEuG/btscvZPGCU7/4beUhScUvV0ZuU6aazH2G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyaEuG%2FbtscvZPGCU7%2F4beUhScUvV0ZuU6aazH2G0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2012&quot; height=&quot;648&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;별다른 설정을 하지 않아, HelloBot이 등록된 상태. 테스트 실패&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slave2 프로젝트에서는 &lt;b&gt;HelloBot이 필요하지 않으므로, 제외하는 설정이 추가적으로 필요&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1682338727886&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package me.bbbicb.slave2;

import me.bbbicb.masterspringbootautoconfigure.HelloAutoConfigure;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(exclude = HelloAutoConfigure.class)
public class Slave2Application {

  public static void main(String[] args) {
    SpringApplication.run(Slave2Application.class, args);
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`@SpringBootApplication` 어노테이션 속성으로 제외할 `AutoConfiguration` 클래스를 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfdtvE/btsczMaHbMq/9JmIKJjgvXKR1NBBx4CGpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfdtvE/btsczMaHbMq/9JmIKJjgvXKR1NBBx4CGpk/img.png&quot; data-alt=&quot;예외 설정 후, HelloBot Bean이 등록되지 않았다. 테스트 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfdtvE/btsczMaHbMq/9JmIKJjgvXKR1NBBx4CGpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfdtvE%2FbtsczMaHbMq%2F9JmIKJjgvXKR1NBBx4CGpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2014&quot; height=&quot;628&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예외 설정 후, HelloBot Bean이 등록되지 않았다. 테스트 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;scanBasePackage는 나쁜 걸까?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://bbbicb.tistory.com/64&quot;&gt;spring-boot-starter로 바꾸기(1)&lt;/a&gt;에서 &lt;b&gt;문제점으로 언급했던 빈 등록 방식은 scanBasePackage 방식&lt;/b&gt;이다. 모듈 사용자가 필요 없는 Bean까지 등록되기 때문에 일종의 꼼수(나쁜 코드)라고 생각했고, 3개의 포스팅에 걸쳐 AutoConfigure방식으로 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 작성하면서 몇몇 글을 참고하다 &lt;a href=&quot;https://techblog.woowahan.com/2637/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;scanBasePackage 방식을 장려하는 글&lt;/a&gt;을 발견했고, 이내 내가 잘못생각하고 있다는 것을 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제는 모듈 간의 의존성이다.&lt;/b&gt; 모듈이 점점 거대해지면 당연히 각 프로젝트마다 불필요한 설정이 생기기 마련이다(내가 계속 예시로 들었던, 배치프로젝트에서 웹과 관련된 Bean들이 필요 없는 것).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제의 핵심 원인은 모듈을 분리하지 않은 것&lt;/b&gt;이고, 목적에 따른 &lt;b&gt;별개의 모듈 프로젝트로 분리함으로써 해결했어야 했다.&lt;/b&gt; 공통 모듈이라는 것은 정말 모든 프로젝트에서 사용될만한 것들만 정의하고, 웬만하면 사용을 지양해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈이 목적에 맞게 잘 분리돼있으면, 어플리케이션 프로젝트에서 모듈을 사용하겠다고 의존성을 명시한다는 것은 모듈의 대부분의 Bean들이 필요하다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 모듈의 Bean들은 편의를 위해 scanBasePackage 방식으로 등록한다. 단, 모듈의 시스템 설정과 관련된 Bean들만 AutoConfigure로 관리하고, 사용자가 확장할 수 있도록 `@Conditional` 혹은 `Customizer`방식을 제공한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세한 코드는 아래 깃허브에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/HyoJip/start-gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/HyoJip/start-gradle&lt;/a&gt;&lt;/p&gt;</description>
      <category>Backend/Java</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/69</guid>
      <comments>https://bbbicb.tistory.com/69#entry69comment</comments>
      <pubDate>Sun, 23 Apr 2023 18:07:48 +0900</pubDate>
    </item>
    <item>
      <title>Stream에 Decorator 패턴 써먹기</title>
      <link>https://bbbicb.tistory.com/67</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;span style=&quot;caret-color: #009a87;&quot;&gt;&lt;b&gt;&quot;데이터 가공이 필요해요.&quot;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 기능(엑셀 다운로드)에서 메모리를 절약하기 위해 대용량 데이터를 조회할 경우 마이바티스 `&lt;b&gt;Cursor&lt;/b&gt;` 타입을 매개변수로 받도록 설계했다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;여기서 문제를 명확히 하기 위해 커서 데이터가 전부 소비됐으면, 예외를 던져주기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1680522207723&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ExcelFile {
	public void write(Cursor&amp;lt;?&amp;gt; cursor) {
    	if (cursor.isConsumed()) throw new IllegalArgumentException(&quot;데이터가 없어요.&quot;);
        try (cursor) {
            // cursor 데이터를 엑셀 파일에 작성하는 로직
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 기능을 사용하는 개발자는 커서 데이터 타입을 인자로 넘겨줘야 하는데, 흔히 &lt;b&gt;개발자들이 DB에서 조회된 이 커서 데이터를 인자로 넘겨주기 전에 가공해야 하는 경우가 생긴다&lt;/b&gt;. 흔한 컬렉션(List, Set, Map) 타입은 개발자들이 인자로 넘겨주기 전에 가공하면 되지만, &lt;b&gt;문제는 이 커서 타입 데이터는 스트림이라는 것이다.&lt;/b&gt; 커서 데이터를 한번 읽고 넘겨준다면, 공통 기능이 제대로 동작하지 않을 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1680522409375&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Cursor&amp;lt;Object&amp;gt; objects = repository.findAll(); // db 데이터 조회
for (obj : objects) {
    // 객체 수정
}
ExcelFile file = new ExcelFile();
file.write(objects); // **예외 발생** &quot;데이터가 없어요.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자들이 공통 기능을 사용하기 전에 데이터를 수정할 수 있으면서, (메모리 절약을 위해) 커서 데이터는 유지해야 한다. 이를 해결하기 위해 데코레이터 패턴을 사용한 경험을 작성하고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Mybatis Cursor Stream&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마이바티스에서 제공하는 데이터 타입인데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;대용량 쿼리 조회 시 발생할 수 있는 메모리 부족 문제를 해결해 준다.&lt;/b&gt; 쿼리문 실행 결과를 마치 페이징처리된 것처럼 조금씩&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어플리케이션 메모리에 스트림 방식으로 조금씩 로드&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 스트림방식의 특성상, &lt;b&gt;한 번 소비되면 재사용 될 수 없기 때문에 데이터를 여러 번 읽을 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1680523330246&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Cursor (mybatis 3.5.13 API)&quot; data-og-description=&quot;All Superinterfaces: AutoCloseable, Closeable, Iterable All Known Implementing Classes: DefaultCursor Cursor contract to handle fetching items lazily using an Iterator. Cursors are a perfect fit to handle millions of items queries that would not normally f&quot; data-og-host=&quot;mybatis.org&quot; data-og-source-url=&quot;https://mybatis.org/mybatis-3/apidocs/org/apache/ibatis/cursor/Cursor.html&quot; data-og-url=&quot;https://mybatis.org/mybatis-3/apidocs/org/apache/ibatis/cursor/Cursor.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://mybatis.org/mybatis-3/apidocs/org/apache/ibatis/cursor/Cursor.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mybatis.org/mybatis-3/apidocs/org/apache/ibatis/cursor/Cursor.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Cursor (mybatis 3.5.13 API)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;All Superinterfaces: AutoCloseable, Closeable, Iterable All Known Implementing Classes: DefaultCursor Cursor contract to handle fetching items lazily using an Iterator. Cursors are a perfect fit to handle millions of items queries that would not normally f&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mybatis.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;데코레이터(Decorator) 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;패턴에 대한 것은 잘 설명된 글이 많아 간단히만 설명하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 데코레이터 패턴에 대해 한 단어로 설명하자면 &lt;b&gt;객체꾸미기&lt;/b&gt;라고 말할 수 있다. 객체를 같은 타입으로 계속해서 겉에 꾸며나가는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 예로 많이 드는 카페에서 주문을 받는 상황에서, 카페라떼에 휘핑크림추가 + 두유변경 + 사이즈업과 같은 여러 개의 옵션을 추가하는 기능을 구현할 때 적합한 패턴이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;java.io 패키지를 보면 대부분이 데코레이터 패턴을 사용했다. 같은 타입의 데코레이터를 원하는 만큼 감싸서 기능을 추가하도록 설계되었다. 아래 코드는 &lt;s&gt;백준에서 습관적으로 작성하는&lt;/s&gt;&amp;nbsp;읽기∙쓰기 작업 속도를 향상시키기 위해 버퍼 기능을 추가한 것이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1680455473445&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
     BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out))) {}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Cursor Decorator&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서 데이터를 읽을 때, &lt;b&gt;특정 로직을 수행할 수 있도록 하는 추가 기능 데코레이터를 제공&lt;/b&gt;했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680523643319&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class T CursorDecorator&amp;lt;T&amp;gt; implements Cursor {
    private final Cursor&amp;lt;T&amp;gt; cursor;
    private final Consumer&amp;lt;T&amp;gt; consumer;
    
    public CursorDecorator(Cursor&amp;lt;T&amp;gt; cursor, Consumer&amp;lt;T&amp;gt; consumer) {
    	this.cursor = cursor;
        this.consumer = consumer;
    }
    
    @Override
    public void forEach(Consumer&amp;lt;? super T&amp;gt; action) {
        for (c : cursor) {
            this.consumer.accept(c);
            action.accept(c);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 데이터 가공이 필요한 개발자들은 아래와 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1680523908075&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Cursor&amp;lt;Object&amp;gt; objects = repository.findAll(); // db 데이터 조회
Cursor&amp;lt;Object&amp;gt; modifiedObjects = new CursorDecorator(objects, (obj) -&amp;gt; {
    // 객체 수정
});
ExcelFile file = new ExcelFile();
file.write(modifiedObjects); // 성공&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남들이 보기엔 별 문제도 아닌 간단한 상황일 수도 있지만, 개인적으로 예전에 공부했던 디자인패턴을 실제 문제 상황에 적용해 해결했다는 것이 뿌듯하기도 하고 재밌었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RDiYG/btr7GzoWvya/nzHIpftu47larfjYSfuaMk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RDiYG/btr7GzoWvya/nzHIpftu47larfjYSfuaMk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RDiYG/btr7GzoWvya/nzHIpftu47larfjYSfuaMk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRDiYG%2Fbtr7GzoWvya%2FnzHIpftu47larfjYSfuaMk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;600&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Backend/Java</category>
      <author>비비빅B</author>
      <guid isPermaLink="true">https://bbbicb.tistory.com/67</guid>
      <comments>https://bbbicb.tistory.com/67#entry67comment</comments>
      <pubDate>Mon, 3 Apr 2023 02:40:13 +0900</pubDate>
    </item>
  </channel>
</rss>