<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>베베개발자</title>
    <link>https://sujin-su-sss.tistory.com/</link>
    <description>주니어개발자가 되기 위한 베이비개발자의 실무에서 겪은 문제 해결 기록</description>
    <language>ko</language>
    <pubDate>Fri, 17 Jul 2026 23:08:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>베베개발자</managingEditor>
    <image>
      <title>베베개발자</title>
      <url>https://tistory1.daumcdn.net/tistory/4803090/attach/5bce9d72c5ab4a87ad11dfbea27ea8b7</url>
      <link>https://sujin-su-sss.tistory.com</link>
    </image>
    <item>
      <title>성능 튜닝 삽질기 (2) - 20초에서 5초 이내로 쿼리 튜닝</title>
      <link>https://sujin-su-sss.tistory.com/entry/%EC%84%B1%EB%8A%A5-%ED%8A%9C%EB%8B%9D-%EC%82%BD%EC%A7%88%EA%B8%B0-2-20%EC%B4%88%EC%97%90%EC%84%9C-5%EC%B4%88-%EC%9D%B4%EB%82%B4%EB%A1%9C-%EC%BF%BC%EB%A6%AC-%ED%8A%9C%EB%8B%9D</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 성능 튜닝을 하면서 이것저것 많이 부딪혀보았는 데, 그 이후부터 성능 이슈가 생기면 자연스럽게 내게 튜닝 요청이 오기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 고객사에서 조회 메뉴의 속도가 너무 느리가는 요청이 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느린 해당 메뉴는 단순 조회(페이징 처리 있음)한 후에 해당 데이터에 필요한 정보를 each문에 담고 있는 로직이다.&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;연구소에서는 약 8,476건을 조회하는 데 2.77초 현장에서는 정확한 건수를 알 수 없지만 20초 이상이 걸리고 있는 상황이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 예전처럼 가장 먼저 조회 대상(모수)을 줄일 수 있는 지부터 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 튜닝을 할 때 가장 먼저 보는 것은 SQL이 아니라 불필요하게 많은 데이터를 읽고 있는 가를 확인하는 거라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 조건을 확인하는 데, 고객사 요청으로 기본 검색 조건이 모두 '전체'로 설정되어 있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 신청 상태를 기본적으로 &quot;신청중&quot;으로 조회하여 대상 데이터를 줄이도록 되어 있지만, 고객 요구사항으로 인해 모든 데이터를 조회하도록 변경된 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 몇 년 동안 누적된 데이터를 모두 조회하여 페이징 처리를 하여 데이터를 보여주는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 데이터가 계속 쌓일 수록 성능이 떨어질 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 조건을 변경할 수 없는 상황이었기 때문에 SQL 자체에서 개선할 수 있는 부분을 찾아보기로 했다.&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; EXPLAIN PLAN으로 실행 계획 확인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 부장님께서 요즘은 &lt;b&gt;EXPLAIN PLAN FOR&lt;/b&gt; 로 실행 계획을 먼저 확인한다고 알려주셔서 이번 기회에 처음 사용해보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 SQL 앞에 다음과 같이 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1784108867635&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN PLAN FOR
