<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>엄범</title>
    <link>https://umbum.tistory.com/</link>
    <description>ㅤㅤㅤㅤㅤUniv. of Seoulㅤㅤㅤㅤㅤ
ㅤㅤKITRI BoB 7th BEST 10ㅤㅤ
ㅤㅤworks at Naver Financialㅤㅤ</description>
    <language>ko</language>
    <pubDate>Sun, 17 May 2026 18:38:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>umbum</managingEditor>
    <image>
      <title>엄범</title>
      <url>https://tistory1.daumcdn.net/tistory/2281233/attach/0e65b9ae6ab446ed878841c5852ddbdf</url>
      <link>https://umbum.tistory.com</link>
    </image>
    <item>
      <title>return (code, data) 함께 반환하기</title>
      <link>https://umbum.tistory.com/1362</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때때로 method 내에서 정상 로직 진행에 실패한 경우에도 data를 상위 메서드에 전달해야 하는 경우가 있다. (e.g., 추가인증필요, 기처리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;a href=&quot;https://umbum.dev/1360&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;throw Exception을 return 처럼 data 반환 용도로 사용하는 것은 불가능&lt;/a&gt; 하기 때문에, return 기반으로 접근해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size16&quot;&gt;직접 return 타입 정의해 사용하기&lt;/h3&gt;
&lt;pre id=&quot;code_1696652744715&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class FooResult(
    val code: Code,
    val field1OnlyIfSuccess,
    val field2OnlyIfSuccess,
    val field3OnlyIfFailure
} {
    enum Code { SUCCESS, FAILURE }
 }&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;size16&quot;&gt;Result&amp;lt;T, R&amp;gt; 타입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;R 타입에 code 없고 성공+실패 data field만 존재하는 경우 유용.&lt;/li&gt;
&lt;li&gt;보편적으로 많이 쓰이기 때문에 정의한 것이지 없어도 되는 타입. (각자 FooResult 정의해서 사용해도 된다)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1696652218768&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Result&amp;lt;T, R&amp;gt; 타입
{
    &quot;code&quot; : T
    &quot;message&quot; : {String}
    &quot;data&quot; : R
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1696652625024&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum FooCode { SUCCESS, FAILURE }
data class FooData(
    val field1OnlyIfSuccess,
    val field2OnlyIfSuccess,
    val field3OnlyIfFailure
}

Result&amp;lt;FooCode, FooData&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Result&amp;lt;T, R&amp;gt;과 rust, kotlin의 Result&amp;lt;T, E&amp;gt; 와는 다르다.&lt;br /&gt;E는 Exception 타입을 의미하고, R은 data의 타입이다.&lt;br /&gt;애초에 Result&amp;lt;T, E&amp;gt;는 callee 측에서 '반환타입을 Result&amp;lt;T, E&amp;gt;로 해서 반환하겠습니다' 해도 의미가 없고 (믿을 수 없으니)&lt;br /&gt;caller가 호출 할 때 '결과를 Result&amp;lt;T, E&amp;gt;로 감싸서 받겠습니다' 해야 의미가 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;code, data를 함께 반환한다고 해서 caller에서 catch를 안해도 되는 것은 아니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://umbum.dev/1361&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;caller는 callee를 믿지 않는 것이 좋다 (어차피 catch 해야 한다)&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 정상 실패가 아닌 모든 예상치 못한 에러 (e.g., 파라미터 에러나 WebClient 에러)까지 (code, data)로 감싸서 반환 해서는 안된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 예상치 못한 에러는 그냥 throw하게끔 두는 것이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>System Design/Method design</category>
      <category>return 설계</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1362</guid>
      <comments>https://umbum.tistory.com/1362#entry1362comment</comments>
      <pubDate>Sat, 7 Oct 2023 13:26:38 +0900</pubDate>
    </item>
    <item>
      <title>caller는 callee를 믿지 않는 것이 좋다 (어차피 catch 해야 한다)</title>
      <link>https://umbum.tistory.com/1361</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중요한 처리로직 인 경우, 어차피 caller는 callee를 믿어서는 안된다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 callee는 다른 class 혹은 다른 class의 메서드를 의미한다.&lt;/li&gt;
&lt;li&gt;같은 class 내 private method를 호출하는 경우라면, 자기 자신이 직접 정의한 기능이므로 믿을 수 있으나, class 바깥의 다른 class와 메시지를 주고 받는 상황이라면 신뢰하지 않는 것을 기본 전제로 깔고가야 한다.&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;예시 ) 금액 소진 메서드를 제공하는 callee가 Exception을 반환하지 않겠다고 docstring에 명시해두었다면 믿을 것인가?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;금액 소진 기능인데, callee의 docstring과 현재 구현된 메서드 내부만 믿고 caller는 catch 안할 것인가? &lt;b&gt;그럴리가 없다&lt;/b&gt;는 것이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;caller 입장에서 callee는 언제든 변경될 수 있는 대상으로 바라보는 것이 더 안전하다.&lt;/li&gt;
&lt;li&gt;catch를 해야 하는 중요 구간이라면, callee가 throw하지 않겠다고 하더라도 caller는 catch를 어차피 넣을거다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1696650191115&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val result: Result&amp;lt;T, R&amp;gt; = callee {
    try {
        throwableCall()
    } catch { return Result(FAILURE, null) }
    return Result(SUCCESS, data)
}
if (result.code != SUCCESS) { ... }
val data: T = result.data
data ...&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드 예시 ) callee가 자기 자신을 try-catch로 감싸서 절대로 Exception이 발생하지 않도록 한 다음, &lt;a href=&quot;https://umbum.dev/1362&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Result&amp;lt;T, R&amp;gt;을 반환&lt;/a&gt;하는 방식인데&lt;/p&gt;
&lt;p 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;caller 코드에서 try-catch를 제거 할 수 있어 깔끔해 보일 수는 있겠으나, 아래와 같이&amp;nbsp;처리하기에는&amp;nbsp;불안한&amp;nbsp;감이&amp;nbsp;있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;caller는&amp;nbsp;callee가&amp;nbsp;절대로&amp;nbsp;Exception을&amp;nbsp;던지지&amp;nbsp;않을&amp;nbsp;것이라고&amp;nbsp;믿을&amp;nbsp;수&amp;nbsp;있겠는가?&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;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;size16&quot;&gt;그래서 if와 try-catch의 일원화는 불가능하다고 봐야 한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상된 실패 및 데이터가 존재하는 실패는 if로 처리하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예상치 못한 실패를 위한 백업 try-catch는 항상 존재해야 한다.&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;</description>
      <category>System Design/Method design</category>
      <category>return 설계</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1361</guid>
      <comments>https://umbum.tistory.com/1361#entry1361comment</comments>
      <pubDate>Sat, 7 Oct 2023 11:56:27 +0900</pubDate>
    </item>
    <item>
      <title>throw Exception을 return 처럼 data 반환 용도로 사용하는 것은 불가능하다</title>
      <link>https://umbum.tistory.com/1360</link>
      <description>&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: #f6e199;&quot;&gt;&lt;b&gt;return 대신 throw Exception 하면서 유용한 객체를 함께 반환하는 것은 불가능하다.&lt;/b&gt;&lt;/span&gt;&lt;/p&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;&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;return Type 개수 만큼 Exception class를 정의 할 수는 없으니 generic을 이용해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CommonException&amp;lt;T&amp;gt; 같이 정의하고 data: T에 데이터 담아서 던지면 되는거 아닌가 생각이 들 수 있는데&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;runtime에는 T가 사라지기 때문에 type-safe한 catching이 불가능하다. ( &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://stackoverflow.com/questions/501277/why-doesnt-java-allow-generic-subclasses-of-throwable&quot;&gt;link&lt;/a&gt; )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;subclass of 'throwable' may not have type parameters&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 컴파일타임 에러가 발생하기 때문에 CommonException을 Generic으로 만들 수 없다.&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;가능한 방법은 data: Any 로 쓰는 방법 뿐인데, 꺼내는 시점(runtime)에 직접 원하는 타입으로 형변환 해주어야 해서 굉장히 fragile하다.&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;i&gt;즉, 성공/실패 여부를 제외한 어떤 programmatic한 정보를 return이 아니라 throw를 이용해서 상위로 전달하는 것은 구조적으로 조금 어색하다. &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(나이스하게 처리 할 수 있는 방법이 없다.)&lt;/i&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;h3 style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Effective Kotlin 아이템 7. 일단&amp;nbsp;예외는&amp;nbsp;정보를&amp;nbsp;전달하는&amp;nbsp;방법으로&amp;nbsp;사용해서는&amp;nbsp;안된다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://umbum.dev/1285&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Effective Kotlin] 1장. 안정성&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size16&quot;&gt;그렇다면 정상 로직 진행에 실패했음에도 data를 상위 메서드에 전달해야 하는 경우에는 어떻게 해야 하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://umbum.dev/1362&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;return (code, data) 함께 반환하기&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Coding Note</category>
      <category>return 설계</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1360</guid>
      <comments>https://umbum.tistory.com/1360#entry1360comment</comments>
      <pubDate>Sat, 7 Oct 2023 00:59:36 +0900</pubDate>
    </item>
    <item>
      <title>afterStep 에서 Exception을 던져도 다음 Step이 이어서 실행된다.</title>
      <link>https://umbum.tistory.com/1355</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;afterStep에서 검증 로직 돌린 후, 다음 Step 실행하지 않고 배치를 종료하고 싶은 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 애플리케이션을 아예 종료해버리는 방법도 있지만, 보다 graceful 하게 처리하고 싶은 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 Step이 실행되지 않도록 하려면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size16&quot;&gt;종료 상태를 나타내는 Status는 BatchStatus와 ExitStatus 두개가 있다.&lt;/h3&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://docs.spring.io/spring-batch/docs/current/reference/html/index-single.html#batchStatusVsExitStatus&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-batch/docs/current/reference/html/index-single.html#batchStatusVsExitStatus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ExitStatus&lt;/b&gt; represents&amp;nbsp;the&amp;nbsp;status&amp;nbsp;of&amp;nbsp;a&amp;nbsp;Step&amp;nbsp;after&amp;nbsp;it&amp;nbsp;finishes&amp;nbsp;execution.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;on(ExitStatus...) 로 Step의 종료 상태에 따른 분기 처리를 위해 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BatchStatus&lt;/b&gt;&amp;nbsp;is&amp;nbsp;an&amp;nbsp;enumeration&amp;nbsp;that&amp;nbsp;is&amp;nbsp;a&amp;nbsp;property&amp;nbsp;of&amp;nbsp;both&amp;nbsp;JobExecution&amp;nbsp;and&amp;nbsp;StepExecution&amp;nbsp;and&amp;nbsp;is&amp;nbsp;used&amp;nbsp;by&amp;nbsp;the&amp;nbsp;framework&amp;nbsp;to&amp;nbsp;record&amp;nbsp;the&amp;nbsp;status&amp;nbsp;of&amp;nbsp;a&amp;nbsp;Job&amp;nbsp;or&amp;nbsp;Step.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job과 Step의 전반적인 상태를 관리하기 위해 사용한다. ( 더 큰 범위 )&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;어떤 Step에서 문제가 발생했을 때, 다음 Step이 실행되지 않도록 하는 방법은 2가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size16&quot;&gt;1. 비정상 ExitStatus를 반환하고 on() 으로 분기해주는 방법&lt;/h4&gt;
