<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>차근차근정확하게</title>
    <link>https://gradualprecision.tistory.com/</link>
    <description>하나를 배우더라도, 이해를 넘어서서 응용이 가능하도록 생각하기!
느리더라도 꾸준히 발전하는 개발자!</description>
    <language>ko</language>
    <pubDate>Thu, 2 Jul 2026 17:52:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>거북이의 기술블로그</managingEditor>
    <image>
      <title>차근차근정확하게</title>
      <url>https://tistory1.daumcdn.net/tistory/7069672/attach/d97ef39dd6bb468f88e2eb639414e4d5</url>
      <link>https://gradualprecision.tistory.com</link>
    </image>
    <item>
      <title># 테스트가 설계를 바꾸기 시작했다: TDD를 체험해보다</title>
      <link>https://gradualprecision.tistory.com/286</link>
      <description>&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1편에서 이야기했듯, 나는 초기에 테스트를 잘못 이해한 방식으로 사용하고 있었다.&lt;br /&gt;테스트에서 기능을 만들어보지만 실제 프로덕션 코드는 다시 처음부터 작성하는 구조였고,&lt;br /&gt;테스트는 참고용 코드에 불과했다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;하지만, 이 시행착오를 겪은 덕분에&lt;/b&gt;, 테스트를 제대로 활용하는 방식,&lt;br /&gt;&lt;b&gt;즉 테스트 &amp;rarr; 설계 &amp;rarr; 프로덕션 코드 승격&lt;/b&gt;이라는 진짜 &lt;b&gt;TDD적 흐름&lt;/b&gt;을 이해하게 되었다.&lt;br /&gt;&lt;br /&gt;이 글에서는 내가 테스트를 활용하는 방식을 어떻게 바꿨고,&lt;br /&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;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;611&quot; data-start=&quot;578&quot; data-ke-size=&quot;size23&quot;&gt;# 테스트에서 시작하고, 설계로 이어지는 흐름을 만들다&lt;/h3&gt;
&lt;blockquote data-end=&quot;686&quot; data-start=&quot;613&quot; data-ke-style=&quot;style2&quot;&gt;예전처럼 테스트에서 기능을 만들고 프로덕션에서 처음부터 다시 만드는 대신,&lt;br /&gt;나는 다음과 같은 흐름으로 개발을 진행하기 시작했다.&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;862&quot; data-start=&quot;688&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;732&quot; data-start=&quot;688&quot;&gt;&lt;b&gt;테스트에서 간단한 프로토타입을 작성하고 원하는 동작을 검증한다.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;754&quot; data-start=&quot;733&quot;&gt;이 동작이 타당하다고 판단되면&lt;/li&gt;
&lt;li data-end=&quot;794&quot; data-start=&quot;755&quot;&gt;&lt;b&gt;테스트 안에서 만든 코드를 main으로 &amp;lsquo;승격&amp;rsquo;시키고&lt;/b&gt;,&lt;/li&gt;
&lt;li data-end=&quot;819&quot; data-start=&quot;795&quot;&gt;해당 기능의 책임을 명확히 나눈다.&lt;/li&gt;
&lt;li data-end=&quot;862&quot; data-start=&quot;820&quot;&gt;이후 @SpringBootTest 기반 DI 테스트로 리팩토링한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;921&quot; data-start=&quot;864&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 사용하자,&lt;br /&gt;테스트는 단순 검증 도구가 아니라 &lt;b&gt;설계를 이끄는 도구&lt;/b&gt;가 되기 시작했다.&lt;/p&gt;
&lt;p data-end=&quot;921&quot; data-start=&quot;864&quot; 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;h4 data-end=&quot;954&quot; data-start=&quot;928&quot; data-ke-size=&quot;size20&quot;&gt;# 테스트 코드가 설계를 요구하기 시작했다&lt;/h4&gt;
&lt;p data-end=&quot;983&quot; data-start=&quot;956&quot; data-ke-size=&quot;size16&quot;&gt;테스트를 작성하면 자연스럽게 이런 고민이 생긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1086&quot; data-start=&quot;985&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1016&quot; data-start=&quot;985&quot;&gt;&amp;ldquo;이 메서드는 너무 많은 일을 하고 있지 않나?&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;1051&quot; data-start=&quot;1017&quot;&gt;&amp;ldquo;여기 의존성은 분리해야 테스트하기 쉬울 것 같은데?&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;1086&quot; data-start=&quot;1052&quot;&gt;&amp;ldquo;이 로직을 재사용하려면 별도 클래스로 빼는 게 맞겠다.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1125&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;테스트하기 어려운 코드는 설계가 잘못되었다는 신호&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1127&quot; data-ke-size=&quot;size16&quot;&gt;아래는 실제로 내가 겪은 예시다.&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1127&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;451&quot; data-start=&quot;399&quot; data-ke-style=&quot;style1&quot;&gt;Step 1) POJO 스타일로, 테스트 메서드 내부에서 &amp;lsquo;기능을 직접 구현&amp;rsquo;&lt;/blockquote&gt;
&lt;p data-end=&quot;519&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;519&quot; data-start=&quot;453&quot; data-ke-style=&quot;style2&quot;&gt;아직 프로덕션 코드는 전혀 만들지 않은 상태.&lt;br /&gt;테스트 메서드 안에서 내가 원하는 기능을 &amp;ldquo;그냥 작성해보는 단계&amp;rdquo;다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1763885101488&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void saveFile_basicPrototype() throws Exception {
    // POJO 스타일, 모든 로직이 여기 들어 있음
    File file = new File(&quot;test.txt&quot;);
    String content = &quot;Hello&quot;;

    boolean result = false;
    try (FileWriter fw = new FileWriter(file)) {
        fw.write(content);
        result = true;
    } catch (Exception e) {
        result = false;
    }

    assertTrue(result);
}&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;테스트와 프로덕션을 나누지 않고, 기능을 하나의 테스트 내부에 전부 작성&lt;/li&gt;
&lt;li&gt;일종의, &quot;기능 프로토타입&quot;&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;style1&quot;&gt;Step 2) 테스트 폴더 내에서 'Method로 1차 분리'&lt;/blockquote&gt;
&lt;pre id=&quot;code_1763885188935&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void saveFile_extractMethod() throws Exception {
    File file = new File(&quot;test.txt&quot;);
    String content = &quot;Hello&quot;;

    boolean result = save(file, content);

    assertTrue(result);
}

private boolean save(File file, String content) {
    try (FileWriter fw = new FileWriter(file)) {
        fw.write(content);
        return true;
    } catch (Exception e) {
        return false;
    }
}&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;save()라는 메서드로 분리&lt;/li&gt;
&lt;li&gt;아직 src/test/ 아래에 메서드 존재&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;style1&quot;&gt;Step 3) 책임을 가진 메서드를 'static 내부 클래스' 로 이동&lt;/blockquote&gt;
&lt;pre id=&quot;code_1763885251751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void saveFile_withInnerClass() throws Exception {
    File file = new File(&quot;test.txt&quot;);

    FileStorage storage = new FileStorage();
    boolean result = storage.save(file, &quot;Hello&quot;);

    assertTrue(result);
}