SELECT ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1075&quot; data-start=&quot;1060&quot; data-ke-size=&quot;size16&quot;&gt;그리고 실행 계획을 조회하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1142&quot; data-start=&quot;1077&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1096&quot; data-start=&quot;1077&quot; data-section-id=&quot;1ktzxep&quot;&gt;TABLE ACCESS FULL&lt;/li&gt;
&lt;li data-end=&quot;1115&quot; data-start=&quot;1097&quot; data-section-id=&quot;m6qz06&quot;&gt;INDEX RANGE SCAN&lt;/li&gt;
&lt;li data-end=&quot;1125&quot; data-start=&quot;1116&quot; data-section-id=&quot;tw033y&quot;&gt;JOIN 방식&lt;/li&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1126&quot; data-section-id=&quot;1ah6nco&quot;&gt;예상 Rows&lt;/li&gt;
&lt;li data-end=&quot;1142&quot; data-start=&quot;1136&quot; data-section-id=&quot;1j416tf&quot;&gt;Cost&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1160&quot; data-start=&quot;1144&quot; data-ke-size=&quot;size16&quot;&gt;등의 정보를 확인할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1242&quot; data-start=&quot;1162&quot; data-ke-size=&quot;size16&quot;&gt;아직 실행 계획을 읽는 것이 익숙하지 않아 ChatGPT와 함께 하나씩 분석하면서 어떤 부분이 가장 비용을 많이 사용하는지 확인해보기 시작했다.&lt;/p&gt;
&lt;p data-end=&quot;1264&quot; data-start=&quot;1244&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1264&quot; data-start=&quot;1244&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; 첫 번째 병목 - 같은 테이블을 반복해서 JOIN &lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1356&quot; data-start=&quot;1302&quot; data-ke-size=&quot;size16&quot;&gt;실행 계획을 보면서 가장 먼저 눈에 들어온 것은 같은 테이블을 계속 JOIN하고 있다는 점이었다.&lt;/p&gt;
&lt;p data-end=&quot;1380&quot; data-start=&quot;1358&quot; data-ke-size=&quot;size16&quot;&gt;실제로 SQL은 아래와 비슷한 구조였다.&lt;/p&gt;
&lt;pre id=&quot;code_1784108942713&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LEFT JOIN PROPERTY P1 ...
LEFT JOIN PROPERTY P2 ...
LEFT JOIN PROPERTY P3 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 테이블을 조건만 다르게 해서 대략 15번 정도를 Join하고 있는 형태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1569&quot; data-start=&quot;1528&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터가 많을수록 같은 테이블을 반복해서 읽는 비용도 계속 증가&lt;/b&gt;하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;1569&quot; data-start=&quot;1528&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면 해당 데이터를 전체를 읽으면서 조건에 맞는 것만 뽑아오도록 하는 것인데, 동일한 테이블이 여러개 있으면 그런 행위를 여러번을 하기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;1569&quot; data-start=&quot;1528&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1612&quot; data-start=&quot;1571&quot; data-ke-size=&quot;size16&quot;&gt;그래서 먼저 PROPERTY 테이블을 한 번만 조회하도록 구조를 변경했다.&lt;/p&gt;
&lt;pre id=&quot;code_1784109047693&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH PROPERTY AS
(
    SELECT
        ID,
        MAX(CASE WHEN TYPE='A' THEN VALUE END) AS VALUE_A,
        MAX(CASE WHEN TYPE='B' THEN VALUE END) AS VALUE_B,
        MAX(CASE WHEN TYPE='C' THEN VALUE END) AS VALUE_C
    FROM PROPERTY
    GROUP BY ID
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1612&quot; data-start=&quot;1571&quot; data-ke-size=&quot;size16&quot;&gt;필요한 데이터를 미리 집계한 뒤 메인 SQL에서는 with 테이블을 한 번만 JOIN하도록 수정하였다.&lt;/p&gt;
&lt;p data-end=&quot;1612&quot; data-start=&quot;1571&quot; data-ke-size=&quot;size16&quot;&gt;동일한 테이블을 반복해서 읽는 작업이 사라지면서 실행 비용이 크게 줄어드는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1784109098595&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LEFT JOIN PROPERTY P
ON A.ID = P.ID&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; 두 번째 병목 - SELECT 안에서 반복 실행되는 COUNT &lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2127&quot; data-start=&quot;2081&quot; data-ke-size=&quot;size16&quot;&gt;다음으로 눈에 들어온 것은 SELECT 절 안에서 수행되는 COUNT 서브쿼리였다.&lt;/p&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;기존 SQL은 아래와 비슷한 구조였다.&lt;/p&gt;
&lt;pre id=&quot;code_1784109146585&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    T.ID,
    (
        SELECT COUNT(*)
        FROM REVIEW R
        WHERE R.TABLE_ID = T.ID
    ) AS REVIEW_COUNT
FROM TABLE T&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 서브쿼리라서 느린 건가 라는 생각을 했다.&lt;/p&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실행 계획을 보면서 GPT와 이야기해보니 실제 원인을 파악하게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;메인 조회 결과가 1,000건이라면 Oracle은 내부적으로 COUNT도 1,000번 수행하게 되는 문제가 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;1행 조회 &amp;gt; count / 2행 조회 &amp;gt; count / ............ n행 조회 &amp;gt; count&lt;/p&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;와 같은 방식으로 반복 실행되는 구조였다.&lt;/p&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2129&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2560&quot; data-start=&quot;2515&quot; data-ke-size=&quot;size16&quot;&gt;특히 COUNT 안에서도 JOIN이나 조건 검색이 존재하면 비용은 계속 누적된다.&lt;/p&gt;
&lt;p data-end=&quot;2600&quot; data-start=&quot;2562&quot; data-ke-size=&quot;size16&quot;&gt;그래서 COUNT를 먼저 집계한 뒤 JOIN하는 방식으로 변경하였다.&lt;/p&gt;
&lt;p data-end=&quot;2600&quot; data-start=&quot;2562&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1784109282271&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH REVIEW AS
(
    SELECT
        TABLE_ID,
        COUNT(*) REVIEW_COUNT
    FROM REVIEW
    GROUP BY TABLE_ID
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2600&quot; data-start=&quot;2562&quot; data-ke-size=&quot;size16&quot;&gt;그리고 메인 SQL에서는 첫번째 병목해소 처럼 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;with 테이블을 한 번만 JOIN하도록 수정하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2600&quot; data-start=&quot;2562&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2600&quot; data-start=&quot;2562&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 변경하니 COUNT는 한 번만 수행되고 메인 조회에서는 집계된 결과만 JOIN하면 되기 때문에 반복 수행 비용을 줄일 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2600&quot; data-start=&quot;2562&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2600&quot; data-start=&quot;2562&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; 실행 계획을 보면서 느낀 점 &lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2941&quot; data-start=&quot;2915&quot; data-ke-size=&quot;size16&quot;&gt;이번에 실행 계획을 보면서 가장 크게 느낀 것은&lt;/p&gt;
&lt;p data-end=&quot;2987&quot; data-start=&quot;2943&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQL이 길다고 해서 모든 부분을 튜닝해야 하는 것은 아니라는 점이었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3025&quot; data-start=&quot;2989&quot; data-ke-size=&quot;size16&quot;&gt;수백 줄의 SQL이었지만 실제 병목은 몇 군데에 집중되어 있었다.&lt;/p&gt;
&lt;p data-end=&quot;3031&quot; data-start=&quot;3027&quot; data-ke-size=&quot;size16&quot;&gt;이번에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3085&quot; data-start=&quot;3033&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3058&quot; data-start=&quot;3033&quot; data-section-id=&quot;hxic0e&quot;&gt;동일한 테이블을 반복해서 JOIN하는 구조&lt;/li&gt;
&lt;li data-end=&quot;3085&quot; data-start=&quot;3059&quot; data-section-id=&quot;7sglxz&quot;&gt;SELECT 안에서 반복 수행되는 COUNT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3114&quot; data-start=&quot;3087&quot; data-ke-size=&quot;size16&quot;&gt;이 두 부분만 수정했는데도 성능 차이가 상당했다.&lt;/p&gt;
&lt;p data-end=&quot;3163&quot; data-start=&quot;3116&quot; data-ke-size=&quot;size16&quot;&gt;연구소에서는 &lt;b&gt;8,476건 조회 기준 2.77초에서 약 1.03초&lt;/b&gt;까지 줄었고,&lt;/p&gt;
&lt;p data-end=&quot;3203&quot; data-start=&quot;3165&quot; data-ke-size=&quot;size16&quot;&gt;현장에서도 &lt;b&gt;20초 이상 걸리던 조회가 5초 이내&lt;/b&gt;로 개선되었다.&lt;/p&gt;
&lt;p data-end=&quot;3226&quot; data-start=&quot;3205&quot; data-ke-size=&quot;size16&quot;&gt;물론 근본적인 문제는 아직 남아 있다.&lt;/p&gt;
&lt;p data-end=&quot;3290&quot; data-start=&quot;3228&quot; data-ke-size=&quot;size16&quot;&gt;현재 기본 검색 조건이 모두 '전체'이기 때문에 데이터가 계속 증가하면 다시 성능 저하가 발생할 가능성은 있다.&lt;/p&gt;
&lt;p data-end=&quot;3348&quot; data-start=&quot;3292&quot; data-ke-size=&quot;size16&quot;&gt;즉, 이번 튜닝은 구조 자체를 바꾼 것이 아니라 현재 구조 안에서 가장 큰 병목을 제거한 작업이었다.&lt;/p&gt;
&lt;p data-end=&quot;3348&quot; data-start=&quot;3292&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;style5&quot; /&gt;
&lt;p data-end=&quot;3390&quot; data-start=&quot;3362&quot; data-ke-size=&quot;size16&quot;&gt;예전에는 SQL만 보면서 감으로 튜닝하려고 했었다.&lt;/p&gt;
&lt;p data-end=&quot;3495&quot; data-start=&quot;3392&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이번에는 EXPLAIN PLAN을 통해 실행 계획을 확인하고, Oracle이 어떤 순서로 데이터를 읽고 어디에서 비용을 많이 사용하는지 보면서 접근하니 훨씬 원인을 찾기 쉬웠다.&lt;/p&gt;
&lt;p data-end=&quot;3588&quot; data-start=&quot;3497&quot; data-ke-size=&quot;size16&quot;&gt;특히 ChatGPT와 함께 실행 계획을 분석하면서 &lt;b&gt;왜 이 부분이 느린지&lt;/b&gt;, &lt;b&gt;왜 이렇게 변경하면 성능이 좋아지는지&lt;/b&gt;를 이해하는 데도 많은 도움이 되었다.&lt;/p&gt;
&lt;p data-end=&quot;3703&quot; data-start=&quot;3590&quot; data-ke-size=&quot;size16&quot;&gt;아직 실행 계획을 완벽하게 읽을 수 있는 수준은 아니지만, 예전처럼 무작정 SQL부터 수정하기보다는 &lt;b&gt;실행 계획을 먼저 확인하고 병목을 찾는 습관&lt;/b&gt;이 생겼다는 점이 이번 튜닝에서 가장 큰 수확이었다.&lt;/p&gt;
&lt;p data-end=&quot;3766&quot; data-start=&quot;3705&quot; data-ke-size=&quot;size16&quot;&gt;가끔 현장에서 튜닝을 해야하는 상황이 조금씩 발생하고 있는 데, GPT를 사용할 수 없는 경우에도 실행계획을 보면서 수정할 수 있도록 보는 법을 조금씩 배워야겠다는 생각이 들었다.&lt;/p&gt;</description>
      <category>회사회고록</category>
      <category>개발자일지</category>
      <category>베베개발자</category>
      <category>성능튜닝</category>
      <category>실행계획</category>
      <category>튜닝</category>
      <category>튜닝개발자</category>
      <category>튜닝법</category>
      <category>회사일지</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/17</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/%EC%84%B1%EB%8A%A5-%ED%8A%9C%EB%8B%9D-%EC%82%BD%EC%A7%88%EA%B8%B0-2-20%EC%B4%88%EC%97%90%EC%84%9C-5%EC%B4%88-%EC%9D%B4%EB%82%B4%EB%A1%9C-%EC%BF%BC%EB%A6%AC-%ED%8A%9C%EB%8B%9D#entry17comment</comments>
      <pubDate>Wed, 15 Jul 2026 19:03:36 +0900</pubDate>
    </item>
    <item>
      <title>boolean과 Boolean의 차이 - 언제 무엇을 사용해야 할까?</title>
      <link>https://sujin-su-sss.tistory.com/entry/boolean%EA%B3%BC-Boolean%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%96%B8%EC%A0%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;운영계 패치 후, 서버 기동하다가 NullPointerException이 발생했다.&lt;br /&gt;원인을 분석해 보니 옵션 값을 boolean으로 선언했는 데, 설정 파일(Property)에 해당 옵션이 존재하지 않아 Null이 전달되면서 예외가 발생한 것이었다.&lt;br /&gt;&lt;br /&gt;이 경험을 통해 Boolean과 boolean의 차이가 무엇인지 정리를 하면서 원시타입과 참조타입도 간단하게 정리해보려고 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;boolean 데이터 타입&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;boolean은 원시타입(Primitive Type)에 속하는 데이터 타입이다.&lt;br /&gt;원시타입은 객체를 생성하지 않고 &lt;b&gt;실제 값을 직접 저장하는 데이터 타입을 의미&lt;/b&gt;한다. &lt;br /&gt;&lt;br /&gt;Java에서 데이터를 저장하는 방식은 크게 값 자체를 저장하는 방식과 객체의 주소를 저장하는 방식으로 나눌 수 있다.&lt;br /&gt;&lt;br /&gt;이 중 원시타입인 boolean은 첫 번째 방식에 해당한다.&lt;br /&gt;즉, true/false라는 값을 자신의 메모리 공간에 직접 저장한다.&lt;br /&gt;&lt;br /&gt;그래서 boolean은 Null 데이터를 저장할 수 없으며, true/false와 같이 두 가지 값만 가질 수 있다.&lt;br /&gt;&lt;br /&gt;그리고 원시 타입은 객체를 생성하지 않기 때문에 메모리 사용량이 적으며, 객체 생성 과정이 없기 때문에 처리 속도가 빠르다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Boolean 데이터 타입&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Boolean은 boolean을 객체로 감싼 Wrapper Class이다.&lt;br /&gt;&lt;br /&gt;원시 타입과 달리 실제 값을 저장하는 것이 아니라 &lt;b&gt;객체의 참조를 저장&lt;/b&gt;하기 때문에 참조 타입(Reference Type)이라고 한다.&lt;br /&gt;&lt;br /&gt;따라서 객체가 존재하지 않은 상태를 의미하는&amp;nbsp;&amp;nbsp;Null도 저장할 수 있다.&lt;br /&gt;&lt;br /&gt;대신 객체를 생성해야 하기 때문에 boolean보다 메모리를 좀 더 사용하며 처리 속도가 느리다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;언제 boolean을 사용하고 Boolean은 언제 사용해야 할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;boolean은 반드시 true/false 중 하나의 값이 존재해야 하는 경우 사용하는 것이 적절하다.&lt;br /&gt;&lt;br /&gt;예를 들자면 회원가입 시 약관 동의 여부가 있다.&lt;br /&gt;약관 동의는 동의/미동의 두 가지 상태만 존재하며, &amp;lsquo;미 선택&amp;rsquo; &amp;lsquo;모름&amp;rsquo;과 같은 상태는 존재하지 않기 때문이다.&lt;br /&gt;&lt;br /&gt;그리고 연산이 자주 수행되는 변수 나 성능이 중요한 경우에도 boolean을 사용하는 것이 일반적이다.&lt;br /&gt;&lt;br /&gt;반면 Boolean은 아직 값이 정해지지 않았음을 표현해야 하는 경우에 사용이 된다.&lt;br /&gt;&lt;br /&gt;예를 들어 관리자가 승인해야 하는 항목의 승인 여부에는 승인/반려에 미 결재상태와 같이 null값이 들어갈 수 있는 상태가 존재하는 항목에 사용한다.&lt;br /&gt;&lt;br /&gt;이뿐만 아니라 &lt;b&gt;List/Map&lt;/b&gt;에서도 참조 타입인 &lt;b&gt;Boolean을 사용&lt;/b&gt;한다.&lt;br /&gt;왜냐하면 List/Map과 같은 컬렉션은 객체만 저장할 수 있기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;HTML&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;HTML&quot;&gt;&lt;code&gt;List&amp;lt;Boolean&amp;gt; a = new ArrayList&amp;lt;&amp;gt;(); &amp;mdash;&amp;mdash;- O

List&amp;lt;boolean&amp;gt; a = new ArrayList&amp;lt;&amp;gt;(); &amp;mdash;&amp;mdash;- X&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;원시 타입인 boolean은 사용이 불가하고 객체 타입인 Boolean만 사용이 가능하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;boolean과 Boolean의 차이를 정확하게 몰랐고, 이 데이터가 null이 될 수 있을 까라는 것을 고려하지 않고 다른 소스만을 참고하여 데이터 타입을 선택하여 이런 에러가 발생하였다.&lt;br /&gt;이런 일이 반복해서 일어나지 않도록 boolean과 Boolean의 차이를 이해하면서 원시 타입과 참조 타입까지 공부하게 되었다.&lt;br /&gt;&lt;br /&gt;이 학습과 장애를 통해서 단순히 익숙한 타입을 사용하는 것이 아니라, 해당 데이터가 어떤 상태를 표현해야 하는 지까지 고려해서 데이터 타입을 선택해야겠다고 생각했다.&lt;/p&gt;</description>
      <category>Java,Groovy</category>
      <category>BOOLEAN</category>
      <category>java</category>
      <category>데이터타입</category>
      <category>데이터타입사용</category>
      <category>베베개발자</category>
      <category>원시타입</category>
      <category>원시타입VS참조타입</category>
      <category>참조타입</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/16</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/boolean%EA%B3%BC-Boolean%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%96%B8%EC%A0%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C#entry16comment</comments>
      <pubDate>Sun, 5 Jul 2026 00:41:33 +0900</pubDate>
    </item>
    <item>
      <title>운영계에서만 발생한 NullPointerException - 데이터 타입의 중요성</title>
      <link>https://sujin-su-sss.tistory.com/entry/%EC%9A%B4%EC%98%81%EA%B3%84%EC%97%90%EC%84%9C%EB%A7%8C-%EB%B0%9C%EC%83%9D%ED%95%9C-NullPointException-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;오늘 운영계 패치 후 서버가 정상적으로 기동 되지 않는 문제가 발생했다.&lt;br&gt;개발계 패치 시에는 정상적으로 기동이 되었기 때문에, 처음에는 운영계 패치 파일이 누락된 것은 아닐까 의심했다.&lt;br&gt;&lt;br&gt;일단 정상적으로 서버가 기동이 되지 않아 기동로그를 추출한 후에 원복 하여 재기동을 진행하였다.&lt;br&gt;&lt;br&gt;에러 로그를 확인하니 NullPointerException이 발생한 상태였다.&lt;br&gt;기동 과정에서 어떤 데이터를 읽지 못하는 것인지 확인하면서, 패치 누락 여부도 함께 점검하였다.&lt;br&gt;&lt;br&gt;운영계와 개발계의 패치 내역을 하나씩 비교해 보니, 이번 기능 개선 과정에서 추가한 option이 운영계 properties 파일에는 정의되어 있지 않은 것을 확인했다.&lt;br&gt;&lt;br&gt;원인 확인하기를 위해 개발계에서도 동일하게 해당 option을 제거한 뒤 기동해보니, 운영계와 동일한 형태의 오류가 재현되었다.&lt;br&gt;그 뒤에 다시 option을 추가하니 정상적으로 기동이 되어 제품에 접근이 가능했다.&lt;br&gt;&lt;br&gt;그런데 여기서 한 가지 궁금한 점이 생겼다.&lt;br&gt;이전에도 기본 properties에는 정의되어 있지 않고 사이트 전용 properties에만 존재하는 option을 본 적이 있었는데, 그때는 문제없이 서버가 기동 되었기 때문이다.&lt;br&gt;&lt;br&gt;그리고 이번에 새로 추가한 option은 3개였지만, 하나에서는 정의하지 않으면 오류가 발생하였고 나머지 둘은 상관없이 정상적으로 기동이 되었다.&lt;br&gt;&lt;br&gt;해당 부분에 무슨 차이가 있는지 확인해 보니 데이터 타입이 달랐다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Java&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Value(“#{applicationProperties[‘옵션명’]}”)
Private String stringType = ‘’;
@Value(“#{applicationProperties[‘옵션명’]}”)
Private Boolean bigBooleanType = false;
@Value(“#{applicationProperties[‘옵션명’]}”)
Private boolean smallBooleanType = false;
&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;String과 Boolean은 null 값을 가질 수 있지만,&lt;br&gt;boolean은 null을 허용하지 않는다.&lt;br&gt;따라서 설정값을 읽는 과정에서 값이 존재하지 않아 null이 들어오면서 예외가 발생했던 것이다.&lt;br&gt;&lt;br&gt;이번 일을 통해 같은 option이라도 선언한 데이터 타입에 따라 서버 기동 여부까지 영향을 받을 수 있다는 것을 몸소 경험했다.&lt;br&gt;&lt;br&gt;앞으로는 option을 추가할 때 단순히 필요한 타입을 선택하는 것이 아니라, 설정값이 존재하지 않는 상황까지 고려하여 Boolean과 boolean 중 어떤 타입이 적절한지 함께 판단해야겠다는 생각이 들었다.&lt;br&gt;&lt;br&gt;또한 기존 코드를 확인해 보니 Boolean과 boolean이 혼용되어 있는 부분도 확인할 수 있었다.&lt;br&gt;이번 사례를 팀에 공유하여, 앞으로 boolean타입의 option을 선언할 때 어떤 타입을 사용하는 것이 적절한지 기준을 함께 정해야겠다는 생각이 들었다.&lt;/p&gt;</description>
      <category>회사회고록</category>
      <category>BOOLEAN</category>
      <category>nullpointExcpetion</category>
      <category>option</category>
      <category>properties누락</category>
      <category>개발오류</category>
      <category>개발일지</category>
      <category>데이터타입</category>
      <category>베베개발자</category>
      <category>운영계오류</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/15</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/%EC%9A%B4%EC%98%81%EA%B3%84%EC%97%90%EC%84%9C%EB%A7%8C-%EB%B0%9C%EC%83%9D%ED%95%9C-NullPointException-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1#entry15comment</comments>
      <pubDate>Thu, 2 Jul 2026 21:49:35 +0900</pubDate>
    </item>
    <item>
      <title>성능 튜닝 삽질기 - 조회 1분 걸리던 화면을 10~15초까지 줄이기</title>
      <link>https://sujin-su-sss.tistory.com/entry/%EC%84%B1%EB%8A%A5-%ED%8A%9C%EB%8B%9D-%EC%82%BD%EC%A7%88%EA%B8%B0-%EC%A1%B0%ED%9A%8C-1%EB%B6%84-%EA%B1%B8%EB%A6%AC%EB%8D%98-%ED%99%94%EB%A9%B4%EC%9D%84-1015%EC%B4%88%EA%B9%8C%EC%A7%80-%EC%A4%84%EC%9D%B4%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;일을 하다 보면 자주 요청 오는 것 중에 하나가 성능 튜닝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 화면이 많은 제품이다 보니 데이터가 쌓일수록 조회 속도가 느려지고, 유지보수 사이트에서는 조회 메뉴의 성능 개선 요청이 자주 들어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 회사 소속 성능튜닝 담당자가 현장에 방문해 힌트를 적용하거나, 고객사 DBA가 튜닝한 SQL을 전달해 주면 해당 SQL을 제품에 적용한 뒤 조회 결과가 기존과 동일한지, 데이터에 문제가 없는지만 확인하는 경우가 대부분이었다.&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;하지만 이번에 담당한 사이트는 이미 성능 튜닝 담당자가 방문한 상태였지만, 5개의 메뉴가 평균 20~30초 심한 경우 1분 이상 걸리는 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 다시 튜닝 요청이 들어왔고 이번에는 직접 튜닝을 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;첫 튜닝 시도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 SQL 튜닝을 제대로 해본 적이 없었다. 그래서 우선 내가 할 수 있는 것 부터 시도해 보기로 했다.&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;width: 100%;&quot;&gt;1. 실행 계획을 분석하면서 AI와 함께 튜닝 방향을 검토 &lt;br /&gt;2. select 문에서 불필요하게 *로 되어있는 부분 정확하게 컬럼명시하기&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;필요하지 않은 컬럼까지 모두 조회하는 것도 성능에 영향을 주는 요인 중에 하나라고 하여, 필요한 컬럼만 조회하도록 수정하였다.&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;또한 실행 계획을 기반으로 AI에게 튜닝 방향을 물어보며, AI가 제안한 JOIN 순서 변경이나 조건 변경 등을 실제로 적용하여 비교해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 AI가 알려준 방식이 항상 더 빠른 것은 아니였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 기존보다 느린 부분도 존재해서 수정한 쿼리와 기존 쿼리를 옵션으로 분리하여 현장에서 직접 비교할 수 있도록 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 어느 부분에서 시간이 오래 걸리는지 확인하기 위해 제품에서 구현해 둔 실행 시간 측정 메서드를 이용해서 SQL이 수행되는 메서드의 실행 시간을 로그로 남겼다.&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;1차로 패치 진행 결과 기존보다는 속도가 빨라지기는 했지만 고객이 체감할 정도의 성능 개선이 아니어서 재 튜닝 요청이 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오랫동안 해당 제품을 담당했던 수석님께서 도움요청을 드렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 이야기하신 부분이 불필요한 join이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 JOIN도 옵티마이저의 처리 비용을 증가시키기 때문에 제거할 수 있는 부분부터 확인해 보라고 하셨다.&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;2차 튜닝 시도 - Join 절&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서 실제 사용하는 데이터가 무엇인지 다시 확인한 뒤 필요하지 않은 join과 컬럼들을 하나씩 제거했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 조회와 Excel 다운로드에서 동일한 메서드를 사용하는 부분들이 있어서 해당 부분을 분리하여 적용하였다.&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;그 결과 5개의 메뉴 중 일반 조회화면 3개는 성능이 눈에 띄게 개선되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 20~30초 정도 걸리던 화면이&amp;nbsp; 30건 정도 데이터를 조회해도 약 3초 이내로 나왔으며. 더 많은 데이터를 조회한 경우에도 약 5초 정도로 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 2개의 메뉴는 아직도 속도가 개선되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1808&quot; data-start=&quot;1727&quot; data-ke-size=&quot;size16&quot;&gt;한 메뉴는 조회 화면처럼 보이지만 내부에서는 데이터를 삭제하고 다시 적재한 뒤 UPDATE까지 수행하는 등 40개가 넘는 로직을 수행하고 있었고,&lt;/p&gt;
&lt;p data-end=&quot;1863&quot; data-start=&quot;1810&quot; data-ke-size=&quot;size16&quot;&gt;다른 하나는 승인 화면으로 SELECT, INSERT, UPDATE, DELETE가 모두 발생하지만 SQL만 튜닝해서 해결할 수 있는 문제가 아니라 로직 자체를 변경이 필요한 단계였다.&lt;/p&gt;
&lt;p data-end=&quot;1972&quot; data-start=&quot;1922&quot; data-ke-size=&quot;size16&quot;&gt;반면 조회 화면은 SELECT 구문을 조금 더 개선할 여지가 있어 다시 튜닝을 진행하였다.&lt;/p&gt;
&lt;p data-end=&quot;1972&quot; data-start=&quot;1922&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3차 튜닝 시도 - 조건 재 확인 및 Join 수정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2043&quot; data-start=&quot;1998&quot; data-ke-size=&quot;size16&quot;&gt;튜닝 경험이 많은 분들에게 도움을 요청하였고, 여러 가지 조언을 들을 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2104&quot; data-start=&quot;2045&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 들었던 이야기는 &lt;b&gt;조회의 시작이 되는 테이블의 데이터 양을 최대한 줄여야 한다&lt;/b&gt;는 것이었다.&lt;/p&gt;
&lt;p data-end=&quot;2128&quot; data-start=&quot;2106&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 활용할 수 있는 조건인데 빠져 있는 것은 없는지, 조금이라도 데이터를 더 줄일 수 있는 조건은 없는지를 다시 확인하였다.&lt;/p&gt;
&lt;p data-end=&quot;2128&quot; data-start=&quot;2106&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2222&quot; data-start=&quot;2171&quot; data-ke-size=&quot;size16&quot;&gt;그리고 서브쿼리도 가능한 경우에는 JOIN으로 변경하는 것이 좋다는 조언을 받아 적용하였다.&lt;/p&gt;
&lt;p data-end=&quot;2283&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;물론 모든 서브쿼리를 JOIN으로 바꾼 것은 아니지만, 변경 가능한 부분은 하나씩 수정하면서 비교해 보았다.&lt;/p&gt;
&lt;p data-end=&quot;2283&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2283&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로는 해당 사이트 버전에는 Java 소스에서 SQL 문자열을 직접 조합하는 방식이었다.&lt;/p&gt;
&lt;p data-end=&quot;2283&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 신청 유형에 따라서&lt;/p&gt;
&lt;pre id=&quot;code_1782880309072&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if(param == 1){
 pstm.append(&quot; where id1= 222&quot;)
}else{
  pstm.append(&quot; where id1= 333&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2283&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;과 같이 서로 다른 SQL이 생성되고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2283&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;470&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;튜닝을 도와주신 분께서는 옵티마이저가 실행 계획을 재사용할 수 있도록 SQL 형태를 최대한 동일하게 유지하는 것이 좋다고 조언해 주셨다.&lt;/p&gt;
&lt;p data-end=&quot;526&quot; data-start=&quot;475&quot; data-ke-size=&quot;size16&quot;&gt;그래서 리터럴 값을 직접 사용하는 대신 바인드 변수(?)를 사용하는 방식으로 수정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방식으로 &lt;b&gt;약 6,283건의 데이터&lt;/b&gt;를 조회&amp;middot;가공하는 화면 속도가&amp;nbsp;&lt;b&gt;기존 60초에서 25초로 나오도록 수정&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;하지만 고객은 이전부터 모든 화면이 3초 이내에 조회되기를 강하게 요청하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분을 수석님, 다른 분들과 논의한 결과 해당 로직 및 승인로직은 쿼리 튜닝만으로는 3초 이내로 만드는 것은 현실적으로 어렵다는 결론이 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 파트장님이 직접 고객사를 방문하여 해당 두 메뉴는 로직 자체를 변경하지 않는 이상 요구 성능을 만족하기 어렵다는 점을 설명하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후에도 전체 수행 시간은 약 25초였지만, 로그를 확인해 보니 그중 특정 SELECT가 각각 약 11초와 4초를 차지하고 있는 부분이 존재하여 회사 소속 튜닝 담당자에게 현장 튜닝을 요청하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;힌트절을 적용한 결과 해당 SELECT는 각각 약 0.5초와 1.4초까지 줄었고, 전체 수행 시간도 개발계 기준 약 10~15초 수준으로 개선되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;성능튜닝 어려운 그것...&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1966&quot; data-start=&quot;1924&quot; data-ke-size=&quot;size16&quot;&gt;이번 튜닝을 하면서 가장 크게 느낀 것은 SQL 자체보다 &quot;병목을 찾는 과정&quot;이 더 중요하다는 점이었다.&lt;/p&gt;
&lt;p data-end=&quot;2028&quot; data-start=&quot;1971&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 실행 계획도 제대로 볼 줄 몰랐고, AI가 알려준 SQL을 적용하면 빨라질 것이라고 생각했다.&lt;/p&gt;
&lt;p data-end=&quot;2093&quot; data-start=&quot;2033&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 AI가 제안한 SQL이 더 느려지는 경우도 있었고, 힌트절 하나로 해결되는 문제도 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;2093&quot; data-start=&quot;2033&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2208&quot; data-start=&quot;2098&quot; data-ke-size=&quot;size16&quot;&gt;결국 중요한 것은 &lt;b&gt;병목이 어디서 발생하는지 찾고, 불필요한 JOIN을 제거하고, 조건을 다시 확인하고, 실행 시간을 직접 측정하면서 하나씩 줄여나가는 과정&lt;/b&gt;이라는 것을 많이 배울 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2290&quot; data-start=&quot;2213&quot; data-ke-size=&quot;size16&quot;&gt;아직 운영계에서 최종 성능은 확인하지 못했지만, 다음에 비슷한 성능 이슈가 발생한다면 이번보다 훨씬 빠르게 원인을 찾을 수 있을 것 같다.&lt;/p&gt;
&lt;p data-end=&quot;3088&quot; data-start=&quot;3006&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3088&quot; data-start=&quot;3006&quot; data-ke-size=&quot;size16&quot;&gt;그리고 아마 이다음은 이제 로직 자체 수정을 하게 되지 않을까라는 생각도 든다.&lt;/p&gt;</description>
      <category>회사회고록</category>
      <category>SQL</category>
      <category>베베개발자</category>
      <category>베이비개발자</category>
      <category>성능튜닝</category>
      <category>성능튜닝삽질</category>
      <category>조회속도</category>
      <category>조회속도튜닝</category>
      <category>주니어개발자</category>
      <category>쿼리튜닝</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/14</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/%EC%84%B1%EB%8A%A5-%ED%8A%9C%EB%8B%9D-%EC%82%BD%EC%A7%88%EA%B8%B0-%EC%A1%B0%ED%9A%8C-1%EB%B6%84-%EA%B1%B8%EB%A6%AC%EB%8D%98-%ED%99%94%EB%A9%B4%EC%9D%84-1015%EC%B4%88%EA%B9%8C%EC%A7%80-%EC%A4%84%EC%9D%B4%EA%B8%B0#entry14comment</comments>
      <pubDate>Wed, 1 Jul 2026 13:40:49 +0900</pubDate>
    </item>
    <item>
      <title>전수 테스트 중 서버 멈춤 현상 해결- DB connection close</title>
      <link>https://sujin-su-sss.tistory.com/entry/%EC%A0%84%EC%88%98-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A4%91-%EC%84%9C%EB%B2%84-%EB%A9%88%EC%B6%A4-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0-DB-connection-close</link>
      <description>&lt;p data-end=&quot;167&quot; data-start=&quot;124&quot; data-ke-size=&quot;size16&quot;&gt;전수 테스트를 위해 구성해 둔 서버가 어제부터 계속 멈추는 현상이 발생했다.&lt;/p&gt;
&lt;p data-end=&quot;167&quot; data-start=&quot;124&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;275&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 디버그 모드로 며칠 동안 사용하면서 로그가 많이 쌓여 용량 부족으로 발생한 문제라고 생각하여 로그 파일을 정리해 보았지만 동일하게 서버가 먹통이 되는 현상이 계속 발생했다.&lt;/p&gt;
&lt;p data-end=&quot;275&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;388&quot; data-start=&quot;277&quot; data-ke-size=&quot;size16&quot;&gt;연구소에 방문한 기술지원팀 소속 담당자에게 현재 상황을 설명하고 서버 점검을 요청하였는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;388&quot; data-start=&quot;277&quot; data-ke-size=&quot;size16&quot;&gt;서버가 죽은 것이 아니라 &lt;b&gt;멈춰있는 상태&lt;/b&gt;이며 서버 자체 문제는 아닌 것으로 보인다는 의견을 받았다.&lt;/p&gt;
&lt;p data-end=&quot;388&quot; data-start=&quot;277&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;왜 서버가 멈추는지 고민하던 중, 테스트를 진행하던 담당자가 특정 메뉴에서 신청을 3번 수행하면 멈춘다는 이야기를 해주었다.&lt;/p&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;그 이야기를 듣고 IntelliJ로 직접 기동하여 동일한 조건으로 테스트해보니 동일한 멈춤 현상이 발생했다.&lt;/p&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;579&quot; data-start=&quot;514&quot; data-ke-size=&quot;size16&quot;&gt;먼저 DB Lock이 걸려있는지 확인하였고, Cursor 관련 문제도 의심하여 점검했지만 둘 다 문제가 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;708&quot; data-start=&quot;581&quot; data-ke-size=&quot;size16&quot;&gt;멈추는 구간을 다시 확인해보니 select 결과가 0건인 이후에 동작이 멈추는 것을 확인하였다.&lt;/p&gt;
&lt;p data-end=&quot;708&quot; data-start=&quot;581&quot; data-ke-size=&quot;size16&quot;&gt;과거 프로젝트에서 select 결과가 0건일 때 간헐적으로 멈추는 문제가 있었던 경험이 있어 동일한 원인일 가능성을 생각했었다.&lt;/p&gt;
&lt;p data-end=&quot;847&quot; data-start=&quot;710&quot; data-ke-size=&quot;size16&quot;&gt;해당 로직은 메일 발송 시 필요한 정보를 조회하는 부분이었고, 메일 사용 여부 옵션에 따라 해당 로직을 타지 않도록 설정할 수 있었던 곳이어서 옵션에 따라서 로직 수행할 수 있도록 변경한 뒤 다시 테스트를 진행했지만 동일하게 멈추는 현상이 발생했고, 해당 부분은 원인이 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;847&quot; data-start=&quot;710&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;949&quot; data-start=&quot;849&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 멈추기 직전에 실행된 쿼리와 관련된 로직을 따라가며 확인하던 중, 다른 개발자가 DB connection을 생성한 후 close 처리를 하지 않은 부분을 발견하였다.&lt;/p&gt;
&lt;p data-end=&quot;949&quot; data-start=&quot;849&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1082&quot; data-start=&quot;951&quot; data-ke-size=&quot;size16&quot;&gt;connection을 생성한 후 &lt;b&gt;close 하지 않으면 connection 누수가 발생&lt;/b&gt;하고, 일정 수 이상의 &lt;b&gt;connection이 쌓이게 되면 더 이상 connection을 생성하지 못해 애플리케이션이 멈추는 현상이 발생&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1082&quot; data-start=&quot;951&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1191&quot; data-start=&quot;1084&quot; data-ke-size=&quot;size16&quot;&gt;해당 로직을 수정하여 finally 구문에서 connection이 close 되도록 변경한 후 다시 테스트를 진행하였고, 이후 서버가 멈추는 현상 없이 정상적으로 동작하는 것을 확인하였다.&lt;/p&gt;
&lt;p data-end=&quot;1191&quot; data-start=&quot;1084&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1316&quot; data-start=&quot;1193&quot; data-ke-size=&quot;size16&quot;&gt;최근에는 MyBatis나 ORM을 사용하면서 Java 코드에서 직접 DB connection을 다루는 경우가 많지 않지만, 오래된 코드나 레거시 시스템에서는 여전히 직접 connection을 관리하는 경우가 존재한다.&lt;/p&gt;
&lt;p data-end=&quot;1316&quot; data-start=&quot;1193&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;396&quot; data-start=&quot;320&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 connection을 생성한 후에는 반드시 close 처리를 해주는 습관이 중요하다는 것을 다시 한번 느끼게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;또한 서버가 완전히 죽은 것이 아니라 멈춰있는 현상이 발생할 경우, connection 누수 가능성도 하나의 원인으로 고려해야 한다는 점을 경험을 통해 알게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;586&quot; data-start=&quot;494&quot; data-ke-size=&quot;size16&quot;&gt;이번 문제를 해결하면서 단순히 오류를 수정하는 것에서 끝나는 것이 아니라, 레거시 코드에서 유사한 패턴이 존재하는지 추가적으로 점검을 하며 해당 내용을 팀원들과 공유하여 동일한 문제가 발생하지 않도록 해야겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회사회고록</category>
      <category>connection누수</category>
      <category>베베개발자</category>
      <category>베이비개발자</category>
      <category>트러블슈팅</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/13</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/%EC%A0%84%EC%88%98-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A4%91-%EC%84%9C%EB%B2%84-%EB%A9%88%EC%B6%A4-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0-DB-connection-close#entry13comment</comments>
      <pubDate>Fri, 3 Apr 2026 00:29:14 +0900</pubDate>
    </item>
    <item>
      <title>전수 테스트를 위한 데이터 마이그레이션</title>
      <link>https://sujin-su-sss.tistory.com/entry/%EC%A0%84%EC%88%98-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
      <description>&lt;p data-end=&quot;248&quot; data-start=&quot;203&quot; data-ke-size=&quot;size16&quot;&gt;사이트 요청으로 연구소에서 제품 전수 테스트를 진행해야 하는 상황이 발생했다.&lt;/p&gt;
&lt;p data-end=&quot;391&quot; data-start=&quot;250&quot; data-ke-size=&quot;size16&quot;&gt;최대한 사이트 환경과 동일하게 테스트하기 위해, 제품 3 버전에서 데이터를 생성한 뒤 초기 데이터만 설정되어 있는 제품 4 버전으로 데이터를 옮겨 테스트 환경을 구성하기로 했다.&lt;/p&gt;
&lt;p data-end=&quot;391&quot; data-start=&quot;250&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 과정에서 DB Link를 이용하여 데이터를 옮기는 방법을 선택했다.&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;393&quot; data-ke-size=&quot;size16&quot;&gt;DB Link를 생성하고 데이터를 확인하려고 했지만, &lt;b&gt;제품 3버전 DB와 제품 4 버전 DB가 서로 다른 네트워크 영역&lt;/b&gt;에 있어 &lt;b&gt;방화벽을 열지 않으면 연결이 불가능한 상황&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-end=&quot;580&quot; data-start=&quot;494&quot; data-ke-size=&quot;size16&quot;&gt;방화벽을 열어 연결하는 방법도 고려했지만, 제품 4 버전 환경에 초기값만 설정된 제품 3 버전 DB가 존재하는 것을 확인하여 해당 DB를 활용하기로 했다.&lt;/p&gt;
&lt;p data-end=&quot;580&quot; data-start=&quot;494&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;695&quot; data-start=&quot;582&quot; data-ke-size=&quot;size16&quot;&gt;먼저 해당 데이터를 기반으로 제품이 정상적으로 동작하는지 확인하고, 필요한 메뉴가 모두 활성화되어 있는지 점검했다.&lt;/p&gt;
&lt;p data-end=&quot;695&quot; data-start=&quot;582&quot; data-ke-size=&quot;size16&quot;&gt;이후 테스트에 필요한 데이터 목록을 정리하고, 제품에서 테스트 데이터를 생성하였다.&lt;/p&gt;
&lt;p data-end=&quot;695&quot; data-start=&quot;582&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;782&quot; data-start=&quot;697&quot; data-ke-size=&quot;size16&quot;&gt;매핑 정의서를 보면서 데이터 이관 방식을 고민했다.&lt;/p&gt;
&lt;p data-end=&quot;782&quot; data-start=&quot;697&quot; data-ke-size=&quot;size16&quot;&gt;단순히 데이터를 복사하는 것이 아니라, 제품 4버전에는 이미 초기 기초 데이터가 존재했기 때문에 충돌 없이 데이터를 이관하는 방법이 필요했다.&lt;/p&gt;
&lt;p data-end=&quot;782&quot; data-start=&quot;697&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #3d3d3a; text-align: start;&quot;&gt;그래서 이전 프로젝트에서 다른 분이 작성했던 마이그레이션 SQL 파일을 참고해서 전략을 잡았다.&lt;/span&gt; &lt;span style=&quot;color: #3d3d3a; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 60px;&quot;&gt;- 그대로 이관 가능한 데이터는 PK 충돌만 고려하여 이동 &lt;br /&gt;&lt;span&gt;- 기존 키 값과 중복되는 경우, 테이블의 MAX 값 이후 값으로 조정&lt;br /&gt;&lt;/span&gt;- &lt;span&gt;PK 변경 시 연결 데이터들이 유실 되지 않도록 동일한 방식으로 연결 테이블의&amp;nbsp; 값 수정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;1046&quot; data-start=&quot;975&quot; data-ke-size=&quot;size16&quot;&gt;※ A 테이블과 B 테이블이 연결되어 있을 경우, C 테이블에 A의 pk_id와 B의 pk_id를 저장하고 있는 상황&lt;/p&gt;
&lt;p data-end=&quot;1046&quot; data-start=&quot;975&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1178&quot; data-start=&quot;1048&quot; data-ke-size=&quot;size16&quot;&gt;또한 신규 컬럼이 추가된 데이터의 경우&amp;nbsp; &lt;span style=&quot;color: #3d3d3a; text-align: start;&quot;&gt;이관 후 일괄 UPDATE로 보정하는 방식으로 하는 것을 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1870&quot; data-start=&quot;1761&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1870&quot; data-start=&quot;1761&quot; data-ke-size=&quot;size16&quot;&gt;먼저 pk_id가 중복되지 않는 데이터는 아래와 같은 방식으로 제품 4 버전의 db로 옮겼다.&lt;/p&gt;
&lt;pre id=&quot;code_1775098031940&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO v4.v4_table (pk_id,class_id, name, parent_id)
SELECT pk_id,class_id, name, parent_id
FROM v3.v3_table
 where v3.pk_id &amp;gt; (select max(pk_id) from v4.v4_table)
   and class_id in (그대로 이관해도 되는 구분값들)
   ;&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;그리고 &lt;b&gt;pk가 중복되는 값들은 일괄적으로 같은 값을 더해&lt;/b&gt; pk_id가 겹치지 않도록 하여 이관하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) pk_id + 11000&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;이후 PK가 변경된 데이터에 맞게 연결 정보를 가지고 있는 테이블도 동일한 방식으로 수정하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1775098818387&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insert into v4.connection_tbl (pk_id,class_id,a_pk_id,b_pk_id)
  select pk_id,class_id,a_pk_id,b_pk_id
    from v3.connection_tbl
 
 ------------------------------------------
 update v4.connection_tbl
   set a_pk_id = case when a_pk_id &amp;lt;= 기준점 then a_pk_id + 일괄 값 else a_pk_id end
     , b_pk_id = case when b_pk_id &amp;lt;= 기준점 then b_pk_id + 일괄 값 else b_pk_id end
    where class_id = 조건
      and ( a_pk_id &amp;lt;= 기준점 OR b_pk_id &amp;lt;= 기준점)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 해서 일괄적으로 연결 될 수 있도록 데이터 수정을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 데이터이관이 완료된 후에, 제품을 기동한 후에 조회, 신규, 수정, 삭제 창 등을 한번씩 눌러보며 에러가 발생하는 부분을 확인하면서 추가적으로 데이터 보정을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #3d3d3a; text-align: start;&quot;&gt;대부분은 UPDATE 누락이거나 컬럼 이동을 잡지 못한 케이스였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 마이그를 해본 적이 없는 상태에서 이틀 안에 데이터를 옮겨서 정상적으로 작동을 해야 하다 보니, 다소 주먹구식으로 진행한 부분도 있었고, 추후 재사용이 가능할지에 대한 고민도 생겼다. 이번 작업을 통해 어느 사이트에서도 재사용할 수 있는 마이그레이션 절차와 가이드를 문서화할 필요성을 느끼게 되었다.&lt;/p&gt;</description>
      <category>회사회고록</category>
      <category>pk충돌</category>
      <category>데이터마이그</category>
      <category>마이그레이션</category>
      <category>마이그주의점</category>
      <category>베베개발자</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/12</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/%EC%A0%84%EC%88%98-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98#entry12comment</comments>
      <pubDate>Thu, 2 Apr 2026 12:11:07 +0900</pubDate>
    </item>
    <item>
      <title>[Oracle] DB Link로 두 DB 연결하고 데이터 마이그레이션하기 (준비단계)</title>
      <link>https://sujin-su-sss.tistory.com/entry/Oracle-DB-Link-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사이트 대응을 위해 사이트처럼 데이터를 가공하기 위해서 제품 v3 데이터를 제품 v4 DB에 데이터를 옮겨야 하는 작업을 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제품 v4에서 데이터를 만들어서 테스트할 때는 오류 없이 정상적으로 작동이 되어 배포를 하였지만, 현장 데이터로 하면 오류가 발생하여 문제가 생겼다.&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;그래서 차이가 데이터 마이그 여부가 아닐까라는 생각에, 제품 v3에 있는 데이터를 제품 v4에 옮기는 작업을 하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겸사겸사 마이그 연습이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그를 할 때, 데이터를 옮기는 방법이 다양하지만 이번에는&amp;nbsp;DB&amp;nbsp;Link를&amp;nbsp;사용하여&amp;nbsp;비교적&amp;nbsp;간단하게&amp;nbsp;데이터를&amp;nbsp;옮겨보려고&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;DB Link 연결하는 법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해하기 쉽게 &lt;b&gt;A DB(제품 v4)에서 B DB (제품 v3)로 연결&lt;/b&gt;을 한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB&amp;nbsp;Link를&amp;nbsp;연결하기&amp;nbsp;위해서는&amp;nbsp;몇&amp;nbsp;가지&amp;nbsp;준비가&amp;nbsp;필요하다.&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;width: 100%;&quot;&gt;1. DB Link 걸 수 있는 권한&lt;br /&gt;2. 링크 당할 DB의 계정과 비밀번호&lt;br /&gt;3. 링크 당할 DB의 IP주소와 Port번호 그리고 서비스명&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;1. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;DB Link 걸 수 있는 권한&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp;DB&amp;nbsp;Link를&amp;nbsp;생성할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;권한이&amp;nbsp;있는지&amp;nbsp;확인해야&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A DB에서 해당 쿼리를 실행했을 때, 데이터에 &quot;CREATE DATABASE LINK&quot;가 나오면 권한이 있다는 의미이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774516669224&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM USER_SYS_PRIVS
WHERE PRIVILEGE LIKE '%DATABASE LINK%';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnuNR8/dJMcacWNWSV/1oNpVGXyVRyZeQCw9YA9ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnuNR8/dJMcacWNWSV/1oNpVGXyVRyZeQCw9YA9ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnuNR8/dJMcacWNWSV/1oNpVGXyVRyZeQCw9YA9ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnuNR8%2FdJMcacWNWSV%2F1oNpVGXyVRyZeQCw9YA9ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1107&quot; height=&quot;144&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약&amp;nbsp;권한이&amp;nbsp;없다면&amp;nbsp;DBA&amp;nbsp;계정으로&amp;nbsp;아래&amp;nbsp;쿼리를&amp;nbsp;실행하여&amp;nbsp;권한을&amp;nbsp;부여해야&amp;nbsp;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774516972662&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- DBA 계정으로 실행
GRANT CREATE DATABASE LINK TO A DB 명;

-- PUBLIC 링크도 필요하면 추가
GRANT CREATE PUBLIC DATABASE LINK TO A DB 명;&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;준비물 세 개가 다 준비가 되었으면, 이제 DB Link를 걸 차례다.&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;2. DB Link 연결하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A DB에 가서 아래 쿼리를 채워서 실행하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1774517113390&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE DATABASE LINK 링크명
CONNECT TO 접속유저 IDENTIFIED BY &quot;비밀번호&quot;
USING '(DESCRIPTION=
  (ADDRESS=(PROTOCOL=TCP)(HOST=IP주소)(PORT=1521))
  (CONNECT_DATA=(SERVICE_NAME=서비스명))
)';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비가 완료되었다면 DB Link를 생성한다. A&amp;nbsp;DB에서&amp;nbsp;아래&amp;nbsp;쿼리를&amp;nbsp;작성하여&amp;nbsp;실행하면&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;만약&amp;nbsp;대상&amp;nbsp;DB가&amp;nbsp;SID를&amp;nbsp;사용하는&amp;nbsp;경우에는&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;변경한다. &lt;br /&gt;(CONNECT_DATA=(SID=서비스명))&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저거 봤을 때, 아하 보다는 잉? 이였던 나와 같은 사람을 위해 예시를 첨부한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774517270183&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE DATABASE LINK 링크명
CONNECT TO B DB 계정명 IDENTIFIED BY &quot;B DB 비밀번호&quot;
USING '(DESCRIPTION=
  (ADDRESS=(PROTOCOL=TCP)(HOST=B DB IP주소)(PORT=B DB의 Port))
  (CONNECT_DATA=(SERVICE_NAME=B DB 서비스명))
)';&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;DB가 연결되었는지 한번 select문을 실행시키면 Link 걸기 끝이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774517514460&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from B db의 테이블명@링크명&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해서 데이터가 나오면 DB Link가 성공적으로 연결된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 마이그해봐야징~&lt;/p&gt;</description>
      <category>SQL</category>
      <category>dblink</category>
      <category>DB링크</category>
      <category>DB연결</category>
      <category>oracle</category>
      <category>베이비개발자</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/11</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/Oracle-DB-Link-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95#entry11comment</comments>
      <pubDate>Thu, 26 Mar 2026 18:44:20 +0900</pubDate>
    </item>
    <item>
      <title>@SessionAttributes 제거 일지(4) - 운영 적용 및 결과</title>
      <link>https://sujin-su-sss.tistory.com/entry/SessionAttributes-%EC%A0%9C%EA%B1%B0-%EC%9D%BC%EC%A7%804-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9-%EA%B2%B0%EA%B3%BC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대략 한 달 넘게 고생을 하면서 해당 사이트에서 사용하는 메뉴의 @SessionAttributes를 제거하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;연구소 이중화 테스트 서버&lt;/b&gt;에서 검증을 진행하였고, 적용한 모든 메뉴에서 &lt;b&gt;끊김이나 데이터 유실 없이 정상적으로 동작&lt;/b&gt;하는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 일부 메뉴에서 DB 조회 방식으로 변경되면서 속도 저하가 발생하였고, 해당 부분은 1차 패치 이후 나머지 메뉴 적용 전에 속도 저하가 발생한 부분을 수정하기로 결정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능이 정상적으로 동작한다는 것을 사이트에 전달하기 위해서, 결과보고서를 작성하여 사이트에 제출한 후에 개발계 패치를 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사이트 개발계&lt;/b&gt;는 이중화 상태가 안되어있어 이중화 오류를 확인하는 것이 아닌 &lt;b&gt;사이드 이펙트 없이 기능이 정상적&lt;/b&gt;으로 동작하는지에 초점을 맞춰 검증을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 고객 확인 및 승인 후에 운영계 배포 일자를 잡기로 결정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객&amp;nbsp;측에서&amp;nbsp;약&amp;nbsp;일주일간&amp;nbsp;테스트를&amp;nbsp;진행한&amp;nbsp;결과,&amp;nbsp;별다른&amp;nbsp;사이드&amp;nbsp;이펙트&amp;nbsp;없이&amp;nbsp;정상적으로&amp;nbsp;동작하는&amp;nbsp;것을&amp;nbsp;확인하였고, &lt;br /&gt;&lt;b&gt;운영계에&amp;nbsp;반영&amp;nbsp;이후&lt;/b&gt;에도&amp;nbsp;A&amp;nbsp;서버에서&amp;nbsp;B&amp;nbsp;서버로&amp;nbsp;전환되는&amp;nbsp;과정에서&amp;nbsp;&lt;b&gt;데이터&amp;nbsp;유실이나&amp;nbsp;에러&amp;nbsp;없이&amp;nbsp;정상적으로&amp;nbsp;동작&lt;/b&gt;하는&amp;nbsp;것을&amp;nbsp;확인하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영계 반영 후 모든 메뉴의 기능을 점검하지 못했기 때문에, 실제 사용하면서 문제가 발생하면 전달하기로 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객에게 해당 기능이 수정되면서 성능 저하 부분이 발생하여 해당 부분은 자체적으로 개선하려고 한다는 부분을 전달하였고 해당 부분은 추후에 패치를 요청하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;운영계에&amp;nbsp;패치&amp;nbsp;후&amp;nbsp;약&amp;nbsp;한&amp;nbsp;달이&amp;nbsp;지난&amp;nbsp;시점까지도&amp;nbsp;별다른&amp;nbsp;이슈&amp;nbsp;없이&amp;nbsp;안정적으로&amp;nbsp;사용되고&amp;nbsp;있다는&amp;nbsp;피드백을&amp;nbsp;받았다. &lt;br /&gt;&lt;br /&gt;모든&amp;nbsp;메뉴를&amp;nbsp;사전에&amp;nbsp;완벽하게&amp;nbsp;검증하지는&amp;nbsp;못했지만, &lt;br /&gt;(연구소) 테스트 서버 &amp;rarr; 개발계 &amp;rarr; 운영계로 이어지는 단계별 검증과 실제&amp;nbsp;사용자&amp;nbsp;테스트를&amp;nbsp;통해&amp;nbsp;큰&amp;nbsp;문제없이&amp;nbsp;안정적으로&amp;nbsp;적용되었다는&amp;nbsp;점에서&amp;nbsp;의미&amp;nbsp;있는&amp;nbsp;결과라고&amp;nbsp;생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;원래는 현장에서 오류가 발생한 메뉴만 수정하는 범위였지만, 작업을 진행하면서 관련된 부분까지 함께 수정하는 방향으로 범위를 확장하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그 과정에서 작업 범위가 예상보다 커지면서 내부적으로도 다양한 의견이 있었지만,단순히&amp;nbsp;특정&amp;nbsp;기능만&amp;nbsp;수정하는&amp;nbsp;것이&amp;nbsp;아니라, &lt;br /&gt;유사한&amp;nbsp;문제가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;구조를&amp;nbsp;함께&amp;nbsp;보완하는 것이&amp;nbsp;더&amp;nbsp;필요하다고&amp;nbsp;판단했다. &lt;br /&gt;&lt;br /&gt;결과적으로 수정 범위는 커졌지만, 현재&amp;nbsp;담당하고&amp;nbsp;있는&amp;nbsp;제품에서는&amp;nbsp;&lt;b&gt;추가적인&amp;nbsp;오류&amp;nbsp;없이&amp;nbsp;안정적으로&amp;nbsp;운영&lt;/b&gt;되고&amp;nbsp;있으며, &lt;br /&gt;다른 제품에서 이슈로 지속적인 대응이 이루어지고 있는 상황과 비교했을 때 의미&amp;nbsp;있는&amp;nbsp;결과였다고&amp;nbsp;생각한다.&lt;br /&gt;&lt;br /&gt;다만 일부 메뉴에서 조회 방식 변경으로 인한 성능 저하가 확인된 만큼, 해당&amp;nbsp;부분은&amp;nbsp;별도의&amp;nbsp;개선&amp;nbsp;작업을&amp;nbsp;통해&amp;nbsp;보완할&amp;nbsp;예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회사회고록</category>
      <category>500error</category>
      <category>SessionAttributes</category>
      <category>개발회고록</category>
      <category>베이비개발자</category>
      <category>세션</category>
      <category>제거일지</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/10</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/SessionAttributes-%EC%A0%9C%EA%B1%B0-%EC%9D%BC%EC%A7%804-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9-%EA%B2%B0%EA%B3%BC#entry10comment</comments>
      <pubDate>Tue, 24 Mar 2026 22:12:28 +0900</pubDate>
    </item>
    <item>
      <title>@RequestParam VS @RequestPart의 차이</title>
      <link>https://sujin-su-sss.tistory.com/entry/RequestParam-VS-RequestPart%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 @SessionAttributes 제거를 하면서 애를 먹었던 이슈 중 하나가 엑셀업로드 할 때, Excel과 함께 parameter들이 제대로 넘어오지 못하고 에러를 뱉는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 서버가 &amp;nbsp;@SessionAttributes로 필요한 모든 정보를 가지고 있어서 @RequestParam으로 엑셀 파일만 전달하면 되어 문제가 없었지만 해당 로직을 수정하면서 2가지의 문제가 발생하였다.&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;1. 엑셀 파일과 필요한 정보(Json)를 전달해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 엑셀 파일 + Json 정보가 정상적으로 바인딩되지 않음&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;이를 어떻게 해결을 할지 고민을 하면서 AI를 도움받아 알게 된 것이 @RequestPart였고 해당 어노테이션을 활용해 기능을 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 도대체 그&amp;nbsp; 두 개가 뭐가 다르기 때문에 @RequestParam은 안되고 @RequestPart은 되었던 걸까?&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;@RequestParam &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서를 참고해서 보면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;메서드 인수 타입이 String 또는 MultipartFile/Part가 아닌 경우 등록된 Converter 또는 PropertyEditor를 통한 형식 변환에 의존을 하고 있다. 그리고 이름-값 form 필드와 함께 사용되고 있다.&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;즉, 문자열/숫자 등과 같은 단순한 데이터나 MultipartFile 단일처리에는 사용할 수 있지만, &lt;b&gt;Json과 같은 구조화된 데이터를 자동으로 DTO로 변환하는 데에는 적절하지 않다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774237502895&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//UI
const formData = new FormData();

formData.append(&quot;data&quot;, JSON.stringify({ name: &quot;test&quot;, age: 20 }));
formData.append(&quot;file&quot;, file);

fetch(&quot;/upload&quot;, {
  method: &quot;POST&quot;,
  body: formData
});


//Back
@PostMapping(&quot;/upload&quot;)
public void upload(
    @RequestParam Dto dto,
    @RequestParam MultipartFile file
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestParam 은 기본적으로 String 기반으로 데이터를 처리하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;{ name: &quot;test&quot;, age: 20 } 부분을 하나의 문자열로 인식을 하여, 해당 Json을 자동으로 DTO로 변환해주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(단,&amp;nbsp;&lt;b&gt;key-value&amp;nbsp;형태로&amp;nbsp;전달되는&amp;nbsp;경우에는&amp;nbsp;Setter를&amp;nbsp;통해&amp;nbsp;DTO로&amp;nbsp;바인딩이&amp;nbsp;가능&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;그래서 Json 형태의 데이터가 문자열로 전달되면서, DTO의 필드 단위로 매핑할 수 있는 key-value 구조가 아니기 때문에 바인딩이 되지 않아 에러가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에 시도했던 DTO에 file 필드를 추가하여 @RequestParam으로 한 번에 처리하려고 했던 것은, 요청 데이터 구조 자체가 맞지 않아 바인딩이 정상적으로 이루어지지 않아 실패했다..&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;@RequestPart&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;요청 부분의 Content-Type 헤더를 고려하는 HttpMessageConverters에 의존하여 변환을 하며, JSON, XML등과 같은 복잡한 내용의 Part도 함께 사용이 가능하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서는 위와 같이 설명이 되어있는 데, 찾아본 내용과 해서 정리를 하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multipart/form-data &amp;nbsp;요청에서 각 part를 HTTP 메시지 단위로 처리를 하고 Content-Type 기반으로 데이터를 파싱 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;데이터를 각각 독립적으로 처리한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre id=&quot;code_1774237173733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestPart(&quot;data&quot;) Dto dto,
@RequestPart(&quot;file&quot;) MultipartFile file&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;width: 100%;&quot;&gt;data -&amp;gt; JSON -&amp;gt; DTO로 변환&lt;br /&gt;file -&amp;gt; file (변환 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동으로 해당 데이터를 DTO로 변환을 해주기 때문에 문제없이 해당 데이터가 바인딩이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, @RequestParam은 데이터를 &amp;ldquo;쪼개서 받는 방식&amp;rdquo; @RequestPart는 데이터를 &amp;ldquo;그대로 해석하는 방식&amp;rdquo;이라고 이해하면 된다.&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;처음에는 해당 부분의 문제가 file를 함께 보내서 생긴 문제라고 인식을 했었는 데, 해당 부분을 공부하면서 file 쪽이 아닌 Json 데이터를 문자열 형태로 전달하면서&amp;nbsp; @RequestParam의 바인딩 방식과 맞지 않아 발생한 문제라는 것을 알게 되면서 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;@RequestParam을 사용하면서 해결할 수 있었던 방법도 있지 않을까 라는 생각이 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 만약 @RequestParam을 유지하고 싶다면, JSON을 문자열로 전달하는 대신 key-value 형태로 분해해서 전달해야 정상적으로 바인딩된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Server</category>
      <category>JSON</category>
      <category>RequestParam</category>
      <category>RequestParamVSRequestPart</category>
      <category>REQUESTPART</category>
      <category>개발공부</category>
      <category>개발자</category>
      <category>베이비개발자</category>
      <category>파라미터바인딩</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/8</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/RequestParam-VS-RequestPart%EC%9D%98-%EC%B0%A8%EC%9D%B4#entry8comment</comments>
      <pubDate>Mon, 23 Mar 2026 13:26:13 +0900</pubDate>
    </item>
    <item>
      <title>Null과 공백의 차이 by Oracle,PostgreSQL</title>
      <link>https://sujin-su-sss.tistory.com/entry/Null%EA%B3%BC-%EA%B3%B5%EB%B0%B1%EC%9D%98-%EC%B0%A8%EC%9D%B4-by-OraclePostgreSQL</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 제품 유지보수 및 개발을 하고 있는 제품은 DB종류에 따라서 각 DB 전용 XML 쿼리를 분리하여 실행하는 구조로 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에, 한 DB의 쿼리를 수정하거나 개발하게 되면 다른 DB에 맞게 문법을 변경한 후 적용해야 한다.&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;NULL과 공백의 차이&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(그리고 오늘도 이 차이 때문에 조건을 다시 수정하고 있다&amp;hellip;)&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;1.Oracle - 공백은 Null이다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서는 공백을 Null로 취급한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmwqUW/dJMcajac74y/9PkRTAzQROPuSBUyQNX0F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmwqUW/dJMcajac74y/9PkRTAzQROPuSBUyQNX0F0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmwqUW/dJMcajac74y/9PkRTAzQROPuSBUyQNX0F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmwqUW%2FdJMcajac74y%2F9PkRTAzQROPuSBUyQNX0F0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;255&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 Oracle에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 공백&amp;nbsp; &amp;gt; Null&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Null&amp;nbsp; &amp;gt; NUll&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 취급하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 , 공백과 Null을 구분하지 않는다.&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;따라서 조건을 아래와 같이 작성하게 되면 공백과 Null 둘 다 조건에 걸리게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1772163461893&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE column IS NULL&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;b&gt;2. PostgreSQL &amp;ndash; 공백과 NULL은 다르다 &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서는 &lt;b&gt;공백과 Null을 명확하게 구분&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 같은 쿼리를 돌려 데이터를 확인하게 되면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvMBZr/dJMcahQ2ovD/etyNnL6MtvSMB3sk0LLRT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvMBZr/dJMcahQ2ovD/etyNnL6MtvSMB3sk0LLRT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvMBZr/dJMcahQ2ovD/etyNnL6MtvSMB3sk0LLRT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvMBZr%2FdJMcahQ2ovD%2FetyNnL6MtvSMB3sk0LLRT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;585&quot; height=&quot;292&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 공백 &amp;gt; 길이가 0인 문자열&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;Null &amp;gt; 값없음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 차이로 인해서 조건문에서 문제가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772163655359&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;WHERE column IS NULL&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;이런 식으로 조건을 달게 되면 Null만 조건에 걸리고, 공백은 조건에 걸리지 않게 된다.&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. PostgreSQL에서 NULL + 공백을 함께 처리하려면? &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 조건에 포함시키려면 명시적으로 작성해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5qyf4/dJMcafMt3KT/qBi9fKwVUO48xny4K65Kr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5qyf4/dJMcafMt3KT/qBi9fKwVUO48xny4K65Kr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5qyf4/dJMcafMt3KT/qBi9fKwVUO48xny4K65Kr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5qyf4%2FdJMcafMt3KT%2FqBi9fKwVUO48xny4K65Kr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;729&quot; height=&quot;358&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 조건을 추가하지 않으면, 공백 데이터는 필터링이 되지 않아 기대한 값과 다르게 표출이 되는 상황이 발생한다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 이런 식으로 Null로 치환해서 하는 방법도 존재한다&lt;/p&gt;
&lt;pre id=&quot;code_1772171305929&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select nullif(컬럼,'') 
  from 테이블&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-end=&quot;112&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;이러한 차이로 인한 조건문 누락은 생각보다 쉽게 발생한다.&lt;br /&gt;급하게 수정하거나 기존 로직을 그대로 이식하는 과정에서, 하지만 이 작은 차이를 그대로 둔 채 고객사에 배포하게 되면, 운영 환경에서는 예상하지 못한 데이터 오류나 로직 오동작으로 이어질 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-end=&quot;334&quot; data-start=&quot;241&quot; data-ke-size=&quot;size16&quot;&gt;따라서 멀티 DB 환경에서는 문법 차이만 점검할 것이 아니라, 동일한 데이터 조건에서 쿼리를 직접 실행해 보고 결과 값이 의도대로 반환되는지까지 검증해야 한다.&lt;/p&gt;
&lt;h1 data-end=&quot;1353&quot; data-start=&quot;1349&quot;&gt;정리&lt;/h1&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1534&quot; data-start=&quot;1355&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1447&quot; data-start=&quot;1415&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1423&quot; data-start=&quot;1415&quot;&gt;공백&lt;/td&gt;
&lt;td data-end=&quot;1434&quot; data-start=&quot;1423&quot; data-col-size=&quot;sm&quot;&gt;NULL로 처리&lt;/td&gt;
&lt;td data-end=&quot;1447&quot; data-start=&quot;1434&quot; data-col-size=&quot;sm&quot;&gt;빈 문자열로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1488&quot; data-start=&quot;1448&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1461&quot; data-start=&quot;1448&quot;&gt;IS NULL 조건&lt;/td&gt;
&lt;td data-end=&quot;1476&quot; data-start=&quot;1461&quot; data-col-size=&quot;sm&quot;&gt;NULL + '' 포함&lt;/td&gt;
&lt;td data-end=&quot;1488&quot; data-start=&quot;1476&quot; data-col-size=&quot;sm&quot;&gt;NULL만 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1534&quot; data-start=&quot;1489&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1503&quot; data-start=&quot;1489&quot;&gt;공백 처리 필요 여부&lt;/td&gt;
&lt;td data-end=&quot;1515&quot; data-start=&quot;1503&quot; data-col-size=&quot;sm&quot;&gt;별도 처리 불필요&lt;/td&gt;
&lt;td data-end=&quot;1534&quot; data-start=&quot;1515&quot; data-col-size=&quot;sm&quot;&gt;= '' 조건 추가 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SQL</category>
      <category>db</category>
      <category>null</category>
      <category>Null과공백차이</category>
      <category>oracle</category>
      <category>oraclenull</category>
      <category>PostgreSQL</category>
      <category>PostgreSql문법</category>
      <category>공백차이</category>
      <category>기술블로그</category>
      <category>베이비개발자</category>
      <author>베베개발자</author>
      <guid isPermaLink="true">https://sujin-su-sss.tistory.com/7</guid>
      <comments>https://sujin-su-sss.tistory.com/entry/Null%EA%B3%BC-%EA%B3%B5%EB%B0%B1%EC%9D%98-%EC%B0%A8%EC%9D%B4-by-OraclePostgreSQL#entry7comment</comments>
      <pubDate>Fri, 27 Feb 2026 12:55:34 +0900</pubDate>
    </item>
  </channel>
</rss>