&lt;pre id=&quot;code_1695113005667&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return jobBuilderFactory.get(jobName)
    .start(step1)
    .on(ExitStatus.FAILED.getExitCode()).end()
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. BatchStatus가 COMPLETED가 아니도록 하는 방법&lt;/h4&gt;
&lt;pre id=&quot;code_1695112895610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BatchStatus가 COMPLETED가 아니면 다음 step을 실행하지 않는다
class SimpleJob {
    protected void doExecute(JobExecution execution) throws JobInterruptedException, JobRestartException, StartLimitExceededException {
        while(steps.hasNext()) {
            Step step = (Step)steps.next();
            stepExecution = this.handleStep(step, execution);
            if (stepExecution.getStatus() != BatchStatus.COMPLETED) {
                break;
            }
        }
        ...&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;기본적으로 일일히 on()을 이용해 비정상 ExitCode에 대해 분기처리 해주지 않아도, 예외가 발생하면 BatchStatus가 비정상으로 세팅되어 다음 Step 실행이 되지 않게끔 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;beforeStep, tasklet에서 발생한 Exception에 대해서는 그렇다. 하지만, afterStep에서 Exception이 발생하는 경우에는 다르다.&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;afterStep에서의 Exception은 배치를 종료시키지 않는다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`` AbstractStep::execute`` 부분 확인해보면, [beforeStep, Tasklet]에서 Exception 던지는 경우 BatchStatus.FAILED로 변경하기 때문에 배치가 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 [afterStep]에서 Exception throw 하는 경우 로그만 찍고 넘어가버려 다음 Step이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size16&quot;&gt;afterStep에서의&lt;span&gt;&amp;nbsp;&lt;/span&gt;setTerminateOnly는 배치를 종료시키지 않는다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`` AbstractStep::execute`` 부분 확인해보면, [beforeStep, Tasklet]에서 setTerminateOnly 하는 경우 BatchStatus.STOPPED로 변경하기 때문에 배치가 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 [afterStep]에서 setTerminateOnly 하는 경우 동작에 아무런 차이가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;afterStep 호출 후 isTerminateOnly 체크 하지 않는다. 그냥 다음 Step이 실행된다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;참고) afterStep 실행부는 finally block에 있기 때문에 beforeStep, tasklet에서 Exception이 일어나도 항상 실행된다.&lt;/blockquote&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;size16&quot;&gt;afterStep에서 직접 BatchStatus를 변경하면, on 분기를 사용하지 않아도 다음 Step 실행을 막을 수 있다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;afterStep에서 검증 로직 돌린 후, 다음 Step 실행하지 않고 배치를 종료하고 싶다면? (on 분기를 사용하지 않으면서)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BatchStatus를 STOPPED | FAILED 로 세팅하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1695112828274&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun afterStep(stepExecution: StepExecution): ExitStatus {
    stepExecution.upgradeStatus(BatchStatus.STOPPED)
    stepExecution.exitStatus = ExitStatus.STOPPED
    return stepExecution.exitStatus
}&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;</description>
      <category>Java Stack/Spring Batch</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1355</guid>
      <comments>https://umbum.tistory.com/1355#entry1355comment</comments>
      <pubDate>Tue, 19 Sep 2023 16:57:16 +0900</pubDate>
    </item>
    <item>
      <title>인덱스 관련 총정리</title>
      <link>https://umbum.tistory.com/1283</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size16&quot;&gt;인덱스 관련 이론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://umbum.dev/420&quot;&gt;6장. 물리적 데이터베이스 설계 : 인덱스 관련&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dataonair.or.kr/db-tech-reference/d-guide/sql/?uid=366&amp;amp;mod=document&amp;amp;pageid=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dataonair.or.kr/인덱스 기본 원리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/1155&quot;&gt;https://d2.naver.com/helloworld/1155&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/cd/E11882_01/server.112/e40540/indexiot.htm#CNCPT721&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.oracle.com/cd/E11882_01/server.112/e40540/indexiot.htm#CNCPT721&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size16&quot;&gt;커버링 인덱스 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/476&quot;&gt;https://jojoldu.tistory.com/476&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javadb/10.8.3.0/tuning/ctunoptimz30768.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.oracle.com/javadb/10.8.3.0/tuning/ctunoptimz30768.html&lt;/a&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;커버링 인덱스는 SELECT의 모든 컬럼이 인덱스에 들어있어야 적용된다.&lt;br /&gt;허나 그렇지 않다고 하더라도, Data Filter 보다 Key Filter를 적용하는게 성능에 큰 도움이 된다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data Filter 이전에 Key Filter를 넣어주면...&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;인덱스의 크기는 데이터 레코드의 크기에 비해 훨씬 작기 때문에, 한 block을 읽었을 때 가져올 수 있는 item의 수가 훨씬 많다. 따라서 데이터 레코드를 읽고 filtering하는 것 보다, 인덱스 단계에서 filtering 하는 것이 읽어야 하는 block 수를 절약할 수 있어 더 낫다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size16&quot;&gt;NDV, 선택도, 카디날리티&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NDV(Number of Distinct Value)&lt;/li&gt;
&lt;li&gt;선택도(selectivity)
&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;인덱스&amp;nbsp;스캔이&amp;nbsp;테이블&amp;nbsp;순차&amp;nbsp;스캔보다&amp;nbsp;항상&amp;nbsp;빠르지는&amp;nbsp;않다.&amp;nbsp;보통&amp;nbsp;선택도(selectivity)가&amp;nbsp;5~10%&amp;nbsp;이내인&amp;nbsp;경우에&amp;nbsp;인덱스&amp;nbsp;스캔이&amp;nbsp;우수하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카디날리티(Cardinality)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(선택도 * 총 row 수)&lt;/li&gt;
&lt;li&gt;즉 해당 predicate로 선택된 row의 수. Optimizer가 plan 짤 때 참고하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스는 NDV가 높아야 의미가 있다. (e.g., 성별 같이 NDV가 2이면 인덱스 거는 의미가 없다.)&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;size16&quot;&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;a href=&quot;https://jojoldu.tistory.com/243&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jojoldu.tistory.com/243&lt;/a&gt; &amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 분석글에서는 차이가 있는 것 처럼 보이지만, 실제로는 이 자체만으로는 큰 차이가 없는 것 같다. (-&amp;gt; 차이가 없는게 맞다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dba.stackexchange.com/questions/174227/cardinality-column-order-in-compound-index-mysql-innodb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dba.stackexchange.com/questions/174227/cardinality-column-order-in-compound-index-mysql-innodb&lt;/a&gt; &amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://richardfoote.wordpress.com/2008/02/13/its-less-efficient-to-have-low-cardinality-leading-columns-in-an-index-right/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://richardfoote.wordpress.com/2008/02/13/its-less-efficient-to-have-low-cardinality-leading-columns-in-an-index-right/&lt;/a&gt;&amp;nbsp; &amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;A common myth or mis-perception is that when deciding how to order the columns in a concatenated, multi-column index, one should avoid placing low cardinality columns in front.&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;즉, 쿼리 조건이 모두 = 동등비교로 인덱스를 타는 경우라면, 인덱스가 어떤 순서로 구성되어 있든 블록IO 개수는 똑같다! 따라서 성능도 같다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(다른 원인으로 미미한 차이가 있을 수 있겠지만 결국 쿼리 성능을 좌우하는 main factor는 몇개의 블록IO가 발생하느냐 이다)&lt;/li&gt;
&lt;li&gt;다만 = 동등비교가 아니라 범위 &amp;lt;, &amp;gt;, BETWEEN 질의라면 얘기가 크게 달라진다.&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;/h3&gt;
&lt;pre id=&quot;code_1692099820996&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM tbl  
WHERE a &amp;gt; 1 AND a &amp;lt; 5  
AND b &amp;lt; 'K'  
AND c &amp;gt; 10000  
ORDER BY b;&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;545&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nUhV3/btsrdineijY/0WEGefpbNKQkhaYNxik0gK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nUhV3/btsrdineijY/0WEGefpbNKQkhaYNxik0gK/img.png&quot; data-alt=&quot;https://d2.naver.com/helloworld/1155 - CUBRID에 대한 설명이지만 대부분의 RDB가 비슷할 것이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nUhV3/btsrdineijY/0WEGefpbNKQkhaYNxik0gK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnUhV3%2FbtsrdineijY%2F0WEGefpbNKQkhaYNxik0gK%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;545&quot; height=&quot;396&quot; data-origin-width=&quot;545&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://d2.naver.com/helloworld/1155 - CUBRID에 대한 설명이지만 대부분의 RDB가 비슷할 것이다.&lt;/figcaption&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;[1, 2, 3 ...]은 첫번째 컬럼인 a이고, [AAA, BBB, CCC...] 는 두 번째 컬럼인 b이다. 초록색이 복합 인덱스 (a, b)를 의미한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key Range: 인덱스 스캔 범위로 활용하는 조건이다(a &amp;gt; 1 AND a &amp;lt; 5).&lt;/li&gt;
&lt;li&gt;Key Filter: Key Range에 포함할 수 없지만 인덱스 키로 처리 가능한 조건이다(b &amp;lt; 'K').&lt;/li&gt;
&lt;li&gt;Data Filter: 인덱스를 사용할 수 없는 조건이다. 테이블에서 레코드를 읽어야만 처리 가능한 조건이다(c &amp;gt; 10000).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;즉... 인덱스의 선두 컬럼 a가 범위 질의이면, 컬럼 a의 범위에 해당하는 인덱스 블록을 일단 모두 읽어들인 후, 인덱스 블록 데이터에 포함된 인덱스 컬럼 b를 기준으로 filtering해서 조건에 맞지 않는 항목들을 버리는 식으로 동작한다.&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;a, b를 동시에 만족하는 인덱스 블록들만을 한번에 찾아가서 읽어들이는 것이 아니다!&lt;/b&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;1&amp;lt;a&amp;lt;5 AND b='DDD' 조건일 때, 상위 인덱스가 [(2, AAA), (2, KKK), (3, AAA)...] 수준으로 나뉘어져 있다면 인덱스 블록 탐색 범위를 반으로는 줄일 수 있을 것 같은데... [(2, AAA), (3, AAA)...]만 읽으면 되니까.&lt;/li&gt;
&lt;li&gt;실제로 돌려보면 이렇게 인덱스를 검색해서 탐색하는 것 보다, 일단 a를 기준으로만 다 읽고 filtering 하는 방식이 더 빠르기 때문일까? 아니면 인덱스가 너무 세밀해서 trade-off가 발생하기 때문일까? 여기까진 잘 모르겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;a의 범위가 커도 충분히 빠를까? =&amp;gt; 그렇지 않다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컬럼 a가 시간이고, 컬럼 b가 userId라고 가정해보자. 하루치 데이터는 200만건이다. 인덱스 (a, b)는 보조인덱스다.&lt;/li&gt;
&lt;li&gt;특정 유저의 1일치 데이터를 가져오려면, 2023-08-01&amp;lt;a&amp;lt;2023-08-02 AND b='userId'&lt;/li&gt;
&lt;li&gt;Key Range: 일단 1일치에 해당하는 인덱스 블록을 모조리 가져온다. (보조인덱스 이므로 200만건)&lt;/li&gt;
&lt;li&gt;Key Filter: 200만건을 b='userId'로 filtering 한다.&lt;/li&gt;
&lt;li&gt;본질적으로 200만건에 대한 필터링이므로, b='userId'인 row가 1만건이어도 a의 범위가 크면 클 수록 느려진다. (실제로 성능 좋은 Oracle에서 돌려보니 5s 걸렸다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;a의 범위가 큰데, 인덱스 순서를 (b, a)로 바꾸면?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 동일한 상황에서 인덱스 순서를 (b, a)로 바꾸게 되면&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Key Range: b='userId'에 해당하는 인덱스 블록 가져온다. (보조인덱스 이므로 1만건)&lt;/li&gt;
&lt;li&gt;Key Filter: 1만건을 2023-08-01&amp;lt;a&amp;lt;2023-08-02로 flitering 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;즉 범위 질의 하는 경우 NDV에 따른 복합 인덱스의 순서가 매우 중요해진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&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;nbsp;&lt;/p&gt;</description>
      <category>RDBMS/Query design</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1283</guid>
      <comments>https://umbum.tistory.com/1283#entry1283comment</comments>
      <pubDate>Tue, 15 Aug 2023 22:10:56 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] as와 타입 캐스팅. 런타임 에러. 타입 파라미터 소거(erasure)</title>
      <link>https://umbum.tistory.com/1345</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691937200209&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun typeCastTest() {
    val stringMap = mapOf(
        &quot;a&quot; to &quot;0&quot;,
        &quot;b&quot; to &quot;10.01&quot;
    )
    val bigDecimalMap = stringMap as Map&amp;lt;String, BigDecimal&amp;gt;
    println(bigDecimalMap)  /* 문제 없이 실행된다. */