static class FileStorage {
    public boolean save(File file, String content) {
        try (FileWriter fw = new FileWriter(file)) {
            fw.write(content);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/li&gt;
&lt;li&gt;설계 개선을 위해서, &lt;b&gt;책임을 분리하는 단계&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 클래스로 만들어서 객체를 new로 생성하여 이용&lt;/b&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;style1&quot;&gt;Step 4) 어느정도 구조가 자리를 잡으면, 프로덕션(main)코드로 승격&lt;/blockquote&gt;
&lt;pre id=&quot;code_1763885338869&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class FileStorage {

    public boolean save(File file, String content) {
        try (FileWriter fw = new FileWriter(file)) {
            fw.write(content);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}&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;내부클래스 &amp;gt;&amp;gt; src/main/java/.../FileStorage.java로 이동&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트에서 만든 내부클래스를 그대로 승격&lt;/b&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;style1&quot;&gt;Step 5) @SpringbootTest로 전환하여 DI 주입&lt;/blockquote&gt;
&lt;pre id=&quot;code_1763885417250&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class FileStorageTest {

    @Autowired
    FileStorage fileStorage;

    @Test
    void givenFile_whenSave_thenSuccess() {
        // Given
        File file = new File(&quot;sample.txt&quot;);
        String content = &quot;Sample&quot;;

        // When
        boolean result = fileStorage.save(file, content);

        // Then
        assertTrue(result);
    }
}&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;실제 프로덕션 코드를 주입받아서 검증하는 &lt;b&gt;단위테스트&lt;/b&gt;가 된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해당 테스트 코드는 프로타입이 아닌, 구현된 기능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Given - When - Then 구조만 남음 (깔끔한 테스트 코드 상태)&lt;/b&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;style1&quot;&gt;Step 6) 리펙토링 내성이 생기고, 생산성이 극적으로 증가&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이제 FileStorage를 아래처럼 바꿔도 테스트가 보호해준다&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3551&quot; data-start=&quot;3481&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3493&quot; data-start=&quot;3481&quot;&gt;저장 경로 분리&lt;/li&gt;
&lt;li data-end=&quot;3506&quot; data-start=&quot;3494&quot;&gt;경로 전략 적용&lt;/li&gt;
&lt;li data-end=&quot;3523&quot; data-start=&quot;3507&quot;&gt;네트워크 저장소로 전환&lt;/li&gt;
&lt;li data-end=&quot;3538&quot; data-start=&quot;3524&quot;&gt;AWS S3로 변경&lt;/li&gt;
&lt;li data-end=&quot;3551&quot; data-start=&quot;3539&quot;&gt;예외 정책 변경&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;3606&quot; data-start=&quot;3553&quot; data-ke-size=&quot;size16&quot;&gt;그 어떤 구조 변경을 하더라도&lt;br /&gt;테스트가 실패하지 않으면 &amp;ldquo;&lt;b&gt;그대로 동작함&lt;/b&gt;&amp;rdquo;을 &lt;b&gt;확신&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3606&quot; data-start=&quot;3553&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3606&quot; data-start=&quot;3553&quot; 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-end=&quot;3606&quot; data-start=&quot;3553&quot; data-ke-size=&quot;size23&quot;&gt;# 전체 흐름 다시 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 124px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;단계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;테스트 메서드 내부에서 POJO 스타일로 기능 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;메서드 분리 (테스트 폴더 내)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;static 내부 클래스로 책임 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;충분히 검증되면 프로덕션(main)으로 승격&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;@SpringBootTest + DI 기반 테스트로 재정비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&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;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;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;TDD에서는&lt;br /&gt;1. 실패하는 테스트케이스 작성 (RED)&lt;br /&gt;2. 성공하는 테스트 작성 (GREEN)&lt;br /&gt;3. 리펙토링 하기 (REFECTOR)&lt;br /&gt;를 하게되는데, RED작업을 많이 해보면 할수록 많은 예외사항을 처리할 수 있게된다.&lt;br /&gt;&lt;br /&gt;많은 시간이 없다면, GREEN과 REFECTOR를 통해서 작성만 해두어도 나중에 많은 시간을 절약할 수 있고 리펙토링을 할때에도 겁먹지 않고 편안하게 할 수 있게된다.&lt;br /&gt;&lt;br /&gt;하지만, 적응하는데에는 꽤나(?) 시간이 걸릴거같은 생각이든다...&lt;br /&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;&lt;b&gt;※ 참고) 리펙토링 방법&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;▷&amp;nbsp;켄트 백(Kent Beck)&amp;nbsp;&lt;/b&gt;&lt;/p&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;text-align: center;&quot;&gt;&lt;b&gt;단계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;이름 변경 (Rename)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;매직넘버 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;중복 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;함수 추출 (Extract Method)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;조건문 단순화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;가드절 도입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;부작용(side-effect) 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;임시 변수 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;책임 분리(Extract Class)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;데이터 구조 개선(파라미터 객체)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;11&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;조건문(if)을 반복문(loop) 기반 흐름으로 변환&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;12&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;반복문을 재귀(recursion)로 단순화&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;13&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;최종 단계: 재귀 제거 &amp;rarr; 반복문으로 최적화 (거의 가지 않음)&lt;/b&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;</description>
      <category>토이프로젝트/테스트</category>
      <category>green</category>
      <category>Java</category>
      <category>Kent Beck</category>
      <category>Red</category>
      <category>RED-GREEN-REFECTOR</category>
      <category>REFECTOR</category>
      <category>Spring</category>
      <category>TDD</category>
      <category>켄트백</category>
      <category>테스트 주도 개발</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/286</guid>
      <comments>https://gradualprecision.tistory.com/286#entry286comment</comments>
      <pubDate>Sun, 23 Nov 2025 17:26:39 +0900</pubDate>
    </item>
    <item>
      <title>#1 테스트 코드의 필요성을 절실히 느낀 이유...</title>
      <link>https://gradualprecision.tistory.com/285</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;테스트의 시작&lt;/blockquote&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;br /&gt;처음에는 만들면서 케이스들을 고려하여 구현하게되고, 마무리 단계에서 몇가지의 테스트들을 통해 직접 검증하는 방식으로 확인하였다.&lt;br /&gt;&lt;br /&gt;하지만, 그런 검증 방식에서 당연히 되야한다고 생각했던것들에서 &lt;b&gt;항상 1~2개씩 놓치게되어 다시 재배포하는 경우가 발생하게 되었다.&lt;/b&gt;&lt;br /&gt;처음에는 QA단계에서 발생하여 다행이다라고 생각을 하게되었지만, 몇번 이런 일들이 생기게되니 점차 스스로도 신뢰하지 못하는 단계에 이르게 되었다.&lt;br /&gt;&lt;br /&gt;그래서 테스트의 필요성을 깨닫고, &lt;b&gt;테스트를 시작하며 직면한 문제(?)에 대해서 기록해보고자 한다.&lt;/b&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;1. 완성했다고 생각한 기능에서 계속되는 문제 발생&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기능을 구현하고 &quot;이제 동작하겠지&quot;라고 생각했는데 실제로 테스트해보면 &lt;b&gt;자꾸 예상치 못한 케이스에서 오류가 발생&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1763883264199&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public int getLength(String input){
    return input.length();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot; 위의 코드에서 고려해야하는 것들이 어떤게 있을까? &quot;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-style=&quot;style2&quot;&gt;빈 문자열 처리 (&quot;&quot;)&lt;/li&gt;
&lt;li data-ke-style=&quot;style2&quot;&gt;공백 처리 (&quot; &quot;)&lt;/li&gt;
&lt;li data-ke-style=&quot;style2&quot;&gt;null 입력&lt;/li&gt;
&lt;li data-ke-style=&quot;style2&quot;&gt;특수문자 처리&lt;/li&gt;
&lt;li data-ke-style=&quot;style2&quot;&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;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;2. 리펙토링을 하고 싶어도, 어떤일이 발생할지(?) 짐작이 안가서 도전을 못함&lt;/blockquote&gt;
&lt;blockquote data-end=&quot;1087&quot; data-start=&quot;1037&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;1087&quot; data-start=&quot;1039&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;리팩토링 후 코드가 정상 동작하는지 확인할 방법이 &amp;lsquo;직접 검증&amp;rsquo;밖에 없었다.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1168&quot; data-start=&quot;1089&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1168&quot; data-start=&quot;1089&quot; data-ke-size=&quot;size16&quot;&gt;기능이 하나일 때는 직접 테스트하는 게 어렵지 않았지만,&lt;br /&gt;기능이 복잡해지고 외부 서비스와 얽히기 시작하면 직접 검증은 사실상 불가능해진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1246&quot; data-start=&quot;1170&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1187&quot; data-start=&quot;1170&quot;&gt;API 호출 직접 테스트&lt;/li&gt;
&lt;li data-end=&quot;1205&quot; data-start=&quot;1188&quot;&gt;파일 업로드 직접 테스트&lt;/li&gt;
&lt;li data-end=&quot;1225&quot; data-start=&quot;1206&quot;&gt;데이터베이스 상태 직접 확인&lt;/li&gt;
&lt;li data-end=&quot;1246&quot; data-start=&quot;1226&quot;&gt;특정 조건 재현을 위한 환경 세팅&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1333&quot; data-start=&quot;1248&quot; data-ke-size=&quot;size16&quot;&gt;이 모든 것을 수동으로 반복하는 것은 개발 효율을 크게 떨어뜨렸다.&lt;br /&gt;결국 리팩토링을 하고 싶어도, **&amp;ldquo;두려워서 할 수 없는 코드&amp;rdquo;**가 되어버렸다.&lt;/p&gt;
&lt;p data-end=&quot;1377&quot; data-start=&quot;1335&quot; data-ke-size=&quot;size16&quot;&gt;이때부터 자동으로 검증해주는 테스트가 필요하다는 것을 진짜로 깨닫게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;1377&quot; data-start=&quot;1335&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1377&quot; data-start=&quot;1335&quot; 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;p data-end=&quot;1377&quot; data-start=&quot;1335&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1377&quot; data-start=&quot;1335&quot; data-ke-size=&quot;size20&quot;&gt;# 테스트를 도입했지만, 잘못된 방식으로 사용하고 있었다&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;테스트의 필요성을 크게 느낀 후, 나는 곧바로 테스트 작성에 들어갔다.&lt;br /&gt;문제는, &lt;b&gt;그 방식이 TDD에서 말하는 테스트 기반 개발 흐름과 전혀 달랐다는 점이다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-end=&quot;1540&quot; data-start=&quot;1516&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1540&quot; data-start=&quot;1516&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;❌ 테스트 방식은 이런 흐름이었다&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1628&quot; data-start=&quot;1542&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1576&quot; data-start=&quot;1542&quot;&gt;테스트 폴더로 이동한다. (src/test/java)&lt;/li&gt;
&lt;li data-end=&quot;1594&quot; data-start=&quot;1577&quot;&gt;테스트 케이스를 작성한다.&lt;/li&gt;
&lt;li data-end=&quot;1628&quot; data-start=&quot;1595&quot;&gt;테스트 안에서 직접 메서드를 만들어본다.&lt;/li&gt;
&lt;li data-end=&quot;1628&quot; data-start=&quot;1595&quot;&gt;테스트 안에서 만든 기능이 어느 정도 작동하는 것 같으면,&lt;br /&gt;&lt;b&gt;프로덕션 코드로 옮기는 것이 아니라&amp;hellip;&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2030&quot; data-start=&quot;1935&quot;&gt;&lt;b&gt;main 폴더에서 기능을 처음부터 다시 만든다. (테스트 케이스의 내용들을 참고하며...)&lt;/b&gt;&lt;br /&gt;즉, 테스트는 테스트대로 있고, 프로덕션 코드는 다시 처음부터 작성하는 구조가 되어버렸다.&lt;/li&gt;
&lt;/ol&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;h4 data-end=&quot;2059&quot; data-start=&quot;2037&quot; data-ke-size=&quot;size20&quot;&gt;# 왜 이 방식이 비효율적이었나?&lt;/h4&gt;
&lt;p data-end=&quot;2095&quot; data-start=&quot;2061&quot; data-ke-size=&quot;size18&quot;&gt;1) 테스트 코드와 실제 코드가 완전히 따로 놀았다&lt;/p&gt;
&lt;blockquote data-end=&quot;2162&quot; data-start=&quot;2096&quot; data-ke-style=&quot;style2&quot;&gt;테스트 안에서 프로토타입처럼 코드를 작성했지만,&lt;br /&gt;그 코드를 그대로 프로덕션으로 승격하지 않고 별도로 다시 구현했다.&lt;/blockquote&gt;
&lt;p data-end=&quot;2169&quot; data-start=&quot;2164&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2169&quot; data-start=&quot;2164&quot; data-ke-size=&quot;size16&quot;&gt;이 때문에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2258&quot; data-start=&quot;2171&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2183&quot; data-start=&quot;2171&quot;&gt;로직이 중복되고&lt;/li&gt;
&lt;li data-end=&quot;2214&quot; data-start=&quot;2184&quot;&gt;테스트와 프로덕션이 서로 다른 방향으로 진행되고&lt;/li&gt;
&lt;li data-end=&quot;2240&quot; data-start=&quot;2215&quot;&gt;버그가 발생해도 양쪽을 모두 봐야 했고&lt;/li&gt;
&lt;li data-end=&quot;2258&quot; data-start=&quot;2241&quot;&gt;생산성은 눈에 띄게 낮아졌다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-end=&quot;2307&quot; data-start=&quot;2265&quot; data-ke-size=&quot;size20&quot;&gt;# 테스트는 &lt;b&gt;검증 도구&lt;/b&gt;가 아니라 &lt;b&gt;참고용 코드&lt;/b&gt;가 되어버렸다&lt;/h4&gt;
&lt;blockquote data-end=&quot;2406&quot; data-start=&quot;2308&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;테스트의 근본적인 의미는 &amp;ldquo;동작을 보장하는 자동화된 안전망&amp;rdquo;이다.&lt;/b&gt;&lt;br /&gt;하지만 나는 테스트 코드를 단순히 참고용 예제처럼 사용했기 때문에 테스트의 장점을 전혀 살리지 못했다.&lt;/blockquote&gt;
&lt;p data-end=&quot;2410&quot; data-start=&quot;2408&quot; data-ke-size=&quot;size16&quot;&gt;결국&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2485&quot; data-start=&quot;2412&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2435&quot; data-start=&quot;2412&quot;&gt;테스트는 작성하는 데만 시간이 들고&lt;/li&gt;
&lt;li data-end=&quot;2462&quot; data-start=&quot;2436&quot;&gt;실제 코드 품질 개선에는 기여하지 못하며&lt;/li&gt;
&lt;li data-end=&quot;2485&quot; data-start=&quot;2463&quot;&gt;리팩토링의 안전망 역할도 하지 못했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2522&quot; data-start=&quot;2487&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;테스트의 목적 자체를 잘못 사용하고 있었던 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;2522&quot; data-start=&quot;2487&quot; 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;h4 data-end=&quot;2558&quot; data-start=&quot;2529&quot; data-ke-size=&quot;size20&quot;&gt;# 지금 돌아보면 TDD와는 정반대 흐름이었다&lt;/h4&gt;
&lt;p data-end=&quot;2576&quot; data-start=&quot;2560&quot; data-ke-size=&quot;size16&quot;&gt;1) TDD는 다음 흐름을 따른다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2654&quot; data-start=&quot;2578&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2598&quot; data-start=&quot;2578&quot;&gt;실패하는 테스트 작성 (RED)&lt;/li&gt;
&lt;li data-end=&quot;2634&quot; data-start=&quot;2599&quot;&gt;테스트를 통과시키기 위한 최소한의 코드 작성 (GREEN)&lt;/li&gt;
&lt;li data-end=&quot;2654&quot; data-start=&quot;2635&quot;&gt;구조 개선 (REFACTOR)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;2670&quot; data-start=&quot;2656&quot; data-ke-size=&quot;size16&quot;&gt;2) 하지만 나는 이렇게 했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2765&quot; data-start=&quot;2672&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2707&quot; data-start=&quot;2672&quot;&gt;테스트에서 기능을 만들어봄 (RED/GREEN 뒤섞임)&lt;/li&gt;
&lt;li data-end=&quot;2732&quot; data-start=&quot;2708&quot;&gt;프로덕션 코드에서 기능을 다시 구현&lt;/li&gt;
&lt;li data-end=&quot;2752&quot; data-start=&quot;2733&quot;&gt;테스트는 참고용으로만 사용&lt;/li&gt;
&lt;li data-end=&quot;2765&quot; data-start=&quot;2753&quot;&gt;설계 개선은 없음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;2833&quot; data-start=&quot;2767&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;lsquo;테스트를 쓰긴 했지만&amp;rsquo;&lt;br /&gt;&lt;b&gt;테스트가 개발을 이끄는 구조가 아니라 개발을 방해하는 구조가 되어버린 셈이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2522&quot; data-start=&quot;2487&quot; 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;h4 data-end=&quot;2872&quot; data-start=&quot;2840&quot; data-ke-size=&quot;size20&quot;&gt;# 하지만 이 시행착오가 다음 단계의 시작이 되었다&lt;/h4&gt;
&lt;p data-end=&quot;2910&quot; data-start=&quot;2874&quot; data-ke-size=&quot;size16&quot;&gt;이 비효율적인 경험 덕분에 나는 한 가지 중요한 사실을 깨달았다.&lt;/p&gt;
&lt;blockquote data-end=&quot;2955&quot; data-start=&quot;2912&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;2955&quot; data-start=&quot;2914&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;테스트의 목적은 기능 확인이 아니라, 설계를 더 좋게 만드는 것&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;3046&quot; data-start=&quot;2957&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3046&quot; data-start=&quot;2957&quot; data-ke-size=&quot;size16&quot;&gt;이 깨달음 이후,&lt;/p&gt;
&lt;p data-end=&quot;3046&quot; data-start=&quot;2957&quot; data-ke-size=&quot;size16&quot;&gt;1. 나는 테스트를 기반으로 책임을 분리하고,&lt;br /&gt;2. 기능을 테스트에서 검증한 후 &lt;b&gt;프로덕션 코드로 자연스럽게 승격시키는 구조&lt;/b&gt;를 만들기 시작했다.&lt;/p&gt;
&lt;p data-end=&quot;3072&quot; data-start=&quot;3048&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3072&quot; data-start=&quot;3048&quot; data-ke-size=&quot;size16&quot;&gt;이 과정은 2편에서 자세히 다뤄보려고 한다.&lt;/p&gt;
&lt;p data-end=&quot;3072&quot; data-start=&quot;3048&quot; 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;h1 data-end=&quot;3091&quot; data-start=&quot;3079&quot;&gt;  2편 예고&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3260&quot; data-start=&quot;3092&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3115&quot; data-start=&quot;3092&quot;&gt;테스트에서 작은 기능을 먼저 만들고&lt;/li&gt;
&lt;li data-end=&quot;3150&quot; data-start=&quot;3116&quot;&gt;그 기능을 main 코드로 승격시키는 진짜 TDD 흐름&lt;/li&gt;
&lt;li data-end=&quot;3175&quot; data-start=&quot;3151&quot;&gt;책임 분리, 클래스 분리, DI 구성&lt;/li&gt;
&lt;li data-end=&quot;3205&quot; data-start=&quot;3176&quot;&gt;@SpringBootTest 기반 테스트 개선&lt;/li&gt;
&lt;li data-end=&quot;3236&quot; data-start=&quot;3206&quot;&gt;Given&amp;ndash;When&amp;ndash;Then만 남는 깔끔한 구조&lt;/li&gt;
&lt;li data-end=&quot;3260&quot; data-start=&quot;3237&quot;&gt;테스트가 리팩토링을 가능하게 만든 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3279&quot; data-start=&quot;3262&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서 이어서 설명하겠다.&lt;/p&gt;</description>
      <category>토이프로젝트/테스트</category>
      <category>springboot</category>
      <category>TDD</category>
      <category>단위테스트</category>
      <category>테스트 주도 개발</category>
      <category>테스트 케이스 실패</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/285</guid>
      <comments>https://gradualprecision.tistory.com/285#entry285comment</comments>
      <pubDate>Sun, 23 Nov 2025 16:52:36 +0900</pubDate>
    </item>
    <item>
      <title>[유효성검증 #3]  유효성 검증 아키텍처 설계</title>
      <link>https://gradualprecision.tistory.com/277</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&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-filename=&quot;ValidateFactory.jpg&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiyiRj/btsOt3OwRRE/vNTfNkoWaUpujZp6lMgZyk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiyiRj/btsOt3OwRRE/vNTfNkoWaUpujZp6lMgZyk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiyiRj/btsOt3OwRRE/vNTfNkoWaUpujZp6lMgZyk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiyiRj%2FbtsOt3OwRRE%2FvNTfNkoWaUpujZp6lMgZyk%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;691&quot; height=&quot;401&quot; data-filename=&quot;ValidateFactory.jpg&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;401&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;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Spring AOP 기반 유효성 검증 처리 구조 정리&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring에서 DTO 유효성 검증을 처리할 때, 기본 제공되는 BeanValidation만으로는 부족한 경우가 있다.&lt;br /&gt;이럴 때 커스텀 Validator를 추가로 적용할 수 있도록 구조를 설계하며, 두 가지 방식을 함께 활용한다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ BeanValidation + CustomValidation을 함께 사용하도록 구성한다.&lt;/li&gt;
&lt;li&gt;✅ 유효성 검증을 AOP로 감싸 공통 포맷으로 처리하도록 만든다.&lt;/li&gt;
&lt;li&gt;✅ 어노테이션(@CustomValidate)을 통해 자동으로 Validator를 스캔하고 매핑한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름 개요&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Controller (with @CheckValidation)
        &amp;darr;
ValidationAspect (AOP)
    ├─ BeanValidation 실행
    └─ CustomValidation 실행
         └─ DTO에 @CustomValidate가 붙어 있을 경우 해당 Validator 연결
    &lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  핵심 구성 요소&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. @CheckValidation 어노테이션&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Controller에서 유효성 검증 처리를 위한 어노테이션&lt;br /&gt;(AOP의 trigger 역할)&lt;/blockquote&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckValidation {}
    &lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. AOP - ValidationAspect&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;유효성검증 공통 처리를 위한 AOP&lt;br /&gt;1. Bean Validation을 먼저 수행&lt;br /&gt;2. Custom Validation을 수행 (getValidator()를 이용한 등록된 커스텀 검증 클래스를 가져옴)&lt;/blockquote&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Around(&quot;@annotation(CheckValidation)&quot;)
public Object validationAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    for (Object arg : joinPoint.getArgs()) {
        // 1. BeanValidation 수행
        springValidator.validate(arg);

        // 2. CustomValidator 수행
        Validator validator = validFactory.getValidator(arg.getClass());
        if (validator != null) {
            DataBinder binder = new DataBinder(arg);
            binder.addValidators(validator);
            binder.validate();
        }
    }
    return joinPoint.proceed();
}
    &lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. @CustomValidate 어노테이션&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DTO 클래스에 이 어노테이션을 붙이면 커스텀 Validator 대상으로 인식된다.&lt;/blockquote&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomValidate {}
    &lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. ValidateFactoryImpl - Validator 자동 등록&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;해당 패키지 하위에 있는, @CustomValidate 붙은 커스텀 유효섬 검증 클래스를 등록&lt;br /&gt;(scanValidateTarget() 5번 코드 참고)&lt;/blockquote&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;public ValidateFactoryImpl(List&amp;lt;Validator&amp;gt; validators) {
    Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; targets = scanValidateTarget(&quot;패키지명&quot;);

    for (Validator v : validators) {
        if (isSpringDefault(v)) continue;

        for (Class&amp;lt;?&amp;gt; dto : targets) {
            if (v.supports(dto)) {
                map.put(dto, v);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. @CustomValidate 스캔 메서드&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ClassPathScanningCandidateComponentProvider를 이용해 DTO (@CustomValidate) 를 스캔한다.&lt;br /&gt;( Custom 유효성 검증 클래스를 자동 등록하기 위한 Annotation )&lt;/blockquote&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; scanValidateTarget(String basePackage) {
    ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);

    scanner.addIncludeFilter(new AnnotationTypeFilter(CustomValidate.class));

    for (BeanDefinition bd : scanner.findCandidateComponents(basePackage)) {
        results.add(Class.forName(bd.getBeanClassName()));
    }
    return results;
}
    &lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Controller 예시&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@CheckValidation
@PostMapping(&quot;/post/member&quot;)
public ResponseEntity&amp;lt;?&amp;gt; postMember(@RequestBody MemberDto dto) {
    return ResponseEntity.ok().build();
}
    &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서는 단순히 &lt;code&gt;@CheckValidation&lt;/code&gt;만 붙이면, 내부적으로 모든 유효성 검사가 자동 수행된다.&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/section&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;section&gt;&lt;/section&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;장점&lt;/i&gt;&lt;/h2&gt;
&lt;p data-end=&quot;125&quot; data-start=&quot;87&quot; data-ke-size=&quot;size18&quot;&gt;1. &lt;b&gt;다양한 DTO마다 다른 유효성 검증이 필요한 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;246&quot; data-start=&quot;126&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;178&quot; data-start=&quot;126&quot;&gt;예: 회원가입 DTO, 주문 DTO, 주소 DTO 등에서 각각 다른 검증 로직이 필요할 때&lt;/li&gt;
&lt;li data-end=&quot;246&quot; data-start=&quot;179&quot;&gt;@CustomValidate + Validator 조합으로 &lt;b&gt;DTO별 맞춤 검증 로직&lt;/b&gt;을 적용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;289&quot; data-start=&quot;248&quot; data-ke-size=&quot;size18&quot;&gt;2. &lt;b&gt;기본 BeanValidation으로는 한계가 있는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;409&quot; data-start=&quot;290&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;367&quot; data-start=&quot;290&quot;&gt;예: 한 필드만 판단해서는 안 되고, 다른 필드와 조합해 유효성을 판단해야 하는 경우 (ex. startDate &amp;lt; endDate)&lt;/li&gt;
&lt;li data-end=&quot;409&quot; data-start=&quot;368&quot;&gt;Validator 내부에서 객체 전체를 보고 로직 작성이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;448&quot; data-start=&quot;411&quot; data-ke-size=&quot;size18&quot;&gt;3. &lt;b&gt;유효성 검증을 공통 포맷으로 통일하고 싶은 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;538&quot; data-start=&quot;449&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;489&quot; data-start=&quot;449&quot;&gt;AOP로 감싸기 때문에 유효성 실패 응답도 일관되게 처리할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;538&quot; data-start=&quot;490&quot;&gt;에러 응답 포맷을 커스터마이징하고, 프론트엔드와 사전 약속된 형태로 맞출 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;582&quot; data-start=&quot;540&quot; data-ke-size=&quot;size18&quot;&gt;4. &lt;b&gt;유효성 로직이 컨트롤러를 지저분하게 만드는 것이 싫을 때&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;644&quot; data-start=&quot;583&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;644&quot; data-start=&quot;583&quot;&gt;@CheckValidation 하나로 유효성 검증이 끝나므로, 컨트롤러는 핵심 로직에 집중할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;주의할 점&lt;/i&gt;&lt;/h2&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&gt;&lt;b&gt;런타임 스캔 비용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;@CustomValidate 스캔은 클래스 수가 많을수록 비용이 발생함 (앱 초기 구동 시)&lt;br /&gt;&lt;b&gt;(해결 : 스캔할 영역을 최대한 제한 -&amp;gt; DTO를 모아두는 패키지에서만 스캔 적용)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;동적 등록의 복잡성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;프레임워크 초심자 입장에서는 구조를 이해하는 데 진입장벽이 있음&lt;br /&gt;&lt;b&gt;(해결 : 아키텍처 문서 필요)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;AOP 사용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;AOP를 사용하므로, 디버깅 시 흐름을 추적하기가 다소 어려움 (특히 유효성 오류가 잘못 매핑될 경우)&lt;br /&gt;&lt;b&gt;( 해결 : AOP 내부 진입 로그 (JoinPoint 진입 클래스 로깅) + DTO 로그 + 예외 발생 전후 로깅 필요 )&lt;/b&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. BeanValidation과 CustomValidation을 함께 적용할 수 있도록 설계한다. &lt;br /&gt;(Bean Validation도 같이 사용하므로 Field 단위 어노테이션도 사용 가능)&lt;br /&gt;&lt;br /&gt;2. AOP로 유효성 검증을 감싸 컨트롤러 로직을 간결하게 유지한다.&lt;br /&gt;&lt;br /&gt;3. @CustomValidate를 통해 DTO에 대응하는 Validator를 자동 등록한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;확장 아이디어&lt;br /&gt;1. @ValidPhone, @ValidName 같은 필드 단위 어노테이션 추가&lt;br /&gt;2. @Validated(Group.class) 등 그룹 유효성 검증 도입&lt;br /&gt;3. 다국어 메시지 처리를 위한 messages.properties 연동&lt;/blockquote&gt;</description>
      <category>토이프로젝트/유효성검사 (Validation)</category>
      <category>AOP</category>
      <category>custom validate</category>
      <category>Java</category>
      <category>Spring</category>
      <category>공통 처리</category>
      <category>아키텍처</category>
      <category>유효성 검증</category>
      <category>유효성 검증 아키텍처</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/277</guid>
      <comments>https://gradualprecision.tistory.com/277#entry277comment</comments>
      <pubDate>Mon, 9 Jun 2025 22:34:02 +0900</pubDate>
    </item>
    <item>
      <title>[유효성검증 #2] Bean Validation를 이용한 유효성 검증</title>
      <link>https://gradualprecision.tistory.com/276</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;/p&gt;
&lt;h1&gt;Spring Bean Validation과 커스텀 어노테이션 &lt;code&gt;&lt;/code&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Boot에서 사용자 입력값을 검증할 때 가장 많이 사용하는 방식은&lt;u&gt;&lt;b&gt; Bean Validation&lt;/b&gt;&lt;/u&gt;이다. 간단한 유효성 조건은 어노테이션 한 줄로 처리할 수 있고, 복잡한 로직이 필요한 경우에는 &lt;b&gt;커스텀 유효성 어노테이션&lt;/b&gt;을 직접 만들어 적용할 수 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 글에서는 Bean Validation의 기본 사용법과 함께 &lt;u&gt;&lt;b&gt;@PasswordValidate&lt;/b&gt;&lt;/u&gt;라는 커스텀 어노테이션을 만들어서 비밀번호 조건을 검사하는 과정을 정리해본다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. DTO에 유효성 어노테이션 적용&lt;/h2&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;public static class TestV2Dto {

    // spring bean validation (기본)
    @NotBlank
    private String name;

    // bean validation custom (커스텀 유효성 검사)
    @PasswordValidate
    private String password;

    // spring bean validation (기본 제공 : 맞춤 유효성 검사 제공)
    @Pattern(regexp = &quot;^([0-9]{0,3})-([0-9]{0,4})-([0-9]{0,4})$&quot;)
    private String phone;

    // getter/setter 생략
}&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;&lt;code&gt;@NotBlank&lt;/code&gt; - null, 빈 문자열, 공백 문자열 모두 허용하지 않음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PasswordValidate&lt;/code&gt; - 직접 만든 비밀번호 유효성 검사 어노테이션&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Pattern&lt;/code&gt; - 전화번호는 하이픈 포함된 형식으로 입력해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 컨트롤러에서 검증 적용&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/api/v2/test&quot;)
public ResponseEntity&amp;lt;?&amp;gt; validateTestV2(@Validated @RequestBody TestV2Dto testDto,
                                        BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return new ResponseEntity&amp;lt;&amp;gt;(bindingResult.getFieldError().getDefaultMessage(),
                                    HttpStatus.BAD_REQUEST);
    }
    return new ResponseEntity&amp;lt;&amp;gt;(HttpStatus.OK);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;@Validated&lt;/code&gt;&lt;/b&gt;를 통해 DTO에 선언된 유효성 어노테이션들이 동작하게 되며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BindingResult&lt;/code&gt;를 통해 에러가 존재하는지 검사할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;사실 BindingResult는 생략해도 된다. 생략하면 유효성 실패 시 MethodArgumentNotValidException이 발생하고, Spring이 자동으로 400 에러를 내려준다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 커스텀 어노테이션 &lt;code&gt;@PasswordValidate&lt;/code&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;pre class=&quot;monkey&quot;&gt;&lt;code&gt;@Documented
@Constraint(validatedBy = PasswordValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordValidate {
    String message() default &quot;비밀번호 형식이 유효하지 않습니다.&quot;;
    Class&amp;lt;?&amp;gt;[] groups() default {};
    Class&amp;lt;? extends Payload&amp;gt;[] payload() default {};
}&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;필수 선언 메서드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;message&lt;/b&gt; () : 유효성 검사 실패시 나오게 될 메시지&lt;/li&gt;
&lt;li&gt;groups () : 그룹별 유효성 검사 지정을 위한 메서드 ( 유효성 검사를 위한 group을 지정할 떄 구현 )&lt;/li&gt;
&lt;li&gt;payload() : 메타데이터 관련 spring framework 내부적으로 사용되는 메서드 ( 주로 사용되지 않음 )&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;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Validator 클래스 구현&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class PasswordValidator implements ConstraintValidator&amp;lt;PasswordValidate, String&amp;gt; {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        String regex = &quot;^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$&quot;;
        return value != null &amp;amp;&amp;amp; value.matches(regex);
    }
}&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;Spring에서 추상화한 ConstraintValidator 를 이용하여 PasswordValidate 어노테이션을 지정 (Spring에서 가져다 사용할 수 있도록)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;isValid()&lt;/b&gt;를 오버라이딩 하여, 유효성 검증 구현 로직 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 정규식은 아래 조건을 만족한다:&lt;br /&gt;- 8자 이상&lt;br /&gt;- 소문자 1개 이상&lt;br /&gt;- 대문자 1개 이상&lt;br /&gt;- 숫자 1개 이상&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 테스트 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요청 (잘못된 입력)&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;&quot;,
  &quot;password&quot;: &quot;1234&quot;,
  &quot;phone&quot;: &quot;01012345678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;응답 예시&lt;/h3&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;HTTP/1.1 400 Bad Request
&quot;비밀번호 형식이 유효하지 않습니다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마무리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Spring Bean Validation을 활용하면 복잡한 로직 없이도 사용자 입력 검증을 손쉽게 적용할 수 있다. @NotBlank, @Pattern 등 기본 제공 어노테이션만으로 충분한 경우도 많지만, 직접 비즈니스에 맞는 커스텀 어노테이션을 만들어 사용하는 것도 매우 유용하다.&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;다음 글에서는 유효성 검사를 더욱 체계적으로 관리하기 위해 Validator 인터페이스, @InitBinder, AOP와 어노테이션 기반 유효성 검사 확장 전략에 대해 소개할 예정이다.&lt;/blockquote&gt;</description>
      <category>토이프로젝트/유효성검사 (Validation)</category>
      <category>Bean Validation</category>
      <category>Java</category>
      <category>Spring</category>
      <category>Spring bean</category>
      <category>validation</category>
      <category>빈 유효성검사</category>
      <category>유효섬 검증</category>
      <category>유효성 검사</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/276</guid>
      <comments>https://gradualprecision.tistory.com/276#entry276comment</comments>
      <pubDate>Fri, 6 Jun 2025 22:21:01 +0900</pubDate>
    </item>
    <item>
      <title>[유효성검증 #1] Validator + BindingResult를 이용한 유효성 검증</title>
      <link>https://gradualprecision.tistory.com/275</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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Spring에서 Validator를 이용한 커스텀 유효성 검사와 BindingResult 활용&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring에서는 사용자 입력 값을 검증하기 위해 Bean Validation(JSR-380)을 많이 사용하지만, 더 복잡한 조건이 있거나 커스터마이징이 필요한 경우에는 org.springframework.validation.Validator를 직접 구현하여 사용할 수 있다. 이 포스팅에서는 Validator를 이용한 유효성 검사와 BindingResult를 통해 검증 결과를 처리하는 방법을 소개한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Validator 인터페이스 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring의 Validator 인터페이스는 두 가지 메서드를 구현해야 한다:&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;supports(Class&amp;lt;?&amp;gt; clazz)&lt;/code&gt;: 이 Validator가 어떤 클래스 타입을 지원하는지 명시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate(Object target, Errors errors)&lt;/code&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;Ex) 이름(name), 비밀번호(password), 전화번호(phone) 세 가지 필드를 가진 DTO를 검증하도록 구현했다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;@Component
public class TestV1Validate implements Validator {

    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; clazz) {
    	// TestDto에 대한 Validator라는 것을 의미
        return clazz.isAssignableFrom(V1Controller.TestDto.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        V1Controller.TestDto testDto = (V1Controller.TestDto) target;
        String regex = &quot;^([0-9]{0,3})-([0-9]{0,4})-([0-9]{0,4})$&quot;;

        if (testDto.getName() == null || testDto.getName().trim().equals(&quot;&quot;)) {
            errors.rejectValue(&quot;name&quot;, &quot;name.required&quot;);
        }
        if (testDto.getPassword() == null || testDto.getPassword().trim().equals(&quot;&quot;)) {
            errors.rejectValue(&quot;password&quot;, &quot;password.required&quot;);
        }
        if (testDto.getPhone() == null || testDto.getPhone().trim().equals(&quot;&quot;)) {
            errors.rejectValue(&quot;phone&quot;, &quot;phone.required&quot;);
        } else if (!testDto.getPhone().matches(regex)) {
            errors.rejectValue(&quot;phone&quot;, &quot;phone.invalid&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. DTO 클래스 정의&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Data
public static class TestDto {
    private String name;
    private String password;
    private String phone;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Validator 등록: @InitBinder 사용&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 testValidate는 아직 &lt;b&gt;Spring에 등록된 상태가 아니므로&lt;/b&gt;, InitBinder를 통해 특정 Controller 혹은 객체에 등록을 해줘야한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@InitBinder 대신해서 전역적으로 적용할경우, 해당 validator가 자동으로 검증 단계에 사용될 수 있음 (주의)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.addValidators([CustomValidator]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 컨트롤러에서 BindingResult 처리하기&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/api/v1/test&quot;)
public ResponseEntity&amp;lt;?&amp;gt; TestDtoVersion1(@Validated @RequestBody TestDto testDto, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return new ResponseEntity&amp;lt;&amp;gt;(
            bindingResult.getFieldError().getCodes(),
            HttpStatus.BAD_REQUEST
        );
    }
    return new ResponseEntity&amp;lt;&amp;gt;(testDto, HttpStatus.OK);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Validated&lt;/code&gt;는 &lt;code&gt;Validator&lt;/code&gt;와 연계되어 동작하며, 내부적으로 &lt;code&gt;WebDataBinder&lt;/code&gt;가 등록된 Validator를 사용하게 된다. &lt;code&gt;BindingResult&lt;/code&gt;를 바로 옆 파라미터로 선언하면 Spring이 자동으로 유효성 검사 결과를 바인딩해준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 응답 결과&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요청:&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;POST /api/v1/test
Content-Type: application/json

{
  &quot;name&quot;: &quot;admin&quot;,
  &quot;password&quot;: &quot;1234&quot;,
  &quot;phone&quot;: &quot;010-1234&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;응답:&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  &quot;phone.invalid&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6.&amp;nbsp; 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Spring의 Validator 인터페이스를 직접 구현하고, &lt;code&gt;BindingResult&lt;/code&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;에서는 보다 널리 쓰이는 방식인 &lt;code&gt;@NotNull&lt;/code&gt;, &lt;code&gt;@Pattern&lt;/code&gt; 등으로 대표되는 &lt;b&gt;Bean Validation&lt;/b&gt;을 소개할 예정이다.&lt;br /&gt;그 이후에는 Validator와 Bean Validation을 조합하고, &lt;b&gt;AOP 및 사용자 정의 어노테이션을 활용한 유효성 검사 아키텍처의 확장&lt;/b&gt; 방법을 다룰 계획이다.&lt;/p&gt;</description>
      <category>토이프로젝트/유효성검사 (Validation)</category>
      <category>@Validated</category>
      <category>custom validate</category>
      <category>databinder</category>
      <category>InitBinder</category>
      <category>Java</category>
      <category>Spring</category>
      <category>validator</category>
      <category>커스텀 유효성 검사</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/275</guid>
      <comments>https://gradualprecision.tistory.com/275#entry275comment</comments>
      <pubDate>Fri, 6 Jun 2025 13:29:23 +0900</pubDate>
    </item>
    <item>
      <title>[트랜잭션 #1] @Transactional 프록시 생략으로 인한 Rollback 실패 사례</title>
      <link>https://gradualprecision.tistory.com/273</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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;트랜잭션(Transaction) 실패 경험을 통한 내용 정리&lt;br /&gt;&lt;br /&gt;1.. 예외처리로인한 Transaction Proxy 실패&lt;br /&gt;2. self Call로 인한 @Transactional 실패&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;155&quot; data-start=&quot;132&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;트랜잭션(Transaction) 개요&lt;/i&gt;&lt;/h2&gt;
&lt;blockquote data-end=&quot;322&quot; data-start=&quot;157&quot; data-ke-style=&quot;style3&quot;&gt;스프링 프레임워크에서 트랜잭션은 &lt;b&gt;ACID&lt;/b&gt;(원자성&amp;middot;일관성&amp;middot;격리성&amp;middot;지속성) 규칙을 따르며,&lt;br /&gt;하나의 처리 흐름이 모두 성공해야 커밋되고, 중간에 문제가 발생하면 전부 롤백되도록 보장&lt;br /&gt;주로 @Transactional 애노테이션을 통해 스프링 AOP 프록시가 트랜잭션 경계를 관리&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;538&quot; data-start=&quot;324&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;380&quot; data-start=&quot;324&quot;&gt;&lt;b&gt;원자성(Atomicity)&lt;/b&gt;: 작업 전체가 성공하거나, 하나라도 실패하면 모두 롤백&lt;/li&gt;
&lt;li data-end=&quot;433&quot; data-start=&quot;381&quot;&gt;&lt;b&gt;일관성(Consistency)&lt;/b&gt;: 트랜잭션 전후에 데이터베이스 일관성이 유지&lt;/li&gt;
&lt;li data-end=&quot;485&quot; data-start=&quot;434&quot;&gt;&lt;b&gt;격리성(Isolation)&lt;/b&gt;: 동시성 제어를 통해 트랜잭션 간 간섭을 방지&lt;/li&gt;
&lt;li data-end=&quot;538&quot; data-start=&quot;486&quot;&gt;&lt;b&gt;지속성(Durability)&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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-end=&quot;568&quot; data-start=&quot;545&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;@Transactional의 특성&lt;/i&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 기본 예외 처리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;821&quot; data-start=&quot;591&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;650&quot; data-start=&quot;591&quot;&gt;런타임 예외(RuntimeException)나 Error 발생 시에만 기본적으로 롤백&lt;/li&gt;
&lt;li data-end=&quot;821&quot; data-start=&quot;654&quot;&gt;체크 예외(Exception)까지 롤백하려면 rollbackFor 속성을 명시해야 롤백이 진행 (rollbackFor 옵션)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1745996420651&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)
public void someMethod() { &amp;hellip; }&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;2. 트랜잭션 매니저&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1017&quot; data-start=&quot;843&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;908&quot; data-start=&quot;843&quot;&gt;스프링 환경에서는 PlatformTransactionManager(txManager)가 내부적으로 트랜잭션 경계를 관리합니다.&lt;/li&gt;
&lt;li data-end=&quot;1017&quot; data-start=&quot;912&quot;&gt;직접 트랜잭션을 제어하려면, &lt;b&gt;TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()&lt;/b&gt; 등을 활용할 수 있습니다. &lt;b&gt;(직접 롤백 요청)&lt;/b&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;b&gt;3. 예외 처리 시 주의사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1159&quot; data-start=&quot;1043&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1115&quot; data-start=&quot;1043&quot;&gt;&lt;b&gt;메서드 내에서 try&amp;ndash;catch로 예외를 모두 처리해 버리면, 트랜잭션 프록시가 예외를 인지하지 못해 Commit이 진행&lt;/b&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 data-end=&quot;1115&quot; data-start=&quot;1043&quot;&gt;&lt;b&gt; &lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;catch 블록에서 정상화를 진행하게 될경우, TransactionAspectSupport를 이용해 직접 롤백을 지정 )&lt;/b&gt; &lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1159&quot; data-start=&quot;1043&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1115&quot; data-start=&quot;1043&quot;&gt;&lt;b&gt; 자기 호출(self-invocation)을 할 경우, 메서드를 직접 호출하게 되어 AOP 프록시가 작동하지 않으므로 @Transactional이 적용되지 않음&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;i&gt; 주요 트러블슈팅 사례 &lt;/i&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; 1. try&amp;ndash;catch로 예외를 모두 처리해 버린 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745996681059&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)
public Map&amp;lt;String,Object&amp;gt; run() {
    Map&amp;lt;String, Object&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
    try {
        // 엔티티 생성 및 저장 로직
        save(transportation, bus);
        result.put(&quot;code&quot;, &quot;1&quot;);
    } catch(Exception e) {
        log.error(&quot;Service Error: {}&quot;, e);
        result.put(&quot;code&quot;, &quot;9999&quot;);
        // 롤백 의도 명시
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return result;
}&lt;/code&gt;&lt;/pre&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 data-end=&quot;1751&quot; data-start=&quot;1699&quot;&gt;&lt;b&gt;문제&lt;/b&gt;: catch 블록에서 예외를 잡아 버리면, &lt;b&gt;실제로는 Commit 진행&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1887&quot; data-start=&quot;1752&quot;&gt;&lt;b&gt;해결&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1887&quot; data-start=&quot;1766&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1844&quot; data-start=&quot;1766&quot;&gt;방법1) TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 호출&lt;/li&gt;
&lt;li data-end=&quot;1887&quot; data-start=&quot;1847&quot;&gt;방법2) 예외를 다시 던져서(AOP 프록시가 인지하도록) 자동 롤백 유도&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1919&quot; data-start=&quot;1894&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-end=&quot;1919&quot; data-start=&quot;1894&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. AOP 기반 커스텀 트랜잭션 처리&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-end=&quot;2003&quot; data-start=&quot;1921&quot; data-ke-style=&quot;style2&quot;&gt;스프링 @Transactional 대신 직접 PlatformTransactionManager를 사용해 트랜잭션을 관리하는 AOP 예제 (@CustomTransactional)&amp;nbsp;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1745996746739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
public class TransactionAspect {

    private final PlatformTransactionManager txManager;

    public TransactionAspect(PlatformTransactionManager txManager) {
        this.txManager = txManager;
    }

    @Around(&quot;@annotation(CustomTransaction)&quot;)
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
        try {
            Object ret = pjp.proceed();
            txManager.commit(status);
            return ret;
        } catch(Throwable e) {
            txManager.rollback(status);
            Map&amp;lt;String, Object&amp;gt; errorResult = new HashMap&amp;lt;&amp;gt;();
            log.error(&quot;[Error] {}&quot;, e.getMessage());
            errorResult.put(&quot;code&quot;, &quot;404&quot;);
            return errorResult;
        }
    }
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2884&quot; data-start=&quot;2849&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 트랜잭션 로직을 통합 관리 가능&lt;/li&gt;
&lt;li data-end=&quot;2968&quot; data-start=&quot;2885&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: AOP 포인트컷이 정확히 적용되는지, 트랜잭션 전파 속성(PROPAGATION_REQUIRED 등)이 적절한지 확인이 필요&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; 3. 자기 호출(Self-invocation)으로 인한 트랜잭션 미적용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745996783930&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Map&amp;lt;String,Object&amp;gt; run() {
    // ...
    save(transportation, bus);  // 같은 클래스 내부 메서드 호출
    // ...
}

@Transactional
public void save(Transportation t, Bus b) {
    transportationRepository.save(t);
    busRepository.save(b);
}

//======================================================

// **해결(Proxy로 자신 클래스를 가져와서 직접 호출 (ApplicationContext에서 자기 자신 빈을 주입 받아 호출 ) **//
@Service
public class MyService {
    private final MyService selfProxy;
    public MyService(MyService selfProxy) {
        this.selfProxy = selfProxy;
    }

    public void run() {
        // 트랜잭션 적용을 위해 프록시를 통해 호출
        selfProxy.save(...);
    }

    @Transactional
    public void save(...) { &amp;hellip; }
}&lt;/code&gt;&lt;/pre&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 data-end=&quot;3394&quot; data-start=&quot;3316&quot;&gt;&lt;b&gt;문제&lt;/b&gt;: 스프링은 프록시 기반이기 때문에, 동일 클래스의 내부 메서드 호출은 프록시를 거치지 않아 트랜잭션이 적용이 되지 않음&lt;/li&gt;
&lt;li data-end=&quot;3482&quot; data-start=&quot;3395&quot;&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 data-end=&quot;3482&quot; data-start=&quot;3395&quot;&gt;방법1 ) 외부에서 호출되도록 구조를 변경&lt;/li&gt;
&lt;li data-end=&quot;3482&quot; data-start=&quot;3437&quot;&gt;방법 2) ApplicationContext에서 자기 자신 빈을 주입 받아 호출&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. MethodUtil.inboke()와 같은 외부 유틸리티를 통한 Method() 직접 호출&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_1746004401186&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MethodUtil {

    public static void invokeMethod(Object target, String methodName) throws Exception {
        Method method = target.getClass().getMethod(methodName);
        method.invoke(target);  // !! 프록시를 타지 않음 &amp;rarr; @Transactional 등 동작 안 함
    }
}


/* 해결방법 1 */
@Autowired
private MyService myService; // 프록시 객체

public void invokeThroughProxy() throws Exception {
    Method method = myService.getClass().getMethod(&quot;someTransactionalMethod&quot;);
    method.invoke(myService);  // AOP 적용됨
}


/* 해결방법 2 */
@Autowired
private ApplicationContext context;

public void invokeFromContext() throws Exception {
    MyService proxy = context.getBean(MyService.class); // 프록시 객체
    Method method = proxy.getClass().getMethod(&quot;someTransactionalMethod&quot;);
    method.invoke(proxy);  // AOP 적용됨
}&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 data-start=&quot;3316&quot; data-end=&quot;3394&quot;&gt;&lt;b&gt;문제&lt;/b&gt;: Spring은 &lt;b&gt;프록시 기반 AOP&lt;/b&gt;를 사용합니다. 따라서 외부 유틸리티(예: MethodUtil)를 통해 리플렉션으로 메서드를 직접 호출하면, 해당 호출은 &lt;b&gt;프록시 객체를 경유하지 않고 실제 객체 인스턴스의 메서드를 직접 실행&lt;/b&gt;하게 된다. 이는 Spring 컨테이너가 제공하는 @Transactional, @Cacheable, @Async 등의 AOP 기능이 &lt;b&gt;작동하지 않게 되는 원인&lt;/b&gt;이 됩니다. 다시 말해, &lt;b&gt;스프링의 프록시 관리를 벗어난 직접 호출&lt;/b&gt;은 AOP 적용 대상에서 제외된다&lt;/li&gt;
&lt;li data-start=&quot;3395&quot; data-end=&quot;3482&quot;&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 data-start=&quot;3395&quot; data-end=&quot;3482&quot;&gt;방법1 ) &lt;b&gt;AOP 프록시 객체를 이용한 리플렉션 호출&lt;/b&gt;&lt;br /&gt;&amp;rarr; 실제 빈이 아닌 &lt;b&gt;스프링 컨테이너에 등록된 프록시 객체를 이용해서 invoke&lt;/b&gt;&lt;/li&gt;
&lt;li data-start=&quot;3437&quot; data-end=&quot;3482&quot;&gt;방법 2) &lt;b&gt;ApplicationContext를 통해 스프링이 관리하는 프록시 객체를 가져와 사용&lt;/b&gt;&lt;br /&gt;&amp;rarr; @Autowired, 혹은 직접적으로 ApplicationContext.getBean()을 사용해서 프록시 객체를 확보&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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-end=&quot;3821&quot; data-start=&quot;3809&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;정리&lt;/i&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;4253&quot; data-start=&quot;3823&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3967&quot; data-start=&quot;3823&quot;&gt;&lt;b&gt;예외 처리 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3967&quot; data-start=&quot;3844&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3908&quot; data-start=&quot;3844&quot;&gt;비즈니스 로직 내 try&amp;ndash;catch는 최소화하고, 예외는 되도록 밖으로 던져 프록시가 인지하도록 설계&lt;/li&gt;
&lt;li data-end=&quot;3967&quot; data-start=&quot;3912&quot;&gt;체크 예외일 경우, 롤백하려면 rollbackFor = Exception.class를 지정&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4060&quot; data-start=&quot;3969&quot;&gt;&lt;b&gt;트랜잭션 전파(Propagation)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4060&quot; data-start=&quot;4002&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4060&quot; data-start=&quot;4002&quot;&gt;내부 호출 구조나 재진입 호출이 많은 복잡한 서비스 레이어에서는 전파 속성을 명확히 설계 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4159&quot; data-start=&quot;4062&quot;&gt;&lt;b&gt;AOP와 프록시 주의사항&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4159&quot; data-start=&quot;4088&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4159&quot; data-start=&quot;4088&quot;&gt;자기 호출 시 프록시 우회 문제를 인지하고, 필요한 경우 selfProxy 주입이나 인터페이스 기반 프록시를 활용필요&lt;/li&gt;
&lt;li data-end=&quot;4159&quot; data-start=&quot;4088&quot;&gt;또는, 외부 유틸리티의 사용 혹은 직접 호출하게 될경우, 프록시 객체가 적용이 안되므로 스프링이 지원하는 AOP를 사용할 수 없게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4253&quot; data-start=&quot;4161&quot;&gt;&lt;b&gt;공통 처리(AOP) 활용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4253&quot; data-start=&quot;4187&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4253&quot; data-start=&quot;4187&quot;&gt;여러 서비스에서 동일한 트랜잭션 로직&amp;middot;예외 처리를 한다면, AOP로 묶어서 관리하면 코드 중복 방지 가능&lt;/li&gt;
&lt;/ul&gt;
&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;</description>
      <category>데이터베이스/트랜잭션</category>
      <category>Proxy</category>
      <category>transactional</category>
      <category>트랜잭션</category>
      <category>프록시 우회</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/273</guid>
      <comments>https://gradualprecision.tistory.com/273#entry273comment</comments>
      <pubDate>Wed, 30 Apr 2025 16:24:03 +0900</pubDate>
    </item>
    <item>
      <title>[게임서버프로그래밍#6] 클라이언트-서버 구현을 위한 소켓 기초 정리</title>
      <link>https://gradualprecision.tistory.com/272</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;0.게임서버프로그래머책.jpeg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lkQUn/btsNC0SMKur/tKJVj2fhMWogkZCCwCsPVK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lkQUn/btsNC0SMKur/tKJVj2fhMWogkZCCwCsPVK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lkQUn/btsNC0SMKur/tKJVj2fhMWogkZCCwCsPVK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlkQUn%2FbtsNC0SMKur%2FtKJVj2fhMWogkZCCwCsPVK%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;300&quot; height=&quot;386&quot; data-filename=&quot;0.게임서버프로그래머책.jpeg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;이 글에서는 소켓 통신의 기초부터, Blocking/Non-Blocking 구조, Winsock2 함수(ioctlsocket, getsockopt, select) 사용법, 클라이언트-서버 프로그램 기본 흐름까지 실제 코드 예제와 함께 설명한다.&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;
&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;299&quot; data-start=&quot;279&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;소켓(Socket)이란 무엇인가&lt;/i&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;364&quot; data-start=&quot;301&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;330&quot; data-start=&quot;301&quot;&gt;소켓은 &lt;b&gt;네트워크 연결을 제어하는 핸들&lt;/b&gt;이다.&lt;/li&gt;
&lt;li data-end=&quot;364&quot; data-start=&quot;331&quot;&gt;파일 핸들처럼 읽고 쓰기를 담당하지만 대상이 네트워크다.&lt;/li&gt;
&lt;/ul&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;style1&quot; /&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;
&lt;h2 data-end=&quot;388&quot; data-start=&quot;371&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;Blocking 통신 구조&lt;/i&gt;&lt;/h2&gt;
&lt;p data-end=&quot;436&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;Blocking 통신은 소켓 함수 호출 시 완료될 때까지 대기하는 구조를 의미한다.&lt;/p&gt;
&lt;h3 data-end=&quot;459&quot; data-start=&quot;438&quot; data-ke-size=&quot;size23&quot;&gt;* 클라이언트 Blocking 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;619&quot; data-start=&quot;461&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;485&quot; data-start=&quot;461&quot;&gt;socket() : 소켓 핸들 생성&lt;/li&gt;
&lt;li data-end=&quot;511&quot; data-start=&quot;486&quot;&gt;bind() : 클라이언트 포트 설정&lt;/li&gt;
&lt;li data-end=&quot;550&quot; data-start=&quot;512&quot;&gt;connect() : 서버 연결 (연결 완료될 때까지 대기)&lt;/li&gt;
&lt;li data-end=&quot;598&quot; data-start=&quot;551&quot;&gt;send() : 송신 버퍼에 데이터 저장 (버퍼가 꽉 차면 Blocking)&lt;/li&gt;
&lt;li data-end=&quot;619&quot; data-start=&quot;599&quot;&gt;close() : 소켓 닫기&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-end=&quot;639&quot; data-start=&quot;621&quot; data-ke-size=&quot;size23&quot;&gt;* 서버 Blocking 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;820&quot; data-start=&quot;641&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;665&quot; data-start=&quot;641&quot;&gt;socket() : 소켓 핸들 생성&lt;/li&gt;
&lt;li data-end=&quot;688&quot; data-start=&quot;666&quot;&gt;bind() : 서버 포트 설정&lt;/li&gt;
&lt;li data-end=&quot;713&quot; data-start=&quot;689&quot;&gt;listen() : 연결 요청 대기&lt;/li&gt;
&lt;li data-end=&quot;759&quot; data-start=&quot;714&quot;&gt;accept() : 클라이언트 연결 수락 (연결 전까지 Blocking)&lt;/li&gt;
&lt;li data-end=&quot;799&quot; data-start=&quot;760&quot;&gt;recv() : 데이터 수신 (버퍼에 데이터 올 때까지 대기)&lt;/li&gt;
&lt;li data-end=&quot;820&quot; data-start=&quot;800&quot;&gt;close() : 소켓 닫기&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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-end=&quot;853&quot; data-start=&quot;827&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;비동기(Non-Blocking) 통신 구조&lt;/i&gt;&lt;/h2&gt;
&lt;p data-end=&quot;908&quot; data-start=&quot;855&quot; data-ke-size=&quot;size16&quot;&gt;Non-Blocking 통신은 소켓 함수 호출이 즉시 반환되어 다른 작업을 할 수 있도록 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;989&quot; data-start=&quot;910&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;951&quot; data-start=&quot;910&quot;&gt;ioctlsocket()을 사용하여 소켓을 비동기 모드로 설정한다.&lt;/li&gt;
&lt;li data-end=&quot;989&quot; data-start=&quot;952&quot;&gt;select()를 통해 소켓의 I/O 가능 여부를 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1016&quot; data-start=&quot;991&quot; data-ke-size=&quot;size23&quot;&gt;* Non-Blocking 클라이언트 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1210&quot; data-start=&quot;1018&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1042&quot; data-start=&quot;1018&quot;&gt;socket() : 소켓 핸들 생성&lt;/li&gt;
&lt;li data-end=&quot;1093&quot; data-start=&quot;1043&quot;&gt;ioctlsocket(sock, FIONBIO, &amp;amp;mode) : 비동기 모드 설정&lt;/li&gt;
&lt;li data-end=&quot;1133&quot; data-start=&quot;1094&quot;&gt;connect() 호출 &amp;rarr; Would Block 오류 무시&lt;/li&gt;
&lt;li data-end=&quot;1160&quot; data-start=&quot;1134&quot;&gt;select()로 연결 완료 여부 확인&lt;/li&gt;
&lt;li data-end=&quot;1210&quot; data-start=&quot;1161&quot;&gt;send()/recv() 전에 select()로 I/O 가능 상태를 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-end=&quot;1234&quot; data-start=&quot;1212&quot; data-ke-size=&quot;size23&quot;&gt;* Non-Blocking 서버 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1410&quot; data-start=&quot;1236&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1260&quot; data-start=&quot;1236&quot;&gt;socket() : 소켓 핸들 생성&lt;/li&gt;
&lt;li data-end=&quot;1311&quot; data-start=&quot;1261&quot;&gt;ioctlsocket(sock, FIONBIO, &amp;amp;mode) : 비동기 모드 설정&lt;/li&gt;
&lt;li data-end=&quot;1336&quot; data-start=&quot;1312&quot;&gt;listen()으로 연결 요청 대기&lt;/li&gt;
&lt;li data-end=&quot;1374&quot; data-start=&quot;1337&quot;&gt;select()를 통해 accept() 가능 여부 탐지&lt;/li&gt;
&lt;li data-end=&quot;1410&quot; data-start=&quot;1375&quot;&gt;연결된 소켓에 대해 recv(), send() 진행&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;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1428&quot; data-start=&quot;1417&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;주요 함수 정리&lt;/i&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1447&quot; data-start=&quot;1430&quot; data-ke-size=&quot;size23&quot;&gt;1. ioctlsocket()&lt;/h3&gt;
&lt;p data-end=&quot;1472&quot; data-start=&quot;1449&quot; data-ke-size=&quot;size16&quot;&gt;소켓 핸들에 명령을 내려 속성을 설정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1637&quot; data-start=&quot;1474&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1499&quot; data-start=&quot;1474&quot;&gt;&lt;b&gt;FIONBIO&lt;/b&gt; : 동기/비동기 설정&lt;/li&gt;
&lt;li data-end=&quot;1533&quot; data-start=&quot;1500&quot;&gt;&lt;b&gt;FIONREAD&lt;/b&gt; : 읽을 수 있는 데이터 양 확인&lt;/li&gt;
&lt;li data-end=&quot;1581&quot; data-start=&quot;1534&quot;&gt;&lt;b&gt;SIO_KEEPALIVE_VALS&lt;/b&gt; : TCP Keep-Alive 시간 변경&lt;/li&gt;
&lt;li data-end=&quot;1637&quot; data-start=&quot;1582&quot;&gt;&lt;b&gt;SIO_GET_EXTENSION_FUNCTION_POINTER&lt;/b&gt; : 확장 함수 포인터 획득&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1642&quot; data-start=&quot;1639&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1660&quot; data-start=&quot;1644&quot; data-ke-size=&quot;size23&quot;&gt;2. getsockopt()&lt;/h3&gt;
&lt;p data-end=&quot;1687&quot; data-start=&quot;1662&quot; data-ke-size=&quot;size16&quot;&gt;소켓의 현재 상태나 설정값을 조회할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1863&quot; data-start=&quot;1689&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1719&quot; data-start=&quot;1689&quot;&gt;&lt;b&gt;SO_REUSEADDR&lt;/b&gt; : 주소 재사용 허용&lt;/li&gt;
&lt;li data-end=&quot;1752&quot; data-start=&quot;1720&quot;&gt;&lt;b&gt;SO_KEEPALIVE&lt;/b&gt; : 유휴 연결 상태 유지&lt;/li&gt;
&lt;li data-end=&quot;1791&quot; data-start=&quot;1753&quot;&gt;&lt;b&gt;SO_LINGER&lt;/b&gt; : 소켓 종료 시 데이터 전송 여부 설정&lt;/li&gt;
&lt;li data-end=&quot;1834&quot; data-start=&quot;1792&quot;&gt;&lt;b&gt;SO_SNDBUF / SO_RCVBUF&lt;/b&gt; : 송수신 버퍼 크기 설정&lt;/li&gt;
&lt;li data-end=&quot;1863&quot; data-start=&quot;1835&quot;&gt;&lt;b&gt;SO_ERROR&lt;/b&gt; : 소켓 오류 상태 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1868&quot; data-start=&quot;1865&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1882&quot; data-start=&quot;1870&quot; data-ke-size=&quot;size23&quot;&gt;3. select()&lt;/h3&gt;
&lt;p data-end=&quot;1948&quot; data-start=&quot;1884&quot; data-ke-size=&quot;size16&quot;&gt;복수 소켓의 상태를 감시할 수 있다.&lt;br /&gt;&lt;b&gt;FD_ZERO()&lt;/b&gt;, &lt;b&gt;FD_SET()&lt;/b&gt; 매크로와 함께 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2157&quot; data-start=&quot;1950&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;fd_set 종류&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;감시 내용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2077&quot; data-start=&quot;2038&quot;&gt;
&lt;td data-end=&quot;2053&quot; data-start=&quot;2038&quot;&gt;readfds&lt;/td&gt;
&lt;td data-end=&quot;2077&quot; data-start=&quot;2053&quot;&gt;읽기(read) 가능 여부 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2117&quot; data-start=&quot;2078&quot;&gt;
&lt;td data-end=&quot;2093&quot; data-start=&quot;2078&quot;&gt;writefds&lt;/td&gt;
&lt;td data-end=&quot;2117&quot; data-start=&quot;2093&quot;&gt;쓰기(write) 가능 여부 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2157&quot; data-start=&quot;2118&quot;&gt;
&lt;td data-end=&quot;2133&quot; data-start=&quot;2118&quot;&gt;exceptfds&lt;/td&gt;
&lt;td data-end=&quot;2157&quot; data-start=&quot;2133&quot;&gt;오류 발생 여부 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1745855642070&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FD_ZERO(&amp;amp;readSet);
FD_SET(sock, &amp;amp;readSet);
timeval timeout = {1, 0}; // 1초 대기
int result = select(0, &amp;amp;readSet, nullptr, nullptr, &amp;amp;timeout);
if (result &amp;gt; 0 &amp;amp;&amp;amp; FD_ISSET(sock, &amp;amp;readSet)) {
    recv(sock, buffer, size, 0);
}&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2450&quot; data-start=&quot;2420&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;Blocking vs Non-Blocking 비교&lt;/i&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2775&quot; data-start=&quot;2452&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Blocking 구조&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Non-Blocking 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2654&quot; data-start=&quot;2597&quot;&gt;
&lt;td data-end=&quot;2609&quot; data-start=&quot;2597&quot;&gt;대기 여부&lt;/td&gt;
&lt;td data-end=&quot;2630&quot; data-start=&quot;2609&quot;&gt;함수 호출 시 완료될 때까지 대기&lt;/td&gt;
&lt;td data-end=&quot;2654&quot; data-start=&quot;2630&quot;&gt;함수 호출 즉시 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2719&quot; data-start=&quot;2655&quot;&gt;
&lt;td data-end=&quot;2666&quot; data-start=&quot;2655&quot;&gt;구현 난이도&lt;/td&gt;
&lt;td data-end=&quot;2691&quot; data-start=&quot;2666&quot;&gt;비교적 간단&lt;/td&gt;
&lt;td data-end=&quot;2719&quot; data-start=&quot;2691&quot;&gt;select/poll 로직 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2775&quot; data-start=&quot;2720&quot;&gt;
&lt;td data-end=&quot;2734&quot; data-start=&quot;2720&quot;&gt;성능&lt;/td&gt;
&lt;td data-end=&quot;2755&quot; data-start=&quot;2734&quot;&gt;적은 클라이언트 처리에 적합&lt;/td&gt;
&lt;td data-end=&quot;2775&quot; data-start=&quot;2755&quot;&gt;대규모 동시 접속 처리에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2805&quot; data-start=&quot;2782&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;Non-Blocking에서 주의할 점&lt;/i&gt;&lt;/h2&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-end=&quot;3093&quot; data-start=&quot;2807&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2884&quot; data-start=&quot;2807&quot;&gt;&lt;b&gt;Would Block 에러는 정상:&lt;/b&gt;&lt;br /&gt;아직 소켓이 준비되지 않았다는 의미이며, select()로 대기 후 재시도해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;2960&quot; data-start=&quot;2886&quot;&gt;&lt;b&gt;0바이트 송신법:&lt;/b&gt;&lt;br /&gt;Non-Blocking connect() 후, 연결 성공 여부를 확인할 때 0바이트를 송신해본다.&lt;/li&gt;
&lt;li data-end=&quot;3032&quot; data-start=&quot;2962&quot;&gt;&lt;b&gt;CPU 낭비 주의:&lt;/b&gt;&lt;br /&gt;select()에 timeout을 설정하여 무한 루프에서 CPU를 소모하지 않도록 한다.&lt;/li&gt;
&lt;li data-end=&quot;3093&quot; data-start=&quot;3034&quot;&gt;&lt;b&gt;UDP 주의사항:&lt;/b&gt;&lt;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 클라이언트&lt;/p&gt;
&lt;pre id=&quot;code_1745855756770&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Winsock winsock;
ClientSocket client(&quot;localhost&quot;, &quot;27015&quot;);
client.send(&quot;Hello&quot;);
auto response = client.receive(512);&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_1745855768552&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Winsock winsock;
ServerSocket server;
server.accept_connection();
server.socket_recv(&quot;Hello from server&quot;);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발 서적 리뷰/게임서버 프로그래머 책</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/272</guid>
      <comments>https://gradualprecision.tistory.com/272#entry272comment</comments>
      <pubDate>Tue, 29 Apr 2025 00:56:17 +0900</pubDate>
    </item>
    <item>
      <title>[File I/O #6] File이외의 I/O Stream 정리 (Object, Audio, Piped 등)</title>
      <link>https://gradualprecision.tistory.com/271</link>
      <description>&lt;nav&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/nav&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;목차&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://gradualprecision.tistory.com/266&quot;&gt;1. Resource와 Stream I/O 이해&lt;/a&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/267&quot;&gt;2. Resource 구현체 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/268&quot;&gt;3. InputStream/OutputStream 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/269&quot;&gt;4. InputStreamReader, BufferedReader 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/270&quot;&gt;5. Multipartfile 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/271&quot;&gt;6. File 이외의 I/O stream 정리&lt;/a&gt;&lt;/p&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;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;File I/O #6 &amp;mdash; 고급 입출력 스트림 정리 (Object, Audio, Piped 등)&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. ObjectInputStream / ObjectOutputStream&lt;/h3&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;직렬화(Serialization)&lt;/b&gt;와 &lt;b&gt;역직렬화(Deserialization)&lt;/b&gt; 기능을 제공하죠.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 객체 저장 (직렬화 - 바이트로 저장)
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(&quot;object.dat&quot;));
oos.writeObject(new User(&quot;홍길동&quot;, 30));
oos.close();

// 객체 읽기 (역직렬화 - 바이트 읽기)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(&quot;object.dat&quot;));
User user = (User) ois.readObject();
ois.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; 직렬화하려는 클래스는 &lt;code&gt;implements Serializable&lt;/code&gt;&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;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. AudioInputStream&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 &lt;b&gt;WAV, AU 등 오디오 파일을 읽고 재생하거나 분석&lt;/b&gt;할 때 사용되는 스트림입니다.&lt;br /&gt;(Java의 &lt;code&gt;javax.sound.sampled&lt;/code&gt; 패키지)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;File audioFile = new File(&quot;example.wav&quot;);
AudioInputStream audioStream = AudioSystem.getAudioInputStream(audioFile);

// 포맷 확인
AudioFormat format = audioStream.getFormat();
System.out.println(&quot;Sample Rate: &quot; + format.getSampleRate());
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. PipedInputStream / PipedOutputStream&lt;/b&gt;&lt;/h3&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;PipedInputStream pipedIn = new PipedInputStream();
PipedOutputStream pipedOut = new PipedOutputStream(pipedIn);

// 쓰기 스레드
new Thread(() -&amp;gt; {
    try {
        pipedOut.write(&quot;스레드 간 통신 데이터&quot;.getBytes());
        pipedOut.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

// 읽기 스레드
new Thread(() -&amp;gt; {
    try {
        int data;
        while ((data = pipedIn.read()) != -1) {
            System.out.print((char) data);
        }
        pipedIn.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. SequenceInputStream&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 InputStream을 하나로 연결&lt;/b&gt;하여 연속적으로 데이터를 읽을 수 있게 해줍니다.&lt;br /&gt;예를 들어 여러 파일을 하나의 스트림처럼 처리하고 싶을 때 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;InputStream input1 = new FileInputStream(&quot;file1.txt&quot;);
InputStream input2 = new FileInputStream(&quot;file2.txt&quot;);

SequenceInputStream sis = new SequenceInputStream(input1, input2);
int data;
while ((data = sis.read()) != -1) {
    System.out.print((char) data);
}
sis.close();
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고급 Stream I/O 정리&lt;/h3&gt;
&lt;table border=&quot;1&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;스트림&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;사용 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ObjectInputStream&lt;br /&gt;ObjectOutputStream&lt;/td&gt;
&lt;td&gt;객체를 저장하고 불러오기&lt;/td&gt;
&lt;td&gt;세이브 데이터, 설정 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AudioInputStream&lt;/td&gt;
&lt;td&gt;오디오 파일 처리&lt;/td&gt;
&lt;td&gt;음악 재생, 오디오 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PipedInputStream&lt;br /&gt;PipedOutputStream&lt;/td&gt;
&lt;td&gt;읽기/쓰기 스레드 간 실시간 통신&lt;/td&gt;
&lt;td&gt;실시간 처리 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SequenceInputStream&lt;/td&gt;
&lt;td&gt;여러개의 InputStream 연결&lt;/td&gt;
&lt;td&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;5. 채널 기반 입출력 (Channel I/O)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Java NIO(New I/O)에서는 기존의&lt;span&gt;&amp;nbsp;&lt;/span&gt;InputStream,&lt;span&gt;&amp;nbsp;&lt;/span&gt;OutputStream보다&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;Channel&lt;/b&gt;을 제공합니다.&lt;br /&gt;Channel은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;읽기와 쓰기가 모두 가능한 이중 구조&lt;/u&gt;이며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;버퍼(Buffer)&lt;/b&gt;와 함께 사용됩니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;주요 채널 인터페이스&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ReadableByteChannel&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 읽기 전용&lt;/li&gt;
&lt;li&gt;WritableByteChannel&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 쓰기 전용&lt;/li&gt;
&lt;li&gt;FileChannel&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 파일 기반 입출력 지원 (읽기/쓰기)&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;FileChannel 사용 예제&lt;/h4&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// 파일에서 읽기
try (FileInputStream fis = new FileInputStream(&quot;data.txt&quot;);
     FileChannel channel = fis.getChannel()) {

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (channel.read(buffer) &amp;gt; 0) {
        buffer.flip(); // 읽을 준비
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
        }
        buffer.clear(); // 버퍼 비우기
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// 파일에 쓰기
try (FileOutputStream fos = new FileOutputStream(&quot;output.txt&quot;);
     FileChannel channel = fos.getChannel()) {

    String data = &quot;Hello, FileChannel!&quot;;
    ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
    channel.write(buffer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;FileChannel vs Stream 비교&lt;/h4&gt;
&lt;table style=&quot;color: #333333; text-align: start; 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;text-align: center;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;InputStream / OutputStream&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Channel (NIO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기반&lt;/td&gt;
&lt;td&gt;바이트 기반 스트림&lt;/td&gt;
&lt;td&gt;채널 &amp;amp; 버퍼 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;상대적으로 느림&lt;/td&gt;
&lt;td&gt;&lt;b&gt;빠름&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(DirectBuffer, OS 지원)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기능&lt;/td&gt;
&lt;td&gt;순차적 읽기/쓰기&lt;/td&gt;
&lt;td&gt;랜덤 액세스, 이중 채널, 멀티스레드 효율적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티스레드&lt;/td&gt;
&lt;td&gt;지원 제한적&lt;/td&gt;
&lt;td&gt;동기화에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;Resource와 Channel 함께 사용하기&lt;/i&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring의 Resource는&lt;span&gt;&amp;nbsp;&lt;/span&gt;readableChannel()&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드로 NIO 채널도 지원합니다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Resource resource = new ClassPathResource(&quot;data.txt&quot;);
ReadableByteChannel channel = Channels.newChannel(resource.getInputStream());

ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) &amp;gt; 0) {
    buffer.flip();
    while (buffer.hasRemaining()) {
        System.out.print((char) buffer.get());
    }
    buffer.clear();
}
channel.close();
&lt;/code&gt;&lt;/pre&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Channel I/O 요약&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FileChannel&lt;/b&gt;은 파일 입출력을 더 빠르고 효율적으로 처리 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ByteBuffer&lt;/b&gt;를 이용해 메모리 직접 제어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Resource + Channel&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;조합으로 Spring에서도 활용 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대용량 파일 처리&lt;/b&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;성능 최적화&lt;/b&gt;에 매우 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시리즈 마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 총 6편에 걸쳐 Java의 파일 입출력(File I/O)에 대해 알아보았다.&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;#1&lt;/b&gt; 기본 개념: Resource, InputStream, OutputStream&lt;/li&gt;
&lt;li&gt;&lt;b&gt;#2&lt;/b&gt; Resource 구현체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;#3&lt;/b&gt; InputStream과 OutputStream&lt;/li&gt;
&lt;li&gt;&lt;b&gt;#4&lt;/b&gt; InputStreamReader &amp;amp; BufferedReader&lt;/li&gt;
&lt;li&gt;&lt;b&gt;#5&lt;/b&gt; MultipartFile 업로드 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;#6&lt;/b&gt; File이외의&amp;nbsp;I/O&amp;nbsp;Stream&amp;nbsp;정리&amp;nbsp;(Object,&amp;nbsp;Audio,&amp;nbsp;Piped&amp;nbsp;등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>토이프로젝트/파일 업로드&amp;amp;다운로드</category>
      <category>ByteBuffer</category>
      <category>CHANNEL</category>
      <category>File I/O</category>
      <category>Java</category>
      <category>Spring</category>
      <category>Stream</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/271</guid>
      <comments>https://gradualprecision.tistory.com/271#entry271comment</comments>
      <pubDate>Sun, 20 Apr 2025 00:36:20 +0900</pubDate>
    </item>
    <item>
      <title>[File I/O #5] MultiPartFile (Form데이터) 처리하기</title>
      <link>https://gradualprecision.tistory.com/270</link>
      <description>&lt;nav&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/nav&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;목차&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://gradualprecision.tistory.com/266&quot;&gt;1. Resource와 Stream I/O 이해&lt;/a&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/267&quot;&gt;2. Resource 구현체 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/268&quot;&gt;3. InputStream/OutputStream 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/269&quot;&gt;4. InputStreamReader, BufferedReader 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/270&quot;&gt;5. Multipartfile 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/271&quot;&gt;6. File 이외의 I/O stream 정리&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;File I/O #5 &amp;mdash; MultipartFile 업로드 처리 흐름&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;i&gt;MultipartFile이란?&lt;/i&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;Spring MVC에서 클라이언트가 전송한 &lt;b&gt;파일 업로드 요청&lt;/b&gt;을 다루기 위한 인터페이스입니다.&lt;br /&gt;HTML의 &lt;b&gt;&lt;code&gt;&amp;lt;input type=&quot;file&quot;&amp;gt;&lt;/code&gt;&lt;/b&gt;에서 전송된 데이터를 서버에서 받기 위해 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface MultipartFile {
    String getName();                         // 파라미터 이름
    String getOriginalFilename();             // 업로드된 원본 파일명
    String getContentType();                  // MIME 타입
    boolean isEmpty();                        // 파일이 비어있는지 확인
    long getSize();                           // 파일 크기
    byte[] getBytes() throws IOException;     // 바이트 배열로 읽기
    InputStream getInputStream() throws IOException; // InputStream으로 읽기
    void transferTo(File dest) throws IOException;   // 파일로 저장
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;i&gt;MultipartFile &amp;rarr; InputStream 흐름&lt;/i&gt;&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[HTML 파일 업로드]
       &amp;darr;
[Spring Controller: MultipartFile 수신]
       &amp;darr;
MultipartFile.getInputStream()
       &amp;darr;
InputStream (&amp;rarr; BufferedReader 또는 직접 처리)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 MultipartFile은 내부에 저장된 파일 내용을 &lt;b&gt;InputStream으로 꺼내어 읽을 수 있게 해주는 구조&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.&amp;nbsp; InputStream으로 읽기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 내용을 바이트 스트림으로 처리&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/upload&quot;)
public String upload(@RequestParam(&quot;file&quot;) MultipartFile file) {
    try (InputStream is = file.getInputStream()) {
        int data;
        while ((data = is.read()) != -1) {
            System.out.print((char) data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return &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;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. BufferedReader로 줄 단위 읽기 (주로 사용됨)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 파일인 경우 가장 자주 사용되는 방식&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;@PostMapping(&quot;/upload&quot;)
public String upload(@RequestParam(&quot;file&quot;) MultipartFile file) {
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {

        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(&quot;줄 내용: &quot; + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return &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;size23&quot;&gt;&lt;b&gt;3. byte[]로 한 번에 읽기 (파일 내용 한번에 읽을 때 유용)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리 파일이나 전체 내용을 통째로 읽고 싶을 때&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;byte[] data = file.getBytes();
String content = new String(data, StandardCharsets.UTF_8);
System.out.println(&quot;파일 전체 내용: &quot; + content);
&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;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 파일로 저장하기 (transferTo)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 파일을 직접 저장하고 싶을 때 사용&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;File targetFile = new File(&quot;/tmp/&quot; + file.getOriginalFilename());
file.transferTo(targetFile);
System.out.println(&quot;파일 저장 완료: &quot; + targetFile.getAbsolutePath());
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;i&gt;어떤 방식이 적절할까?&lt;/i&gt;&lt;/h3&gt;
&lt;table border=&quot;1&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;적합한 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getInputStream()&lt;/td&gt;
&lt;td&gt;스트림 기반 읽기 (InputStream 가져오기)&lt;/td&gt;
&lt;td&gt;대부분의 파일 처리, 성능 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BufferedReader&lt;/td&gt;
&lt;td&gt;&lt;b&gt;줄 단위&lt;/b&gt; 텍스트 읽기&lt;/td&gt;
&lt;td&gt;CSV, TXT, 로그 등 텍스트 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getBytes()&lt;/td&gt;
&lt;td&gt;&lt;b&gt;전체 내용&lt;/b&gt;을 &lt;b&gt;바이트로&lt;/b&gt; 한 번에&lt;/td&gt;
&lt;td&gt;파일 크기가 작을 때, JSON 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;transferTo()&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;파일로 저장 (다른이름으로 저장 같은 느낌)&lt;/b&gt;&lt;/td&gt;
&lt;td&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- MultipartFile : &lt;b&gt;업로드된 파일&lt;/b&gt;을 &lt;b&gt;추상화&lt;/b&gt;한 객체&lt;br /&gt;- getInputStream() : 파일 내용을 InputStream으로 읽을 수 있음&lt;br /&gt;- BufferedReader :&amp;nbsp; &lt;b&gt;줄 단위&lt;/b&gt;로 쉽게 읽을 수 있음&lt;br /&gt;- &lt;b&gt;transferTo()&lt;/b&gt; :&amp;nbsp; 서버에 파일을 저장할 수도 있음 (서버에 &lt;b&gt;&quot;파일을 다른이름으로 저장&quot;&lt;/b&gt; 같은 느낌으로 저장)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>토이프로젝트/파일 업로드&amp;amp;다운로드</category>
      <category>HTTP</category>
      <category>InputStream</category>
      <category>java i/o</category>
      <category>mulripartfile</category>
      <category>Spring</category>
      <category>transferto()</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/270</guid>
      <comments>https://gradualprecision.tistory.com/270#entry270comment</comments>
      <pubDate>Sun, 20 Apr 2025 00:32:47 +0900</pubDate>
    </item>
    <item>
      <title>[File I/O #4] InputStreamReader &amp;amp; BufferedReader 완전 분석</title>
      <link>https://gradualprecision.tistory.com/269</link>
      <description>&lt;nav&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/nav&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;목차&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://gradualprecision.tistory.com/266&quot;&gt;1. Resource와 Stream I/O 이해&lt;/a&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/267&quot;&gt;2. Resource 구현체 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/268&quot;&gt;3. InputStream/OutputStream 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/269&quot;&gt;4. InputStreamReader, BufferedReader 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/270&quot;&gt;5. Multipartfile 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://gradualprecision.tistory.com/271&quot;&gt;6. File 이외의 I/O stream 정리&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;File I/O #4 &amp;mdash; InputStreamReader &amp;amp; BufferedReader 완전 분석&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;i&gt;InputStreamReader란?&lt;/i&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;InputStreamReader&lt;/b&gt;는 &lt;u&gt;바이트 스트림(InputStream)을 문자 스트림(Reader)으로 변환&lt;/u&gt;해주는 클래스입니다.&lt;br /&gt;즉, 파일이나 네트워크에서 읽은 바이트를 UTF-8 같은 문자 인코딩을 고려해서 &lt;b&gt;문자 단위&lt;/b&gt;로 읽게 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;InputStream is = new FileInputStream(&quot;text.txt&quot;);
InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8);

int ch;
while ((ch = reader.read()) != -1) {
    System.out.print((char) ch);
}

reader.close();
&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;h3 data-ke-size=&quot;size23&quot;&gt;BufferedReader란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BufferedReader&lt;/b&gt;는 &lt;u&gt;Reader(예: InputStreamReader)를 감싸서 성능을 높이고, 편하게 줄 단위로 읽을 수 있게&lt;/u&gt; 해줍니다.&lt;br /&gt;가장 많이 사용하는 기능은 &lt;code&gt;readLine()&lt;/code&gt; 메서드로 한 줄씩 읽는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;InputStream is = new FileInputStream(&quot;text.txt&quot;);
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));

String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}

reader.close();
&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;i&gt;InputStreamReader vs BufferedReader 차이&lt;/i&gt;&lt;/h3&gt;
&lt;table style=&quot;height: 96px;&quot; border=&quot;1&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;InputStreamReader&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;BufferedReader&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;바이트 &amp;rarr; &lt;b&gt;문자 변환 (UTF-8 변환)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;문자 스트림 &amp;rarr; 줄 단위 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;주요 메서드&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;read()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;readLine()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;단독 사용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;O (가능)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;O (Reader 기반이면 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;함께 사용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;InputStream &amp;rarr; InputStreamReader &amp;rarr; BufferedReader&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 어떤 걸 써야 할까?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 파일이 텍스트 파일이고 &lt;b&gt;줄 단위&lt;/b&gt;로 읽고 싶다 &amp;rarr; &lt;b&gt;BufferedReader&lt;/b&gt;&lt;br /&gt;- &lt;b&gt;파일을 문자 단위&lt;/b&gt;로 읽고 싶다 &amp;rarr; &lt;b&gt;InputStreamReader&lt;/b&gt;&lt;br /&gt;- 파일 내용을 &lt;b&gt;한 줄씩 읽어서 파싱&lt;/b&gt;하고 싶다 &amp;rarr;&lt;b&gt; 둘을 함께 사용&lt;/b&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;i&gt;실전 예제: Resource + BufferedReader&lt;/i&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resource를 통해 파일을 가져오고 줄 단위로 읽는 가장 흔한 패턴입니다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;Resource resource = new ClassPathResource(&quot;data.txt&quot;);

try (BufferedReader reader = new BufferedReader(
         new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
    
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(&quot;읽은 줄: &quot; + line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
&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;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- &lt;b&gt;InputStreamReader&lt;/b&gt;: 바이트 &amp;rarr; 문자로 변환해줌&lt;br /&gt;- &lt;b&gt;BufferedReader&lt;/b&gt;: 문자 스트림을 줄 단위로 효율적으로 읽기&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>토이프로젝트/파일 업로드&amp;amp;다운로드</category>
      <category>BufferedReader</category>
      <category>File I/O</category>
      <category>InputStream</category>
      <category>InputStreamReader</category>
      <category>Java</category>
      <author>거북이의 기술블로그</author>
      <guid isPermaLink="true">https://gradualprecision.tistory.com/269</guid>
      <comments>https://gradualprecision.tistory.com/269#entry269comment</comments>
      <pubDate>Sun, 20 Apr 2025 00:31:04 +0900</pubDate>
    </item>
  </channel>
</rss>