    bigDecimalMap.forEach {
        val bigDecimal: BigDecimal = it.value
        println(bigDecimal)
        /* 컴파일은 잘 되지만.. */
        /* 런타임에 class java.lang.String cannot be cast to class java.math.BigDecimal 에러 발생한다 */
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, bigDecimal는 BigDecimal 타입이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;it.value는 String 타입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헌데 컴파일 타임에 문제가 발생하지 않는다. 캐스팅도 잘 되는 것 처럼 보이는데 실제로 사용하는 부분에서 Exception이 발생한다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM의 제네릭스는 보통 타입 소거(type erasure)를 사용해 구현되기 때문에,&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;예를 들어 ``kt List&amp;lt;String&amp;gt;`` 객체를 만들고 그 안에 문자열이 들어있더라도,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;런타임에는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그 객체를 오직 ``kt List``로만 인식할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;* 원소를 하나 얻어서 타입 검사를 수행할 수 있겠지만 여러 원소가 서로 다른 타입일 수도 있기 때문에 좋은 방법이 아니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;* 일반적인 경우 ``kt List&amp;lt;String&amp;gt;``에는 문자열만 들어있음을 가정할 수 있는 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;컴파일 타임에 컴파일러가&lt;/b&gt;&amp;nbsp;타입 인자를 인식해 올바른 타입의 값만 리스트에 넣도록 보장&lt;/span&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;런타임에는 타입 인자 정보가 없기 때문에, 강제로 다른 타입 인자로 캐스팅해도 캐스팅이 성공한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 어떤 경우에는 문제 없이 동작하지만, 또 어떤 경우에는 CastException이 발생하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1691937517972&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun listSum(li: List&amp;lt;*&amp;gt;) {
    val intList = li as List&amp;lt;Int&amp;gt;    // 캐스팅은 성공하고
    println(intList.sum())    // 여기서 런타임 Exception 발생
}
&amp;gt;&amp;gt;&amp;gt; listSum(listOf(1, 2, 3))
6
&amp;gt;&amp;gt;&amp;gt; listSum(listOf(&quot;a&quot;, &quot;b&quot;))
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number&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;* 위와 같은 예제는 ``kt &amp;lt;T: Int&amp;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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;as 캐스팅으로 껍데기만 바꾸지 말고, ObjectMapper 사용해서 본질까지 변환하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://umbum.dev/611&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Kotlin] 제네릭 : 타입 파라미터 소거(erasure), inline 실체화(reified)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java Stack/Kotlin</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1345</guid>
      <comments>https://umbum.tistory.com/1345#entry1345comment</comments>
      <pubDate>Sun, 13 Aug 2023 23:43:41 +0900</pubDate>
    </item>
    <item>
      <title>Spring JDBC와 JdbcOperations</title>
      <link>https://umbum.tistory.com/1340</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JDBC를 사용할 때, CrudRepository와 자동생성 쿼리 만으로는 커버가 되지 않는 경우가 반드시 생기고, 이 경우 @Query 보다는 JdbcOperations를 쓰는 것이 낫다. (see &lt;a href=&quot;https://umbum.dev/1240&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Data JDBC] docs&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;따라서 CrudRepository를 아래와 같이 Dao로 확장해서 관리하는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1691658108689&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface MerchantInfoRepository : NfcJdbcRepository&amp;lt;MerchantInfo, MerchantInfo.CompositeKey&amp;gt;

@Repository
class MerchantInfoDao(
    private val merchantInfoRepository: MerchantInfoRepository,
    private val jdbcOperations: NamedParameterJdbcOperations
): MerchantInfoRepository by merchantInfoRepository {  // delegate 처리해서 interface 노출
    fun upsert(merchantInfo: MerchantInfo): Int {
        return jdbcOperations.update(
            upsertQuery,
            ObjectSqlParameterSource(merchantInfo)
        )
    }
    private val upsertQuery = &quot;&quot;&quot;
        MERGE INTO mrc
        USING DUAL
        ON (mrc_no = :merchantNo...)    // bind는 반드시 $가 아니라 :로. (prepared statement)
        ...&quot;&quot;&quot;
}&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;size16&quot;&gt;:param bind&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 3가지 방법 사용 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1691658394469&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1.
BeanPropertySqlParameterSource(obj)

2.
MapSqlParameterSource()
    .addValue(&quot;param1&quot;, param1)
    
3.
mapOf(&quot;param1&quot; to param1)&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;여기서 LocalDateTime 같은 타입에 대한 Converter를 자동으로 적용하려면, 아래 2가지 방법 사용 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1691658462880&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1.
objectMapper.converValue(map으로 변환)
objectMapper에 Converter가 등록되어 있어야 한다. (보통은 Auto-config)
이 방법도 나쁘지 않다.

2.
ObjectSqlParameterSource를 직접 정의하고 override해서 Convert 처리

class ObjectSqlParameterSource(
    val obj: Any
): BeanPropertySqlParameterSource(obj) {

    override fun getValue(paramName: String): Any? {
        return when (val value = super.getValue(paramName)) {
            is LocalDateTime -&amp;gt; LocalDateTimeToStringConverter.convert(value)
            is CustomLocalDate -&amp;gt; CustomLocalDateToStringConverter.convert(value)
            is CardNumber -&amp;gt; CardNumberToStringConverter.convert(value)
            is CurrencyCode -&amp;gt; CurrencyCodeToStringConverter.convert(value)
            else -&amp;gt; value
        }
    }
    
    /**
     * BeanPropertySqlParameterSource.getSqlType을 보면 
     * JavaType이 Date이면 SqlType을 Timestamp로 만들어버린다.
     * 따라서 반드시 override 필요함.
     */
    override fun getSqlType(paramName: String): Int {
        val sqlType = super.getSqlType(paramName)
        return if (sqlType == Types.TIMESTAMP) {
            Types.VARCHAR
        } else {
            sqlType
        }
    }
}&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;size16&quot;&gt;참고&lt;/h3&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://github.com/benelog/spring-jdbc-tips/blob/master/spring-jdbc-core.md&quot;&gt;spring-jdbc-tips/spring-jdbc-core.md at master &amp;middot; benelog/spring-jdbc-tips&lt;/a&gt;&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java Stack/Persistence</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1340</guid>
      <comments>https://umbum.tistory.com/1340#entry1340comment</comments>
      <pubDate>Thu, 10 Aug 2023 12:11:14 +0900</pubDate>
    </item>
    <item>
      <title>전문 해석기 배치 - File Line to Domain Model 변환</title>
      <link>https://umbum.tistory.com/1333</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전문 해석 시 고려해야 하는 것들은, align, padding, trim, 날짜 포맷, 숫자 포맷 변환 등이다.&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;문자 타입은 끝문자 trim 정도만 처리하면 제대로 매핑되지만,&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;- e.g., 0.8%을 어디서는 00080000 으로 보내고, 어디서는 00.80으로 보냄.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- padding도 어디서는 (0, LEFT)로, 어디서는 (' ', RIGHT) RIGHT로 할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 날짜를 어디서는 yyyyMMdd를 사용하고, 어디서는 yyMMdd 사용함.&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;따라서 전문 해석 케이스를 정리해보면, 크게 2가지 클래스가 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;class A &amp;rArr; fieldSet에서 꺼내서 type 변환하고, align에 따라 padding trim처리 하는 클래스 하나 (&amp;rArr; 어떤 전문이든 공통으로 적용되는 기능)&lt;/li&gt;
&lt;li&gt;class B &amp;rArr; 날짜 포맷 커버, 숫자 포맷 커버하는 디테일한 변환을 담당하는 클래스 하나. (&amp;rArr; 해당 전문에 specific하게 적용되는 기능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. file &amp;rarr; (enum+Domain Model)로 변환하는 방식.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 없이, 파일에서 바로 읽어서 fileLine &amp;rarr; Domain Model 로 매핑.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 FieldSetMapper를 각 data 전문마다 정의해야 함.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;enum class MerchantInfoDataTokenInfo(
    val tokenName: String,
    val range: Range,
    val type: TokenType,
    val alignment: AlignmentType
) {
    recDvCd(&quot;recDvCd&quot;, Range(1, 1), STRING, LEFT),
    regDvCd(&quot;regDvCd&quot;, Range(2, 2), String, LEFT),
    ...;

    companion object {
        val tokenizer = FixedByteLengthTokenizer(Charset.forName(&quot;euc-kr&quot;)).apply {
            setNames(*values().map { it.tokenName }.toTypedArray())
            setColumns(*values().map { it.range }.toTypedArray())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class MerchantInfoFieldSetMapper: FieldSetMapper&amp;lt;MerchantInfo&amp;gt; {
    override fun mapFieldSet(fieldSet: FieldSet): MerchantInfo {
        return MerchantInfo(
            registrationDate = CustomLocalDate.parse(fieldSet.readString(MerchantInfoDataTokenInfo.regDate.tokenName).trimEnd(), dateFormatter),
            cancelDate = if (fieldSet.readString(MerchantInfoDataTokenInfo.cnlDate.tokenName).trimEnd().isNotEmpty()) {
                CustomLocalDate.parse(fieldSet.readString(MerchantInfoDataTokenInfo.cnlDate.tokenName).trimEnd(), dateFormatter)
            } else null,
            merchantNo = fieldSet.readString(MerchantInfoDataTokenInfo.merNo.tokenName).trimEnd(),
            phoneNumber = fieldSet.readString(MerchantInfoDataTokenInfo.bizlTelRgnNo.tokenName).trimEnd() + &quot;-&quot; +
                fieldSet.readString(MerchantInfoDataTokenInfo.bizlTelHno.tokenName).trimEnd() + &quot;-&quot; +
                fieldSet.readString(MerchantInfoDataTokenInfo.bizlTelRapsNo.tokenName).trimEnd(),
            creditCardCommissionRate = fieldSet.readBigDecimal(MerchantInfoDataTokenInfo.feeRtN8.tokenName).movePointLeft(7),
            installmentAvailabilityYn = fieldSet.readString(MerchantInfoDataTokenInfo.selDvCd.tokenName).trimEnd().let {
                when (it) {
                    &quot;1&quot; -&amp;gt; YnEnum.Y
                    &quot;2&quot; -&amp;gt; YnEnum.N
                    &quot;&quot; -&amp;gt; null
                    else -&amp;gt; throw IllegalStateException(&quot;### Unknown value : $it&quot;)
                }
            },
            ...
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;setTokenizers(
    mapOf(
        &quot;H*&quot; to MerchantInfoHeaderTokenInfo.tokenizer,
        &quot;D*&quot; to MerchantInfoDataTokenInfo.tokenizer,
        &quot;T*&quot; to MerchantInfoTrailerTokenInfo.tokenizer
    )
)
setFieldSetMappers(
    mapOf(
        &quot;H*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramHeader::class.java),
        &quot;D*&quot; to MerchantInfoFieldSetMapper(),
        &quot;T*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramTrailer::class.java)
    )
)

fun itemProcessor() = ItemProcessor&amp;lt;MerchantInfoInterface, MerchantInfo&amp;gt;() {
    when (it) {
        is MerchantInfoTelegramHeader -&amp;gt; {     // 애는 DTO
            logger.info { &quot;### Header : $it&quot; }
            null
        }
        is MerchantInfo -&amp;gt; {                   // 얘는 Domain Model
            logger.debug { &quot;### Data : $it&quot; }
            it
        }
        is MerchantInfoTelegramTrailer -&amp;gt; {    // 애는 DTO
            logger.info { &quot;### Trailer : $it&quot; }
            null
        }
        else -&amp;gt; throw IllegalStateException(&quot;unknown telegram type : ${it.recDvCd}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;trim 같은 공통 처리하는 class A &amp;rarr; enum&lt;/li&gt;
&lt;li&gt;디테일한 변환 처리하는 class B &amp;rarr; MerchantInfoFieldSetMapper&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단점)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;processor에서 Domain Model을 바로 받기 때문에 DTO에 대한 로깅을 하기가 애매하다.&lt;/li&gt;
&lt;li&gt;Header, Trailer는 DTO인데 Data영역은 Domain Model을 전달받으니 통일성이 없어보인다.&lt;/li&gt;
&lt;li&gt;Header, Data, Trailer를 묶어줄 MerchantInfoInterface라는 공통의 상위타입을 정의해야 Reader &amp;rarr; Processor로 전달이 가능한데&amp;hellip; DTO도 이걸 구현하고, Domain Model도 이걸 구현하니 상속계층이 좀 애매해진다. 단순히 processor로 전달하기 위한 목적의 Interface라..?&lt;/li&gt;
&lt;li&gt;FieldSetMapper에서 readString 및 trim 처리를 enum으로 옮길 수는 있지만, 그렇게 해도 엄청 깨끗해지지는 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. file &amp;rarr; (DTO+enum) &amp;rarr; Domain Model로 변환하는 방식.&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;enum class MerchantInfoDataTokenInfo(
    val tokenName: String,
    val range: Range,
    val type: TokenType,
    val alignment: AlignmentType
) {
    recDvCd(&quot;recDvCd&quot;, Range(1, 1), STRING, LEFT),
    regDvCd(&quot;regDvCd&quot;, Range(2, 2), String, LEFT),
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class MerchantInfoTelegramData(
    override val recDvCd: String,
    ...
) : MerchantInfoTelegram() {

    fun toEntity(): MerchantInfo {
        return MerchantInfo(
            registrationDate = CustomLocalDate.parse(regDate, dateFormatter),
            ...
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;setTokenizers(
    mapOf(
        &quot;H*&quot; to MerchantInfoHeaderTokenInfo.tokenizer,
        &quot;D*&quot; to MerchantInfoDataTokenInfo.tokenizer,
        &quot;T*&quot; to MerchantInfoTrailerTokenInfo.tokenizer
    )
)
setFieldSetMappers(
    mapOf(
        &quot;H*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramHeader::class.java),
        &quot;D*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramData::class.java),
        &quot;T*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramTrailer::class.java)
    )
)

fun itemProcessor() = ItemProcessor&amp;lt;MerchantInfoTelegram, MerchantInfo&amp;gt;() {
    when (it) {
        is MerchantInfoTelegramHeader -&amp;gt; {   // DTO
            logger.info { &quot;### Header : $it&quot; }
            null
        }
        is MerchantInfoTelegramData -&amp;gt; {     // DTO
            logger.debug { &quot;### Data : $it&quot; }
            it.toEntity()
        }
        is MerchantInfoTelegramTrailer -&amp;gt; {  // DTO
            logger.info { &quot;### Trailer : $it&quot; }
            null
        }
        else -&amp;gt; throw IllegalStateException(&quot;unknown telegram type : ${it.recDvCd}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;trim 같은 공통 처리하는 class A &amp;rarr; TelegramFieldSetMapper&lt;/li&gt;
&lt;li&gt;디테일한 변환 처리하는 class B &amp;rarr; DTO에서 담당 (toEntity 메서드)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1 대비 장점)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Processor에서 전달받는 Header, Data, Trailer는 모두 DTO라 통일성이 있음.&lt;/li&gt;
&lt;li&gt;모두 DTO이므로 MerchantInfoTelegram를 상속받는 것에 문제가 없음. (abstract class로 만들어 레코드 구분 필드를 사용 할 수도 있음)&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;전문 필드 이름, range, 타입, align 정보 등은 MerchantInfoDataTokenInfo enum에서 관리하고 전문 필드 매핑은 MerchantInfoTelegramData dto에서 담당하니 전문을 나타내는 표현 클래스가 2개로 이원화됨.&lt;/li&gt;
&lt;li&gt;tokenizer에서 사용할 이름은 enum에서 관리하고, 이 이름을 dto로 매핑하게 되는데 컴파일 타임 연관관계를 가지는 것이 아니라서 fragile함. (enum에서만 이름을 바꿨다면?)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. file &amp;rarr; (DTO+annotation) &amp;rarr; Domain Model로 변환하는 방식.&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class MerchantInfoTelegramData(
    @field:TelegramField(1, 1, STRING, LEFT)
    override val recDvCd: String,
    @field:TelegramField(2, 2, STRING, LEFT)
    val regDvCd: String,
    ...
) : MerchantInfoTelegram() {
    fun toEntity(): MerchantInfo { ... }

    companion object {
        val tokenizer = FixedByteLengthTokenizer(Charset.forName(&quot;euc-kr&quot;)).apply {
            val telegramFields = TelegramField.listOf(MerchantInfoTelegramData::class.java)
            setNames(*telegramFields.map { it.first }.toTypedArray())
            setColumns(*telegramFields.map { Range(it.second.start, it.second.end) }.toTypedArray())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class TelegramField(
    val start: Int,
    val end: Int,
    val type: TokenType,
    val alignment: AlignmentType
) {
    companion object {
        fun &amp;lt;T&amp;gt; listOf(cls: Class&amp;lt;T&amp;gt;): List&amp;lt;Pair&amp;lt;String, TelegramField&amp;gt;&amp;gt; {
            return cls.declaredFields
                .filter { it.isAnnotationPresent(TelegramField::class.java) }
                .map {
                    ReflectionUtils.makeAccessible(it)
                    Pair(it.name, it.getAnnotation(TelegramField::class.java))
                }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;setTokenizers(
    mapOf(
        &quot;H*&quot; to MerchantInfoTelegramHeader.tokenizer,
        &quot;D*&quot; to MerchantInfoTelegramData.tokenizer,
        &quot;T*&quot; to MerchantInfoTelegramHeader.tokenizer
    )
)
setFieldSetMappers(
    mapOf(
        &quot;H*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramHeader::class.java),
        &quot;D*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramData::class.java),
        &quot;T*&quot; to TelegramFieldSetMapper(MerchantInfoTelegramTrailer::class.java)
    )
)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;trim 같은 공통 처리하는 class A &amp;rarr; TelegramFieldSetMapper&lt;/li&gt;
&lt;li&gt;디테일한 변환 처리하는 class B &amp;rarr; DTO에서 담당 (toEntity 메서드)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2 대비 장점)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전문 필드 이름, range, 타입, align 정보 관리, 전문 필드 매핑을 모두 단일 클래스 MerchantInfoTelegramData에서 담당하므로 관리포인트 감소&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. file &amp;rarr; (DTO+TField) &amp;rarr; Domain Model로 변환하는 방식.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3과 거의 유사하므로 생략.&lt;/p&gt;
&lt;pre id=&quot;code_1695780652994&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 애너테이션 방식
@TelegramField(1, 2, STRING, LEFT...)
String field

// 객체 방식
TelegramField field = new TelegramField(1, 2, STRING, LEFT...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리 포인트 관점에서 두 방식에 차이가 있지는 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 TelegramField 객체 사용하는 방식은 필드 타입이 원시타입이 아닌 객체이기 때문에, 기존에 predefined 된 FieldSetMapper나 ObjectMapper를 바로 사용 할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 FieldSetMapper를 직접 커스텀으로 구현하거나, TelegramField 내부에 deserialize를 위한 로직을 넣어주어야 한다.&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;게다가 DTO 필드인 TelegramField는 DTO 생성하면서 미리 초기화 되어 있고, 나중에 전문을 받으면 DTO.field.data = ... 로 데이터를 채워넣는 방식이 될 수 밖에 없어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 불변성 측면에서 좋지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 메타데이터 관리 책임과 실제 데이터 관리 책임이 둘 다 TelegramField로 모이게 된다는 단점이 있다.&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java Stack/Spring Batch</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1333</guid>
      <comments>https://umbum.tistory.com/1333#entry1333comment</comments>
      <pubDate>Sat, 29 Jul 2023 17:56:13 +0900</pubDate>
    </item>
    <item>
      <title>전문 해석기 배치 LineMapper - TelegramFieldSetMapper</title>
      <link>https://umbum.tistory.com/1331</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecordFieldSetMapper로 모든 케이스의 전문 변환이 커버 가능할까? =&amp;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;pre id=&quot;code_1690359886451&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ConversionService {
    override fun convert(source: Any?, sourceType: TypeDescriptor?, targetType: TypeDescriptor): Any?
}

// 위 메서드는 아래 호출 구문을 통해서 넘어오는데...

public class RecordFieldSetMapper&amp;lt;T&amp;gt; implements FieldSetMapper&amp;lt;T&amp;gt; {
    public T mapFieldSet(FieldSet fieldSet) {
        args[i] = this.typeConverter.convertIfNecessary(fieldSet.readRawString(name), type);
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;즉, ConversionService에서 알 수 있는 정보는, [source (&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;abcde&amp;nbsp;&amp;nbsp;&quot;), sourceType (String), targetType (String)] 뿐이다.&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;&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;abcde&amp;nbsp;&amp;nbsp;&quot;가&amp;nbsp;FieldSet에서&amp;nbsp;어떤&amp;nbsp;name이었는지는&amp;nbsp;여기서&amp;nbsp;알&amp;nbsp;수가&amp;nbsp;없다.&lt;br /&gt;따라서 name을 기반으로 찾아야만 하는 enum도 찾을 수가 없고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;enum을 찾을 수 없으니 align, tokenType도 알 수가 없다. (결과 dto type은 targetType으로 넘어와 알 수 있지만 tokenType은 알 수 없다.)&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;그렇다면... 해결 방안은? ConversionService에 name을 함께 넘겨주는 FieldSetMapper를 구현하는 방법 뿐이다.&lt;/b&gt;&lt;br /&gt;(애너테이션&amp;nbsp;기반이든,&amp;nbsp;TField&amp;nbsp;기반이든&amp;nbsp;모두&amp;nbsp;똑같다.&amp;nbsp;FieldSetMapper를&amp;nbsp;구현해야만&amp;nbsp;align,&amp;nbsp;tokenType을&amp;nbsp;알아낼&amp;nbsp;수&amp;nbsp;있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size16&quot;&gt;But, TokenInfo를 반드시 보아야 하는가?에 대해서 생각해 볼 필요가 있다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tokenType에 따른 디테일한 align 처리나 trim 처리가 필요하다면 직접 FieldSetMapper를 구현해야 하는게 맞으나&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;대부분의 경우 뒤쪽 공백 trim이거나, 양쪽 trim해도 상관 없고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;숫자에 대한 처리는 앞에 오는 0이나 ' ' 공백을 없애버려도 무방하다.&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;즉, 대부분의 경우 RecordFieldSetMapper에 trim을 더한 StringToStringConverter만 추가하는 것으로 요구사항을 충분히 만족시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(make things right 관점에서는 FieldSetMapper를 직접 구현하는 것이 옳지만..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size16&quot;&gt;RecordFieldSetMapper를 활용한 TelegramFieldSetMapper&lt;/h3&gt;
&lt;pre id=&quot;code_1690366192498&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 전문 필드 타입 STRING에 대해, 항상 오른쪽 trim하는 경우만 있었기 때문에 오른쪽 trim하는 FieldSetMapper.
 * NUMBER 타입은 자동으로 leading 0s 제거하고 변환됨.
 *
 * align, trim에 대한 디테일한 처리가 필요하다면 RecordFieldSetMapper에 대한 Composition을 제거하고
 * 직접 align, trim 정보를 enum 또는 annotation으로 가져와 처리하는 방식으로 확장.
 *
 * R이 필요한 이유?
 * 실제 targetClass의 타입은 각각 ATelegramHeader, ATelegramData, ATelegramTrailer이지만
 * 필요한 타입은 이들의 상위타입인 FieldSetMapper&amp;lt;ATelegram&amp;gt;이기 때문.
 * FieldSetMapper가 무공변이기 때문에 out 지정 불가하고, 별도로 R을 써주어야 한다.
 */
class TelegramFieldSetMapper&amp;lt;T: Any, R: T&amp;gt;(
    targetType: Class&amp;lt;R&amp;gt;,
) : FieldSetMapper&amp;lt;T&amp;gt; {

    private val recordFieldSetMapper = RecordFieldSetMapper(
        targetType,
        DefaultConversionService().apply {
            this.addConverter(String::class.java, String::class.java) { it.trimEnd() }
        }
    )

    override fun mapFieldSet(fieldSet: FieldSet): T {
        return recordFieldSetMapper.mapFieldSet(fieldSet)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1690366215833&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@StepScope
@Bean(STEP_NAME + &quot;ItemReader&quot;)
fun itemReader(): FlatFileItemReader&amp;lt;ATelegram&amp;gt; {
    ...

    return FlatFileItemReader&amp;lt;ATelegram&amp;gt;().apply {
        setEncoding(&quot;utf-8&quot;)
        setResource(FileSystemResource(filePath))
        setLineMapper(PatternMatchingCompositeLineMapper&amp;lt;ATelegram&amp;gt;().apply {
            setTokenizers(
                mapOf(
                    &quot;H*&quot; to AHeaderTokenInfo.tokenizer,
                    &quot;D*&quot; to ADataTokenInfo.tokenizer,
                    &quot;T*&quot; to ATrailerTokenInfo.tokenizer
                )
            )
            setFieldSetMappers(
                mapOf(
                    &quot;H*&quot; to TelegramFieldSetMapper(ATelegramHeader::class.java),
                    &quot;D*&quot; to TelegramFieldSetMapper(ATelegramData::class.java),
                    &quot;T*&quot; to TelegramFieldSetMapper(ATelegramTrailer::class.java)
                )
            )
        })
    }
}&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;</description>
      <category>Java Stack/Spring Batch</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1331</guid>
      <comments>https://umbum.tistory.com/1331#entry1331comment</comments>
      <pubDate>Wed, 26 Jul 2023 17:29:56 +0900</pubDate>
    </item>
    <item>
      <title>Enum VS String : 외부 API 요청에 대한 응답 수신 코드로 enum을 쓰는게 좋을까?</title>
      <link>https://umbum.tistory.com/1330</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-ke-size=&quot;size16&quot;&gt;상황 1) 외부 API 요청에 대한 응답 수신 코드로 enum을 쓰는게 좋을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약 ) 외부 API 요청에 대한 응답 코드나, 내 API에 대한 요청 코드의 타입은&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;enum으로 정의하는 것 보다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;String으로 정의하고 enum 변환하는게 더 유연하다. (&lt;i&gt;fault&amp;nbsp;tolerance&lt;/i&gt;)&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;enum에 정의 되어 있지 않은 값이 응답 코드로 들어올 수 있기 때문에 이에 대한 처리를 생각해 주어야 한다. (e.g., 예고 없이 갑자기 추가된 응답 코드)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Exception 발생 해도 상관 없는 경우)&lt;/b&gt; 기본적으로 Exception 발생하게 되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1694066721115&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;기본 Exception 메시지에서 @JsonValue 기준 값이 로깅된다.
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `com.naver.nfcard.support.externalclients.point.dto.PgId` from String &quot;95005&quot;: not one of the values accepted for Enum class: [80133, 80006]&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;Exception 발생 안하고 넘어가야 하는 경우)&lt;/b&gt; `` @JsonEnumDefaultValue``로 디폴트 enum value로 매핑할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1694066825855&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum EnumType {
    A, B,
    @JsonEnumDefaultValue UNKNOWN;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;하지만&amp;nbsp;디폴트값&amp;nbsp;매핑하는&amp;nbsp;경우&amp;nbsp;응답&amp;nbsp;코드에&amp;nbsp;대한&amp;nbsp;정보가&amp;nbsp;Enum으로&amp;nbsp;변환되면서&amp;nbsp;사라진다.&amp;nbsp;(Enum에&amp;nbsp;정의되지&amp;nbsp;않은&amp;nbsp;응답&amp;nbsp;코드가&amp;nbsp;들어왔을&amp;nbsp;때&amp;nbsp;로깅&amp;nbsp;없이&amp;nbsp;default&amp;nbsp;Enum으로&amp;nbsp;매핑된다.)&lt;/i&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;Exception 발생 안하고 넘어가면서, 들어온 값에 대한 로깅이 필요한 경우 )&lt;/b&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;응답 코드 타입을 Enum으로 유지하면서, 로깅이 필요하다면 아래 세 가지 방법 사용 할 수 있다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일단 응답을 Map으로 받아서 로깅 후 객체 변환. &lt;br /&gt;즉, [json -&amp;gt; Obj] 바로 매핑하면 유실되니, 로깅을 위해 [json -&amp;gt; Map -&amp;gt; Obj]&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Enum Deserializer를 직접 구현해 로깅을 끼워 넣는 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 두 가지 방법 사용하는 것 보다, &lt;i&gt;&lt;u&gt;DTO에서 필드의 타입을 Enum이 아닌 String으로 변경하는 방법이 더 낫다.&lt;/u&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;String으로 받는 경우 [json -&amp;gt; Obj] 가 가능하다.&lt;/li&gt;
&lt;li&gt;DTO를 로깅하고, Domain Model로 변환 할 때 ``kt Enum.valueOf(String)`` 해주면 된다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;이 처럼 external layer에 대한 DTO 분리는 다양한 상황에서 매우 유용하다.&lt;br /&gt;&lt;a href=&quot;https://umbum.dev/1206&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring] MVC Layering Architecture : DTO와 Domain Model을 분리해야 하는 이유&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size16&quot;&gt;애초에 응답 코드를 enum으로 처리해야 하는 경우가 많지는 않다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;응답 코드에 따른 처리 로직을 묶어주어야 한다면, 처리 로직을 DTO 안에 method로 정의하는 정도로도 충분한 경우가 대부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이렇게 처리하면 분기는 생기지만, 보통은 분기를 DTO 안으로 숨길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;상황 2 ) method param Type 자체를 Enum으로 정의할 것인지? &lt;br /&gt;아니면 String으로 받고 validation check를 할 것인지?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 : loan을 실행할 대상은 반드시 Wallet이 존재해야 한다. &amp;amp; loan 대상은 Wallet의 subset이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;방법 1. LoanTarget이라는 Enum을 정의하고 이를 파라미터로 받는 방법&lt;/h4&gt;
&lt;pre id=&quot;code_1689239775380&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum class LoanTarget(val wallet: Wallet, ...) {
    A(Wallet.A, ...)
}

fun loan(target: LoanTarget) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;방법 2. Enum 정의 없이, param은 String으로 받고 validation check를 하는 방법&lt;/h4&gt;
&lt;pre id=&quot;code_1689239783017&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun loan(target: String) {
    if (!target in Wallet.names) { ... }
    ...
}&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;Wallet이나 Symbol에 대한 in check를 loan 메서드에서 수행하는 방법2 가 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(중복에도 불구하고) enum에 link 하도록 설계하는 방법1을 사용한 이유는&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;컴파일 타임에 실수를 알아챌 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 Loan 대상인데 Wallet에는 누락되어 있는 경우. (LoanTarget은 반드시 Wallet의 subset이어야 한다는 제약조건)&lt;/li&gt;
&lt;li&gt;방법 1은 enum 추가하면서 알아챌 수 있지만, 방법 2는 런타임에 Exception 발생해야 알 수 있다. (심지어 if 안에서 적절히 알림을 주지 않는다면, 영영 알 수 없을 수도.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;String을 Wallet, Symbol 로 캐스팅 하지 않고 member로 접근할 수 있기 때문에 코드 자체가 조금 더 의미를 가지게 된다.&lt;/li&gt;
&lt;li&gt;if 로직의 경우 이름으로 연결하겠다는 얘기인데, 상대적으로 fragile 하며 이름을 다르게 가져가야 하는 경우 수용이 안된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Coding Note</category>
      <category>enum</category>
      <author>umbum</author>
      <guid isPermaLink="true">https://umbum.tistory.com/1330</guid>
      <comments>https://umbum.tistory.com/1330#entry1330comment</comments>
      <pubDate>Thu, 13 Jul 2023 17:49:50 +0900</pubDate>
    </item>
  </channel>
</rss>